Sebuah foto makanan tidak banyak berarti tanpa keterangan “es kopi susu”, dan kalimat “hati-hati, ada mobil” menjadi jauh lebih berguna jika sistem juga melihat gambar jalan. Model deep learning yang kita bahas sejauh ini umumnya mengolah satu jenis data — gambar saja, teks saja, atau sinyal saja. Multimodal learning memperluas kemampuan itu: satu model menerima dua atau lebih modalitas sekaligus dan mempelajari representasi yang menghubungkan keduanya. Section ini menelusuri pola penyatuannya mulai dari proyeksi ruang bersama, pelatihan kontrastif image-text, arsitektur vision-language model, hingga model audio dan video. Chapter pembuka ini memetakan masalahnya, lalu membangun demo fusi paling sederhana dengan angka yang bisa kita periksa satu per satu.

Mengapa Model Perlu Lebih dari Satu Modalitas #

Manusia memahami dunia dengan menggabungkan penglihatan, bunyi, dan bahasa; sistem yang hanya membaca satu kanal informasi akan menemukan banyak tugas yang mustahil diselesaikan. Sebuah caption gambar menyebut “anjing melompat”, tetapi verifikasi kebenarannya menuntut model membaca isi gambar dan teks sekaligus. Demikian pula transkripsi rapat menjadi lebih andal jika model melihat slide presentasi yang sedang dibahas. Kombinasi modalitas yang paling umum dipelajari dirangkum pada tabel berikut:

Kombinasi modalitasTugas khasContoh produk
Gambar + tekscaptioning, retrieval, VQA, klasifikasi zero-shotpencarian gambar dengan kueri bahasa
Audio + teksspeech recognition, text-to-speechdikte suara, pembaca layar
Video + teksringkasan video, pencarian momenpencarian klip berdasarkan deskripsi
Gambar + audiosinkronisasi bicara, deteksi sumber suarapembacaan gerakan mulut
Teks + data terstrukturanalisis sentimen dengan metadataulasan produk dengan rating

Kebutuhan teknis di balik semua baris tabel itu sama: dua jalur masukan dengan bentuk dan satuan yang berbeda harus dipetakan ke representasi yang dapat dibandingkan atau digabungkan.

Tiga Tantangan Penyatuan Modalitas #

Tantangan pertama adalah perbedaan ruang representasi. Keluaran CNN untuk gambar berupa ribuan angka, keluaran encoder teks berupa ratusan dimensi embedding, dan keduanya tidak memiliki satuan yang sama — seperti membandingkan kilogram dengan meter. Tantangan kedua adalah alignment: kita harus tahu bagian mana dari gambar yang sesuai dengan kata mana dari kalimat, padahal panjang urutan keduanya berbeda. Tantangan ketiga adalah modality gap: dua modalitas yang menjelaskan objek sama tetap terpisah di ruang bersama karena statistik masing-masing encoder berbeda — gejala ini terukur pada model image-text modern dan tetap menjadi bahan riset.

Demo: Dua Encoder Mini dengan Fusi #

Kita bangun kasus mainan yang jujur membutuhkan dua modalitas. Data sintetis terdiri dari “gambar” 6 fitur dan “teks” 4 fitur; label ditentukan oleh relasi XOR antara tanda rata-rata kedua modalitas, sehingga model yang hanya melihat satu jalur tidak mungkin melampaui tebakan acak. Program berikut membandingkan tiga model — gambar saja, teks saja, dan fusi keduanya:

python
"""Demo mini multimodal: dua encoder dengan dimensi masukan berbeda
(gambar sintetis 6 fitur, teks sintetis 4 fitur) diproyeksikan ke ruang
bersama berdimensi 8, lalu difusi (concat) menjadi satu klasifier.

Label sengaja dibuat bergantung pada KOMBINASI kedua modalitas
(XOR terhadap tanda rata-rata tiap modalitas) sehingga model satu
modalitas mandek di sekitar 50%, sedangkan model fusi melampaui itu.
Semua angka diuji nyata pada CPU dengan seed tetap. PyTorch 2.14.
"""

import torch
import torch.nn as nn

torch.manual_seed(42)

D_GAMBAR = 6   # dimensi fitur "gambar" sintetis
D_TEKS = 4     # dimensi fitur "teks" sintetis
D_BERSAMA = 8  # dimensi ruang proyeksi bersama
N_LATIH, N_UJI = 240, 60


def buat_data(n, generator):
    """Setiap sampel: mu_gambar, mu_teks bernilai -1 atau +1.
    Label = 1 jika kedua tanda SAMA (mu_gambar * mu_teks > 0) -- relasi
    XOR, sehingga label tidak bisa disimpulkan dari satu modalitas saja."""
    mu = torch.randint(0, 2, (n, 2), generator=generator) * 2 - 1
    gambar = mu[:, 0:1] + 0.6 * torch.randn(n, D_GAMBAR, generator=generator)
    teks = mu[:, 1:2] + 0.6 * torch.randn(n, D_TEKS, generator=generator)
    label = (mu[:, 0] * mu[:, 1] > 0).long()
    return gambar, teks, label


gen_latih = torch.Generator().manual_seed(1)
gen_uji = torch.Generator().manual_seed(2)
g_latih, t_latih, y_latih = buat_data(N_LATIH, gen_latih)
g_uji, t_uji, y_uji = buat_data(N_UJI, gen_uji)

print("=== 1. Bentuk data dua modalitas ===")
print(f"gambar latih : {tuple(g_latih.shape)}  (n, {D_GAMBAR})")
print(f"teks   latih : {tuple(t_latih.shape)}  (n, {D_TEKS})")
print(f"label  latih : {tuple(y_latih.shape)}")
print(f"distribusi label: 0 -> {int((y_latih == 0).sum())}, "
      f"1 -> {int((y_latih == 1).sum())}")
print()


def latih_linear(model, masukan, label, epoch=400, lr=0.05):
    """Latih model satu masukan; kembalikan loss awal dan akhir."""
    opt = torch.optim.Adam(model.parameters(), lr=lr)
    loss_fn = nn.CrossEntropyLoss()
    loss_awal = None
    for e in range(epoch):
        opt.zero_grad()
        loss = loss_fn(model(masukan), label)
        if e == 0:
            loss_awal = loss.item()
        loss.backward()
        opt.step()
    return loss_awal, loss.item()


def akurasi(pred, label):
    return (pred.argmax(dim=1) == label).float().mean().item()


# --- model A: hanya gambar ---
model_gambar = nn.Linear(D_GAMBAR, 2)
la, lk = latih_linear(model_gambar, g_latih, y_latih)
print("=== 2. Model satu modalitas (pembanding) ===")
with torch.no_grad():
    ak_g = akurasi(model_gambar(g_uji), y_uji)
print(f"hanya gambar : loss {la:.3f} -> {lk:.3f}, akurasi uji {ak_g:.3f}")

# --- model B: hanya teks ---
model_teks = nn.Linear(D_TEKS, 2)
la, lk = latih_linear(model_teks, t_latih, y_latih)
with torch.no_grad():
    ak_t = akurasi(model_teks(t_uji), y_uji)
print(f"hanya teks   : loss {la:.3f} -> {lk:.3f}, akurasi uji {ak_t:.3f}")
print()


class ModelFusi(nn.Module):
    """Early fusion: dua encoder + proyeksi ke d=8, concat, lalu kepala
    klasifier MLP kecil (interaksi antar modalitas dibutuhkan karena
    label berbentuk XOR)."""

    def __init__(self):
        super().__init__()
        self.enc_gambar = nn.Linear(D_GAMBAR, D_BERSAMA)
        self.enc_teks = nn.Linear(D_TEKS, D_BERSAMA)
        self.klasifier = nn.Sequential(
            nn.Linear(2 * D_BERSAMA, 16),
            nn.Tanh(),
            nn.Linear(16, 2),
        )

    def forward(self, g, t):
        z_g = torch.tanh(self.enc_gambar(g))   # (n, 8)
        z_t = torch.tanh(self.enc_teks(t))     # (n, 8)
        z = torch.cat([z_g, z_t], dim=1)       # (n, 16) early fusion
        return self.klasifier(z)


model_fusi = ModelFusi()
with torch.no_grad():
    z_g = torch.tanh(model_fusi.enc_gambar(g_uji[:1]))
    z_t = torch.tanh(model_fusi.enc_teks(t_uji[:1]))
    z = torch.cat([z_g, z_t], dim=1)

print("=== 3. Bentuk tensor pada jalur fusi (1 contoh) ===")
print(f"masukan gambar      : {tuple(g_uji[:1].shape)}")
print(f"proyeksi enc_gambar : {tuple(z_g.shape)}")
print(f"masukan teks        : {tuple(t_uji[:1].shape)}")
print(f"proyeksi enc_teks   : {tuple(z_t.shape)}")
print(f"concat (fusi)       : {tuple(z.shape)}")
print()

opt = torch.optim.Adam(model_fusi.parameters(), lr=0.05)
loss_fn = nn.CrossEntropyLoss()
loss_jejak = []
for e in range(400):
    opt.zero_grad()
    loss = loss_fn(model_fusi(g_latih, t_latih), y_latih)
    if e in (0, 199, 399):
        loss_jejak.append(loss.item())
    loss.backward()
    opt.step()

with torch.no_grad():
    ak_f = akurasi(model_fusi(g_uji, t_uji), y_uji)
print("=== 4. Model fusi dua modalitas ===")
print("loss: awal {:.3f} -> tengah {:.3f} -> akhir {:.3f}".format(*loss_jejak))
print(f"akurasi uji fusi    : {ak_f:.3f}")
print(f"jumlah parameter    : {sum(p.numel() for p in model_fusi.parameters())}")

Output yang Dihasilkan:

text
=== 1. Bentuk data dua modalitas ===
gambar latih : (240, 6)  (n, 6)
teks   latih : (240, 4)  (n, 4)
label  latih : (240,)
distribusi label: 0 -> 117, 1 -> 123

=== 2. Model satu modalitas (pembanding) ===
hanya gambar : loss 0.733 -> 0.683, akurasi uji 0.483
hanya teks   : loss 0.857 -> 0.682, akurasi uji 0.500

=== 3. Bentuk tensor pada jalur fusi (1 contoh) ===
masukan gambar      : (1, 6)
proyeksi enc_gambar : (1, 8)
masukan teks        : (1, 4)
proyeksi enc_teks   : (1, 8)
concat (fusi)       : (1, 16)

=== 4. Model fusi dua modalitas ===
loss: awal 0.710 -> tengah 0.000 -> akhir 0.000
akurasi uji fusi    : 1.000
jumlah parameter    : 402

Mari kita telaah kode di atas baris per baris:

  1. Baris 22-30 (buat_data): fungsi ini menarik tanda mu_gambar dan mu_teks dari {-1, +1}, menambahkan derau Gaussian 0,6 ke setiap fitur, lalu menghitung label sebagai hasil kali kedua tanda. Karena label merupakan fungsi kedua modalitas sekaligus, data uji ini secara eksplisit menuntut fusi.
  2. Baris 47-59 (latih_linear): pembanding unimodal dilatih dengan Adam dan CrossEntropyLoss selama 400 epoch; fungsi mengembalikan loss awal dan akhir agar kemajuan pelatihan terlihat.
  3. Baris 66-79: dua model pembanding — nn.Linear(6, 2) untuk gambar dan nn.Linear(4, 2) untuk teks — diuji pada 60 sampel yang tidak pernah dilihat saat pelatihan.
  4. Baris 83-102 (ModelFusi): kedua encoder memproyeksikan masukan ke dimensi bersama 8 melalui nn.Linear + tanh. torch.cat pada baris 101 menempel kedua vektor menjadi 16 fitur — titik fusi sesungguhnya. Kepala klasifier berupa MLP dua lapis karena XOR tidak dapat direpresentasikan oleh lapisan linear tunggal di atas fitur gabungan.
  5. Baris 105-109: sebelum pelatihan, satu contoh uji melewati jalur fusi untuk mencetak bentuk tensor pada setiap tahap.
  6. Baris 119-131: pelatihan model fusi memakai pola yang sama dengan pembanding; loss dicatat pada epoch 0, 199, dan 399.

Hasilnya tegas. Kedua model unimodal tertahan di 0,483 dan 0,500 — persis perilaku tebakan acak — karena satu jalur masukan memang tidak memuat informasi label. Model fusi mencapai akurasi uji 1,000 dengan hanya 402 parameter, sementara loss jatuh dari 0,710 ke 0,000. Bentuk tensor pada bagian ketiga juga memperlihatkan inti mekanikanya: masukan (1, 6) dan (1, 4) menjadi proyeksi (1, 8) dan (1, 8), lalu concat menghasilkan (1, 16) — dua ruang yang berbeda dimensi disatukan lewat proyeksi ke dimensi sama.

Early Fusion dan Late Fusion #

Posisi titik fusi menentukan nama polanya. Demo kita memakai early fusion: representasi kedua modalitas digabungkan segera setelah proyeksi, lalu satu kepala model mengolah gabungannya. Late fusion bekerja sebaliknya — setiap modalitas diproses hingga menghasilkan keputusan sendiri, lalu keputusan-keputusan itu digabungkan (rata-rata skor, voting, atau model kecil di atasnya).

Diagram di atas adalah jalur early fusion milik demo kita. Perbandingan kedua pola dirangkum berikut:

AspekEarly fusionLate fusion
Titik penggabunganlapisan representasi (fitur)lapisan keputusan (skor/logit)
Interaksi antar modalitaspenuh sejak awalterbatas pada aturan penggabungan
Ketahanan modalitas hilangperlu penanganan khusussalah satu jalur tetap bisa bersuara
Biayasatu model gabunganbeberapa model terpisah

Langkah berikutnya menghapus asumsi yang paling menyederhanakan dari demo ini: bahwa pasangan gambar-teks sudah tersedia dan sejajar. Chapter Image-Text Representation membahas bagaimana dua encoder yang berbeda arsitektur diproyeksikan ke satu ruang bersama dan bagaimana kesesuaian pasangannya diukur, sebelum pelatihan kontrastif mengambil alih di chapter Contrastive Image-Text Learning .