Setelah forward propagation menghasilkan sebuah prediksi, kita membutuhkan satu angka yang menyatakan seberapa jauh prediksi tersebut dari jawaban yang benar. Angka itu dihasilkan oleh loss function — fungsi yang membandingkan prediksi model dengan target sebenarnya, lalu meringkas besarnya kesalahan menjadi satu nilai skalar. Nilai inilah sasaran seluruh proses training: neural network mengubah bobotnya agar loss terus menurun. Pilihan loss function tidak bebas, karena bentuknya harus cocok dengan jenis tugas (regresi atau klasifikasi) dan dengan activation function pada layer output. Chapter ini membahas tiga loss yang paling sering dipakai, lengkap dengan implementasi manual NumPy yang hasilnya cocok dengan hitungan tangan.
Peran Loss Function dalam Training #
Training pada dasarnya adalah proses meminimalkan sebuah angka. Loss function menerjemahkan kualitas prediksi menjadi angka tersebut dengan tiga sifat penting:
- Prediksi tepat memberi nilai kecil — pada kasus sempurna nilainya nol.
- Kesalahan besar memberi nilai besar — makin jauh prediksi dari target, makin tinggi loss yang diberikan, sehingga proses perbaikan tahu prioritas mana yang paling mendesak.
- Fungsi dapat diturunkan — gradien loss terhadap setiap bobot menjadi petunjuk arah perbaikan; tanpa kehalusan (smoothness) ini, gradient descent tidak dapat bekerja.
Satu-satunya hal yang dilihat algoritma optimasi adalah nilai loss — algoritma tidak pernah melihat prediksi mentahnya. Karena itu, loss function yang salah pilih akan menuntun training ke arah yang salah walaupun arsitektur modelnya bagus.
Mean Squared Error untuk Regresi #
Untuk tugas regresi yang targetnya bilangan riil, standar industri adalah mean squared error (MSE):
MSE = (1/n) Σ (ŷᵢ − yᵢ)²
Kuadrat melakukan dua pekerjaan sekaligus: meniadakan tanda selisih (kelebihan dan kekurangan sama-sama dianggap galat) dan memberi bobot lebih besar untuk kesalahan lebih besar. Galat 20 memberi kontribusi 400, sedangkan galat 50 memberi 2500 — kesalahan besar menonjol jauh lebih kuat. Karena satuan MSE ikut terkuadratkan, kita sering memakai akarnya (RMSE) agar kembali ke satuan asli data. Berkas mse_manual.py menghitung semuanya selangkah demi selangkah pada empat prediksi harga rumah:
"""Mean squared error (MSE) untuk regresi, dihitung manual dengan NumPy."""
import numpy as np
def mse_manual(prediksi, target):
selisih = prediksi - target
return np.mean(selisih**2)
# Prediksi harga empat rumah (juta rupiah) vs harga sebenarnya
prediksi = np.array([160.0, 700.0, 990.0, 1150.0])
target = np.array([160.0, 720.0, 950.0, 1200.0])
selisih = prediksi - target
kuadrat = selisih**2
print("Prediksi :", prediksi)
print("Target :", target)
print("Selisih :", selisih)
print("Selisih kuadrat :", kuadrat)
print("Jumlah kuadrat :", kuadrat.sum())
print("MSE :", mse_manual(prediksi, target))
print(f"RMSE : {np.sqrt(mse_manual(prediksi, target)):.3f}")Output yang Dihasilkan:
Prediksi : [ 160. 700. 990. 1150.]
Target : [ 160. 720. 950. 1200.]
Selisih : [ 0. -20. 40. -50.]
Selisih kuadrat : [ 0. 400. 1600. 2500.]
Jumlah kuadrat : 4500.0
MSE : 1125.0
RMSE : 33.541Cocokkan dengan hitungan tangan: jumlah kuadrat 0 + 400 + 1600 + 2500 = 4500, dibagi empat sampel menghasilkan MSE 1125.0 — persis seperti keluaran program. Nilai RMSE 33,541 dapat dibaca langsung sebagai rata-rata galat model sekitar 33,5 juta rupiah per rumah. Mari kita telaah kodenya baris per baris:
- Baris 6-8 (
mse_manual): inti rumus dalam dua baris — selisih prediksi terhadap target, lalu rata-rata kuadratnya lewatnp.mean. - Baris 11-13: data contoh empat rumah; prediksi pertama sengaja tepat (selisih 0) agar terlihat kontribusinya nol.
- Baris 15-16: menghitung selisih dan kuadratnya secara eksplisit hanya untuk keperluan tampilan — nilai antara inilah yang dicetak agar rumus transparan.
- Baris 18-24: mencetak seluruh nilai antara, MSE, dan RMSE dengan
np.sqrt.
Cross-Entropy untuk Klasifikasi #
Untuk klasifikasi, prediksi model adalah probabilitas, dan pengukur jarak yang tepat bukan kuadrat selisih melainkan cross-entropy. Bentuk binernya (BCE) untuk label y ∈ {0, 1} dan prediksi ŷ ∈ (0, 1):
BCE = −(1/n) Σ [yᵢ · ln ŷᵢ + (1 − yᵢ) · ln(1 − ŷᵢ)]
Label menyeleksi suku yang aktif: jika y = 1, hanya −ln ŷ yang dihitung; jika y = 0, hanya −ln(1 − ŷ). Karena −ln p menanjak tajam ketika p mengecil, prediksi yang terlalu yakin pada kelas yang salah diberi nilai loss jauh lebih besar daripada prediksi yang ragu-ragu. Untuk multi-class, layer output berukuran jumlah kelas dengan softmax, dan categorical cross-entropy (CCE) cukup membaca probabilitas pada kelas yang benar:
CCE = −(1/n) Σ ln ŷᵢ,kelas_benar
Satu jebakan teknis: ln 0 tidak terdefinisi, sedangkan sigmoid atau softmax dapat menghasilkan probabilitas tepat 0 atau 1 karena pembulatan floating point. Solusi standarnya adalah clipping dengan epsilon, yaitu membatasi prediksi ke rentang [ε, 1−ε] memakai np.clip
sebelum mengambil logaritma. Berkas cross_entropy_manual.py mengimplementasikan BCE dan CCE sekaligus beserta verifikasi hitungan tangan:
"""Cross-entropy untuk klasifikasi: biner (BCE) dan multi-class (CCE)."""
import numpy as np
EPS = 1e-7
def bce_manual(y_pred, y_true):
"""Binary cross-entropy dengan clipping epsilon agar log aman."""
y_pred = np.clip(y_pred, EPS, 1.0 - EPS)
suku = y_true * np.log(y_pred) + (1.0 - y_true) * np.log(1.0 - y_pred)
return -np.mean(suku)
def cce_manual(prob, indeks_kelas):
"""Categorical cross-entropy: prob prediksi vs kelas yang benar."""
prob = np.clip(prob, EPS, 1.0 - EPS)
p_kelas_benar = prob[np.arange(len(indeks_kelas)), indeks_kelas]
return -np.mean(np.log(p_kelas_benar))
# --- Binary cross-entropy: spam (1) vs bukan spam (0) ---
y_pred = np.array([0.9, 0.2, 0.75, 0.05])
y_true = np.array([1.0, 0.0, 1.0, 0.0])
print("=== Binary Cross-Entropy ===")
print("Prediksi :", y_pred)
print("Label :", y_true)
print(f"BCE : {bce_manual(y_pred, y_true):.6f}")
print(f"BCE cek tangan = -(ln 0.9 + ln 0.8 + ln 0.75 + ln 0.95) / 4"
f" = {bce_manual(y_pred, y_true):.6f}")
# Prediksi terlalu yakin pada kelas yang salah
print(f"\nPrediksi 1.0 untuk kelas 0 (yakin tapi salah) : "
f"{bce_manual(np.array([1.0]), np.array([0.0])):.4f}")
# --- Categorical cross-entropy: 3 kelas, 3 sampel ---
prob = np.array([
[0.7, 0.2, 0.1],
[0.1, 0.8, 0.1],
[0.2, 0.3, 0.5],
])
kelas = np.array([0, 1, 2])
print("\n=== Categorical Cross-Entropy ===")
print("Prediksi :\n", prob)
print("Kelas benar :", kelas)
print(f"CCE : {cce_manual(prob, kelas):.6f}")
print(f"CCE cek tangan = -(ln 0.7 + ln 0.8 + ln 0.5) / 3"
f" = {cce_manual(prob, kelas):.6f}")
yakin_benar = np.array([[0.98, 0.01, 0.01]])
yakin_salah = np.array([[0.01, 0.98, 0.01]])
print(f"\nYakin dan benar : {cce_manual(yakin_benar, np.array([0])):.4f}")
print(f"Yakin tapi salah : {cce_manual(yakin_salah, np.array([0])):.4f}")Output yang Dihasilkan:
=== Binary Cross-Entropy ===
Prediksi : [0.9 0.2 0.75 0.05]
Label : [1. 0. 1. 0.]
BCE : 0.166870
BCE cek tangan = -(ln 0.9 + ln 0.8 + ln 0.75 + ln 0.95) / 4 = 0.166870
Prediksi 1.0 untuk kelas 0 (yakin tapi salah) : 16.1181
=== Categorical Cross-Entropy ===
Prediksi :
[[0.7 0.2 0.1]
[0.1 0.8 0.1]
[0.2 0.3 0.5]]
Kelas benar : [0 1 2]
CCE : 0.424322
CCE cek tangan = -(ln 0.7 + ln 0.8 + ln 0.5) / 3 = 0.424322
Yakin dan benar : 0.0202
Yakin tapi salah : 4.6052Dua verifikasi hitungan tangan cocok persis dengan keluaran fungsi. Baris terakhir memperlihatkan karakter cross-entropy: prediksi 0,98 pada kelas yang benar hanya diberi loss 0.0202, tetapi prediksi 0,98 pada kelas yang salah diberi loss 4.6052 — rasio lebih dari dua ratus kali. Model yang ragu-ragu saat keliru menerima nilai kecil; model yang terlalu yakin namun keliru menerima nilai besar sehingga training memprioritaskan perbaikan padanya. Mari kita telaah kodenya baris per baris:
- Baris 5 (
EPS = 1e-7): epsilon untuk clipping — nilai prediksi dibatasi agar logaritma tidak pernah menerima 0. - Baris 8-12 (
bce_manual): rumus BCE utuh — klip dulu, bentuk kedua suku, lalu rata-rata dan balik tanda. - Baris 15-19 (
cce_manual): CCE untuk target berupa indeks kelas. Ekspresiprob[np.arange(len(indeks_kelas)), indeks_kelas]adalah pengindeksan baris-per-baris NumPy: sampel pertama mengambil kolomkelas[0], sampel kedua mengambil kolomkelas[1], dan seterusnya — persis membaca probabilitas kelas yang benar dari tiap baris. - Baris 22-35: kasus biner — empat prediksi spam vs bukan spam, verifikasi hitungan tangan, lalu uji ekstrem prediksi 1.0 pada kelas 0 yang menghasilkan 16.1181 (nilai
−ln ε), bukan galatinf, berkat clipping. - Baris 37-50: kasus multi-class tiga kelas — verifikasi manual CCE dari matriks probabilitas keluaran softmax.
- Baris 52-55: kontras dua prediksi sama-sama yakin, satu benar satu salah, sebagai bukti numerik karakter cross-entropy.
Memilih Loss yang Tepat #
Loss, tugas, dan activation output layer berpasangan tetap. Tabel berikut merangkum pasangan yang benar:
| Loss function | Tugas | Activation output | Target |
|---|---|---|---|
| MSE | Regresi | Linear (tanpa aktivasi) | Bilangan riil |
| Binary cross-entropy | Klasifikasi biner | Sigmoid | 0 atau 1 |
| Categorical cross-entropy | Klasifikasi multi-class | Softmax | Indeks kelas / one-hot |
Ketiga loss ini tersedia siap pakai di framework modern sebagai kelas losses di Keras
dan modul torch.nn di PyTorch, tetapi rumusnya persis seperti yang baru saja kita tulis tangan. Satu pertanyaan kini terbuka: setelah loss terhitung, bagaimana caranya tahu ke arah mana tiap bobot harus digeser agar loss menurun? Jawabannya adalah backpropagation — aturan rantai yang dijalankan dari loss mundur ke seluruh layer.