Dataset pada bagian sebelumnya menyimpan gambar dua dimensi; meratakannya menjadi vektor 256 piksel seperti pada proyek digit akan membuang informasi tetangga antar piksel. Convolutional neural network bekerja sebaliknya: filter kecil menyapu gambar dan mendeteksi pola lokal di mana pun pola itu berada — tepat yang dibutuhkan untuk mengenali telinga kucing yang bergeser dua piksel. Arsitektur kita sengaja dibagi menjadi dua sub-modul, ciri dan kepala, karena pembagian inilah yang memungkinkan transfer learning pada bagian berikutnya.

Arsitektur dan Alur Bentuk Tensor #

Definisi lengkap model hidup di src/model.py:

python src/model.py — class CNNHewan
import torch
import torch.nn as nn


class CNNHewan(nn.Module):
    """Arsitektur Conv-Pool-Conv-Pool-FC untuk 3 kelas hewan."""

    def __init__(self, jumlah_kelas=3):
        super().__init__()
        self.ciri = nn.Sequential(
            nn.Conv2d(1, 8, kernel_size=3, padding=1),   # (N, 8, 16, 16)
            nn.ReLU(),
            nn.MaxPool2d(2),                             # (N, 8, 8, 8)
            nn.Conv2d(8, 16, kernel_size=3, padding=1),  # (N, 16, 8, 8)
            nn.ReLU(),
            nn.MaxPool2d(2),                             # (N, 16, 4, 4)
        )
        self.kepala = nn.Sequential(
            nn.Flatten(),                                # (N, 256)
            nn.Linear(16 * 4 * 4, 32),
            nn.ReLU(),
            nn.Linear(32, jumlah_kelas),
        )

    def forward(self, x):
        return self.kepala(self.ciri(x))

Mari kita telaah alur bentuk tensor pada kode di atas:

  1. Masukan (N, 1, 16, 16): empat dimensi mengikuti konvensi PyTorch — batch, channel, tinggi, lebar; satu channel karena gambar kita grayscale.
  2. Conv2d(1, 8, kernel_size=3, padding=1): delapan filter 3x3 masing-masing menghasilkan satu peta ciri berukuran sama (16x16) berkat padding=1; kedalaman naik dari 1 ke 8 — mekanisme ini dibedah di chapter Convolution Layer .
  3. MaxPool2d(2): setiap blok 2x2 diringkas menjadi satu nilai terbesar sehingga resolusi turun menjadi 8x8; dua kali penurunan resolusi menghasilkan 4x4 — perannya dijelaskan di chapter Pooling Layer .
  4. self.kepala dengan Flatten: peta ciri akhir (N, 16, 4, 4) diratakan menjadi vektor 256 nilai, lalu dua Linear memetakan vektor tersebut ke 3 logits kelas.
  5. self.ciri terpisah dari self.kepala: pemisahan ini bukan gaya penulisan — pada Bagian 3 kita akan membekukan seluruh self.ciri dan mengganti self.kepala tanpa menyentuh layer convolution.

Total parameter model: 80 + 1168 + 8224 + 99 = 9.571 — dihitung dari bobot dan bias tiap layer, bukan angka karangan.

Menguji Model Sebelum Dilatih #

Berkas tests/test_model.py memeriksa bentuk keluaran, peta ciri, jumlah parameter, dan — yang paling khas proyek ini — perilaku pembekuan:

python tests/test_model.py — pemeriksaan pembekuan ekstraktor ciri
model.train()
for parameter in model.ciri.parameters():
    parameter.requires_grad_(False)
loss = nn.CrossEntropyLoss()(model(x), torch.tensor([0, 1, 2, 0]))
loss.backward()
for parameter in model.ciri.parameters():
    assert parameter.grad is None, "ciri dibekukan tetapi menerima gradien"

Baris requires_grad_(False) memutus jalur gradien menuju layer convolution, dan pengujian memastikan parameter.grad benar-benar None setelah backward() — bukan sekadar nol. Inilah mekanisme yang dipakai transfer learning: optimizer yang hanya menerima parameter kepala tidak akan pernah menggeser bobot convolution.

Output yang Dihasilkan (python3 tests/test_model.py):

text
parameter       : 9571 (ciri 1248 + kepala 8323)
bentuk keluaran : (4, 3)
SEMUA TEST LULUS

Model teruji dan strukturnya siap dibekukan sebagian. Bagian berikutnya memanfaatkan desain itu pada skenario data sedikit: berapa banyak yang bisa dipelajari model transfer bila hanya 24 gambar tersedia?