Resep “blok konvolusi berulang + classifier head” dari chapter sebelumnya bukan temuan baru; resep tersebut telah teruji selama lebih dari dua dekade. Dua arsitektur menandai dua tonggak sejarahnya: LeNet-5 tahun 1998 yang membaca digit pada cek bank di Amerika Serikat, dan AlexNet tahun 2012 yang memenangkan kompetisi ImageNet dengan selisih jauh. Keduanya memperkenalkan keputusan desain yang masih kita pakai sampai sekarang. Pada chapter ini kita bedah struktur keduanya, lalu membangun LeNet-5 versi kecil yang dilatih nyata pada dataset digits.

LeNet-5: CNN Pertama yang Produktif #

Yann LeCun beserta rekan-rekannya mempublikasikan LeNet-5 dalam makalah Gradient-Based Learning Applied to Document Recognition (Proceedings of the IEEE, 1998). Sistem ini membaca tulisan tangan pada formulir dan cek bank — komposisi layer-nya sudah memuat seluruh pola dari chapter Arsitektur CNN : convolution, pooling, dan fully connected yang dilatih menyatu lewat backpropagation. Input aslinya adalah citra grayscale 32x32 piksel.

TahapLayerBentuk outputParameter
inputcitra grayscale1 x 32 x 320
C1Conv 6 kernel 5x56 x 28 x 28156
S2Average pooling 2x26 x 14 x 140
C3Conv 16 kernel 5x516 x 10 x 102.416
S4Average pooling 2x216 x 5 x 50
C5Conv 120 kernel 5x5120 x 1 x 148.120
F6Fully connected8410.164
outputFully connected10 kelas850

Struktur di atas menunjukkan dua keputusan zaman itu: aktivasi tanh (ReLU belum populer) dan average pooling sebagai peringkas. Layer C5 menarik perhatian — convolution 5x5 pada peta 5x5 menghasilkan 1x1, sehingga layer tersebut bekerja seperti fully connected terhadap seluruh gambar namun tetap berbagi bobot. Total parameter LeNet-5 hanya sekitar 61.706 — kecil menurut standar hari ini, namun sudah melampaui kebutuhan pembaca digit dengan sangat baik.

Berkas lenet_mini.py membangun struktur asli tersebut di atas dataset digits yang di-upscale dari 8x8 ke 32x32, dengan tanh dan average pooling sesuai paper:

python
"""LeNet-5 versi mini: struktur asli di atas data digits yang di-upscaled.

Struktur mengikuti LeNet-5 (LeCun dkk., 1998): C1 conv 6 kernel 5x5 ->
S2 average pooling -> C3 conv 16 kernel 5x5 -> S4 average pooling ->
C5 conv 120 kernel 5x5 -> F6 fully connected 84 -> output 10.
Gambar digits 8x8 di-upscale ke 32x32 agar seluruh ukuran layer asli
berlaku. Aktivasi tanh dan average pooling mengikuti paper asli.
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

torch.manual_seed(42)


class LeNet5(nn.Module):
    """LeNet-5: conv-pool-conv-pool-conv-fc-fc dengan aktivasi tanh."""

    def __init__(self, jumlah_kelas=10):
        super().__init__()
        self.c1 = nn.Conv2d(1, 6, kernel_size=5)      # 32x32 -> 28x28
        self.s2 = nn.AvgPool2d(kernel_size=2)         # 28x28 -> 14x14
        self.c3 = nn.Conv2d(6, 16, kernel_size=5)     # 14x14 -> 10x10
        self.s4 = nn.AvgPool2d(kernel_size=2)         # 10x10 -> 5x5
        self.c5 = nn.Conv2d(16, 120, kernel_size=5)   # 5x5 -> 1x1
        self.f6 = nn.Linear(120, 84)
        self.keluaran = nn.Linear(84, jumlah_kelas)

    def forward(self, x):
        x = torch.tanh(self.c1(x))
        x = self.s2(x)
        x = torch.tanh(self.c3(x))
        x = self.s4(x)
        x = torch.tanh(self.c5(x))
        x = x.flatten(start_dim=1)                    # (N, 120, 1, 1) -> (N, 120)
        x = torch.tanh(self.f6(x))
        return self.keluaran(x)


# ---------- 1. Data: digits 8x8 di-upscale ke 32x32 ----------
digits = load_digits()
X = digits.data.astype("float32") / 16.0
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
)
mu, sigma = X_latih.mean(), X_latih.std()
X_latih = (X_latih - mu) / sigma
X_uji = (X_uji - mu) / sigma


def ke_gambar_32(matriks):
    """(N, 64) -> tensor (N, 1, 32, 32) lewat upscale bilinear."""
    kecil = torch.tensor(matriks).reshape(-1, 1, 8, 8)
    return F.interpolate(kecil, size=(32, 32), mode="bilinear",
                         align_corners=False)


X_latih_t, y_latih_t = ke_gambar_32(X_latih), torch.tensor(y_latih)
X_uji_t, y_uji_t = ke_gambar_32(X_uji), torch.tensor(y_uji)

# ---------- 2. Struktur: ukuran output dan parameter tiap layer ----------
model = LeNet5()
print(f"{'tahap':6s} {'layer':44s} {'bentuk output':16s} {'parameter':>9s}")
with torch.no_grad():
    jejak = torch.zeros(1, 1, 32, 32)
    print(f"{'input':6s} {'-':44s} {str(tuple(jejak.shape)):16s} {0:9d}")
    for nama, modul in [("C1", model.c1), ("S2", model.s2), ("C3", model.c3),
                        ("S4", model.s4), ("C5", model.c5)]:
        jejak = modul(jejak)                 # tanh tidak mengubah bentuk
        param = sum(p.numel() for p in modul.parameters())
        print(f"{nama:6s} {type(modul).__name__:44s} "
              f"{str(tuple(jejak.shape)):16s} {param:9d}")
    jejak = jejak.flatten(start_dim=1)
    print(f"{'flat':6s} {'Flatten':44s} {str(tuple(jejak.shape)):16s} {0:9d}")
    for nama, modul in [("F6", model.f6), ("out", model.keluaran)]:
        jejak = modul(jejak)
        param = sum(p.numel() for p in modul.parameters())
        print(f"{nama:6s} {type(modul).__name__:44s} "
              f"{str(tuple(jejak.shape)):16s} {param:9d}")
total = sum(p.numel() for p in model.parameters())
print(f"{'total':53s} {total:15d}")

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

JUMLAH_EPOCH = 8
ukuran_batch = 64
indeks = torch.randperm(len(X_latih_t),
                        generator=torch.Generator().manual_seed(42))
for epoch in range(1, JUMLAH_EPOCH + 1):
    model.train()
    total_loss = 0.0
    jumlah_batch = 0
    for awal in range(0, len(indeks), ukuran_batch):
        pilih = indeks[awal:awal + ukuran_batch]
        optimizer.zero_grad()
        loss = criterion(model(X_latih_t[pilih]), y_latih_t[pilih])
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        jumlah_batch += 1

    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} | loss latih {total_loss / jumlah_batch:.4f} "
          f"| akurasi uji {akurasi:.3f}")

print(f"\nakurasi akhir LeNet-5 mini pada digits: {akurasi:.3f}")

Output yang Dihasilkan:

text
tahap  layer                                        bentuk output    parameter
input  -                                            (1, 1, 32, 32)           0
C1     Conv2d                                       (1, 6, 28, 28)         156
S2     AvgPool2d                                    (1, 6, 14, 14)           0
C3     Conv2d                                       (1, 16, 10, 10)       2416
S4     AvgPool2d                                    (1, 16, 5, 5)            0
C5     Conv2d                                       (1, 120, 1, 1)       48120
flat   Flatten                                      (1, 120)                 0
F6     Linear                                       (1, 84)              10164
out    Linear                                       (1, 10)                850
total                                                           61706
epoch 1 | loss latih 1.6342 | akurasi uji 0.807
epoch 2 | loss latih 0.7284 | akurasi uji 0.898
epoch 3 | loss latih 0.3805 | akurasi uji 0.924
epoch 4 | loss latih 0.2350 | akurasi uji 0.944
epoch 5 | loss latih 0.1668 | akurasi uji 0.956
epoch 6 | loss latih 0.1270 | akurasi uji 0.967
epoch 7 | loss latih 0.0979 | akurasi uji 0.973
epoch 8 | loss latih 0.0754 | akurasi uji 0.976

akurasi akhir LeNet-5 mini pada digits: 0.976

Mari kita telaah kode di atas baris per baris:

  1. Baris 19-30 (class LeNet5): tujuh layer dideklarasikan persis mengikuti tabel — c1 sampai c5 adalah Conv2d/AvgPool2d, sedangkan f6 dan keluaran adalah Linear. Nama layer mengikuti penamaan paper asli.
  2. Baris 32-40 (forward): aliran data conv → tanh → pool diulang, lalu flatten(start_dim=1) mengubah (N, 120, 1, 1) menjadi (N, 120) sebelum masuk layer Linear.
  3. Baris 44-52: dataset digits dimuat, piksel diskalakan ke 0..1, dan dibagi latih/uji 75:25 dengan stratifikasi; standarisasi tetap dihitung dari data latih saja.
  4. Baris 55-59 (ke_gambar_32): citra 8x8 diperbesar ke 32x32 lewat F.interpolate mode bilinear — trik yang sama dipakai agar ukuran input Cocok dengan LeNet asli tanpa mengubah isi gambar.
  5. Baris 66-85: tabel struktur dicetak dengan melewatkan tensor nol (1, 1, 32, 32) melewati tiap layer — bentuk output yang tampil persis mengikuti kolom tabel di atas, dari 6 x 28 x 28 sampai 84 lalu 10.
  6. Baris 91-106: pelatihan memakai torch.randperm ber-seed untuk membagi batch 64 — alternatif manual terhadap DataLoader yang memperlihatkan mekanisme shuffle secara eksplisit.
  7. Baris 108-113: mode eval + torch.no_grad() menjaga evaluasi akurasi uji tetap jujur tiap epoch, dan akurasi akhir 0.976 menegaskan struktur tahun 1998 tetap andal pada dataset modern.

AlexNet 2012: Lompatan ImageNet #

Empat belas tahun kemudian, Alex Krizhevsky, Ilya Sutskever, dan Geoffrey Hinton memenangkan kompetisi ILSVRC 2012 (ImageNet) dengan AlexNet, dipublikasikan di konferensi NIPS 2012 dengan judul ImageNet Classification with Deep Convolutional Neural Networks. Dari sekitar 1,2 juta gambar latih dan 1000 kelas, AlexNet mencatat top-5 error 15,3% — jauh di bawah pesaing terdekat yang tersangkut di kisaran 26%. Struktur dasarnya masih resep lama: 5 blok convolution (kernel sampai 11x11 pada blok pertama, input 224x224) diikuti 3 layer fully connected berukuran 4096-4096-1000 dengan total sekitar 60 juta parameter.

Inovasi AlexNetKondisi pada LeNet-5Dampak
Pelatihan GPU (2 kartu GTX 580)CPUjaringan besar tetap terlatih dalam hitungan hari
Aktivasi ReLUtanhkonvergensi beberapa kali lebih cepat pada jaringan dalam
Dropout 0.5 pada layer FCtidak adamenekan overfitting pada 60 juta parameter
Augmentasi data (crop + flip)tidak adadata latih efektif berlipat
Pooling 3x3 stride 2 (menindih)average pooling 2x2ringkasan lebih kaya

Pelajaran Desain yang Masih Relevan #

Empat keputusan AlexNet bertahan sampai hari ini dan layak dihafal sebagai daftar periksa. Pertama, ReLU menjadi aktivasi bawaan hampir semua CNN modern — dibahas tuntas pada chapter Fungsi Aktivasi . Kedua, dropout membuka jalan seluruh keluarga teknik regularisasi di section Regularisasi dan Generalisasi . Ketiga, augmentasi data menunjukkan bahwa menambah variasi latih sama berharganya dengan menambah parameter. Keempat, pelatihan GPU membuktikan bahwa skala — data besar ditambah model besar — adalah bahan bakar utama kemajuan deep learning. AlexNet juga memperlihatkan lewat torchvision yang menyediakan implementasinya di halaman model torchvision , lengkap dengan bobot terlatih ImageNet.

Dua era ini memantapkan resep dasar, tetapi pertanyaan baru muncul segera setelahnya: seberapa dalam dan seberapa lebar sebaiknya CNN dibangun? Tiga arsitektur tahun 2014 menjawab dengan dua arah berbeda — kedalaman seragam ala VGG dan percabangan multi-skala ala Inception — pokok bahasan chapter VGG dan Inception . Katalog layer Conv2d, AvgPool2d, dan Linear yang dipakai kedua arsitektur tersedia di docs.pytorch.org/docs/stable/nn.html .