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 lossTugasBentuk labelAktivasi output khas
MeanSquaredErrorregresiangkalinear
BinaryCrossentropyklasifikasi biner0 / 1sigmoid atau logit
CategoricalCrossentropymulti-kelasone-hotsoftmax
SparseCategoricalCrossentropymulti-kelasindeks kelassoftmax atau logit

Berkas keras_loss.py menguji MSE, BCE, dan SCCE pada angka kecil yang bisa dihitung manual, sekaligus membandingkan dua jalur input BCE:

python
"""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:

text
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.35667488

Mari kita telaah kode di atas baris per baris:

  1. 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 .
  2. 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).
  3. Baris 17-19 (data biner): label [1, 0] berarti contoh pertama kelas positif dan kedua negatif; tf.sigmoid mengubah logit [2, -1] menjadi probabilitas [0.8808, 0.2689].
  4. Baris 21-23 (dua jalur BCE): hitungan manual memberi rata-rata 0.2201 dari −log(0.8808) dan −log(1 − 0.2689); kedua jalur menghasilkan 0.22009483 dan 0.22009486 — selisih di ujung murni pembulatan float32.
  5. Baris 25-29 (SCCE): label 1 pada 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:

OptimizerIde utamaLR bawaanCatatan pemakaian
SGDw ← w − lr·g0.01sederhana dan stabil; sensitif pada pilihan learning rate
RMSpropskala langkah per parameter dengan rata-rata kuadrat gradien0.001andal pada loss yang bergelombang
Adammomentum ditambah skala adaptif ala RMSprop0.001titik 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:

python
"""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:

text
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.101630

Mari kita telaah kode di atas baris per baris:

  1. 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.
  2. Baris 13-16 (data sintetis): 200 sampel x seragam di rentang [-2, 2] dengan target kurva 0.5x² + 0.3x + 1 ditambah derau tipis; hubungan melengkung inilah alasan model memakai hidden layer ReLU.
  3. Baris 19-26 (buat_model): satu fungsi pembangun dipakai dua kali sehingga arsitektur kedua model identik — satu-satunya perbedaan tinggal optimizer.
  4. Baris 29-32 (dua optimizer): SGD(learning_rate=0.05) dan Adam(learning_rate=0.01) — nilai yang sama-sama cukup aman untuk arsitektur sekecil ini.
  5. 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 objek History menyimpan loss tiap epoch.
  6. Baris 44-46 (tabel riwayat): loss SGD turun mulus dari 0.9156 ke 0.0226; loss Adam sempat lebih tinggi di epoch awal (1.9124) karena skala adaptifnya masih menyesuaikan, lalu berakhir di 0.0154 — lebih rendah dari SGD.
  7. Baris 49-51 (evaluate): menilai model terlatih pada data yang sama dan mengembalikan [loss, mae] sesuai urutan metrics; 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.