Convolution, filter, stride, padding, pooling, dan feature map telah kita bongkar satu per satu pada chapter-chapter sebelumnya. Sekarang seluruh komponen dirangkai menjadi arsitektur CNN utuh yang benar-benar dilatih. Pola dasarnya konsisten dari LeNet tahun 1998 sampai backbone modern: blok konvolusi diulang beberapa kali, lalu classifier head fully connected menutup jaringan. Pada chapter ini kita merangkai CNN mini lengkap pada dataset digits, menelusuri jejak bentuk tensor dari input sampai logits, dan merumuskan heuristik desain yang dipakai hampir semua arsitektur klasik.

Pola Umum: Blok Konvolusi + Classifier Head #

Hampir semua CNN terbagi menjadi dua bagian dengan tugas berbeda. Bagian pertama — feature extractor — menumpuk blok Conv → ReLU → Pool beberapa kali untuk mengubah piksel mentah menjadi feature map yang semakin abstrak, mengikuti hierarki fitur dari chapter Feature Map . Bagian kedua — classifier head — menerima fitur yang sudah diratakan (flatten) lalu memetakan fitur ke skor tiap kelas lewat layer Linear. Pemisahan softmax sengaja ditinggalkan di luar model karena CrossEntropyLoss sudah memuat softmax + log secara internal.

Diagram di atas adalah bentuk persis CNN mini yang akan kita latih: dua blok konvolusi mengecilkan resolusi 8x8 menjadi 2x2 sambil menaikkan channel 1 menjadi 32, Flatten menjadikan 128 angka, dan head dua layer Linear menghasilkan 10 logits. Tabel berikut merangkum pembagian tugas kedua bagian:

BagianLayer khasTugas
Blok konvolusi 1Conv2d + ReLU + MaxPool2ddeteksi pola lokal: tepi, garis, lengkung
Blok konvolusi 2..nConv2d + ReLU + MaxPool2dkombinasi pola: motif, bagian objek
SambunganFlattenubah (C, H, W) menjadi vektor C*H*W
Classifier headLinear + ReLU + Lineargabungkan fitur menjadi skor kelas

Heuristik Desain: Channel Naik, Spatial Turun #

Aturan empiris yang hampir selalu dipakai: setiap kali pooling membagi dua resolusi spasial, jumlah channel dikali dua. Resolusi mengecil karena informasi posisi menjadi semakin kasar — pola yang berguna semakin global. Channel bertambah karena layer yang lebih dalam butuh ruang lebih besar untuk menyimpan kombinasi pola yang makin beragam. Perkalian keduanya menjaga volume komputasi tiap blok kurang lebih seimbang, sehingga jaringan tidak timpang antar kedalaman.

Aspek desainPanduan umumAlasan
Kernel convolution3x3 dengan padding 1jangkauan dibangun dari kedalaman, bukan kernel besar
Poolingmax 2x2 stride 2ringkas tanpa parameter, geometri bersih
Channelnaik dua kali lipat tiap blokmuat kombinasi pola yang makin kaya
Spatialturun setengah tiap blokposisi makin kasar, fokus pada keberadaan pola
HeadLinear 1-2 layer kecilfitur sudah siap dipetakan ke kelas
LossCrossEntropyLoss pada logitssoftmax internal, angka stabil

CNN Mini Lengkap pada Dataset Digits #

Berkas cnn_mini.py merangkai seluruh pola di atas menjadi satu program utuh: memuat 1797 citra digits 8x8, menstandarkan piksel dari statistik data latih, melatih 12 epoch, dan mencetak target akurasi di atas 0.95. Struktur model persis mengikuti diagram di awal chapter:

python
"""CNN mini lengkap pada dataset digits sklearn (8x8, 10 kelas).

Pola umum arsitektur CNN: blok konvolusi (Conv -> ReLU -> MaxPool) berulang
dengan channel naik dan spatial turun, lalu flatten dan classifier head
fully connected. Input distandarkan dari statistik data latih, dilatih
12 epoch dengan Adam; target akurasi uji > 0.95.
"""

import torch
import torch.nn as nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader, TensorDataset

torch.manual_seed(42)

# ---------- 1. Data: 1797 gambar 8x8 ----------
digits = load_digits()
X = digits.data.astype("float32") / 16.0        # piksel 0..16 -> 0..1
y = digits.target.astype("int64")
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

# standarkan dari statistik data latih saja
mu, sigma = X_latih.mean(), X_latih.std()
X_latih = (X_latih - mu) / sigma
X_uji = (X_uji - mu) / sigma


def ke_gambar(matriks):
    """Ubah (N, 64) menjadi (N, 1, 8, 8): tambah dimensi channel."""
    return torch.tensor(matriks).reshape(-1, 1, 8, 8)


dataset = TensorDataset(ke_gambar(X_latih), torch.tensor(y_latih))
loader = DataLoader(
    dataset, batch_size=32, shuffle=True,
    generator=torch.Generator().manual_seed(42),
)
X_uji_t, y_uji_t = ke_gambar(X_uji), torch.tensor(y_uji)

# ---------- 2. Model: 2 blok konvolusi + classifier head ----------
model = nn.Sequential(
    # blok konvolusi 1: channel 1 -> 16, spatial 8x8 -> 4x4
    nn.Conv2d(1, 16, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    # blok konvolusi 2: channel 16 -> 32, spatial 4x4 -> 2x2
    nn.Conv2d(16, 32, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    # classifier head
    nn.Flatten(),
    nn.Linear(32 * 2 * 2, 64),
    nn.ReLU(),
    nn.Linear(64, 10),
)

print("jejak bentuk forward untuk satu gambar 8x8:")
with torch.no_grad():
    jejak = torch.randn(1, 1, 8, 8)
    print(f"  {'input':24s} {tuple(jejak.shape)}")
    for modul in model:
        jejak = modul(jejak)
        print(f"  {type(modul).__name__:24s} {tuple(jejak.shape)}")

blok = list(model)[:6]        # 6 modul pertama = bagian konvolusi
head = list(model)[6:]        # sisanya = classifier head
param_blok = sum(p.numel() for m in blok for p in m.parameters())
param_head = sum(p.numel() for m in head for p in m.parameters())
print(f"\nparameter blok konvolusi : {param_blok}")
print(f"parameter classifier head: {param_head}")
print(f"parameter total          : {param_blok + param_head}")

# ---------- 3. Training loop ----------
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

JUMLAH_EPOCH = 12
for epoch in range(1, JUMLAH_EPOCH + 1):
    model.train()
    total_loss = 0.0
    for X_batch, y_batch in loader:
        optimizer.zero_grad()
        loss = criterion(model(X_batch), y_batch)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    model.eval()
    with torch.no_grad():
        benar = (model(X_uji_t).argmax(dim=1) == y_uji_t).sum().item()
        akurasi = benar / len(y_uji_t)
    print(f"epoch {epoch:2d} | loss latih {total_loss / len(loader):.4f} "
          f"| akurasi uji {akurasi:.3f}")

print("\n--- ringkasan ---")
print(f"input         : gambar 1x8x8 terstandarkan")
print(f"output        : logits 10 kelas")
print(f"akurasi akhir : {akurasi:.3f}")
print(f"target > 0.95 : {'tercapai' if akurasi > 0.95 else 'belum tercapai'}")

Output yang Dihasilkan:

text
jejak bentuk forward untuk satu gambar 8x8:
  input                    (1, 1, 8, 8)
  Conv2d                   (1, 16, 8, 8)
  ReLU                     (1, 16, 8, 8)
  MaxPool2d                (1, 16, 4, 4)
  Conv2d                   (1, 32, 4, 4)
  ReLU                     (1, 32, 4, 4)
  MaxPool2d                (1, 32, 2, 2)
  Flatten                  (1, 128)
  Linear                   (1, 64)
  ReLU                     (1, 64)
  Linear                   (1, 10)

parameter blok konvolusi : 4800
parameter classifier head: 8906
parameter total          : 13706
epoch  1 | loss latih 2.1666 | akurasi uji 0.653
epoch  2 | loss latih 1.3102 | akurasi uji 0.811
epoch  3 | loss latih 0.5291 | akurasi uji 0.889
epoch  4 | loss latih 0.3343 | akurasi uji 0.896
epoch  5 | loss latih 0.2281 | akurasi uji 0.929
epoch  6 | loss latih 0.1701 | akurasi uji 0.936
epoch  7 | loss latih 0.1527 | akurasi uji 0.944
epoch  8 | loss latih 0.1266 | akurasi uji 0.958
epoch  9 | loss latih 0.0819 | akurasi uji 0.969
epoch 10 | loss latih 0.0818 | akurasi uji 0.958
epoch 11 | loss latih 0.0705 | akurasi uji 0.953
epoch 12 | loss latih 0.0516 | akurasi uji 0.973

--- ringkasan ---
input         : gambar 1x8x8 terstandarkan
output        : logits 10 kelas
akurasi akhir : 0.973
target > 0.95 : tercapai

Mari kita telaah kode di atas baris per baris:

  1. Baris 15 (torch.manual_seed(42)): mengunci inisialisasi bobot dan urutan shuffle DataLoader sehingga keluaran identik pada setiap eksekusi — kebiasaan dari chapter Training Loop .
  2. Baris 18-23: load_digits() memberi 1797 citra 8x8; piksel 0..16 diskalakan ke 0..1; train_test_split memisahkan 25% data uji dengan stratify=y agar proporsi tiap digit terjaga.
  3. Baris 26-28: rata-rata dan simpangan dihitung dari data latih saja, lalu diterapkan ke kedua subset — statistik data uji tidak boleh menyusup ke preprocessing.
  4. Baris 31-41: ke_gambar menambah dimensi channel sehingga bentuk menjadi (N, 1, 8, 8) — format NCHW dari chapter Representasi Gambar ; DataLoader menyediakan batch 32 dengan shuffle ber-seed.
  5. Baris 44-58: model dirakit dalam nn.Sequential. Blok konvolusi pertama Conv2d(1, 16) + ReLU + MaxPool2d(2) mengubah (1, 8, 8) menjadi (16, 4, 4); blok kedua melanjutkan ke (32, 2, 2); head Flatten → Linear(128, 64) → ReLU → Linear(64, 10) menghasilkan logits.
  6. Baris 60-66: jejak bentuk dicetak dengan melewatkan satu tensor acak melewati tiap modul — resolusi 8x8 → 4x4 → 2x2 dan channel 1 → 16 → 32 persis mengikuti heuristik tabel resep.
  7. Baris 68-74: parameter dihitung terpisah untuk enam modul pertama (blok konvolusi, 4800 parameter) dan sisanya (head, 8906 parameter) — pembacaan hasilnya kita bahas di bawah.
  8. Baris 80-96: training loop memakai cetakan lengkap: model.train(), empat langkah zero_grad → forward → loss → backward → step, lalu model.eval() + torch.no_grad() untuk akurasi uji tiap epoch.
  9. Baris 98-102: ringkasan akhir menegaskan target tercapai — akurasi 0.973 melebihi ambang 0.95 yang kita tetapkan di awal.

Membaca Jejak Bentuk dan Hasil Training #

Dua hal patut dicermati dari keluaran. Pertama, akurasi uji melompat dari 0.653 pada epoch pertama menjadi 0.889 pada epoch ketiga, lalu stabil di sekitar 0.95-0.97 — pola konvergensi cepat khas CNN pada dataset kecil yang telah distandarkan. Kedua, pembagian parameter mengejutkan bagi pemula: blok konvolusi hanya menyimpan 4800 parameter sedangkan classifier head menyimpan 8906 parameter — hampir dua kali lipat. Pada gambar sekecil 8x8, Flatten menghasilkan vektor pendek sehingga head ringan; pada gambar nyata 224x224, hubungan terbalik: blok konvolusi mendominasi parameter karena channel bertambah terus, dan arsitektur modern justru mengecilkan head lewat global average pooling dari chapter Pooling Layer .

Katalog lengkap layer yang dipakai untuk merakit arsitektur tersedia di docs.pytorch.org/docs/stable/nn.html . Resep sudah di tangan; tinggal melihat bagaimana resep tersebut pertama kali dibuktikan di dunia nyata — pada mesin pembaca cek bank — lalu bagaimana satu lompatan tahun 2012 mengubah seluruh bidang computer vision.