Vanishing/exploding gradient, numerical stability và gradient checking
Bài trước ta đã chốt được một kết quả gọn: với cross-entropy và softmax, đạo hàm theo logits chỉ là $\hat{y} - y$, còn đạo hàm theo từng tham số là một chuỗi phép nhân ma trận đi ngược từ loss về lớp đầu. Bài này bóc đúng chuỗi phép nhân đó ra: nó dài thì biên độ gradient đi về đâu, khi nào ta nên nghi ngờ con số mình đang nhìn, và làm sao kiểm tra bằng code chạy được trước khi tin vào training loop.
Sau bài này bạn có thể:
- giải thích vanishing/exploding gradient như tích các Jacobian và ước lượng bậc độ lớn bằng giấy nháp;
- nhận diện các điểm mất ổn định số học trong forward và backward;
- viết numerical gradient check với central difference và đọc relative error đúng ngưỡng;
- chọn cách khởi tạo, chuẩn hóa và clipping khớp với triệu chứng quan sát được.
Gradient đi đâu khi chuỗi phép nhân dài ra
Lan truyền ngược là tích các Jacobian. Với mạng $L$ lớp:
$$ \frac{\partial \mathcal{L}}{\partial W_1} = \delta_L \prod_{l=2}^{L} J_l, \qquad J_l = W_l^{\top} \operatorname{diag}!\left(\sigma'(z_{l-1})\right) $$
Biên độ gradient vì thế bị quyết định bởi tích các norm $\lVert J_l \rVert$. Với sigmoid, $\sigma'(z) \le 0.25$, nên mỗi lớp đóng góp tối đa một hệ số 0.25:
| Số lớp dùng sigmoid | Ước lượng $\lVert \partial \mathcal{L}/\partial W_1 \rVert$ theo $0.25^{L}$ |
|---|---|
| 5 | $9.8 \times 10^{-4}$ |
| 10 | $9.5 \times 10^{-7}$ |
| 20 | $9.1 \times 10^{-13}$ |
Chiều ngược lại cũng đúng: nếu weight lớn làm mỗi Jacobian có norm khoảng 2, thì $2^{20} \approx 10^{6}$, gradient bùng nổ và một bước SGD có thể phá sạch tham số.
Hai cơ chế gây vanishing hay bị bỏ sót: activation bão hòa (sigmoid/tanh ở đuôi, đạo hàm gần 0) và ReLU chết (pre-activation âm vĩnh viễn, gradient đúng bằng 0 chứ không chỉ nhỏ).
flowchart TD
Loss["🎯 Loss L"] --> D3["δ3 = y_hat - y"]
D3 --> D2["δ2 = (W3ᵀ δ3) ⊙ σ'(z2)"]
D2 --> D1["δ1 = (W2ᵀ δ2) ⊙ σ'(z1)"]
D1 --> G1["∂L/∂W1 = δ1 a0ᵀ"]
Bài tập 1. Mạng 2 lớp trong capstone dùng sigmoid, khởi tạo $W \sim \mathcal{N}(0, 1)$ với số input lớn. Dự đoán triệu chứng ở lớp 1 và đề xuất một thay đổi duy nhất.
Xem gợi ý
$z = Wx$ có phương sai lớn, sigmoid bão hòa ngay từ epoch 0, nên $\sigma' \approx 0$ và gradient lớp 1 nhỏ hơn lớp 2 khoảng một bậc độ lớn. Thay đổi rẻ nhất: đổi sang He init với ReLU, hoặc giữ sigmoid nhưng dùng Xavier.
Numerical stability: nơi gradient đúng bị báo sai
Ba nguồn sai số cần nhớ:
- Độ chính xác float32. Machine epsilon khoảng $1.19 \times 10^{-7}$, cùng bậc với sai số của phép sai phân. Gradient check trong float32 gần như vô nghĩa.
- Catastrophic cancellation. Nếu loss cỡ $10^{6}$ và hiệu $f(x+h) - f(x-h)$ cỡ $10^{-3}$, bạn mất gần hết chữ số có nghĩa. Cộng dồn loss và gradient bằng accumulator
float64là cách rẻ nhất để tránh. - Biên giới hàm.
log(0)cho $-\infty$ (xảy ra khi đã softmax rồi mới log),exptràn khi logits lớn. Cả hai đều đã được xử lý bằng logits + log-sum-exp, nhưng vẫn phải giữ kỷ luật đó trong toàn bộ pipeline.
Thói quen tối thiểu: sau mỗi backward, kiểm tra np.all(np.isfinite(grad)) và log norm gradient theo từng layer.
Bài tập 2. Loss in ra nan sau 30 bước. Kể ba nguyên nhân khả dĩ và phép kiểm tra tương ứng.
Xem gợi ý
(a) learning rate quá lớn làm weight nổ, kiểm tra norm weight theo bước; (b) chia cho 0 hoặc log của 0 trong loss, kiểm tra bằng cách chạy loss trên logits giả; (c) giá trị inf lan từ một batch có outlier, kiểm tra np.isfinite(x).all() trên input.
Gradient checking: phép thử trước khi tin vào backward
Central difference:
$$ \frac{\partial f}{\partial x_i} \approx \frac{f(x + h e_i) - f(x - h e_i)}{2h}, \qquad \text{sai số } O(h^2) $$
import numpy as np
def numeric_grad(f, x, h=1e-5):
g = np.zeros_like(x)
it = np.nditer(x, flags=["multi_index"])
while not it.finished:
i = it.multi_index
old = x[i]
x[i] = old + h
fp = f(x)
x[i] = old - h
fm = f(x)
x[i] = old
g[i] = (fp - fm) / (2.0 * h)
it.iternext()
return g
def rel_error(a, b):
return np.max(np.abs(a - b) / np.maximum(1e-12, np.abs(a) + np.abs(b)))
Quy trình chạy:
- Chuyển toàn bộ tham số và dữ liệu sang
float64, tắt dropout, batch norm ở chế độ eval, cố định seed. - Chọn ngẫu nhiên khoảng 10 phần tử của một vài tham số thay vì quét toàn bộ (mỗi phần tử tốn 2 forward pass).
- So
numeric_gradvới gradient analytic bằngrel_error. - Đọc ngưỡng: dưới 1e-7 là tốt, 1e-7 đến 1e-4 là nghi ngờ, trên 1e-2 gần như chắc chắn sai công thức.
- Nếu lỗi chỉ xuất hiện ở một lớp, khoanh vùng theo thứ tự: shape, bias broadcasting, rồi chiều của kernel trong conv.
| Relative error | Diễn giải |
|---|---|
| $< 10^{-7}$ | Khớp, có thể tin backward |
| $10^{-7}$ đến $10^{-4}$ | Nghi ngờ: kiểm tra $h$, float, chỗ chia trung bình |
| $> 10^{-2}$ | Công thức backward sai |
Gradient check là phép thử bắt buộc trước khi tin vào bất kỳ đoạn backprop tự viết nào.
Bài tập 3. Lớp linear cho rel_error $10^{-8}$, lớp conv cho $0.3$. Nêu thứ tự ba chỗ cần kiểm tra.
Xem gợi ý
(a) công thức backward của conv có xử lý đúng stride/padding/dilation không; (b) kernel có bị flip nhầm giữa correlation và convolution không; (c) bias cộng vào mọi vị trí không gian nên gradient của bias phải là tổng theo N, H, W.
Chọn khởi tạo và chuẩn hóa theo triệu chứng
| Triệu chứng | Hành động |
|---|---|
| Gradient lớp đầu nhỏ hơn $10^{-6}$ so với lớp cuối | Xavier cho tanh/sigmoid, He cho ReLU, thêm residual |
| Norm gradient tăng gấp đôi sau mỗi lớp | Gradient clipping theo norm (ngưỡng 1.0), giảm learning rate, warmup |
| Nhiều ReLU có activation bằng 0 vĩnh viễn | He init, LeakyReLU, giảm learning rate |
| Loss nhảy mạnh theo batch | LayerNorm/BatchNorm, tăng batch size, kiểm tra logits scale |
Với Xavier, $\operatorname{Var}(W) = 2/(n_{in} + n_{out})$; với He, $\operatorname{Var}(W) = 2/n_{in}$.
Vài cloze để tự kiểm tra:
- Với sigmoid, đạo hàm lớn nhất là _____, nên sau 10 lớp tích gradient còn cỡ _____.
- Gradient check dùng central difference có sai số cắt cụt bậc _____ và chỉ nên chạy ở _____.
- He initialization đặt phương sai weight bằng _____.
Kinh nghiệm đo và chẩn đoán gradient
- Chạy gradient check ngay sau khi viết backward, trước khi train, và chạy lại sau mỗi lần sửa shape.
- $h$ quá nhỏ gây sai số làm tròn, quá lớn gây sai số cắt cụt; $10^{-5}$ đến $10^{-6}$ là vùng an toàn cho
float64. - Đừng kiểm tra mọi tham số: lỗi công thức gần như luôn lộ ra ở vài phần tử ngẫu nhiên.
- Log norm gradient theo từng layer, không chỉ norm tổng. Một layer tăng 10 lần mỗi bước dễ bị che bởi tổng.
- Chuẩn hóa không thay thế khởi tạo tốt, và clipping không chữa được công thức backward sai.
Chốt lại và bước tiếp theo
- Vanishing/exploding gradient là hệ quả của tích Jacobian: sigmoid giới hạn mỗi lớp ở 0.25, weight lớn thì norm nổ theo cấp số.
- Numerical stability nằm ở ba chỗ: độ chính xác float, catastrophic cancellation, và biên giới
log/exp. - Gradient check với central difference, chạy trên
float64và một tập con tham số, là cách duy nhất để biết backward của bạn đúng.
Deck ôn nhanh:
Front: Vì sao mạng sâu dùng sigmoid bị vanishing gradient? Back: Mỗi Jacobian có norm tối đa 0.25 nên tích L lớp giảm theo cấp số nhân.
Front: Đạo hàm lớn nhất của sigmoid bằng bao nhiêu? Back: 0.25, đạt tại z = 0.
Front: Dấu hiệu của exploding gradient? Back: Norm gradient tăng theo cấp số qua các layer, loss nhảy mạnh hoặc thành NaN.
Front: Machine epsilon của float32 xấp xỉ bao nhiêu? Back: 1.19e-7, cùng bậc với sai số sai phân nên gradient check phải dùng float64.
Front: Công thức central difference? Back: (f(x+h) - f(x-h)) / (2h), sai số cắt cụt O(h^2).
Front: Relative error bao nhiêu thì đáng nghi? Back: Trên 1e-4 là nghi ngờ, trên 1e-2 gần như chắc chắn công thức backward sai.
Front: Vì sao phải tắt dropout khi gradient check? Back: Vì forward không còn tất định, hai lần tính f lệch nhau vì nhiễu chứ không vì h.
Front: Xavier initialization đặt phương sai weight bằng bao nhiêu? Back: 2/(n_in + n_out), phù hợp với tanh và sigmoid.
Front: He initialization dùng cho activation nào? Back: ReLU, với Var(W) = 2/n_in.
Front: Gradient clipping theo norm làm gì? Back: Rescale gradient khi norm vượt ngưỡng, giữ nguyên hướng cập nhật.
Front: Vì sao không dùng forward difference (f(x+h) - f(x))/h? Back: Sai số O(h), kém chính xác hơn central difference khi cùng h.
Front: Vì sao compositional loss cộng dồn nên dùng accumulator float64? Back: Tránh sai số làm tròn tích lũy khi cộng nhiều số lớn với số nhỏ.
Bài tiếp theo là checkpoint của module: bạn sẽ ghép softmax ổn định, cross-entropy, gradient check và chẩn đoán gradient vào cùng một bài kiểm tra ngắn, trên đúng mạng 2 lớp của capstone.