Topik Vision Transformer sengaja dibahas dari dua arah di Course ini. Chapter Vision Transformer pada section Computer Vision
menyelesaikan sisi praktik patchify: memotong citra digits menjadi token dengan NumPy, angka per angka, namun berhenti sebelum encoder — bobot proyeksinya masih acak dan tidak ada satu pun parameter yang dilatih. Chapter ini menutup setengah yang tersisa dari sudut arsitektur: kita merakit ViT utuh dalam PyTorch, melatihnya pada 1.400 citra digits sampai akurasi uji yang nyata, lalu membaca kembali bobot attention token [CLS] untuk melihat patch mana yang jadi sorotan model. Seluruh blok bangunannya berasal dari chapter-chapter sebelumnya — tidak ada komponen baru selain cara menyusunnya.
Dari Piksel ke Urutan Token #
ViT memperlakukan gambar seperti kalimat. Citra H x W dipotong grid patch P x P (makalah asli memakai P = 16 pada gambar 224x224, menghasilkan 196 token), tiap patch diratakan menjadi vektor P x P piksel, lalu satu proyeksi linear nn.Linear(P*P, d_model) memetakan tiap vektor ke dimensi model. Dua pelengkap menyusul: token [CLS] — satu parameter terlatih yang diletakkan di depan urutan dan keluarannya dibaca classifier head — serta positional embedding. Berbeda dengan sinus chapter Positional Encoding
, ViT memakai positional embedding yang ikut dilatih dari awal, satu vektor per posisi token.
Diagram di atas adalah keseluruhan arsitektur program kita: tanpa convolution dan tanpa pooling, hanya urutan 17 token (1 [CLS] + 16 patch) yang melewati satu blok encoder — blok pre-LN yang sama dengan chapter Transformer Encoder
. Mekanika patchify-nya sendiri (loop baris demi baris, isi tiap patch sebagai blok piksel) sudah dibedah tuntas di chapter Computer Vision; implementasi tensor di sini hanya versi vektornya.
Program: ViT Mainan yang Dilatih Nyata #
Program berikut melatih ViTMini pada dataset digits scikit-learn: 1.797 citra 8x8 grayscale, 10 kelas digit. Konfigurasi sengaja kecil — patch 2x2 menghasilkan 16 token, d_model = 16, dua head, satu blok encoder, FFN sempit — namun seluruh jalurnya nyata: loss turun, akurasi naik, dan sorotan [CLS] terbaca setelah pelatihan. Dengan seed tetap, seluruh angka pada output dapat direproduksi:
"""Vision Transformer mainan: digits 8x8, patch 2x2, satu blok encoder.
Alur: citra 8x8 -> 16 patch 2x2 (grid 4x4) -> proyeksi linear 4->16 ->
token [CLS] + positional embedding terlatih -> 1 blok encoder ->
klasifikasi dari token [CLS]. Dilatih dengan Adam pada 1400 citra
digits; akurasi latih dan uji dilaporkan. Bobot attention token [CLS]
atas 16 patch dicetak untuk satu contoh uji. PyTorch 2.14, CPU.
"""
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import load_digits
torch.manual_seed(0)
np.set_printoptions(precision=3, suppress=True)
class MultiHeadAttention(nn.Module):
"""Multi-head attention manual dari chapter multi-head-attention."""
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.h = num_heads
self.d_k = d_model // num_heads
self.wq = nn.Linear(d_model, d_model)
self.wk = nn.Linear(d_model, d_model)
self.wv = nn.Linear(d_model, d_model)
self.wo = nn.Linear(d_model, d_model)
def forward(self, x):
n, panjang, d = x.shape
q = self.wq(x).view(n, panjang, self.h, self.d_k).transpose(1, 2)
k = self.wk(x).view(n, panjang, self.h, self.d_k).transpose(1, 2)
v = self.wv(x).view(n, panjang, self.h, self.d_k).transpose(1, 2)
skor = q @ k.transpose(-2, -1) / self.d_k ** 0.5
a = skor.softmax(dim=-1)
z = (a @ v).transpose(1, 2).contiguous().view(n, panjang, d)
return self.wo(z), a
class FeedForward(nn.Module):
"""Position-wise feed-forward: Linear -> GELU -> Linear."""
def __init__(self, d_model, d_ff):
super().__init__()
self.l1 = nn.Linear(d_model, d_ff)
self.l2 = nn.Linear(d_ff, d_model)
def forward(self, x):
return self.l2(F.gelu(self.l1(x)))
class EncoderBlock(nn.Module):
"""Blok encoder pre-LN dari chapter transformer-encoder.
Keluaran blok dikembalikan bersama bobot attention agar token
[CLS] bisa diperiksa setelah pelatihan.
"""
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
self.attn = MultiHeadAttention(d_model, num_heads)
self.ffn = FeedForward(d_model, d_ff)
self.ln1 = nn.LayerNorm(d_model)
self.ln2 = nn.LayerNorm(d_model)
def forward(self, x):
n = self.ln1(x)
h, bobot = self.attn(n)
h = x + h
h = h + self.ffn(self.ln2(h))
return h, bobot
class ViTMini(nn.Module):
"""Vision Transformer mini: patch 2x2, d_model 16, 1 blok encoder."""
def __init__(self, ukuran_patch, d_model, num_heads, d_ff, n_kelas, n_patch):
super().__init__()
self.p = ukuran_patch
self.proj = nn.Linear(ukuran_patch ** 2, d_model) # patch -> token
self.cls = nn.Parameter(torch.randn(1, 1, d_model) * 0.02)
self.pos = nn.Parameter(torch.randn(1, n_patch + 1, d_model) * 0.02)
self.blok = EncoderBlock(d_model, num_heads, d_ff)
self.ln_akhir = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, n_kelas)
def patchify(self, imgs):
"""(B, 8, 8) -> (B, 16, 4): grid patch 2x2 baris demi baris."""
b, h, w = imgs.shape
p = self.p
x = imgs.view(b, h // p, p, w // p, p)
x = x.permute(0, 1, 3, 2, 4).contiguous()
return x.view(b, -1, p * p)
def forward(self, imgs):
tok = self.proj(self.patchify(imgs)) # (B, 16, D)
b = tok.shape[0]
z = torch.cat([self.cls.expand(b, -1, -1), tok], dim=1) + self.pos
z, bobot = self.blok(z)
cls_out = self.ln_akhir(z[:, 0]) # token [CLS]
return self.head(cls_out), bobot
# ---------- data: digits 8x8, 1797 citra, 10 kelas ----------
digits = load_digits()
X = torch.tensor(digits.data / 16.0, dtype=torch.float32).view(-1, 8, 8)
y = torch.tensor(digits.target, dtype=torch.long)
g = torch.Generator().manual_seed(0)
urutan_acak = torch.randperm(len(X), generator=g)
n_latih = 1400
X_latih, y_latih = X[urutan_acak[:n_latih]], y[urutan_acak[:n_latih]]
X_uji, y_uji = X[urutan_acak[n_latih:]], y[urutan_acak[n_latih:]]
model = ViTMini(ukuran_patch=2, d_model=16, num_heads=2, d_ff=32,
n_kelas=10, n_patch=16)
print("== Vision Transformer mainan pada digits ==")
print(f"data: {len(X)} citra 8x8, 10 kelas "
f"({n_latih} latih / {len(X_uji)} uji)")
print()
print("== Alur satu citra ==")
contoh = X_latih[0:1]
with torch.no_grad():
patch = model.patchify(contoh)
tok = model.proj(patch)
z = torch.cat([model.cls, tok], dim=1) + model.pos
logits, _ = model(contoh)
print(f"gambar : {tuple(contoh.shape)} (kelas {y_latih[0].item()})")
print(f"patch 2x2 : {tuple(patch.shape)} (16 token, tiap 4 piksel)")
print(f"proyeksi linear : {tuple(tok.shape)} (d_model 16)")
print(f"+ [CLS] + PE : {tuple(z.shape)} (1 + 16 token)")
print(f"logits dari [CLS] : {tuple(logits.shape)}")
print()
print("== Pelatihan: Adam lr 1e-3, batch 64, 100 epoch ==")
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
BATCH = 64
@torch.no_grad()
def akurasi(model, X, y):
pred = model(X)[0].argmax(dim=1)
return (pred == y).float().mean().item()
for epoch in range(100):
model.train()
g2 = torch.Generator().manual_seed(epoch) # urutan batch deterministik
urut = torch.randperm(n_latih, generator=g2)
for i in range(0, n_latih, BATCH):
j = urut[i:i + BATCH]
logits, _ = model(X_latih[j])
loss = F.cross_entropy(logits, y_latih[j])
opt.zero_grad()
loss.backward()
opt.step()
if epoch % 25 == 0 or epoch == 99:
model.eval()
print(f" epoch {epoch:2d} : loss batch akhir {loss.item():.4f} "
f"akurasi latih {akurasi(model, X_latih, y_latih):.3f} "
f"akurasi uji {akurasi(model, X_uji, y_uji):.3f}")
print()
model.eval()
print("== Bobot attention token [CLS] atas 16 patch ==")
with torch.no_grad():
logits, bobot = model(X_uji[:1])
peringkat = bobot[0].mean(dim=0)[0, 1:] # [CLS] -> 16 patch, rata-rata head
print(f"contoh uji: kelas asli {y_uji[0].item()}, "
f"prediksi {int(logits[0].argmax())}")
print("grid 4x4 (baris/kolom = posisi patch pada citra):")
print(np.round(peringkat.reshape(4, 4).numpy(), 3))
top = int(peringkat.argmax())
print(f"patch paling diperhatikan: {top} "
f"(baris {top // 4}, kolom {top % 4} pada grid)")Output yang Dihasilkan:
== Vision Transformer mainan pada digits ==
data: 1797 citra 8x8, 10 kelas (1400 latih / 397 uji)
== Alur satu citra ==
gambar : (1, 8, 8) (kelas 6)
patch 2x2 : (1, 16, 4) (16 token, tiap 4 piksel)
proyeksi linear : (1, 16, 16) (d_model 16)
+ [CLS] + PE : (1, 17, 16) (1 + 16 token)
logits dari [CLS] : (1, 10)
== Pelatihan: Adam lr 1e-3, batch 64, 100 epoch ==
epoch 0 : loss batch akhir 2.2815 akurasi latih 0.124 akurasi uji 0.096
epoch 25 : loss batch akhir 0.2326 akurasi latih 0.938 akurasi uji 0.899
epoch 50 : loss batch akhir 0.1234 akurasi latih 0.971 akurasi uji 0.922
epoch 75 : loss batch akhir 0.0478 akurasi latih 0.983 akurasi uji 0.927
epoch 99 : loss batch akhir 0.0143 akurasi latih 0.991 akurasi uji 0.932
== Bobot attention token [CLS] atas 16 patch ==
contoh uji: kelas asli 0, prediksi 0
grid 4x4 (baris/kolom = posisi patch pada citra):
[[0.061 0.016 0.059 0.04 ]
[0.053 0.129 0.111 0.022]
[0.029 0.154 0.066 0.068]
[0.04 0.045 0.035 0.064]]
patch paling diperhatikan: 9 (baris 2, kolom 1 pada grid)Mari kita telaah kode di atas baris per baris:
- Baris 20-41: class
MultiHeadAttention— salinan persis dari chapter Multi-Head Attention : empat proyeksi, pemecahan head, skor terkalakansqrt(d_k), softmax per baris, perangkaian kembali. - Baris 44-53: class
FeedForward— dua layer linear dengan GELU, memroses tiap token secara mandiri. - Baris 56-75: class
EncoderBlock— blok pre-LN dari chapter Transformer Encoder dengan satu perubahan:forwardjuga mengembalikan bobot attention agar sorotan[CLS]dapat diperiksa setelah pelatihan. - Baris 78-89: konstruktor
ViTMini. Baris 84:nn.Linear(4, 16)adalah proyeksi patch-ke-token; baris 85-86:clsdanposadalahnn.Parameterterlatih — inilah perbedaan dengan PE sinus, posisi dihitung sebagai vektor yang dipelajari, bukan rumus tetap; baris 88-89: layer norm akhir dan classifier head16 -> 10. - Baris 91-97:
patchifyversi tensor.view(b, 4, 2, 4, 2)membelah citra menjadi grid 4x4 patch 2x2;permutemenyusun ulang sehingga dimensi patch berdampingan;view(b, -1, 4)meratakan tiap patch menjadi vektor 4 piksel — hasil yang sama dengan loop manual di chapter Computer Vision. - Baris 99-105:
forwardmerangkai alur lengkap: proyeksi patch, penambahan[CLS]lewattorch.cat, penjumlahan positional embedding, satu blok encoder, laluz[:, 0]mengambil token[CLS]sebagai satu-satunya masukan head. - Baris 109-117: data digits dimuat dari
sklearn.datasets, diskalakan/ 16.0ke rentang 0-1, dibentuk kembali(1797, 8, 8), lalu dipecah deterministik 1400 latih / 397 uji lewatrandpermberseed. - Baris 127-138: alur satu citra dicetak tahap demi tahap —
(1, 8, 8)menjadi 16 token berdimensi 4, menjadi(1, 17, 16)setelah[CLS]dan PE, dan keluar sebagai 10 logits. - Baris 141-166: pelatihan. Adam dengan learning rate
1e-3, batch 64;torch.Generator().manual_seed(epoch)membuat urutan batch berbeda tiap epoch namun tetap dapat direproduksi;F.cross_entropymenghitung loss; laporan dicetak tiap 25 epoch. - Baris 169-180: pemeriksaan
[CLS]. Baris 173 mengambil baris bobot attention milik token[CLS](indeks 0) lalu membuang kolom[CLS]itu sendiri ([0, 1:]), rata-rata dua head; hasil diratakan menjadi grid 4x4 sesuai posisi patch pada citra.
Membaca Angka: Akurasi dan Sorotan [CLS] #
Akurasi uji mencapai 0,932 dari 0,096 di epoch nol — bukti bahwa arsitektur berbasis attention sungguh mempelajari isi citra, bukan sekadar menebak kelas mayoritas. Sorotan [CLS] pada contoh uji (kelas 0, diprediksi benar) memuncak di patch 9 dengan bobot 0,154 — posisi baris 2 kolom 1 di kawasan tengah-kiri citra, tempat goresan digit 0 melintas — sementara bobot terkecil 0,016 jatuh pada patch baris 0 kolom 1 di tepi atas. Model tidak pernah diberi tahu bahwa digit 0 berbentuk lingkaran; pola sorotan tersebut murni hasil pelatihan, dibaca dengan cara yang sama seperti kita membaca cross-attention di chapter Encoder-Decoder Transformer
.
CNN lawan ViT #
| Aspek | CNN | Vision Transformer |
|---|---|---|
| Bias inductive | kuat: lokal, translasi-invarian, receptive field bertingkat | nyaris nol: posisi lewat embedding, relasi lewat attention |
| Hubungan fitur jauh | baru terjangkau setelah tumpukan layer | global sejak blok pertama |
| Kebutuhan data | efisien pada dataset sedang | unggul setelah pra-pelatihan besar |
Biaya pada token/piksel N | linear O(N) per layer konvolusi | kuadrat O(N^2) pada panjang urutan |
| Komponen | convolution + pooling, bervariasi | hanya attention + MLP, seragam dan mudah diskalakan |
Tabel tersebut menjelaskan dua wajah ViT sekaligus: tanpa bias spasial, model harus belajar pola lokal dari data sehingga datanya harus besar; tetapi komponen yang seragam membuat penambahan blok dan dimensi menjadi lugas — sifat yang menopang era model berskala raksasa. Dataset digits yang dipakai program kita terdokumentasi di sklearn.datasets.load_digits ; seluruh pelatihan di atas selesai dalam sekitar 15 detik pada CPU dengan PyTorch 2.14.
Encoder, decoder, mask, hingga penerapan pada citra — kita kini telah membangun seluruh keluarga Transformer dari komponen terkecil. Bab selanjutnya menarik kamera ke belakang: bagaimana keturunan arsitektur ini berkembang menjadi BERT, GPT, T5, dan berbagai varian efisiensi modern yang menopang model masa kini.