Chapter Normalisasi dan Standardisasi menormalkan data sebelum masuk jaringan — namun setelah beberapa layer, distribusi aktivasi bergeser lagi dan tidak ada yang menjaganya. Batch Normalization (Ioffe dan Szegedy, 2015) membawa normalisasi ke dalam jaringan: setiap layer Linear/Tanh/ReLU dapat diawali layer khusus yang menormalkan aktivasi datanya sendiri, sehingga skala sinyal terjaga di sepanjang kedalaman arsitektur. Efek sampingnya menguntungkan: training tahan learning rate lebih besar dan konvergen lebih cepat. Tetapi BatchNorm punya perilaku ganda yang sering menjebak pemula — mode train dan mode eval menghitung hasil berbeda — dan chapter ini membedah kedua wajah tersebut sampai ke rumusnya, lalu mengukur efek stabilitasnya pada training nyata.

Apa yang Sebenarnya Dinormalisasi #

Untuk masukan berbentuk (N, D) — N sampel, D fitur — BatchNorm1d(D) menghitung rata-rata dan variansi setiap channel (kolom fitur) sepanjang dimensi batch: channel ke-j dinormalkan memakai statistik N sampel pada kolom ke-j. Ini berbeda dari LayerNorm pada chapter berikutnya yang menormalkan sepanjang dimensi fitur per sampel. Setelah dinormalkan, hasilnya digeser kembali lewat dua parameter learnable per channel: gamma (pengali, disimpan sebagai bn.weight) dan beta (penambah, bn.bias) — jaringan tetap berkuasa membatalkan normalisasi bila memang sehat bagi tugasnya. Selain itu layer menyimpan running_mean dan running_var: rata-rata bergerak statistik batch yang kelak dipakai saat mode eval.

Perbedaan mode adalah sumber kebingungan paling umum dalam penggunaan BatchNorm. Mode train memakai statistik batch yang sedang diproses; mode eval memakai running_mean/running_var yang terkumpul sepanjang training. Berkas batchnorm_train_eval.py membedah seluruh mekanisme tersebut secara nyata:

python
"""Membedah BatchNorm1d: statistik batch, running stats, mode train vs eval.

Bagian 1: anatomi layer — parameter gamma/beta dan statistik berjalan.
Bagian 2: mode train menormalkan tiap channel dengan statistik batch.
Bagian 3: output train bergantung pada isi batch; output eval tidak.
Bagian 4: mode eval memakai running stats; gamma/beta menggeser hasil.
"""

import torch
import torch.nn as nn

torch.manual_seed(42)

# ---------- 1. Anatomi BatchNorm1d(3) ----------
bn = nn.BatchNorm1d(3)
print("gamma (bn.weight) awal :", bn.weight.tolist())
print("beta  (bn.bias)   awal :", bn.bias.tolist())
print("eps =", bn.eps, "| momentum =", bn.momentum)
print("running_mean awal :", bn.running_mean.tolist())
print("running_var  awal :", bn.running_var.tolist())

# ---------- 2. Mode train: statistik batch ----------
x1 = torch.tensor([
    [1.0, 2.0, 3.0],
    [3.0, 4.0, 5.0],
    [5.0, 6.0, 7.0],
    [7.0, 8.0, 9.0],
])
y1 = bn(x1)                                    # mode train aktif
print()
print("output train (tiap channel: mean 0, variansi 1):")
for baris in y1.tolist():
    print("   ", [round(v, 4) for v in baris])
print("rata-rata kolom :", [round(v, 4) for v in y1.mean(dim=0).tolist()])
print("variansi kolom  :", [round(v, 4)
                            for v in y1.var(dim=0, unbiased=False).tolist()])
print("running_mean setelah batch :", [round(v, 3) for v in bn.running_mean.tolist()])
print("running_var  setelah batch :", [round(v, 3) for v in bn.running_var.tolist()])

# ---------- 3. Ketergantungan batch di mode train ----------
bn_baru = nn.BatchNorm1d(3)
s = torch.tensor([[2.0, 2.0, 2.0]])            # satu sampel yang sama
batch_a = torch.cat([s, torch.zeros(1, 3),
                     torch.full((1, 3), 4.0), torch.full((1, 3), 8.0)])
batch_b = torch.cat([s, torch.full((1, 3), 10.0),
                     torch.full((1, 3), 20.0), torch.full((1, 3), 30.0)])
out_a = bn_baru(batch_a)[0]                    # s berada di batch A
out_b = bn_baru(batch_b)[0]                    # s berada di batch B
bn_baru.eval()
out_eval_mandiri = bn_baru(s)[0]               # eval: s sendirian
out_eval_batch = bn_baru(batch_b)[0]           # eval: s di dalam batch B

print()
print("satu sampel sama, tiga jalur berbeda:")
print("  train di batch A :", [round(v, 4) for v in out_a.tolist()])
print("  train di batch B :", [round(v, 4) for v in out_b.tolist()])
print("  eval sendirian   :", [round(v, 4) for v in out_eval_mandiri.tolist()])
print("  eval di batch B  :", [round(v, 4) for v in out_eval_batch.tolist()])
print("  eval identik?    :", torch.allclose(out_eval_mandiri, out_eval_batch))

# ---------- 4. Mode eval: running stats + gamma/beta ----------
bn.eval()
s2 = torch.tensor([[2.0, 4.0, 6.0]])
y_eval = bn(s2)
manual = ((s2 - bn.running_mean)
          / torch.sqrt(bn.running_var + bn.eps))
print()
print("eval sebelum ubah gamma/beta :", [round(v, 4) for v in y_eval[0].tolist()])
print("rumus manual (x-mu)/sd       :",
      [round(v, 4) for v in manual[0].tolist()])

with torch.no_grad():
    bn.weight.copy_(torch.tensor([2.0, 1.0, 0.5]))
    bn.bias.copy_(torch.tensor([10.0, 0.0, -10.0]))
y_afin = bn(s2)
manual_afin = manual * bn.weight + bn.bias
print("eval setelah gamma/beta diubah:")
print("  output layer   :", [round(v, 4) for v in y_afin[0].tolist()])
print("  manual g*x+b   :", [round(v, 4) for v in manual_afin[0].tolist()])
print("  cocok?         :", torch.allclose(y_afin, manual_afin))

Output yang Dihasilkan:

text
gamma (bn.weight) awal : [1.0, 1.0, 1.0]
beta  (bn.bias)   awal : [0.0, 0.0, 0.0]
eps = 1e-05 | momentum = 0.1
running_mean awal : [0.0, 0.0, 0.0]
running_var  awal : [1.0, 1.0, 1.0]

output train (tiap channel: mean 0, variansi 1):
    [-1.3416, -1.3416, -1.3416]
    [-0.4472, -0.4472, -0.4472]
    [0.4472, 0.4472, 0.4472]
    [1.3416, 1.3416, 1.3416]
rata-rata kolom : [0.0, 0.0, 0.0]
variansi kolom  : [1.0, 1.0, 1.0]
running_mean setelah batch : [0.4, 0.5, 0.6]
running_var  setelah batch : [1.567, 1.567, 1.567]

satu sampel sama, tiga jalur berbeda:
  train di batch A : [-0.5071, -0.5071, -0.5071]
  train di batch B : [-1.2828, -1.2828, -1.2828]
  eval sendirian   : [0.0331, 0.0331, 0.0331]
  eval di batch B  : [0.0331, 0.0331, 0.0331]
  eval identik?    : True

eval sebelum ubah gamma/beta : [1.2783, 2.7963, 4.3142]
rumus manual (x-mu)/sd       : [1.2783, 2.7963, 4.3142]
eval setelah gamma/beta diubah:
  output layer   : [12.5566, 2.7963, -7.8429]
  manual g*x+b   : [12.5566, 2.7963, -7.8429]
  cocok?         : True

Mari kita telaah kode di atas baris per baris:

  1. Baris 15-20 (anatomi): layer baru lahir dengan gamma=[1, 1, 1], beta=[0, 0, 0] (identitas — normalisasi murni), eps=1e-05 sebagai penstabil pembagi, momentum=0.1 sebagai laju pembaruan statistik berjalan, dan running_var=[1, 1, 1] sebagai nilai awal.
  2. Baris 23-36 (mode train): keluaran y1 memiliki rata-rata kolom 0 dan variansi kolom 1 — bukti statistik batch bekerja per channel. Kolom masukan sengaja dibuat identik maju (1..7, 2..8, 3..9), sehingga ketiga channel menghasilkan keluaran yang persis sama.
  3. Baris 37-38 (pembaruan running stats): setelah satu batch, running_mean bergeser dari nol ke [0.4, 0.5, 0.6] — tepat 10% (nilai momentum) menuju rata-rata batch [4, 5, 6]; running_var bergeser serupa menuju 6,67.
  4. Baris 41-51 (uji ketergantungan batch): satu sampel s=[2, 2, 2] diproses tiga jalur. Di mode train, hasilnya bergantung pada siapa teman satu batchnya: -0.5071 di batch A, -1.2828 di batch B. Di mode eval, hasilnya 0.0331 baik sendirian maupun di dalam batch — statistik sudah dibekukan.
  5. Baris 62-70 (rumus eval): keluaran eval cocok persis dengan rumus manual (x − running_mean) / √(running_var + eps) — tidak ada statistik batch yang ikut bermain.
  6. Baris 72-80 (peran gamma/beta): setelah gamma=[2, 1, 0.5] dan beta=[10, 0, -10] disalin, keluaran eval bergeser mengikuti gamma·x_hat + beta dan cocok dengan hitungan manual — dua parameter ini adalah bagian BatchNorm yang belajar, biasanya justru mendekati identitas pada model terlatih.

Efek Stabilitas pada Training Nyata #

Manfaat teoretis tersebut terasa paling jelas saat kita memaksa learning rate besar. Berkas mlp_dengan_tanpa_batchnorm.py melatih dua MLP [2, 64, 64, 64, 2] pada make_moons 300 sampel dengan SGD lr=2.0 — nilai agresif untuk jaringan sedalam itu. Kedua model berangkat dari seed identik; satu polos, satu disisipi BatchNorm1d(64) sebelum tiap Tanh:

python
"""Efek BatchNorm pada stabilitas training dengan learning rate besar.

Dataset make_moons 300 sampel dilatih full-batch dengan SGD lr=2.0 —
nilai yang agresif untuk MLP dalam. Dua model dibandingkan: tanpa
BatchNorm dan dengan BatchNorm1d sebelum tiap aktivasi Tanh.
Tanpa BatchNorm, loss terlempar jauh dan tidak pulih; dengan
BatchNorm, training sempat tergoncang lalu konvergen stabil.
"""

import torch
import torch.nn as nn
from sklearn.datasets import make_moons

torch.manual_seed(42)

# ---------- 1. Data ----------
X, y = make_moons(n_samples=300, noise=0.30, random_state=42)
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.long)


# ---------- 2. Dua arsitektur ----------
def buat_tanpa_bn():
    torch.manual_seed(123)
    return nn.Sequential(
        nn.Linear(2, 64), nn.Tanh(),
        nn.Linear(64, 64), nn.Tanh(),
        nn.Linear(64, 64), nn.Tanh(),
        nn.Linear(64, 2),
    )


def buat_dengan_bn():
    torch.manual_seed(123)
    return nn.Sequential(
        nn.Linear(2, 64), nn.BatchNorm1d(64), nn.Tanh(),
        nn.Linear(64, 64), nn.BatchNorm1d(64), nn.Tanh(),
        nn.Linear(64, 64), nn.BatchNorm1d(64), nn.Tanh(),
        nn.Linear(64, 2),
    )


def latih(model, judul):
    print(f"=== {judul} ===")
    optimizer = torch.optim.SGD(model.parameters(), lr=2.0)
    criterion = nn.CrossEntropyLoss()
    for epoch in range(1, 31):
        model.train()
        optimizer.zero_grad()
        loss = criterion(model(X_t), y_t)
        loss.backward()
        optimizer.step()
        if epoch == 1 or epoch % 5 == 0:
            print(f"  epoch {epoch:2d} | loss latih {loss.item():8.4f}")

    model.eval()                                # wajib sebelum evaluasi
    with torch.no_grad():
        logits = model(X_t)
        loss_eval = criterion(logits, y_t).item()
        akurasi = (logits.argmax(dim=1) == y_t).float().mean().item()
    print(f"  eval: loss {loss_eval:.4f} | akurasi {akurasi:.3f}\n")
    return akurasi


print("dataset : make_moons 300 sampel | full-batch | 30 epoch")
print("SGD lr=2.0 | MLP [2, 64, 64, 64, 2] Tanh\n")
latih(buat_tanpa_bn(), "TANPA BatchNorm")
latih(buat_dengan_bn(), "DENGAN BatchNorm1d sebelum tiap Tanh")

Output yang Dihasilkan:

text
dataset : make_moons 300 sampel | full-batch | 30 epoch
SGD lr=2.0 | MLP [2, 64, 64, 64, 2] Tanh

=== TANPA BatchNorm ===
  epoch  1 | loss latih   0.6528
  epoch  5 | loss latih  14.2757
  epoch 10 | loss latih  42.3640
  epoch 15 | loss latih  37.2810
  epoch 20 | loss latih   2.1132
  epoch 25 | loss latih   6.2559
  epoch 30 | loss latih   2.2935
  eval: loss 9.3547 | akurasi 0.520

=== DENGAN BatchNorm1d sebelum tiap Tanh ===
  epoch  1 | loss latih   0.5619
  epoch  5 | loss latih  12.9122
  epoch 10 | loss latih   0.6048
  epoch 15 | loss latih   0.4482
  epoch 20 | loss latih   0.3440
  epoch 25 | loss latih   0.4392
  epoch 30 | loss latih   0.3101
  eval: loss 0.3081 | akurasi 0.877

Mari kita telaah kode di atas baris per baris:

  1. Baris 23-30 (buat_tanpa_bn): model polos tiga hidden layer; seed 123 di-reset agar model pembanding berangkat dari bobot yang sama.
  2. Baris 33-40 (buat_dengan_bn): arsitektur kembar dengan BatchNorm1d(64) disisipkan setelah tiap Linear dan sebelum Tanh — posisi paling umum dalam praktik.
  3. Baris 48 (model.train()): wajib dipanggil tiap epoch — di sinilah BatchNorm memakai statistik batch dan memperbarui running stats.
  4. Baris 56 (model.eval()): pengukuran akhir berpindah ke statistik berjalan — kelalaian memanggil baris ini membuat evaluasi bergantung pada susunan batch dan hasilnya tidak dapat direproduksi.
  5. Baris 65-68 (dua pemanggilan): kedua arsitektur dilatih dengan prosedur serupa; satu-satunya perbedaan struktural adalah keberadaan BatchNorm.

Kedua model sama-sama tergoncang di epoch 5 (loss 14-13) — learning rate 2.0 memang di luar zona nyaman. Bedanya nasib sesudahnya: model tanpa BatchNorm tidak pernah pulih (eval loss 9,3547, akurasi 0,520 — tak jauh dari tebakan acak), sementara model dengan BatchNorm mendarat kembali pada epoch 10 dan menutup training di akurasi 0,877. Normalisasi yang terus-menerus menjaga skala aktivasi membuat parameter tidak tersesat terlalu jauh untuk dipulihkan.

Dokumentasi resmi BatchNorm1d, termasuk makna momentum dan track_running_stats, tersedia di docs.pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html . BatchNorm menjaga kesehatan jaringan lewat kerja sama antar sampel dalam satu batch; bab berikutnya memperkenalkan saudaranya yang bekerja mandiri — satu sampel, satu normalisasi — dan menjadi pilihan utama dunia transformer.