Preprocessing pada Bagian 2 sudah menghasilkan tensor siap pakai; bagian ini mengisi sisi modelnya. Kita merancang CNNDaun — CNN kecil dua blok convolution-pooling — kemudian memutar training loop baku dari chapter Training Loop di atas data latih 225 gambar. Seluruh pelatihan berlangsung sekitar delapan detik pada CPU dengan 40 epoch, dan hasilnya (akurasi uji 0.9067) menjadi bahan evaluasi pada dua bagian berikutnya.

Arsitektur CNNDaun #

Bercak penyakit adalah pola lokal, bukan pola seluruh gambar — jenis masalah yang paling cocok untuk convolution seperti dibahas chapter Arsitektur CNN :

python src/model.py — class CNNDaun
class CNNDaun(nn.Module):
    """Dua blok conv-pool + classifier kecil: 9538 parameter."""

    def __init__(self, jumlah_kelas=2, dropout=0.1):
        super().__init__()
        self.ciri = nn.Sequential(
            nn.Conv2d(1, 8, kernel_size=3, padding=1),   # 16x16 -> 16x16
            nn.ReLU(),
            nn.MaxPool2d(2),                              # 16x16 -> 8x8
            nn.Conv2d(8, 16, kernel_size=3, padding=1),  # 8x8 -> 8x8
            nn.ReLU(),
            nn.MaxPool2d(2),                              # 8x8 -> 4x4
            nn.Flatten(),                                 # 16 x 4 x 4 = 256
        )
        self.klasifikasi = nn.Sequential(
            nn.Linear(16 * 4 * 4, 32),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(32, jumlah_kelas),
        )

    def forward(self, x):
        return self.klasifikasi(self.ciri(x))            # logits (N, 2)

Mari kita telaah arsitektur di atas baris per baris:

  1. nn.Conv2d(1, 8, kernel_size=3, padding=1): blok pertama mengubah 1 kanal menjadi 8 feature map; padding=1 menjaga resolusi 16x16 agar setiap piksel tetap mendapat konteks tepi.
  2. nn.MaxPool2d(2): setiap pooling memangkas resolusi setengah (16→8→4) sambil mempertahankan informasi dominan — mekanisme yang dibedah pada chapter Pooling Layer . Setelah dua blok, posisi bercak cukup kasar untuk tahan geser kecil.
  3. nn.Flatten(): tensor (16, 4, 4) diratakan menjadi vektor 256 yang siap masuk layer linear.
  4. nn.Dropout(0.1): 10 persen unit dimatikan acak saat training sebagai penahan overfitting pada dataset 225 gambar yang kecil.
  5. Keluaran logits (N, 2): dua skor mentah tanpa softmax — nn.CrossEntropyLoss milik PyTorch sudah menggabungkan log-softmax dan negative log-likelihood di dalamnya.

Total parameter hanya 9538 — model sekecil ini memang disengaja: dataset 300 gambar tidak sanggup menopang model besar tanpa overfitting.

Menyusun Data dan Loader #

Seluruh penyiapan data hidup di awal fungsi latih pada src/latih.py — fungsi ini memanggil loader Bagian 2 lalu membungkus data latih ke DataLoader:

python src/latih.py — awal fungsi latih: data dan loader
def latih(jumlah_epoch, path_keluaran, path_data=BERKAS_DATA):
    """Jalankan pipeline lengkap dan simpan checkpoint."""
    torch.manual_seed(SEED)
    np.random.seed(SEED)

    X, y, _ = muat_daun(path_data)
    idx_latih, idx_uji = indeks_latih_uji(y)
    X_latih, y_latih = X[idx_latih], y[idx_latih]
    X_uji, y_uji = X[idx_uji], y[idx_uji]

    dataset_latih = TensorDataset(
        torch.tensor(X_latih, dtype=torch.float32),
        torch.tensor(y_latih, dtype=torch.long),
    )
    loader = DataLoader(
        dataset_latih,
        batch_size=BATCH,
        shuffle=True,
        num_workers=0,
        generator=torch.Generator().manual_seed(SEED),
    )

Dua detail layak digarisbawahi. Pertama, TensorDataset memasangkan gambar dan label sehingga DataLoader mengeluarkannya bersama sebagai (X_batch, y_batch) — pola dari chapter Dataset dan DataLoader . Kedua, generator=torch.Generator().manual_seed(SEED) membuat urutan kocokan batch tertentu, sehingga 40 epoch yang tampak di keluaran berikut ini dapat direproduksi angka demi angka.

Training Loop #

Inti fungsi latih menyalin cetakan baku zero_grad → forward → backward → step():

python src/latih.py — inti training loop (indentasi asli ada di dalam fungsi latih)
for epoch in range(1, jumlah_epoch + 1):
    model.train()
    total_loss = 0.0
    for X_batch, y_batch in loader:
        optimizer.zero_grad()
        logits = model(X_batch)
        loss = criterion(logits, y_batch)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    loss_latih = total_loss / len(loader)

    if epoch == 1 or epoch % 5 == 0:
        akurasi = evaluasi_akurasi(model, X_uji_t, y_uji_t)
        print(f"epoch {epoch:2d} | loss latih {loss_latih:.4f} "
              f"| akurasi uji {akurasi:.3f}")

model.train() mengaktifkan Dropout di awal setiap epoch; fungsi evaluasi_akurasi memanggil model.eval() di dalamnya sehingga pengukuran akurasi uji bebas efek Dropout. Pencatatan hanya pada epoch ke-1 dan kelipatan lima menjaga keluaran tetap ringkas.

Menjalankan Pelatihan #

Dari folder proyek, pelatihan dijalankan tanpa argumen:

bash
python3 src/latih.py

Output yang Dihasilkan:

text
data       : 225 latih, 75 uji (stratified)
model      : CNN 2 blok (9538 parameter)
pelatihan  : Adam lr=0.001, batch 16, 40 epoch

epoch  1 | loss latih 0.6900 | akurasi uji 0.493
epoch  5 | loss latih 0.6832 | akurasi uji 0.800
epoch 10 | loss latih 0.6100 | akurasi uji 0.680
epoch 15 | loss latih 0.4990 | akurasi uji 0.747
epoch 20 | loss latih 0.3005 | akurasi uji 0.867
epoch 25 | loss latih 0.2447 | akurasi uji 0.907
epoch 30 | loss latih 0.2027 | akurasi uji 0.880
epoch 35 | loss latih 0.2043 | akurasi uji 0.920
epoch 40 | loss latih 0.1938 | akurasi uji 0.907

akurasi uji akhir: 0.9067 (68/75)

confusion matrix (baris = label, kolom = prediksi):
                  pred sehat  pred berbintak
label sehat               35               3
label berbintak            4              33

checkpoint tersimpan: /tmp/model_daun.pt

Loss turun dari 0.69 menjadi 0.19 dan akurasi uji naik ke kisaran 0.90-0.92. Perhatikan bahwa kemajuan tidak monoton — akurasi sempat turun ke 0.680 pada epoch 10 lalu pulih; fluktuasi seperti ini wajar pada dataset kecil karena 75 gambar uji berarti satu kesalahan menggeser akurasi sekitar 1.3 poin persen. Checkpoint berisi state_dict plus konstanta skala disimpan ke /tmp/model_daun.pt untuk dipakai analisis pada Bagian 4 dan Bagian 5.

Angka akhir 0.9067 terdengar memuaskan, tetapi satu angka saja belum bercerita apa pun. Bagian berikutnya membaca confusion matrix dan keyakinan prediksi per sampel untuk melihat di mana tujuh kesalahan itu bersembunyi.