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 :
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:
nn.Conv2d(1, 8, kernel_size=3, padding=1): blok pertama mengubah 1 kanal menjadi 8 feature map;padding=1menjaga resolusi 16x16 agar setiap piksel tetap mendapat konteks tepi.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.nn.Flatten(): tensor (16, 4, 4) diratakan menjadi vektor 256 yang siap masuk layer linear.nn.Dropout(0.1): 10 persen unit dimatikan acak saat training sebagai penahan overfitting pada dataset 225 gambar yang kecil.- Keluaran
logits (N, 2): dua skor mentah tanpa softmax —nn.CrossEntropyLossmilik 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:
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():
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:
python3 src/latih.pyOutput yang Dihasilkan:
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.ptLoss 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.