Idenya
Training butuh dua bagian lagi:
- fungsi loss yang mengubah prediksi dan target menjadi satu angka untuk diminimalkan (kamu sudah bertemu MSE dan log loss di track ML);
- optimizer yang memakai gradient untuk memperbarui setiap parameter (gradient descent, dengan berbagai penyempurnaan).
PyTorch menyediakan keduanya, sehingga langkah training menyusut menjadi beberapa baris standar.
Loss untuk setiap jenis masalah
| Masalah | Output model | Loss | Target |
|---|---|---|---|
| regresi | satu angka per contoh | nn.MSELoss() (atau nn.L1Loss() untuk MAE) | nilai float |
| klasifikasi biner | satu logit per contoh | nn.BCEWithLogitsLoss() | float 0.0 / 1.0 |
| multi-kelas (k kelas) | k logit per contoh | nn.CrossEntropyLoss() | indeks kelas 0..k−1 (int64) |
import torch
from torch import nn
pred = torch.tensor([52.0, 61.0, 44.0])
actual = torch.tensor([50.0, 65.0, 44.0])
print(nn.MSELoss()(pred, actual), nn.L1Loss()(pred, actual))
tensor(6.6667) tensor(2.)
Objek loss dibuat sekali, lalu dipanggil seperti fungsi: loss_fn(predictions, targets).
Logit dan BCEWithLogitsLoss
Untuk masalah biner, model mengeluarkan skor mentah (logit), dan loss-nya sendiri yang menerapkan sigmoid:
logits = torch.tensor([2.0, -1.0, 0.5]) # raw model outputs
targets = torch.tensor([1.0, 0.0, 0.0])
loss_fn = nn.BCEWithLogitsLoss()
print(loss_fn(logits, targets))
probs = torch.sigmoid(logits)
manual = -(targets * torch.log(probs) + (1 - targets) * torch.log(1 - probs)).mean()
print(manual)
tensor(0.4714)
tensor(0.4714)
Log loss yang sama seperti di track ML. Kenapa tidak menerapkan sigmoid di model lalu memakai nn.BCELoss pada probabilitas? Karena untuk prediksi yang sangat yakin, log(sigmoid(z)) kehilangan presisi (probabilitas dibulatkan menjadi tepat 1.0, dan log(0) muncul). BCEWithLogitsLoss menggabungkan kedua langkah itu dengan cara yang stabil secara numerik. Aturannya: model mengembalikan logit; loss menerima logit.
Multi-kelas: CrossEntropyLoss
Untuk k kelas (misalnya, ulasan ini membahas yang mana dari 5 kategori produk), model mengembalikan k logit per contoh, dan target-nya adalah indeks kelas yang benar:
logits = torch.tensor([[2.0, 0.5, -1.0], # example 1: class 0 looks likely
[0.1, 0.2, 3.0]]) # example 2: class 2 looks likely
targets = torch.tensor([0, 2]) # correct class indices (int64)
print(nn.CrossEntropyLoss()(logits, targets))
print(torch.softmax(logits, dim=1).round(decimals=3))
tensor(0.1755)
tensor([[0.7860, 0.1750, 0.0390],
[0.0490, 0.0540, 0.8960]])
CrossEntropyLoss menerapkan softmax (yang mengubah k logit menjadi k probabilitas berjumlah 1) dan log loss dalam satu langkah yang stabil. Sekali lagi: tidak ada softmax di model.
Optimizer
Optimizer memegang parameter-parameter model dan memperbaruinya dari .grad masing-masing:
torch.manual_seed(0)
model = nn.Linear(1, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
x = torch.tensor([[1.0], [2.0], [3.0]])
y = torch.tensor([[3.0], [5.0], [7.0]])
loss_fn = nn.MSELoss()
for step in range(3):
optimizer.zero_grad() # 1. clear old gradients
loss = loss_fn(model(x), y) # 2. forward pass + loss
loss.backward() # 3. compute gradients
optimizer.step() # 4. update parameters
print(step, round(loss.item(), 3))
0 22.744
1 4.525
2 0.911
Empat baris itu adalah jantung dari setiap training loop PyTorch. Bandingkan dengan modul autograd: zero_grad() menggantikan panggilan manual .grad.zero_(), dan step() menggantikan update di dalam no_grad().
Optimizer yang mana?
- SGD (
torch.optim.SGD): gradient descent biasa; denganmomentum=0.9ia mempertahankan sebagian arah sebelumnya, sehingga jalurnya lebih mulus. - Adam (
torch.optim.Adam): menyesuaikan ukuran langkah untuk setiap parameter dari riwayat gradient-nya. Bekerja baik dengan sedikit tuning, dan menjadi titik awal yang biasa (lr=1e-3). - AdamW: Adam dengan weight decay (regularisasi L2) yang diterapkan dengan benar: default untuk banyak model modern.
for name, opt_class in [("SGD", torch.optim.SGD), ("Adam", torch.optim.Adam)]:
torch.manual_seed(0)
model = nn.Linear(1, 1)
optimizer = opt_class(model.parameters(), lr=0.05)
for _ in range(200):
optimizer.zero_grad()
loss = loss_fn(model(x), y)
loss.backward()
optimizer.step()
print(name, round(loss.item(), 5), round(model.weight.item(), 3), round(model.bias.item(), 3))
SGD 0.00014 1.986 1.031
Adam 0.04756 1.743 1.57
SGD mencapai w = 2, b = 1 dalam 200 langkah; Adam, dengan learning rate yang sama, masih dalam perjalanan. Ukuran langkah per parameter milik Adam bersinar di masalah besar dan berantakan dengan banyak parameter, bukan di mainan berbobot satu, dan setiap optimizer punya rentang learning rate yang baik masing-masing. Learning rate tetap pengaturan yang paling penting, apa pun optimizer-nya.
Pilih loss dari jenis masalahnya (MSE untuk angka, BCEWithLogits untuk ya/tidak, CrossEntropy untuk k kelas), berikan logit, dan jalankan training dengan zero_grad → loss → backward → step.
Mengapa ini penting untuk kode yang bersih
Loss dan optimizer adalah konfigurasi, bukan logika: buat sekali, di dekat model, dari pengaturan yang jelas (lr, weight_decay). Ketidakcocokan adalah bug senyap yang klasik: softmax di model dan di CrossEntropyLoss (training tetap jalan, tetapi lebih buruk), target integer untuk BCE, target float untuk CrossEntropy. Menjaga konvensi "logit masuk, loss keluar" di mana-mana mencegahnya.
Rangkuman
- Regresi:
nn.MSELoss/nn.L1Loss. Biner:nn.BCEWithLogitsLosspada logit dengan target float 0/1. Multi-kelas:nn.CrossEntropyLosspada k logit dengan indeks kelas int64. - Model mengembalikan logit; loss menerapkan sigmoid/softmax secara internal demi stabilitas numerik.
- Langkah training:
optimizer.zero_grad(),loss = loss_fn(model(x), y),loss.backward(),optimizer.step(). - SGD (± momentum), Adam, AdamW; learning rate yang paling penting.