Backpropagation: lan truyền ngược qua ví dụ tính tay
Quy tắc chuỗi, đồ thị tính toán, upstream và local gradient: tính tay gradient của f = (x + y)z, tự cài mạng 2 lớp bằng NumPy, đối chiếu autograd.
Học xong bài này, bạn sẽ
- Dùng quy tắc chuỗi trên đồ thị tính toán
- Hiểu upstream và local gradient
- Tự cài forward, backward cho mạng 2 lớp bằng NumPy
Nên học trước: Loss function và các optimizer: SGD, Momentum, Adam
Nội dung bài viết
- Vì sao không tính gradient "thủ công" từng trọng số?
- Nền tảng: quy tắc chuỗi (chain rule)
- Đồ thị tính toán và ví dụ f = (x + y)·z
- Quy tắc vàng: upstream × local
- Ví dụ thứ hai: một neuron sigmoid
- Backprop cho cả một lớp Linear (dạng ma trận)
- Tự cài backprop bằng NumPy: mạng 2 lớp học XOR
- Toàn cảnh quá trình học của neural network
- Parameters và Hyperparameters
- Tóm tắt
- Tài liệu tham khảo
Bài trước ta có công thức cập nhật \(W \leftarrow W - \eta\,\partial L/\partial W\). Câu hỏi còn lại: với một mạng có hàng triệu trọng số xếp thành hàng chục lớp, tính \(\partial L/\partial W\) cho từng trọng số bằng cách nào mà không mất cả năm? Câu trả lời là backpropagation (lan truyền ngược), thuật toán đứng sau mọi framework deep learning. Bài này giải thích bằng ví dụ tính tay, rồi tự cài đặt một mạng nhỏ bằng NumPy.
Số trang (ví dụ "trang 81") trong bài chỉ tới slide của bài giảng Deep Learning – Computer Vision – NLP (Thang Nguyen, Viet Nguyen); bài này giải thích chi tiết, kiểm chứng và đính chính các slide đó.
Vì sao không tính gradient "thủ công" từng trọng số?
Cách ngây thơ nhất là xấp xỉ bằng sai phân: nhích trọng số \(w\) một chút \(h\), xem loss đổi bao nhiêu:
\[ \frac{\partial L}{\partial w} \approx \frac{L(w + h) - L(w - h)}{2h} \]Mỗi trọng số cần 2 lần forward pass. ResNet-50 có khoảng 25,6 triệu tham số, tức hơn 51 triệu lần forward cho một bước cập nhật. Không khả thi. Backpropagation tính toàn bộ gradient chỉ với chi phí cỡ một lần forward + một lần backward, nhờ áp dụng quy tắc chuỗi một cách thông minh và tái sử dụng kết quả trung gian.
Nền tảng: quy tắc chuỗi (chain rule)
Nếu \(F(x) = f(g(x))\) thì
\[ F'(x) = f'(g(x)) \cdot g'(x) \]tức "đạo hàm hàm ngoài (giữ nguyên hàm trong) nhân đạo hàm hàm trong". Với hàm lồng nhiều tầng, chỉ cần nhân lần lượt:
\[ \frac{da}{dx} = \frac{da}{db}\cdot\frac{db}{dc}\cdot\frac{dc}{dd}\cdot\frac{dd}{de}\cdot\frac{de}{df}\cdot\frac{df}{dg}\cdot\frac{dg}{dx} \]Một mạng nơ-ron chính là một hàm lồng nhiều tầng như vậy: lớp 1 → activation → lớp 2 → … → loss.
Ví dụ (trang 47): \(y = (4x^3 - 5x^2 + 6x - 7)^{10}\). Hàm ngoài là \(u^{10}\), hàm trong là \(u = 4x^3-5x^2+6x-7\):
\[ \frac{dy}{dx} = 10\,(4x^3-5x^2+6x-7)^9\cdot(12x^2-10x+6) = 20\,(6x^2-5x+3)\,(4x^3-5x^2+6x-7)^9 \]Tại \(x = 1\): \(u = -2\), nên \(\frac{dy}{dx} = 20\cdot4\cdot(-2)^9 = -40960\). PyTorch tính ra đúng con số đó:
import torch
x = torch.tensor(1.0, requires_grad=True)
y = (4*x**3 - 5*x**2 + 6*x - 7) ** 10
y.backward()
print(x.grad) # tensor(-40960.)
Đồ thị tính toán và ví dụ f = (x + y)·z
Ta biểu diễn phép tính thành đồ thị tính toán (computational graph): mỗi nút là một phép toán đơn giản. Với \(f(x,y,z) = (x+y)z\), đặt biến trung gian \(q = x + y\), khi đó \(f = q\cdot z\).
Bước 1: Forward, tính và lưu các giá trị
Với \(x = -2,\ y = 5,\ z = -4\): \(q = 3\), \(f = 3\cdot(-4) = -12\).
Bước 2: Đạo hàm cục bộ (local gradient) tại từng nút
- Nút cộng \(q = x + y\): \(\dfrac{\partial q}{\partial x} = 1,\ \dfrac{\partial q}{\partial y} = 1\).
- Nút nhân \(f = qz\): \(\dfrac{\partial f}{\partial q} = z,\ \dfrac{\partial f}{\partial z} = q\).
Bước 3: Backward, đi ngược từ đầu ra
| Bước | Gradient | Cách tính | Giá trị |
|---|---|---|---|
| 1 | \(\partial f/\partial f\) | Luôn bằng 1 (điểm xuất phát) | 1 |
| 2 | \(\partial f/\partial z\) | \(= q\) | 3 |
| 3 | \(\partial f/\partial q\) | \(= z\) | −4 |
| 4 | \(\partial f/\partial y\) | \(\underbrace{\partial f/\partial q}_{\text{upstream}}\cdot\underbrace{\partial q/\partial y}_{\text{local}} = -4\cdot1\) | −4 |
| 5 | \(\partial f/\partial x\) | \(\partial f/\partial q\cdot\partial q/\partial x = -4\cdot1\) | −4 |
Diễn giải: \(\partial f/\partial x = -4\) nghĩa là tăng \(x\) thêm một lượng nhỏ \(h\) thì \(f\) giảm khoảng \(4h\). Kiểm tra bằng PyTorch:
x = torch.tensor(-2.0, requires_grad=True)
y = torch.tensor(5.0, requires_grad=True)
z = torch.tensor(-4.0, requires_grad=True)
f = (x + y) * z
f.backward()
print(f.item(), x.grad.item(), y.grad.item(), z.grad.item()) # -12.0 -4.0 -4.0 3.0
Quy tắc vàng: upstream × local
Nhìn từ góc độ một nút \(f\) bất kỳ với đầu vào \(x, y\) và đầu ra \(z\) (trang 56):
\[ \underbrace{\frac{\partial L}{\partial x}}_{\text{downstream}} = \underbrace{\frac{\partial L}{\partial z}}_{\text{upstream}} \cdot \underbrace{\frac{\partial z}{\partial x}}_{\text{local}} \]Đây là điểm mấu chốt làm backprop hiệu quả: mỗi nút chỉ cần biết đạo hàm của riêng nó, không cần biết gì về phần còn lại của mạng. Nó nhận gradient "từ trên dội xuống" (upstream), nhân với local gradient, rồi truyền tiếp xuống các đầu vào. Mạng phức tạp cỡ nào cũng chỉ là lặp lại thao tác này qua từng nút theo thứ tự ngược.
Thêm một quy tắc: nếu một biến được dùng ở nhiều nơi (rẽ nhánh), gradient của nó là tổng gradient từ các nhánh.
Các "cổng" hay gặp và cách chúng phân phối gradient
| Cổng | Forward | Backward |
|---|---|---|
| Cộng | \(z = x + y\) | Phân phát: cả \(x\) và \(y\) nhận nguyên upstream gradient |
| Nhân | \(z = xy\) | Đổi chéo: \(x\) nhận upstream·\(y\), \(y\) nhận upstream·\(x\) |
| Max | \(z = \max(x, y)\) | Định tuyến: chỉ đầu vào lớn nhất nhận gradient, cái còn lại nhận 0 (đây là cách ReLU và max-pooling lan truyền ngược) |
| Rẽ nhánh | \(x\) dùng ở 2 nơi | Cộng dồn gradient từ các nhánh |
Ví dụ thứ hai: một neuron sigmoid
\(f(w, x) = \sigma(w_0x_0 + w_1x_1 + w_2)\) với \(w_0 = 2, x_0 = -1, w_1 = -3, x_1 = -2, w_2 = -3\).
- Forward: \(z = -2 + 6 - 3 = 1\), \(f = \sigma(1) \approx 0.731\).
- Local gradient của sigmoid có dạng rất đẹp: \(\sigma'(z) = \sigma(z)(1-\sigma(z)) = 0.731\cdot0.269 \approx 0.197\).
- Từ \(z\) đi xuống qua các cổng nhân/cộng: \(\partial f/\partial w_0 = x_0\cdot0.197 = -0.197\), \(\partial f/\partial x_0 = w_0\cdot0.197 = 0.393\), \(\partial f/\partial w_1 = -0.393\), \(\partial f/\partial x_1 = -0.59\), \(\partial f/\partial w_2 = 0.197\).
Chú ý \(\sigma'(z)\) tối đa chỉ bằng 0.25. Khi xếp chồng 10 lớp sigmoid, gradient bị nhân với các số ≤ 0.25 mười lần và gần như biến mất: đó là vanishing gradient, sẽ phân tích kỹ ở bài Vanishing/Exploding gradient của chặng này.
Backprop cho cả một lớp Linear (dạng ma trận)
Trong thực tế ta không đi từng nút vô hướng mà xử lý cả ma trận. Với lớp \(Y = XW^T + b\) (\(X\): \(N\times D\), \(W\): \(K\times D\)), biết upstream \(dY = \partial L/\partial Y\) (\(N\times K\)):
\[ \frac{\partial L}{\partial W} = dY^T X, \qquad \frac{\partial L}{\partial X} = dY\, W, \qquad \frac{\partial L}{\partial b} = \sum_{n} dY_n \]Tự cài backprop bằng NumPy: mạng 2 lớp học XOR
XOR là bài toán linear classifier không giải được. Mạng dưới đây có một lớp ẩn 8 neuron (tanh), đầu ra sigmoid, loss binary cross-entropy; mọi gradient được tính tay theo quy tắc upstream × local:
import numpy as np
np.random.seed(0)
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float) # (4, 2)
y = np.array([[0], [1], [1], [0]], dtype=float) # (4, 1)
W1 = np.random.randn(2, 8) * 0.5; b1 = np.zeros((1, 8))
W2 = np.random.randn(8, 1) * 0.5; b2 = np.zeros((1, 1))
sigmoid = lambda z: 1 / (1 + np.exp(-z))
lr = 0.5
for step in range(5001):
# ---- forward ----
z1 = X @ W1 + b1 # (4, 8)
h1 = np.tanh(z1) # (4, 8)
z2 = h1 @ W2 + b2 # (4, 1)
p = sigmoid(z2) # (4, 1) xác suất
loss = -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)) # binary cross-entropy
# ---- backward (chain rule, từ cuối về đầu) ----
dz2 = (p - y) / len(X) # dL/dz2: sigmoid + BCE gộp lại rất gọn
dW2 = h1.T @ dz2 # (8, 1)
db2 = dz2.sum(axis=0, keepdims=True)
dh1 = dz2 @ W2.T # (4, 8) upstream cho lớp trước
dz1 = dh1 * (1 - h1 ** 2) # local gradient của tanh
dW1 = X.T @ dz1
db1 = dz1.sum(axis=0, keepdims=True)
# ---- cập nhật ----
W1 -= lr * dW1; b1 -= lr * db1
W2 -= lr * dW2; b2 -= lr * db2
if step % 1000 == 0:
print(f"step {step:4d} loss {loss:.4f}")
print(np.round(p.ravel(), 3)) # gần [0, 1, 1, 0]
Kết quả chạy thật: loss giảm từ 0.80 xuống 0.0005, đầu ra [0. 1. 0.999 0.001].
Gradient check: cách kiểm tra backprop tự viết
Khi tự cài backprop (ví dụ cho một layer mới), hãy so gradient giải tích với gradient số bằng sai phân trung tâm. Sai số tương đối cỡ \(10^{-7}\) trở xuống là đúng:
eps = 1e-5
W1[1, 3] += eps; lp = loss_fn() # loss_fn: chạy forward và trả về loss
W1[1, 3] -= 2 * eps; lm = loss_fn()
W1[1, 3] += eps # trả lại giá trị cũ
num = (lp - lm) / (2 * eps)
print(num, dW1[1, 3]) # 0.0411591695 0.0411591695 (sai số tương đối ~1e-11)
Toàn cảnh quá trình học của neural network
Ghép mọi mảnh lại (trang 57), một vòng lặp huấn luyện gồm:
- Forward propagation: đưa batch dữ liệu qua mạng, được dự đoán \(\hat y\).
- Loss: so \(\hat y\) với nhãn \(y\).
- Backward propagation: tính gradient của loss theo mọi trọng số.
- Optimizer: cập nhật trọng số (SGD, Adam…).
- Lặp lại cho đến khi loss đủ nhỏ hoặc hết số epoch.
Trong PyTorch, autograd tự động ghi lại đồ thị tính toán khi chạy forward và tự làm toàn bộ bước 3 khi gọi loss.backward():
for images, labels in train_loader:
outputs = model(images) # 1. forward
loss = criterion(outputs, labels) # 2. loss
optimizer.zero_grad() # xoá gradient cũ (PyTorch CỘNG DỒN gradient nếu không xoá)
loss.backward() # 3. backward
optimizer.step() # 4. cập nhật trọng số
Parameters và Hyperparameters
| Parameters (tham số mô hình) | Hyperparameters (siêu tham số) | |
|---|---|---|
| Là gì | Các trọng số \(w\), bias \(b\) bên trong mô hình | Các lựa chọn cấu hình do người huấn luyện đặt |
| Ai quyết định | Học tự động bằng gradient descent | Con người (thử nghiệm, grid/random search, Optuna…) |
| Ví dụ (deep learning) | Trọng số Conv, Linear, \(\gamma, \beta\) của BatchNorm | Learning rate \(\eta\), batch size, số epoch, số lớp, số neuron, optimizer, weight decay, tỉ lệ dropout |
| Ví dụ (ML cổ điển, trang 58) | Hệ số hồi quy \(w_0 \ldots w_m\) | max_depth, n_neighbors, C, gamma, kernel, test_size… |
Siêu tham số được chọn dựa trên kết quả trên tập validation, tuyệt đối không dựa trên tập test.
Tóm tắt
- Backprop = quy tắc chuỗi áp dụng có hệ thống trên đồ thị tính toán, chi phí cỡ một lần forward.
- Mỗi nút: downstream = upstream × local; nhánh rẽ thì cộng dồn.
- Cổng cộng phân phát, cổng nhân đổi chéo, cổng max định tuyến.
- Gradient luôn cùng kích thước với biến; dùng gradient check khi tự cài đặt.
- PyTorch:
zero_grad()→backward()→step().
Bài tiếp theo trong chặng: Batch Normalization, Dropout và Receptive Field. Muốn xem một pipeline huấn luyện hoàn chỉnh trên CIFAR-10, đọc thêm PyTorch cho Computer Vision: 7 bước huấn luyện.
Tài liệu tham khảo
- CS231n: Backpropagation, Intuitions (cs231n.github.io/optimization-2).
- Rumelhart, Hinton, Williams (1986). Learning representations by back-propagating errors, Nature.
- Christopher Olah. Calculus on Computational Graphs: Backpropagation (colah.github.io).
- Tài liệu PyTorch: A Gentle Introduction to torch.autograd.
Bài tập thực hành
- Thêm một lớp ẩn vào mạng XOR NumPy trong bài, tự viết phần backward và kiểm tra bằng gradient check.
Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.


