Dua fitur dengan skala berbeda jauh — pendapatan jutaan bersebelahan dengan umur puluhan — membuat gradien loss didominasi fitur berskala besar; permukaan loss menjadi lembah memanjang dan gradient descent meluncur timpang. Chapter ini membahas normalisasi min-max dan standardisasi z-score beserta aturan kuncinya — kalibrasi hanya dari data latih — lalu membuktikan efeknya pada eksperimen gradient descent yang identik kecuali skala input.
Min-Max dan Z-Score #
| Metode | Rumus | Rentang hasil (data latih) | Kapan dipakai |
|---|---|---|---|
| Min-max | x' = (x - min) / (max - min) | tepat 0..1 | rentang keluaran harus dibatasi, data tanpa pencilan |
| Z-score | x' = (x - μ) / σ | rata-rata 0, simpangan 1 | pilihan default untuk training neural network |
| Robust | x' = (x - median) / IQR | sekitar nol, tak terbatas | data memuat banyak nilai pencilan |
Z-score menjadi pilihan default karena tidak mengunci rentang: sampel baru di luar jangkauan data latih tetap dipetakan wajar, sedangkan min-max menjepitnya ke nilai ekstrem. Keduanya tersedia siap pakai sebagai MinMaxScaler dan StandardScaler di sklearn.preprocessing.
Kalibrasi Hanya pada Data Latih #
Scaler menyimpan statistik hasil fit() — minimum–maksimum atau rata-rata–simpangan. Aturannya satu arah: fit() hanya pada data latih; transform() untuk latih, validasi, test, dan data produksi memakai statistik yang sama. Mem-fit ulang tiap subset membuat validasi dan test diskalakan dengan “kunci” berbeda — distribusinya bocor dan evaluasi kehilangan makna; ini kebocoran yang paling sering lolos, berdampingan dengan pembagian subset pada chapter Pembagian Data Training, Validasi, dan Testing
.
Contoh: StandardScaler dan MinMaxScaler #
Berkas skala_fitur.py memakai data latih 40 sampel dan validasi 10 sampel, dua fitur berskala berbeda (pendapatan 3–15, umur 20–60); kedua scaler di-fit pada data latih, lalu statistik yang sama mentransformasi data validasi:
"""StandardScaler dan MinMaxScaler: fit pada data latih saja.
Data latih 40 sampel dan data validasi 10 sampel dengan dua fitur
berskala sangat berbeda (pendapatan jutaan, umur puluhan). Scaler
dikalibrasi (fit) pada data latih, lalu statistik yang sama dipakai
untuk mentransformasi data validasi — bukan fit ulang.
"""
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler
rng = np.random.default_rng(42)
# ---------- 1. Data dengan dua fitur berskala jauh berbeda ----------
X_latih = np.column_stack(
[
rng.uniform(3.0, 15.0, size=40), # pendapatan (juta rupiah)
rng.uniform(20.0, 60.0, size=40), # umur (tahun)
]
)
X_val = np.column_stack(
[
rng.uniform(3.0, 15.0, size=10),
rng.uniform(20.0, 60.0, size=10),
]
)
print("===== STATISTIK DATA ASLI =====")
for nama, data in [("latih", X_latih), ("validasi", X_val)]:
print(f"{nama:9s}: rata-rata {np.round(data.mean(axis=0), 2)} "
f"| simpangan {np.round(data.std(axis=0), 2)}")
# ---------- 2. StandardScaler: z-score, fit HANYA pada data latih ----------
zscore = StandardScaler().fit(X_latih)
Z_latih = zscore.transform(X_latih)
Z_val = zscore.transform(X_val)
print()
print("===== STANDARDSCALER (z-score) =====")
print("mean_ (dari latih) :", np.round(zscore.mean_, 4))
print("scale_ (dari latih) :", np.round(zscore.scale_, 4))
print("Z latih : rata-rata", np.round(Z_latih.mean(axis=0), 4) + 0.0,
"| rentang", round(float(Z_latih.min()), 2), "s.d.", round(float(Z_latih.max()), 2))
print("Z validasi : rata-rata", np.round(Z_val.mean(axis=0), 4),
"| rentang", round(float(Z_val.min()), 2), "s.d.", round(float(Z_val.max()), 2))
print("rata-rata validasi bukan nol -> transformasi memakai statistik latih")
# ---------- 3. MinMaxScaler: skala 0..1, fit HANYA pada data latih ----------
minmax = MinMaxScaler().fit(X_latih)
M_latih = minmax.transform(X_latih)
M_val = minmax.transform(X_val)
print()
print("===== MINMAXSCALER (0..1) =====")
print("data_min_ (dari latih) :", np.round(minmax.data_min_, 2))
print("data_max_ (dari latih) :", np.round(minmax.data_max_, 2))
print("M latih : min", np.round(M_latih.min(axis=0), 4) + 0.0,
"| max", np.round(M_latih.max(axis=0), 4))
print("M validasi : min", np.round(M_val.min(axis=0), 4),
"| max", np.round(M_val.max(axis=0), 4))
print("nilai validasi bisa keluar 0..1 -> memakai batas rentang latih")Output yang Dihasilkan:
===== STATISTIK DATA ASLI =====
latih : rata-rata [ 9.4 39.18] | simpangan [ 3.43 10.03]
validasi : rata-rata [ 7.81 36.55] | simpangan [ 3.2 10.63]
===== STANDARDSCALER (z-score) =====
mean_ (dari latih) : [ 9.4032 39.1798]
scale_ (dari latih) : [ 3.4273 10.0333]
Z latih : rata-rata [0. 0.] | rentang -1.88 s.d. 1.55
Z validasi : rata-rata [-0.4639 -0.2623] | rentang -1.79 s.d. 1.92
rata-rata validasi bukan nol -> transformasi memakai statistik latih
===== MINMAXSCALER (0..1) =====
data_min_ (dari latih) : [ 3.53 20.29]
data_max_ (dari latih) : [14.71 54.14]
M latih : min [0. 0.] | max [1. 1.]
M validasi : min [-0.0226 0.0949] | max [0.8266 1.1282]
nilai validasi bisa keluar 0..1 -> memakai batas rentang latihContoh di atas diuji nyata dengan scikit-learn 1.5.2. Mari kita telaah kode di atas baris per baris:
- Baris 12 (generator):
default_rng(42)membuat data acak yang dapat direproduksi. - Baris 15-26 (data):
column_stackmerangkai dua kolom menjadi matriks(40, 2)dan(10, 2)dengan rentang berbeda tegas. - Baris 34-36 (StandardScaler):
fit(X_latih)menyimpanmean_danscale_dari data latih;transformyang sama diterapkan pada kedua subset. - Baris 39-46 (baca hasil z-score): data latih berakhir rata-rata tepat
[0, 0]— memang kalibrasinya dari sana; validasi berakhir[-0.4639, -0.2623]— bukan nol, dan justru itu perilaku benarnya: kunci skalanya milik data latih. - Baris 49-61 (MinMaxScaler): data latih terpetakan tepat ke
[0, 1], sedangkan validasi melampaui batas hingga-0.0226dan1.1282— min-max menjepit sampel di luar jangkauan latih, alasan z-score lebih aman.
Efek pada Konvergensi #
Berkas konvergensi_skala.py mengubah klaim bahwa penskalaan mempercepat konvergensi menjadi angka: regresi linear dua fitur — fitur pertama berskala 0–100, fitur kedua 0–1 — dilatih tiga kali dengan model, seed, dan 30 epoch yang sama; hanya skala input dan learning rate yang berbeda:
"""Efek standardisasi pada konvergensi gradient descent.
Regresi linear dua fitur: fitur pertama berskala 0-100, fitur kedua
berskala 0-1. Tiga pelatihan identik (model, seed, epoch sama) dibedakan
hanya pada skala input dan learning rate. Tanpa standardisasi, learning
rate yang sehat bagi data terstandar justru divergen; learning rate kecil
yang aman menjadi terlalu lambat.
"""
import numpy as np
import torch
import torch.nn as nn
# ---------- 1. Data sintetis: dua fitur berskala jauh berbeda ----------
rng = np.random.default_rng(42)
n = 300
fitur_1 = rng.uniform(0.0, 100.0, n) # skala besar
fitur_2 = rng.uniform(0.0, 1.0, n) # skala kecil
target = 0.3 * fitur_1 + 2.0 * fitur_2 + rng.normal(0.0, 0.5, n)
X_mentah = np.column_stack([fitur_1, fitur_2])
X_standar = (X_mentah - X_mentah.mean(axis=0)) / X_mentah.std(axis=0)
y_t = torch.tensor(target, dtype=torch.float32).unsqueeze(1)
print("data:", n, "sampel | fitur 1 rentang 0-100 | fitur 2 rentang 0-1")
print("lantai loss (varians derau 0.5^2) ~", 0.5**2)
print()
def latih(nama, X, lr, jumlah_epoch=30):
"""Full-batch gradient descent dengan inisialisasi bobot identik."""
torch.manual_seed(7)
model = nn.Linear(X.shape[1], 1)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
X_t = torch.tensor(X, dtype=torch.float32)
print(f"--- {nama} (lr={lr}) ---")
for epoch in range(1, jumlah_epoch + 1):
optimizer.zero_grad()
loss = loss_fn(model(X_t), y_t)
loss.backward()
optimizer.step()
if epoch in (1, 5, 10, 20, 30):
print(f" epoch {epoch:2d} | loss = {loss.item():.4f}")
print()
latih("tanpa standardisasi, lr kecil", X_mentah, 3e-4)
latih("tanpa standardisasi, lr besar", X_mentah, 0.1)
latih("dengan standardisasi, lr besar", X_standar, 0.1)Output yang Dihasilkan:
data: 300 sampel | fitur 1 rentang 0-100 | fitur 2 rentang 0-1
lantai loss (varians derau 0.5^2) ~ 0.25
--- tanpa standardisasi, lr kecil (lr=0.0003) ---
epoch 1 | loss = 227.2014
epoch 5 | loss = 122.7429
epoch 10 | loss = 57.0917
epoch 20 | loss = 12.8455
epoch 30 | loss = 3.4319
--- tanpa standardisasi, lr besar (lr=0.1) ---
epoch 1 | loss = 227.2014
epoch 5 | loss = 6437456548348056899158016.0000
epoch 10 | loss = inf
epoch 20 | loss = nan
epoch 30 | loss = nan
--- dengan standardisasi, lr besar (lr=0.1) ---
epoch 1 | loss = 312.4241
epoch 5 | loss = 52.3558
epoch 10 | loss = 5.8384
epoch 20 | loss = 0.3286
epoch 30 | loss = 0.2648Mari kita telaah kode di atas baris per baris:
- Baris 15-22 (data):
target = 0.3·fitur_1 + 2.0·fitur_2 + derau; lembah loss memanjang karena kontribusi nilai fitur_1 besar tetapi gradiennya landai. Standardisasi dihitung dari seluruh data karena contoh ini membandingkan konvergensi, bukan generalisasi. - Baris 30-46 (fungsi latih): seed
7di dalam fungsi menjamin ketiga pelatihan memulai dari bobot identik; full-batch gradient descent memakainn.Linear(2, 1)danMSELoss. - Baris 49-51 (tiga pelatihan): lr
3e-4pada data mentah mewakili “lr yang aman”, lr0.1mewakili “lr yang sehat bagi data terstandar”.
Tiga nasib terbaca dari keluaran. Learning rate kecil pada data mentah turun terlalu lambat: 227.2 → 3.43, masih 13 kali di atas lantai loss 0.25. Learning rate 0.1 pada data mentah divergen — loss menembus 6.4e24 di epoch 5, lalu inf dan nan: langkah yang diperbesar fitur bernilai ratusan terlalu panjang, parameter terlempar makin jauh, gradien membesar, saling menguat. Learning rate yang sama pada data terstandar turun mulus ke 0.2648 — mendekati lantai 0.25. Standardisasi tidak mengubah masalah yang dipelajari; standardisasi meratakan jalan menuju solusinya, sehingga satu learning rate bersahabat dengan seluruh dimensi sekaligus.
Dokumentasi kedua scaler dan varian tahan pencilan: scikit-learn modules/preprocessing . Fitur kini seragam, dan pertanyaan berikutnya: seberapa banyak sampel masuk sekali jalan ke model? Ukuran potongan itulah — batch size — penentu kehalusan gradien, memori, dan irama pembaruan.