Transfer learning dimulai dari satu keputusan: model pretrained mana yang dipakai sebagai titik awal. Model nyata seperti ResNet18 menawarkan bobot hasil jutaan gambar, tetapi ukurannya puluhan megabyte dan dirancang untuk masukan 224x224 — tidak praktis untuk diuji penuh di CPU dalam proyek ini. Karena itu kita membuat pengganti yang setara secara struktural: backbone CNN kecil yang dilatih pada tugas A (mengenali digit 0-4), lalu bobotnya disimpan layaknya berkas model pretrained yang diunduh orang lain. Bagian ini membangun backbone tersebut dan membahas kriteria di balik pilihannya.

Kriteria Pemilihan #

Tiga pertanyaan yang sama berlaku untuk model pretrained nyata maupun buatan sendiri:

KriteriaResNet18 nyataBackbone proyek ini
Domain masukanfoto RGB 224x224citra grayscale 16x16
Fitur bawahannyatepi, tekstur, warnagoresan garis dan lengkungan
Ukuran pelatihan ulanghead saja: ~2 ribu parameterhead saja: 2565 parameter

Kuncinya: fitur level bawah (detektor garis, tepi, bercak) berguna lintas tugas selama domain masukannya mirip — digit 0-4 dan digit 5-9 berbagi jenis goresan yang sama, persis seperti foto natural berbagi tekstur dengan foto daun.

Arsitektur Backbone #

Backbone pada src/pretrained.py adalah dua blok convolution-pooling yang menghasilkan vektor 512 fitur:

python src/pretrained.py — class CNNBackbone dan ModelGabung
class CNNBackbone(nn.Module):
    """Backbone konvolusi: citra 16x16 -> vektor 512 fitur."""

    def __init__(self):
        super().__init__()
        self.ciri = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, padding=1),   # 16x16 -> 16x16
            nn.ReLU(),
            nn.MaxPool2d(2),                              # 16x16 -> 8x8
            nn.Conv2d(16, 32, kernel_size=3, padding=1),  # 8x8 -> 8x8
            nn.ReLU(),
            nn.MaxPool2d(2),                              # 8x8 -> 4x4
            nn.Flatten(),                                 # 32 x 4 x 4 = 512
        )

    def forward(self, x):
        return self.ciri(x)                               # (N, 512)


class ModelGabung(nn.Module):
    """Gabungan backbone + head agar bisa dipakai latih_epoch/akurasi."""

    def __init__(self, backbone, kepala):
        super().__init__()
        self.backbone = backbone
        self.kepala = kepala

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

Mari kita telaah dua class di atas baris per baris:

  1. nn.Conv2d(1, 16, ...) lalu nn.Conv2d(16, 32, ...): jumlah kanal bertingkat — layer pertama belajar pola sederhana, layer kedua menyusunnya menjadi pola lebih besar, pola hierarki yang dibahas chapter Arsitektur CNN .
  2. nn.Flatten() menutup backbone: keluaran berupa vektor fitur (N, 512), bukan skor kelas — backbone memang tidak boleh tahu tugas akhirnya, karena head akan diganti pada Bagian 3.
  3. ModelGabung: pembungkus backbone + head menjadi satu nn.Module sehingga fungsi pelatihan dan evaluasi milik pretrained.py dapat dipakai ulang oleh transfer.py dan dari_nol.py tanpa duplikasi.

Menyiapkan Dua Tugas dari Satu Dataset #

Fungsi siapkan_data membelah dataset digits menjadi tugas A dan tugas B:

python src/pretrained.py — fungsi siapkan_data
def siapkan_data():
    """Muat digits; kembalikan tensor tugas A dan B + statistik A.

    Normalisasi memakai mean/deviasi data tugas A -- statistik dataset
    asal backbone ikut dipakai untuk tugas B. Citra 8x8 diperbesar ke
    16x16 (bilinear) agar dua blok pooling punya ruang resolusi.
    """
    digits = load_digits()
    X = digits.data.astype("float32").reshape(-1, 1, 8, 8) / 16.0
    y = digits.target.astype("int64")

    XA, yA = X[y <= 4], y[y <= 4]        # tugas A: digit 0-4
    XB, yB = X[y >= 5], y[y >= 5] - 5    # tugas B: digit 5-9 direlabel 0-4

    mu, sigma = float(XA.mean()), float(XA.std())
    XA = F.interpolate(torch.tensor((XA - mu) / sigma), size=(16, 16),
                       mode="bilinear", align_corners=False)
    XB = F.interpolate(torch.tensor((XB - mu) / sigma), size=(16, 16),
                       mode="bilinear", align_corners=False)
    return XA, torch.tensor(yA), XB, torch.tensor(yB), mu, sigma

Tiga keputusan menonjol di sini. Pertama, y[y >= 5] - 5 melabel ulang digit 5-9 menjadi 0-4 sehingga kedua tugas berbagi format label. Kedua, statistik normalisasi mu, sigma dihitung dari tugas A lalu diterapkan juga ke tugas B — meniru praktik memakai statistik dataset asal model pretrained, bukan dataset target. Ketiga, F.interpolate memperbesar citra 8x8 ke 16x16 agar dua blok max-pooling tidak menggerus resolusi sampai habis.

Melatih dan Menyimpan Backbone #

Fungsi latih_backbone melatih backbone bersama head sementara selama 10 epoch, lalu membuang head itu — hanya bobot backbone yang disimpan:

bash
python3 src/pretrained.py

Output yang Dihasilkan:

text
tugas A: 720 latih + 181 uji (digit 0-4) -> melatih backbone 10 epoch
akurasi tugas A: 1.000
backbone tersimpan: /tmp/backbone_digit.pt (4800 parameter)

Backbone mengklasifikasikan digit 0-4 dengan sempurna pada data uji tugas A, lalu bobotnya berpindah ke /tmp/backbone_digit.pt melalui torch.save(backbone.state_dict(), ...) — pola simpan-muat yang dibahas chapter Penyimpanan dan Pemuatan Model . Head tugas A ikut dibuang karena klasifikasinya tidak relevan untuk tugas B.

Backbone 4800 parameter kini tersimpan dan teruji. Bagian berikutnya menyiapkan sisi penerima manfaatnya: dataset tugas B yang sengaja dibuat kecil.