Mục lục
- Mục tiêu bài học
- Bối cảnh — vấn đề BN giải quyết
- Algorithm — bốn bước của BN
- Vai trò của \( \gamma \) và \( \beta \)
- Inference — running mean và running variance
- Position trong network — trước hay sau activation
- Pros — vì sao BN trở thành chuẩn
- Cons — khi BN gây phiền
- PyTorch API — nn.BatchNorm1d / 2d / 3d
- Tham số chi tiết của nn.BatchNorm
- model.train() và model.eval() cho BN
- Variant 1 — LayerNorm
- Variant 2 — InstanceNorm và GroupNorm
- Variant 3 — RMSNorm
- Chọn normalization theo task
- BN và Dropout — có nên dùng cùng nhau
- Computation overhead
- Internal Covariate Shift có thực sự là lý do BN work
- Code Python — MLP có BN vs không BN
- Common bug
- Bài tập
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Mô tả được bốn bước của Batch Normalization và vai trò của hai tham số học được \( \gamma, \beta \).
- Phân biệt batch statistics (train) và running statistics (inference); biết vì sao quên
model.eval()sẽ làm predict sai. - Đặt BN đúng chỗ trong khối
Linear / Conv → BN → Activation. - Hiểu khi nào dùng
BatchNorm1d,BatchNorm2d,BatchNorm3dvà các tham sốeps,momentum,affine,track_running_stats. - Biết bốn biến thể chính: LayerNorm, InstanceNorm, GroupNorm, RMSNorm — và task nào dùng cái nào.
- Đoán được các tình huống BN gây phiền: batch nhỏ, conflict với Dropout, train/eval mismatch.
Bài này nối Bài 22 — Dropout (regularization theo trục neuron) với Bài 24 — Early Stopping và Checkpoint (kỹ thuật dừng đúng lúc).
Bối cảnh — vấn đề BN giải quyết
Trước 2015, train mạng deep (10+ layer) là việc khó: phải init cực kỹ, learning rate nhỏ, dùng pre-training từng layer kiểu Hinton 2006, hoặc kết hợp nhiều mánh phụ. Ba vấn đề chính:
- Internal Covariate Shift (ICS): khi tham số layer trước thay đổi, phân phối input của layer sau cũng thay đổi. Layer sau phải "đuổi theo" — vừa học task vừa học cách thích nghi với input shifting.
- Sigmoid / tanh saturate: pre-activation có biên độ lớn rơi vào vùng phẳng → derivative ~0 → gradient vanish.
- Init nhạy cảm: lệch một chút khỏi Xavier / He là explode hoặc vanish (xem Bài 20 — Weight Initialization).
Ioffe & Szegedy (2015) đề xuất Batch Normalization với kỳ vọng kiểm soát ICS bằng cách chuẩn hóa input của mỗi layer thành phân phối ổn định. Paper: "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" — ICML 2015 (arXiv:1502.03167). BN nhanh chóng trở thành block mặc định trong CNN, mở đường cho ResNet (He et al., 2016) huấn luyện được mạng 152 layer.
Algorithm — bốn bước của BN
Với mini-batch \( \mathcal{B} = \{x_1, x_2, \dots, x_B\} \) gồm \( B \) sample, mỗi \( x_i \) là pre-activation của một feature dimension cụ thể. BN xử lý độc lập theo từng feature:
1. Batch mean:
\[ \mu_{\mathcal{B}} = \frac{1}{B} \sum_{i=1}^{B} x_i \]
2. Batch variance:
\[ \sigma_{\mathcal{B}}^2 = \frac{1}{B} \sum_{i=1}^{B} (x_i - \mu_{\mathcal{B}})^2 \]
3. Normalize về mean = 0, std = 1:
\[ \hat{x}_i = \frac{x_i - \mu_{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}^2 + \epsilon}} \]
Trong đó \( \epsilon \approx 10^{-5} \) là hằng số nhỏ tránh chia cho 0 khi variance gần 0.
4. Scale và shift bằng hai tham số học được:
\[ y_i = \gamma \, \hat{x}_i + \beta \]
Output \( y_i \) đi vào activation kế tiếp. Với mỗi feature dimension có riêng một cặp \( (\gamma, \beta) \); ví dụ nn.BatchNorm1d(256) có 256 cặp.
Vai trò của \( \gamma \) và \( \beta \)
Vì sao đã chuẩn hóa rồi lại scale + shift? Lý do: bắt buộc activation có mean = 0, std = 1 là quá hạn chế. Ví dụ với sigmoid, đầu vào lý tưởng có khi không phải mean = 0 mà là một vùng cụ thể trên đường cong.
Hai tham số \( \gamma \) (scale) và \( \beta \) (shift) cho mạng quyền học distribution tối ưu cho mỗi layer:
- Nếu \( \gamma = \sqrt{\sigma_{\mathcal{B}}^2 + \epsilon} \) và \( \beta = \mu_{\mathcal{B}} \): BN trở về identity, không làm gì cả.
- Nếu \( \gamma, \beta \) khác — mạng học được tỷ lệ và vị trí phân phối phù hợp với task.
Quan trọng: \( \gamma, \beta \) là parameter (được optimizer cập nhật như weight thường), không phải buffer. Trong khi đó, batch mean \( \mu_{\mathcal{B}} \) và batch variance \( \sigma_{\mathcal{B}}^2 \) là thống kê, không học được.
Default init trong PyTorch: \( \gamma = 1 \), \( \beta = 0 \) — tức ban đầu BN trả về \( \hat{x} \) thuần (không scale/shift).
Inference — running mean và running variance
Lúc inference thường chỉ có một sample (không có batch). Nếu vẫn dùng "batch statistics" với \( B = 1 \): variance = 0, normalize chia cho \( \sqrt{\epsilon} \), kết quả vô nghĩa. Hoặc tệ hơn: dự đoán cùng một sample mà ra kết quả khác nhau tùy nó nằm cạnh sample nào trong batch.
Giải pháp: trong lúc training, BN duy trì running statistics bằng exponential moving average (EMA):
\[ \mu_{\text{run}} \leftarrow (1 - m)\, \mu_{\text{run}} + m\, \mu_{\mathcal{B}} \]
\[ \sigma_{\text{run}}^2 \leftarrow (1 - m)\, \sigma_{\text{run}}^2 + m\, \sigma_{\mathcal{B}}^2 \]
Trong đó \( m \) là momentum (PyTorch default 0.1). Hai biến \( \mu_{\text{run}}, \sigma_{\text{run}}^2 \) được lưu thành buffer (không phải parameter — không gradient, không bị optimizer động đến, nhưng được lưu khi state_dict()).
Khi inference (sau khi gọi model.eval()), BN dùng running statistics thay cho batch statistics:
\[ y = \gamma \cdot \frac{x - \mu_{\text{run}}}{\sqrt{\sigma_{\text{run}}^2 + \epsilon}} + \beta \]
Lưu ý quy ước momentum trong PyTorch ngược với một số framework khác: momentum=0.1 nghĩa là 10% trọng số cho batch mới, 90% cho running cũ — không phải 90/10.
Position trong network — trước hay sau activation
Thứ tự tiêu chuẩn cho một block FC hoặc Conv với BN:
Linear / Conv → BN → Activation (ReLU) → ...
Tức BN trước activation. Lý do paper gốc đưa ra: pre-activation \( z = Wx + b \) là tổng tuyến tính các biến ngẫu nhiên — gần phân phối Gaussian theo CLT, dễ chuẩn hóa. Sau khi ReLU cắt phần âm, phân phối lệch và việc chuẩn hóa kém ý nghĩa hơn.
Tuy nhiên có tranh luận: một số thực nghiệm (như "Identity Mappings in Deep Residual Networks", He et al., 2016) cho rằng BN sau activation đôi khi tốt hơn — đặc biệt khi kết hợp residual connection. Cộng đồng vẫn chưa thống nhất tuyệt đối.
Hiện hành trong CNN cổ điển (ResNet, VGG): Conv → BN → ReLU là default phổ biến nhất. Trong Transformer (sẽ học ở Module 5) thì khác hẳn — dùng LayerNorm với thứ tự pre-norm hoặc post-norm tùy variant.
Một chi tiết hay quên: khi đã có BN ngay sau Linear / Conv, bias của Linear / Conv là dư thừa. Vì BN trừ mean rồi shift bằng \( \beta \) — bias gốc bị triệt tiêu khi tính mean. PyTorch cho phép tắt:
nn.Linear(256, 128, bias=False) # BN sau đó sẽ học beta thay
nn.Conv2d(64, 128, kernel_size=3, padding=1, bias=False)
Pros — vì sao BN trở thành chuẩn
- Tăng tốc convergence: cho phép learning rate lớn hơn 5–10 lần so với không BN, vì gradient không còn nhạy với scale tuyệt đối của activation.
- Regularization nhẹ: batch statistics khác nhau giữa các batch tạo "noise" có lợi tương tự dropout, nhưng yếu hơn. Trong nhiều mạng CNN hiện đại, BN đã đủ để giảm overfitting, không cần thêm dropout.
- Giảm yêu cầu init kỹ: nếu activation đầu layer bị scale lệch, BN re-normalize ngay. Nghĩa là Xavier hay He vẫn nên dùng, nhưng sai số init không gây vỡ training như trước.
- Cho phép mạng sâu hơn: ResNet 152 layer (He et al., 2016) hay Inception-v4 (Szegedy et al., 2017) không thể train ổn nếu thiếu BN.
- Smooth loss landscape: phân tích sau này (Santurkar et al., 2018) chỉ ra BN làm Hessian của loss "êm" hơn — gradient ít dao động mạnh, optimizer đi mượt hơn.
Cons — khi BN gây phiền
- Batch size nhỏ → estimate kém: \( \mu_{\mathcal{B}}, \sigma_{\mathcal{B}}^2 \) với \( B = 2, 4 \) có variance lớn — không đại diện cho phân phối thật. Object detection và segmentation thường buộc dùng batch nhỏ (vì ảnh to, VRAM hạn chế) → BN hoạt động kém. Đây là động lực ra đời GroupNorm.
- Phụ thuộc batch composition: cùng một sample, đặt vào batch khác sẽ ra activation khác lúc training. Reproduce kém nếu không cẩn thận với shuffle / seed.
- Train mode vs eval mode khác behavior: nguồn bug rất hay gặp (chi tiết ở mục 11).
- Conflict với Dropout: Li et al. (2018) ghi nhận trường hợp BN + Dropout dùng chung gây variance shift giữa train và test. Modern CNN thường bỏ dropout, chỉ giữ BN.
- Khó áp dụng cho RNN / sequence model: kích thước batch theo trục time-step không cố định, BN khó định nghĩa "feature" thống nhất. LayerNorm giải quyết vấn đề này.
- Distributed training: batch bị chia nhỏ qua nhiều GPU → mỗi GPU thấy batch nhỏ → estimate kém. Cần SyncBatchNorm (đồng bộ statistics qua GPU) — thêm chi phí giao tiếp.
PyTorch API — nn.BatchNorm1d / 2d / 3d
Ba lớp BN khác nhau ở shape của tensor input, không khác ở thuật toán:
nn.BatchNorm1d(C): input shape \( (B, C) \) cho MLP, hoặc \( (B, C, L) \) cho 1D sequence. Mean / variance tính qua trục batch (và trục \( L \) nếu có), với mỗi channel \( C \).nn.BatchNorm2d(C): input shape \( (B, C, H, W) \) cho CNN ảnh. Mean / variance tính qua \( B \times H \times W \), riêng cho mỗi channel \( C \).nn.BatchNorm3d(C): input shape \( (B, C, D, H, W) \) cho video / volumetric data (3D Conv).
Ví dụ MLP với BN:
import torch.nn as nn
model = nn.Sequential(
nn.Linear(784, 256, bias=False),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Linear(256, 128, bias=False),
nn.BatchNorm1d(128),
nn.ReLU(),
nn.Linear(128, 10),
)
Ví dụ CNN với BN:
cnn = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(),
)
Quan sát: số channel của BatchNorm2d phải khớp với số output channel của Conv liền trước.
Tham số chi tiết của nn.BatchNorm
nn.BatchNorm1d(
num_features,
eps=1e-5,
momentum=0.1,
affine=True,
track_running_stats=True,
)
num_features: số channel / feature \( C \). Phải khớp shape input.eps: hằng số trong mẫu \( \sqrt{\sigma^2 + \epsilon} \). Default \( 10^{-5} \), thường không cần đổi.momentum: trọng số cho EMA của running statistics. Default 0.1. ĐặtNoneđể dùng simple moving average (trung bình tích lũy đều) — hiếm dùng.affine: có học \( \gamma, \beta \) hay không. DefaultTrue. TắtFalsesẽ chỉ chuẩn hóa thuần, không có cặp scale/shift.track_running_stats: có duy trì running statistics hay không. DefaultTrue. TắtFalsethì cả train và eval đều dùng batch statistics — chỉ phù hợp khi inference cũng có batch lớn (hiếm).
Tham số trong state_dict() của một BN layer:
weight= \( \gamma \) — learnable parameter.bias= \( \beta \) — learnable parameter.running_mean— buffer.running_var— buffer.num_batches_tracked— buffer đếm số batch đã forward (dùng khimomentum=None).
Khi save / load checkpoint, các buffer trên đi cùng — quên copy là inference sẽ sai.
model.train() và model.eval() cho BN
BN là một trong các module có behavior khác nhau giữa train và eval mode. Tóm tắt:
| Mode | Statistics dùng | Cập nhật running? | Gradient \( \gamma, \beta \)? |
|---|---|---|---|
model.train() | batch | có (EMA) | có |
model.eval() | running | không | vẫn tính nếu cần, nhưng không update |
Quy trình chuẩn:
for epoch in range(num_epochs):
model.train() # bật train mode trước vòng train
for x, y in train_loader:
logits = model(x)
loss = loss_fn(logits, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval() # chuyển sang eval mode trước khi validate
with torch.no_grad():
for x, y in val_loader:
logits = model(x)
# tính accuracy / val_loss...
Quên model.eval() khi validate / predict sẽ làm BN dùng batch statistics của batch validation hiện tại — kết quả phụ thuộc batch composition, thay đổi mỗi lần chạy. Với batch nhỏ (như inference 1 ảnh) còn vô nghĩa.
Ngược lại, quên model.train() đầu epoch (sau khi đã eval() ở epoch trước) sẽ làm BN không update running statistics, gradient \( \gamma, \beta \) vẫn chạy nhưng dùng running cũ — gây ra "drift" rất khó debug.
Cùng vấn đề tồn tại với Dropout: train mode bật mask ngẫu nhiên, eval mode tắt. Hai module này là lý do chính khiến hai phương thức train() / eval() tồn tại.
Variant 1 — LayerNorm
Layer Normalization (Ba et al., 2016 — arXiv:1607.06450) đảo trục chuẩn hóa: thay vì lấy mean / variance qua batch với mỗi feature, LayerNorm lấy qua feature với mỗi sample.
Với tensor input shape \( (B, C) \), BN tính 1 cặp \( (\mu, \sigma^2) \) cho mỗi trong \( C \) channel (qua \( B \) sample). LayerNorm thì tính 1 cặp cho mỗi trong \( B \) sample (qua \( C \) feature).
Công thức với 1 sample \( x \in \mathbb{R}^C \):
\[ \mu = \frac{1}{C} \sum_{j=1}^{C} x_j, \quad \sigma^2 = \frac{1}{C} \sum_{j=1}^{C} (x_j - \mu)^2 \]
\[ y_j = \gamma_j \cdot \frac{x_j - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta_j \]
Đặc điểm:
- Không phụ thuộc batch → behavior train và eval giống nhau, không cần running statistics.
- Hoạt động tốt với batch nhỏ (kể cả \( B = 1 \)).
- Là default trong Transformer (Vaswani et al., 2017) và mọi kiến trúc kế thừa (BERT, GPT, LLaMA, T5).
- Cũng tốt cho RNN, vì không cần batch fixed.
import torch.nn as nn
# LayerNorm trên 768 chiều embedding (chuẩn Transformer)
ln = nn.LayerNorm(normalized_shape=768)
# Hoặc multi-dim (ảnh, ít gặp)
ln2 = nn.LayerNorm(normalized_shape=[3, 224, 224])
Variant 2 — InstanceNorm và GroupNorm
Instance Normalization (Ulyanov et al., 2016): với tensor ảnh shape \( (B, C, H, W) \), chuẩn hóa qua \( H \times W \) cho từng channel của từng sample. Không lấy trung bình qua batch.
- Tốt cho style transfer và GAN: phong cách ảnh nằm trong statistics per-channel, BN xóa style không mong muốn nhưng InstanceNorm giữ.
- API:
nn.InstanceNorm2d(num_features).
Group Normalization (Wu & He, 2018 — arXiv:1803.08494): chia \( C \) channel thành \( G \) group, chuẩn hóa trong từng group cho mỗi sample.
- Với \( G = 1 \): LayerNorm. Với \( G = C \): InstanceNorm. Group là tổng quát hóa cả hai.
- Không phụ thuộc batch → batch size 2 vẫn ổn. Đây là lựa chọn chính cho object detection (Mask R-CNN) và segmentation, nơi batch thường \( \le 4 \).
- API:
nn.GroupNorm(num_groups, num_channels). Phổ biến:num_groups=32.
# GroupNorm thay BN trong CNN khi batch nhỏ
nn.Conv2d(64, 128, 3, padding=1, bias=False),
nn.GroupNorm(num_groups=32, num_channels=128),
nn.ReLU(),
Bốn biến thể nhìn dưới góc "trục nào lấy mean / variance" với tensor \( (B, C, H, W) \):
- BN: qua \( B, H, W \) (riêng từng \( C \)).
- LayerNorm: qua \( C, H, W \) (riêng từng \( B \)).
- InstanceNorm: qua \( H, W \) (riêng từng \( B, C \)).
- GroupNorm: qua \( H, W \) và một group con của \( C \) (riêng từng \( B \) và mỗi group).
Variant 3 — RMSNorm
Root Mean Square Normalization (Zhang & Sennrich, 2019 — arXiv:1910.07467) lược bớt phần "trừ mean", chỉ chia cho RMS:
\[ \mathrm{RMS}(x) = \sqrt{\frac{1}{C} \sum_{j=1}^{C} x_j^2 + \epsilon}, \quad y_j = \gamma_j \cdot \frac{x_j}{\mathrm{RMS}(x)} \]
Không có \( \beta \) (shift) và không trừ mean. Lợi thế: rẻ hơn LayerNorm khoảng 7–20% (không cần tính mean, không cần shift).
Kết quả thực nghiệm cho thấy RMSNorm chất lượng tương đương LayerNorm trên Transformer. Vì vậy nhiều LLM hiện đại (LLaMA, Mistral, Gemma) dùng RMSNorm thay LayerNorm để tiết kiệm compute.
# PyTorch >= 2.4 có nn.RMSNorm built-in
import torch.nn as nn
rms = nn.RMSNorm(normalized_shape=4096)
Chọn normalization theo task
Bảng tra nhanh:
| Task / kiến trúc | Norm khuyên dùng | Lý do chính |
|---|---|---|
| CNN classification, batch \( \ge 32 \) | BatchNorm | batch lớn → statistics tin cậy |
| CNN detection / segmentation, batch \( \le 8 \) | GroupNorm | không phụ thuộc batch |
| Transformer (text, ViT) | LayerNorm | standard, sequence length thay đổi |
| RNN / LSTM | LayerNorm | BN khó định nghĩa qua time-step |
| Style transfer / GAN | InstanceNorm | giữ style per-sample |
| LLM (LLaMA, Mistral, Gemma) | RMSNorm | tiết kiệm compute, chất lượng tương đương |
| Distributed training, batch lớn chia GPU | SyncBatchNorm | đồng bộ statistics qua GPU |
Khi không chắc và đang làm CNN classification: BatchNorm. Khi không chắc và đang làm Transformer / sequence: LayerNorm. Hai lựa chọn này phủ ~90% trường hợp.
BN và Dropout — có nên dùng cùng nhau
Câu trả lời ngắn: thường không, ít nhất với CNN.
Li et al. (2018) — "Understanding the Disharmony between Dropout and Batch Normalization by Variance Shift" — phân tích: Dropout chia activation cho \( (1 - p) \) khi train, BN lưu running variance dựa trên activation đã scale; lúc eval, dropout tắt thì variance thật khác variance đã lưu. Train và test "thấy" hai phân phối khác nhau → kết quả tệ hơn dùng riêng.
Thực hành:
- CNN modern (ResNet, EfficientNet): chỉ dùng BN, ít hoặc không dùng dropout. Regularization của BN đã đủ trong nhiều bài toán.
- Transformer: LayerNorm + Dropout không có vấn đề tương tự (LayerNorm không phụ thuộc batch). Dropout dùng ở residual, attention, MLP đều phổ biến.
- Nếu vẫn muốn kết hợp BN + Dropout: đặt Dropout sau BN, sau cùng đến activation cuối — không xen kẽ.
Computation overhead
- Forward: BN thêm khoảng 10–30% thời gian so với cùng mạng không BN (phụ thuộc kiến trúc và batch size).
- Backward: gradient của BN phức tạp — qua \( \gamma, \beta \) và qua \( \mu_{\mathcal{B}}, \sigma_{\mathcal{B}}^2 \) (vì hai biến này phụ thuộc input). PyTorch có implementation fused cho hiệu năng tốt.
- Bộ nhớ: cần lưu \( \hat{x}_i \) hoặc \( \mu_{\mathcal{B}}, \sigma_{\mathcal{B}}^2 \) để backward.
Trade-off thường rất đáng: chi phí thêm 20% mỗi step nhưng convergence nhanh gấp 3–5 lần về số epoch — tổng wall-clock vẫn tiết kiệm. Với mạng rất sâu (ResNet-152), không có BN gần như không train được, nên so sánh "có BN vs không BN" không phải lúc nào cũng có ý nghĩa.
Internal Covariate Shift có thực sự là lý do BN work
Paper gốc Ioffe & Szegedy giải thích BN hoạt động vì giảm Internal Covariate Shift. Đây là lý thuyết được chấp nhận rộng rãi đến 2018.
Santurkar et al. (2018) — "How Does Batch Normalization Help Optimization?" (arXiv:1805.11604) — thực nghiệm khác: cố tình bơm noise sau BN để khôi phục lại "shift" — vậy mà network có BN vẫn train tốt hơn không BN. Kết luận của paper: BN hoạt động không phải vì giảm ICS, mà vì làm loss landscape mịn hơn (giảm Lipschitz constant của loss và gradient).
Cuộc tranh luận chưa hoàn toàn kết thúc. Điều có thể nói chắc:
- BN thực sự giúp train mạng sâu — fact đã verify trên nhiều benchmark.
- Cơ chế chính xác giải thích vì sao thì chưa có consensus.
- Là người dùng: vẫn nên dùng BN khi phù hợp; nhưng nên cẩn trọng khi đọc tài liệu cũ giải thích duy nhất qua ICS.
Code Python — MLP có BN vs không BN
So sánh convergence trên MNIST giữa MLP có BN và không BN, learning rate giống nhau:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"
train_ds = datasets.MNIST("./data", train=True, download=True,
transform=transforms.ToTensor())
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)
def make_mlp(use_bn: bool):
layers = [nn.Flatten()]
in_dim = 784
for out_dim in [256, 256, 256]:
layers.append(nn.Linear(in_dim, out_dim, bias=not use_bn))
if use_bn:
layers.append(nn.BatchNorm1d(out_dim))
layers.append(nn.ReLU())
in_dim = out_dim
layers.append(nn.Linear(in_dim, 10))
return nn.Sequential(*layers).to(device)
def train_one(model, epochs=5, lr=0.1):
opt = torch.optim.SGD(model.parameters(), lr=lr)
history = []
for ep in range(epochs):
model.train()
for x, y in train_loader:
x, y = x.to(device), y.to(device)
loss = F.cross_entropy(model(x), y)
opt.zero_grad(); loss.backward(); opt.step()
history.append(loss.item())
return history
h_no_bn = train_one(make_mlp(use_bn=False))
h_bn = train_one(make_mlp(use_bn=True))
print(f"no BN last loss: {h_no_bn[-1]:.4f}")
print(f"with BN last loss: {h_bn[-1]:.4f}")
Kết quả điển hình với lr=0.1 (lr này cố tình lớn): MLP không BN dao động và converge chậm; MLP có BN giảm loss mượt và nhanh hơn rõ rệt.
Demo train vs eval mode khác behavior:
model = make_mlp(use_bn=True)
# train vài batch để BN có running statistics
for x, y in train_loader:
x, y = x.to(device), y.to(device)
F.cross_entropy(model(x), y).backward()
break
x_test = next(iter(train_loader))[0][:1].to(device) # 1 ảnh
model.train()
out_train = model(x_test).argmax(dim=1).item()
model.eval()
with torch.no_grad():
out_eval = model(x_test).argmax(dim=1).item()
print(f"predict ở train mode: {out_train}")
print(f"predict ở eval mode: {out_eval}")
Khi batch chỉ có 1 sample mà chạy ở train mode, BN chia cho \( \sqrt{\sigma_{\mathcal{B}}^2 + \epsilon} \) với \( \sigma_{\mathcal{B}}^2 = 0 \) — kết quả phụ thuộc \( \epsilon \), gần như random. Chuyển eval() mới ổn định.
Common bug
- Quên
model.eval()khi inference: predict thay đổi tùy batch composition; với batch = 1 thì gần như random. Là bug BN phổ biến nhất. - Quên
model.train()sau khieval(): running statistics ngừng cập nhật ở epoch tiếp theo nhưng gradient vẫn chạy, dẫn đến drift train/eval khó debug. - Batch size = 1 trong train mode: variance = 0, output toàn 0 hoặc
nan. PyTorch raise error rõ ràng choBatchNorm1dkhi \( B = 1 \) trong train mode. - Sai loại BN cho shape input: dùng
BatchNorm2dcho input \( (B, C) \) → lỗi shape. DùngBatchNorm1dcho \( (B, C, H, W) \) → cũng lỗi. - Save checkpoint mà bỏ buffer: chỉ lưu
parameters()mà không phảistate_dict()→ khi load lại,running_mean/running_varmất → inference ra kết quả khác hẳn lúc validate trước save. - Distributed: dùng BN thường thay vì SyncBatchNorm: mỗi GPU thấy batch nhỏ → statistics kém. PyTorch có
nn.SyncBatchNorm.convert_sync_batchnorm(model)để chuyển toàn bộ BN sang sync version. - Đặt BN sau softmax / activation cuối: phá output logit, mạng không học được. BN luôn ở giữa layer, không phải cuối.
- Vẫn để bias trong Linear / Conv ngay trước BN: không sai về mặt training nhưng dư parameter và làm chậm nhẹ. Tắt bằng
bias=False.
Bài tập
- Build MLP
784 → 256 → 256 → 256 → 10với ReLU, đặt BN trước mỗi ReLU. Train MNIST 5 epoch với và không có BN, cùnglr=0.1. So sánh loss curve và test accuracy. - Sửa MLP trên cho
lr=1.0(lr cực lớn). Quan sát: mạng không BN sẽnantrong vài batch đầu; mạng có BN vẫn train được (chậm hơn nhưng không phân kỳ). Giải thích tại sao. - Chạy MLP có BN ở batch size 128 và batch size 2. So sánh validation accuracy. Quan sát hiện tượng batch size nhỏ làm BN kém.
- Implement BN bằng tay (chỉ với MLP, BatchNorm1d), không dùng
nn.BatchNorm1d. Forward dùng batch statistics, backward dùng autograd của PyTorch. So sánh output vớinn.BatchNorm1dtrên cùng input để verify (chú ý chế độ train). - Train một CNN nhỏ trên CIFAR-10 với
BatchNorm2d. Save state dict, sau đó load vào model mới và inference. Verify rằngrunning_meanvàrunning_varđược khôi phục đúng (so sánh trước/sau save). - Demo bug quên
model.eval(): lấy 1 ảnh, predict 5 lần ở train mode (mỗi lần kèm 1 batch random khác nhau) — quan sát prediction thay đổi. Chuyển sang eval mode — prediction ổn định. - Thay
BatchNorm2dtrong CNN ở câu 5 bằngGroupNorm(num_groups=32, num_channels=...)với batch size 4. So sánh accuracy. Quan sát GroupNorm ổn hơn khi batch nhỏ.
Đáp án ngắn
- Mạng có BN converge nhanh và đạt accuracy cao hơn (thường >97% sau 5 epoch); không BN dao động và thường <95%.
- Mạng không BN: gradient nhân scale lớn qua nhiều layer → activation explode → loss
nan. Có BN re-normalize activation sau mỗi layer nên không tích lũy được scale explode. - Batch 2: validation accuracy thấp hơn vì running statistics tích lũy từ batch statistics noisy. Mức giảm tùy seed, thường 2–10%.
- Output của BN tay và
nn.BatchNorm1dphải khớp đến \( 10^{-5} \) (chênh doeps). Nếu không khớp, kiểm tra việc tính variance (biased vs unbiased — BN dùng biased variance chia cho \( B \)). - State dict phải có
running_meanvàrunning_varcho mỗi BN layer.torch.equalso sánh tensor trước/sau save phải True. - Train mode: prediction phụ thuộc vào batch composition → thay đổi mỗi lần. Eval mode: dùng running statistics cố định → prediction ổn định.
- GroupNorm ít phụ thuộc batch hơn → accuracy với batch = 4 chỉ giảm nhẹ so với batch lớn; BN giảm mạnh.
Bài kế tiếp: Bài 24 — Early Stopping và Checkpoint — biết khi nào dừng train trước khi overfit, và lưu / khôi phục model state đúng cách (bao gồm cả buffer của BN).
- Ioffe & Szegedy (2015) - Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
- Ba, Kiros & Hinton (2016) - Layer Normalization
- Ulyanov, Vedaldi & Lempitsky (2016) - Instance Normalization: The Missing Ingredient for Fast Stylization
- Wu & He (2018) - Group Normalization
- Zhang & Sennrich (2019) - Root Mean Square Layer Normalization
- Santurkar et al. (2018) - How Does Batch Normalization Help Optimization?
- Li et al. (2018) - Understanding the Disharmony between Dropout and Batch Normalization by Variance Shift
- He et al. (2016) - Deep Residual Learning for Image Recognition (ResNet)
- PyTorch Docs - nn.BatchNorm1d
- PyTorch Docs - nn.BatchNorm2d
- PyTorch Docs - nn.LayerNorm
- PyTorch Docs - nn.GroupNorm
- PyTorch Docs - nn.SyncBatchNorm
- CS231n - Batch Normalization Notes
