Danh sách bài viết

Bài 23: Batch Normalization — chuẩn hóa giữa các layer

Batch Normalization (Ioffe & Szegedy, 2015) chuẩn hóa activation theo mini-batch thành mean = 0, std = 1, sau đó scale và shift bằng hai tham số học được \( \gamma, \beta \). Inference dùng running statistics tích lũy qua training. BN cho phép learning rate lớn hơn, giảm yêu cầu init kỹ, và mở đường cho mạng rất sâu (ResNet 152 layer). Bài này đi qua algorithm, position trong network, train/eval mode, biến thể LayerNorm / Instance / Group / RMSNorm, và khi dùng cái nào.

24/05/2026
14 phút đọc
1 lượt xem
1

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, BatchNorm3d và 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).

2

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.

3

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.

4

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).

5

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.

6

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)
7

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.
8

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.
9

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.

10

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. Đặt None để 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. Default True. Tắt False sẽ 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. Default True. Tắt False thì 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 khi momentum=None).

Khi save / load checkpoint, các buffer trên đi cùng — quên copy là inference sẽ sai.

11

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:

ModeStatistics dùngCập nhật running?Gradient \( \gamma, \beta \)?
model.train()batchcó (EMA)
model.eval()runningkhôngvẫ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.

12

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])
13

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).
14

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)
15

Chọn normalization theo task

Bảng tra nhanh:

Task / kiến trúcNorm khuyên dùngLý do chính
CNN classification, batch \( \ge 32 \)BatchNormbatch lớn → statistics tin cậy
CNN detection / segmentation, batch \( \le 8 \)GroupNormkhông phụ thuộc batch
Transformer (text, ViT)LayerNormstandard, sequence length thay đổi
RNN / LSTMLayerNormBN khó định nghĩa qua time-step
Style transfer / GANInstanceNormgiữ style per-sample
LLM (LLaMA, Mistral, Gemma)RMSNormtiết kiệm compute, chất lượng tương đương
Distributed training, batch lớn chia GPUSyncBatchNormđồ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.

16

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ẽ.
17

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.

18

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.
19

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.

20

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 khi eval(): 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 cho BatchNorm1d khi \( B = 1 \) trong train mode.
  • Sai loại BN cho shape input: dùng BatchNorm2d cho input \( (B, C) \) → lỗi shape. Dùng BatchNorm1d cho \( (B, C, H, W) \) → cũng lỗi.
  • Save checkpoint mà bỏ buffer: chỉ lưu parameters() mà không phải state_dict() → khi load lại, running_mean / running_var mấ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.
21

Bài tập

  1. Build MLP 784 → 256 → 256 → 256 → 10 với ReLU, đặt BN trước mỗi ReLU. Train MNIST 5 epoch với và không có BN, cùng lr=0.1. So sánh loss curve và test accuracy.
  2. Sửa MLP trên cho lr=1.0 (lr cực lớn). Quan sát: mạng không BN sẽ nan trong 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.
  3. 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.
  4. 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ới nn.BatchNorm1d trên cùng input để verify (chú ý chế độ train).
  5. 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ằng running_meanrunning_var được khôi phục đúng (so sánh trước/sau save).
  6. 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.
  7. Thay BatchNorm2d trong CNN ở câu 5 bằng GroupNorm(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
  1. 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%.
  2. 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.
  3. 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%.
  4. Output của BN tay và nn.BatchNorm1d phải khớp đến \( 10^{-5} \) (chênh do eps). 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 \)).
  5. State dict phải có running_meanrunning_var cho mỗi BN layer. torch.equal so sánh tensor trước/sau save phải True.
  6. 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.
  7. 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).