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:
"""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:
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? : TrueMari kita telaah kode di atas baris per baris:
- Baris 15-20 (anatomi): layer baru lahir dengan
gamma=[1, 1, 1],beta=[0, 0, 0](identitas — normalisasi murni),eps=1e-05sebagai penstabil pembagi,momentum=0.1sebagai laju pembaruan statistik berjalan, danrunning_var=[1, 1, 1]sebagai nilai awal. - Baris 23-36 (mode train): keluaran
y1memiliki 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. - Baris 37-38 (pembaruan running stats): setelah satu batch,
running_meanbergeser dari nol ke[0.4, 0.5, 0.6]— tepat 10% (nilaimomentum) menuju rata-rata batch[4, 5, 6];running_varbergeser serupa menuju 6,67. - 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.5071di batch A,-1.2828di batch B. Di mode eval, hasilnya0.0331baik sendirian maupun di dalam batch — statistik sudah dibekukan. - 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. - Baris 72-80 (peran gamma/beta): setelah
gamma=[2, 1, 0.5]danbeta=[10, 0, -10]disalin, keluaran eval bergeser mengikutigamma·x_hat + betadan 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:
"""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:
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.877Mari kita telaah kode di atas baris per baris:
- Baris 23-30 (
buat_tanpa_bn): model polos tiga hidden layer; seed 123 di-reset agar model pembanding berangkat dari bobot yang sama. - Baris 33-40 (
buat_dengan_bn): arsitektur kembar denganBatchNorm1d(64)disisipkan setelah tiapLineardan sebelumTanh— posisi paling umum dalam praktik. - Baris 48 (
model.train()): wajib dipanggil tiap epoch — di sinilah BatchNorm memakai statistik batch dan memperbarui running stats. - 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. - 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.