Chapter sebelumnya meninggalkan dua kursi kosong di siklus pelatihan: fungsi loss yang menghakimi prediksi, dan optimizer yang memindahkan parameter berdasarkan hukuman itu. Keduanya sudah kita bangun manual dengan NumPy di section Dasar Neural Network
; torch.nn dan torch.optim mengambil alihnya. Tetapi pemilihan loss menyimpan dua jebakan numerik: BCELoss yang kehilangan gradien pada prediksi ekstrem, dan CrossEntropyLoss yang diumpani softmax padahal menuntut raw logits. Chapter ini membedah keduanya dengan bukti angka, lalu menutup dengan training mini lengkap yang menyatukan model, loss, dan optimizer.
Loss juga adalah nn.Module #
Fungsi loss di PyTorch adalah nn.Module yang bisa dipanggil: nn.MSELoss()(prediksi, target) mengembalikan tensor skalar siap-backward(). Pemilihan loss mengikuti tugas dan aktivasi output — tabel berikut peta rujukannya:
| Loss | Tugas | Aktivasi output | Input yang diberikan |
|---|---|---|---|
nn.MSELoss | regresi | tanpa aktivasi | prediksi kontinu |
nn.BCELoss | klasifikasi biner | sigmoid di model | probabilitas 0..1 |
nn.BCEWithLogitsLoss | klasifikasi biner | tanpa — sigmoid di dalam loss | raw logit |
nn.CrossEntropyLoss | klasifikasi multikelas | tanpa — softmax di dalam loss | raw logits per kelas |
nn.MSELoss berperilaku persis seperti MSE yang kita hitung manual di chapter Loss Function
. Perhatikan pola dua baris terakhir tabel: loss modern memindahkan aktivasi terakhir ke dalam fungsi loss — bukan gaya penulisan, melainkan strategi stabilitas numerik.
Stabilitas: BCELoss versus BCEWithLogitsLoss #
nn.BCELoss menerima probabilitas hasil sigmoid. Masalahnya, sigmoid(30.0) dibulatkan float32 menjadi tepat 1.0, dan −log(1 − 1.0) adalah −log(0). PyTorch membatasi (clamping) nilainya agar tidak menghasilkan inf, tetapi harganya fatal: gradien menjadi nol tepat ketika model paling salah. nn.BCEWithLogitsLoss menerima raw logit dan menghitung sigmoid + BCE dalam satu ekspresi yang distabilkan lewat trik log-sum-exp. stabilitas_loss.py membuktikan keduanya:
"""Stabilitas loss: BCELoss vs BCEWithLogitsLoss dan jebakan softmax ganda."""
import torch
import torch.nn as nn
torch.manual_seed(42)
# ---- Kasus 1: logit ekstrem, model sangat yakin tetapi keliruan ----
logit = torch.tensor([30.0], requires_grad=True)
target = torch.tensor([0.0]) # kelas sebenarnya 0
prob = torch.sigmoid(logit) # BCELoss minta probabilitas, bukan logit
print("sigmoid(30.0) :", prob.item())
print("BCELoss(prob, target) :", nn.BCELoss()(prob, target).item())
print("BCEWithLogitsLoss(logit, ...) :", nn.BCEWithLogitsLoss()(logit, target).item())
nn.BCELoss()(prob, target).backward()
grad_bce = logit.grad.item()
logit2 = torch.tensor([30.0], requires_grad=True)
nn.BCEWithLogitsLoss()(logit2, target).backward()
grad_stabil = logit2.grad.item()
print(f"gradien thd logit -- BCELoss: {grad_bce:.4f} | BCEWithLogits: {grad_stabil:.4f}")
print()
# ---- Kasus 2: CrossEntropyLoss menuntut raw logits ----
logit_kelas = torch.tensor([[2.0, 1.0, 0.1]])
target_kelas = torch.tensor([0]) # kelas sebenarnya indeks 0
ce = nn.CrossEntropyLoss()
print("CrossEntropyLoss(logits) :", ce(logit_kelas, target_kelas).item())
prob_kelas = torch.softmax(logit_kelas, dim=1) # softmax dulu = kesalahan
print("CrossEntropyLoss(softmax) :", ce(prob_kelas, target_kelas).item())
print("prediksi sama, loss membengkak karena softmax dihitung dua kali")Output yang Dihasilkan:
sigmoid(30.0) : 1.0
BCELoss(prob, target) : 100.0
BCEWithLogitsLoss(logit, ...) : 30.0
gradien thd logit -- BCELoss: 0.0000 | BCEWithLogits: 1.0000
CrossEntropyLoss(logits) : 0.4170299470424652
CrossEntropyLoss(softmax) : 0.8021209239959717
prediksi sama, loss membengkak karena softmax dihitung dua kaliMari kita telaah kode di atas baris per baris:
- Baris 9-12 (skenario): logit
30.0dengan target kelas0— model sangat yakin pada jawaban yang salah.torch.sigmoid(logit)di baris 12 adalah langkah penggunaBCELoss, dan float32 membulatkannya menjadi tepat1.0. - Baris 14-15 (nilai loss):
BCELossmenjawab100.0— angka buatan pembatasan log.BCEWithLogitsLossmenjawab30.0, tepatsoftplus(30) = log(1 + e³⁰)— nilai matematis yang benar. - Baris 17-22 (gradien): bagian terpenting. Gradien
BCELossterhadap logit0.0000— model paling salah justru berhenti belajar;BCEWithLogitsLossmemberi1.0000(sigmoid(30) − 0), sinyal koreksi penuh. - Baris 26-30 (kasus kedua):
CrossEntropyLossdiumpani raw logits[2.0, 1.0, 0.1]dan target indeks0; hasil0.4170cocok dengan hitungan manual−log(e²/(e²+e¹+e⁰·¹)). - Baris 32-33 (softmax ganda): setelah
torch.softmaxditerapkan dulu, loss melonjak ke0.8021padahal prediksi argmax tidak berubah —CrossEntropyLossmenerapkan log-softmax sendiri di dalam, sehingga softmax kedua meratakan kembali distribusi yang sudah rata.
Optimizer: SGD dan Adam #
Optimizer membawa gradien dari .grad menjadi pembaruan parameter. torch.optim.SGD menjalankan aturan yang kita tulis manual di chapter Gradient Descent
: w ← w − lr · grad. torch.optim.Adam menghitung ukuran langkah berbeda untuk tiap parameter — gradien konsisten mendapat langkah besar, gradien goyah mendapat langkah kecil — sehingga lr lazimnya jauh lebih kecil (0.001–0.01). Keduanya menerima kontrak yang sama: model.parameters(), hyperparameter lr, lalu siklus zero_grad() dan step(). Adam umumnya konvergen lebih cepat; SGD unggul kesederhanaan dan hemat memori.
Training Mini Lengkap #
Berkas latih_mlp.py merangkai seluruhnya: data sintetis dua gumpalan, MLP dari chapter sebelumnya, CrossEntropyLoss, lalu SGD dan Adam berlomba dari inisialisasi bobot yang sama:
"""Training mini lengkap: pasangan model-loss-optimizer pada data sintetis.
Dua gumpalan titik 2D dilatih dengan MLP kecil + CrossEntropyLoss.
SGD dan Adam mengambil inisialisasi bobot yang sama supaya perbandingannya adil.
"""
import torch
import torch.nn as nn
torch.manual_seed(42)
# Data sintetis: dua gumpalan yang saling menempel sebagian
n = 50
X0 = torch.randn(n, 2) - 1.0 # kelas 0 berkumpul di sekitar (-1, -1)
X1 = torch.randn(n, 2) + 1.0 # kelas 1 berkumpul di sekitar (+1, +1)
X = torch.cat([X0, X1])
y = torch.cat([torch.zeros(n, dtype=torch.long),
torch.ones(n, dtype=torch.long)])
print("Data:", tuple(X.shape), "| kelas:", y.tolist().count(0), "dan", y.tolist().count(1))
print()
def buat_model():
"""Model dengan inisialisasi bobot identik untuk tiap optimizer."""
torch.manual_seed(7)
return nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 2), # 2 logits: satu per kelas
)
def latih(nama, kelas_optimizer, lr, jumlah_epoch=15):
model = buat_model()
loss_fn = nn.CrossEntropyLoss()
optimizer = kelas_optimizer(model.parameters(), lr=lr)
print(f"optimizer {nama} (lr={lr}):")
for epoch in range(1, jumlah_epoch + 1):
optimizer.zero_grad()
logits = model(X)
loss = loss_fn(logits, y)
loss.backward()
optimizer.step()
if epoch in (1, 5, 10, 15):
print(f" epoch {epoch:2d}: loss = {loss.item():.4f}")
with torch.no_grad():
benar = (model(X).argmax(dim=1) == y).sum().item()
print(f" akurasi akhir: {benar / len(y):.3f}")
return model
model_sgd = latih("SGD ", torch.optim.SGD, lr=0.1)
print()
model_adam = latih("Adam", torch.optim.Adam, lr=0.01)Output yang Dihasilkan:
Data: (100, 2) | kelas: 50 dan 50
optimizer SGD (lr=0.1):
epoch 1: loss = 0.6297
epoch 5: loss = 0.4705
epoch 10: loss = 0.3729
epoch 15: loss = 0.3194
akurasi akhir: 0.870
optimizer Adam (lr=0.01):
epoch 1: loss = 0.6297
epoch 5: loss = 0.4713
epoch 10: loss = 0.3424
epoch 15: loss = 0.2689
akurasi akhir: 0.880Mari kita telaah kode di atas baris per baris:
- Baris 12-18 (data): dua gumpalan
randndigeser ke sekitar(-1, -1)dan(+1, +1)— menempel sebagian, sehingga loss tidak akan pernah nol dan akurasi berhenti di kisaran 0,9. Label disimpan sebagaitorch.longkarenaCrossEntropyLossmenuntut indeks kelas, bukan one-hot. - Baris 23-30 (
buat_model):torch.manual_seed(7)dipanggil setiap fungsi berjalan, sehingga SGD dan Adam menerima bobot awal identik — epoch 1 keduanya loss0.6297, bukti perbandingan adil. Layer output(16, 2)menghasilkan dua raw logits tanpa aktivasi. - Baris 33-36 (pasangan model-loss-optimizer): tiga baris ini seluruh konfigurasi training PyTorch — model dari chapter nn.Module
, loss dari tabel di atas, optimizer dengan
model.parameters()danlr. - Baris 38-43 (siklus lima langkah): identik dengan chapter Automatic Differentiation
, kini dengan model utuh —
zero_grad, forward, loss,backward,step— seluruh batch sekali per epoch. - Baris 46-48 (evaluasi):
argmax(dim=1)memilih kelas ber-logsit terbesar; blokno_grad()menandai ini inferensi. - Baris 52-54 (perlombaan): SGD berhenti di
0.3194(akurasi0.870); Adam mencapai0.2689(0.880) dengan epoch sama — Adam sedikit lebih cepat pada dataset mungil ini.
Dokumentasi keluarga loss tersedia di docs.pytorch.org/docs/stable/nn.html , optimizer beserta hyperparameternya di docs.pytorch.org/docs/stable/optim.html , dan detail stabilitas BCEWithLogitsLoss di docs.pytorch.org/docs/stable/generated/torch.nn.BCEWithLogitsLoss.html . Model, loss, dan optimizer kini lengkap — yang tersisa sebelum training loop sungguhan adalah bahan bakunya: menyuapi data ke model dalam batch yang rapi, tugas chapter Dataset dan DataLoader .