Model yang kita susun pada Chapter sebelumnya sudah bisa menghitung keluaran, tetapi belum tahu seberapa jauh keluarannya meleset — apalagi bagaimana memperbaiki bobotnya. Dua bahan itu melengkapi resep pelatihan: fungsi loss mengukur besarnya kesalahan, optimizer menentukan aturan pembaruan bobot berdasarkan gradien loss. Pada section dasar neural network kita menghitung keduanya dengan tangan; Keras membungkus pasangan ini ke dalam satu panggilan model.compile(). Chapter ini menguji fungsi loss pada angka kecil yang bisa dihitung tangan, membahas arti from_logits yang kerap menimbulkan galat senyap, lalu melatih dua model dengan SGD dan Adam pada 200 sampel data sintetis.
Fungsi Loss yang Sering Dipakai #
Pemilihan loss mengikuti bentuk tugas dan label. Tabel berikut memetakan empat fungsi yang paling sering kita jumpai sepanjang Course ini:
| Fungsi loss | Tugas | Bentuk label | Aktivasi output khas |
|---|---|---|---|
MeanSquaredError | regresi | angka | linear |
BinaryCrossentropy | klasifikasi biner | 0 / 1 | sigmoid atau logit |
CategoricalCrossentropy | multi-kelas | one-hot | softmax |
SparseCategoricalCrossentropy | multi-kelas | indeks kelas | softmax atau logit |
Berkas keras_loss.py menguji MSE, BCE, dan SCCE pada angka kecil yang bisa dihitung manual, sekaligus membandingkan dua jalur input BCE:
"""Fungsi loss Keras pada angka kecil yang bisa dihitung manual."""
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import numpy as np
import tensorflow as tf
mse = tf.keras.losses.MeanSquaredError()
target = np.array([[2.0], [4.0]])
prediksi = np.array([[3.0], [6.0]])
print("MSE prediksi [3, 6] vs target [2, 4] :", mse(target, prediksi).numpy())
bce_proba = tf.keras.losses.BinaryCrossentropy(from_logits=False)
bce_logit = tf.keras.losses.BinaryCrossentropy(from_logits=True)
label_biner = np.array([1.0, 0.0])
logit = tf.constant([2.0, -1.0])
proba = tf.sigmoid(logit)
print()
print("proba = sigmoid(logit) :", proba.numpy())
print("BCE pada probabilitas (from_logits=False):", bce_proba(label_biner, proba).numpy())
print("BCE pada logit (from_logits=True) :", bce_logit(label_biner, logit).numpy())
sce = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False)
label_klas = np.array([1])
proba_klas = tf.constant([[0.1, 0.7, 0.2]])
print()
print("SCCE label 1 pada proba [0.1 0.7 0.2]:", sce(label_klas, proba_klas).numpy())Output yang Dihasilkan:
MSE prediksi [3, 6] vs target [2, 4] : 2.5
proba = sigmoid(logit) : [0.8807971 0.26894143]
BCE pada probabilitas (from_logits=False): 0.22009483
BCE pada logit (from_logits=True) : 0.22009486
SCCE label 1 pada proba [0.1 0.7 0.2]: 0.35667488Mari kita telaah kode di atas baris per baris:
- Baris 10-13 (MSE): prediksi
[3, 6]terhadap target[2, 4]menghasilkan((3−2)² + (6−4)²) / 2 = 2.5— cocok dengan keluaran, dan rumusnya persis MSE yang kita tulis manual pada Chapter Loss Function . - Baris 15-16 (dua BCE): dua objek loss yang identik kecuali satu hal —
from_logits. Yang pertama menerima probabilitas, yang kedua menerima skor mentah (logit). - Baris 17-19 (data biner): label
[1, 0]berarti contoh pertama kelas positif dan kedua negatif;tf.sigmoidmengubah logit[2, -1]menjadi probabilitas[0.8808, 0.2689]. - Baris 21-23 (dua jalur BCE): hitungan manual memberi rata-rata
0.2201dari−log(0.8808)dan−log(1 − 0.2689); kedua jalur menghasilkan0.22009483dan0.22009486— selisih di ujung murni pembulatan float32. - Baris 25-29 (SCCE): label
1pada probabilitas[0.1, 0.7, 0.2]dihitung sebagai−ln(0.7) = 0.3567— hanya kelas yang benar yang masuk rumus. Kata sparse berarti label berupa indeks kelas, bukan vektor one-hot.
from_logits: Konsistensi Aktivasi dan Loss #
from_logits memberi tahu loss bentuk apa yang keluar dari layer terakhir model: skor mentah sebelum sigmoid/softmax, atau probabilitas hasil aktivasi. Kedua pasangan berikut konsisten dan sah dipakai:
- layer output linear + loss dengan
from_logits=True - layer output sigmoid/softmax + loss dengan
from_logits=False
Di model.compile, ketiga nama loss juga sah ditulis sebagai string — "mse", "binary_crossentropy", "sparse_categorical_crossentropy" — dan Keras akan membangun objek yang setara dengan kelas di atas.
Optimizer: Aturan Pembaruan Bobot #
Optimizer adalah strategi memakai gradien. SGD menjalankan persis aturan yang kita tulis manual pada Chapter Automatic Differentiation
: w ← w − lr × gradien. Varian lain menambahkan informasi riwayat gradien agar langkah lebih efisien:
| Optimizer | Ide utama | LR bawaan | Catatan pemakaian |
|---|---|---|---|
SGD | w ← w − lr·g | 0.01 | sederhana dan stabil; sensitif pada pilihan learning rate |
RMSprop | skala langkah per parameter dengan rata-rata kuadrat gradien | 0.001 | andal pada loss yang bergelombang |
Adam | momentum ditambah skala adaptif ala RMSprop | 0.001 | titik awal standar industri |
Learning rate mengatur panjang langkah tiap pembaruan — terlalu besar membuat pelatihan tidak stabil, terlalu kecil membuat kemajuan lambat, sebagaimana dibahas tuntas pada Chapter Gradient Descent . Daftar lengkap optimizer beserta parameternya tersedia di keras.io/api/optimizers .
model.compile dan model.fit #
compile mengawinkan model dengan loss, optimizer, dan metrik; fit menjalankan siklus yang selama ini kita tulis tangan — forward di dalam GradientTape, hitung loss, gradien, lalu pembaruan bobot oleh optimizer:
Berkas compile_fit.py melatih dua model berarsitektur sama — hidden layer 16 unit ReLU — pada data kurva kuadratik 200 sampel, satu dengan SGD dan satu dengan Adam, lalu membandingkan riwayat lossnya:
"""model.compile dan model.fit: SGD dan Adam pada data sintetis 200 sampel."""
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import numpy as np
import tensorflow as tf
np.random.seed(42)
tf.keras.utils.set_random_seed(42)
n = 200
x = np.random.uniform(-2.0, 2.0, size=(n, 1)).astype(np.float32)
y = (0.5 * x ** 2 + 0.3 * x + 1.0
+ np.random.normal(0.0, 0.05, size=(n, 1))).astype(np.float32)
def buat_model():
return tf.keras.Sequential(
[
tf.keras.Input(shape=(1,)),
tf.keras.layers.Dense(16, activation="relu"),
tf.keras.layers.Dense(1),
]
)
optimizer_config = [
("SGD", tf.keras.optimizers.SGD(learning_rate=0.05)),
("Adam", tf.keras.optimizers.Adam(learning_rate=0.01)),
]
riwayat_semua = {}
model_semua = {}
for nama, optimizer in optimizer_config:
tf.keras.utils.set_random_seed(42)
model = buat_model()
model.compile(optimizer=optimizer, loss="mse", metrics=["mae"])
riwayat = model.fit(x, y, epochs=10, verbose=0)
riwayat_semua[nama] = riwayat.history["loss"]
model_semua[nama] = model
print("epoch | loss SGD | loss Adam")
for epoch in range(10):
print(f"{epoch + 1:5d} | {riwayat_semua['SGD'][epoch]:12.6f} | {riwayat_semua['Adam'][epoch]:11.6f}")
print()
for nama, model in model_semua.items():
loss_akhir, mae_akhir = model.evaluate(x, y, verbose=0)
print(f"evaluate {nama:4s}: loss {loss_akhir:8.6f}, mae {mae_akhir:7.6f}")Output yang Dihasilkan:
epoch | loss SGD | loss Adam
1 | 0.915634 | 1.912422
2 | 0.049561 | 0.718991
3 | 0.027919 | 0.118388
4 | 0.025998 | 0.047127
5 | 0.025135 | 0.076569
6 | 0.024441 | 0.032537
7 | 0.023868 | 0.018760
8 | 0.023390 | 0.020431
9 | 0.022983 | 0.016885
10 | 0.022630 | 0.015377
evaluate SGD : loss 0.022020, mae 0.123199
evaluate Adam: loss 0.015078, mae 0.101630Mari kita telaah kode di atas baris per baris:
- Baris 10-11 (seed ganda):
np.random.seed(42)menjaga pembangkitan data,tf.keras.utils.set_random_seed(42)menjaga inisialisasi bobot — kombinasi yang membuat tabel loss identik di setiap eksekusi. - Baris 13-16 (data sintetis): 200 sampel
xseragam di rentang[-2, 2]dengan target kurva0.5x² + 0.3x + 1ditambah derau tipis; hubungan melengkung inilah alasan model memakai hidden layer ReLU. - Baris 19-26 (
buat_model): satu fungsi pembangun dipakai dua kali sehingga arsitektur kedua model identik — satu-satunya perbedaan tinggal optimizer. - Baris 29-32 (dua optimizer):
SGD(learning_rate=0.05)danAdam(learning_rate=0.01)— nilai yang sama-sama cukup aman untuk arsitektur sekecil ini. - Baris 36-42 (latih dua model): seed direset sebelum tiap model supaya bobot awal identik dan perbandingan adil;
compile(loss="mse", metrics=["mae"])memakai alias string;fit(..., epochs=10, verbose=0)melatih tanpa mencetak progres, dan objekHistorymenyimpan loss tiap epoch. - Baris 44-46 (tabel riwayat): loss SGD turun mulus dari
0.9156ke0.0226; loss Adam sempat lebih tinggi di epoch awal (1.9124) karena skala adaptifnya masih menyesuaikan, lalu berakhir di0.0154— lebih rendah dari SGD. - Baris 49-51 (
evaluate): menilai model terlatih pada data yang sama dan mengembalikan[loss, mae]sesuai urutanmetrics; Adam unggul pada keduanya.
Hasil ini tidak berlaku universal: dengan jadwal learning rate yang diatur baik, SGD kerap menyusul bahkan melampaui Adam pada pelatihan panjang — topik section training dan optimasi. Yang terbukti di sini adalah mekanismenya: compile menyatukan loss dan optimizer, fit memutar siklus gradien, dan History merekam kemajuannya.
Ringkasan #
Kita kini memilih loss sesuai tugas — MSE untuk regresi, BCE untuk biner, SCCE untuk multi-kelas — sambil menjaga konsistensi from_logits dengan aktivasi output. Tiga keluarga optimizer memberi pilihan laju pembaruan, dengan Adam sebagai titik awal lazim. model.compile() dan model.fit() menuntaskan rantai dari tensor mentah sampai model terlatih: loss turun dari 0.92 ke 0.02 dalam sepuluh epoch tanpa satu baris loop manual. Satu keterbatasan tersisa — fit di atas memakai array NumPy yang termuat penuh di memori; data nyata yang besar mengalir lewat pipeline tf.data, topik Chapter berikutnya.