Backpropagation
Lan truyền ngược
Mạng nơ-ron vừa đưa ra dự đoán SAI. Bạn cần sửa hàng triệu trọng số để nó dự đoán đúng hơn. Cách nào khả thi nhất?
Hình minh họa
Đồ thị tính toán — Forward & Backward Pass
Mạng 2 đầu vào → 3 nơ-ron ẩn → 1 đầu ra (tổng 9 trọng số). Nhấn Forward để xem dữ liệu chảy trái → phải. Nhấn Backward để xem gradient ∂L/∂w của mọi trọng số được tính theo chain rule chảy phải → trái. Nhấn Huấn luyện 1 bước để cập nhật trọng số bằng gradient descent.
α nhỏ → học chậm nhưng ổn định. α lớn → học nhanh nhưng có thể dao động hoặc phân kỳ.
Bước
0
ŷ
0.568
Target
0.800
Loss
0.0269
Chain rule — trái tim của backpropagation
Loss L phụ thuộc vào ŷ, ŷ phụ thuộc vào z_out, z_out phụ thuộc vào h, h phụ thuộc vào z, z phụ thuộc vào w. Đạo hàm của L theo w là tích các đạo hàm trung gian:
- Đầu ra của forward pass (z, h, ŷ) phải được lưu lại— backward sẽ cần dùng để tính đạo hàm sigmoid.
- Đạo hàm sigmoid đẹp ở chỗ chỉ cần giá trị activation: σ′(z) = σ(z)(1 − σ(z)).
- Với ReLU: σ′(z) = 1 nếu z > 0, ngược lại = 0 — rất rẻ tính.
Bạn đang huấn luyện mạng 100 lớp, mỗi lớp dùng sigmoid. Đạo hàm sigmoid max = 0.25. Gradient ở lớp đầu tiên sẽ như thế nào?
Đạo hàm ∂L/∂W2₁ = 0.12 với learning rate α = 0.5. Trọng số W2₁ hiện tại = 0.80. Sau cập nhật, W2₁ mới bằng bao nhiêu?
Giải thích
Lịch sử ngắn gọn
Ý tưởng backpropagation có nguồn gốc từ công trình của Seppo Linnainmaa (1970) về vi phân tự động theo kiểu ngược (reverse-mode automatic differentiation), và Paul Werbos (1974) áp dụng cho mạng nơ-ron trong luận án tiến sĩ. Tuy nhiên phải đến bài báo "Learning representations by back-propagating errors" của Rumelhart, Hinton & Williams (1986) thì cộng đồng mới nhìn ra sức mạnh của thuật toán này cho mạng đa tầng. Gần 40 năm sau, mọi framework deep learning (PyTorch, TensorFlow, JAX) đều xây dựng quanh nó.
Công thức cập nhật trọng số
Backprop chỉ trả ra gradient. Để thực sự điều chỉnh trọng số, ta cần kết hợp với thuật toán tối ưu hóa — phổ biến nhất là gradient descent:
Trong đó là learning rate (tốc độ học) và là gradient — đạo hàm riêng của hàm mất mát theo trọng số đó. Kết hợp với kỹ thuật vi tích phân, chain rule giúp tính gradient qua hàng trăm lớp một cách hiệu quả.
Cài đặt bằng tay (NumPy)
Đây là toàn bộ backprop cho mạng 2-3-1 của chúng ta, viết bằng NumPy thuần — không dùng framework nào. Bạn sẽ thấy nó ngắn đến không tưởng.
import numpy as np
# ─── Forward pass ─────────────────────────────────────────
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
# Dữ liệu & mục tiêu
x = np.array([0.6, 0.9])
y = 0.8
# Trọng số khởi tạo (tương ứng mạng 2-3-1 trong bài)
W1 = np.array([[0.15, -0.20],
[0.25, 0.30],
[-0.10, 0.40]]) # (3, 2)
W2 = np.array([0.35, -0.30, 0.50]) # (3,)
# Forward
z1 = W1 @ x # (3,)
h = sigmoid(z1) # (3,)
z2 = W2 @ h # scalar
y_hat = sigmoid(z2)
loss = 0.5 * (y - y_hat) ** 2
print(f"ŷ = {y_hat:.4f}, loss = {loss:.5f}")
# ─── Backward pass (chain rule bằng tay) ──────────────────
dL_dy = -(y - y_hat) # ∂L/∂ŷ
dy_dz2 = y_hat * (1 - y_hat) # σ'(z2)
dL_dz2 = dL_dy * dy_dz2 # scalar
dL_dW2 = dL_dz2 * h # (3,)
dL_dh = dL_dz2 * W2 # (3,)
dh_dz1 = h * (1 - h) # σ'(z1), (3,)
dL_dz1 = dL_dh * dh_dz1 # (3,)
dL_dW1 = np.outer(dL_dz1, x) # (3, 2)
# ─── Gradient descent update ──────────────────────────────
lr = 0.8
W1 -= lr * dL_dW1
W2 -= lr * dL_dW2
# Forward lại để xem loss đã giảm
h = sigmoid(W1 @ x)
y_hat = sigmoid(W2 @ h)
new_loss = 0.5 * (y - y_hat) ** 2
print(f"Sau 1 bước: ŷ = {y_hat:.4f}, loss = {new_loss:.5f}")Chạy script trên, bạn sẽ thấy loss giảm sau đúng 1 bước cập nhật. Chạy 50 bước trong vòng lặp, loss về gần 0. Đây chính xác là điều đang diễn ra trong đồ thị ở trên.
import torch
import torch.nn as nn
import torch.optim as optim
# Mạng 2-3-1
model = nn.Sequential(
nn.Linear(2, 3),
nn.Sigmoid(),
nn.Linear(3, 1),
nn.Sigmoid(),
)
x = torch.tensor([0.6, 0.9])
y = torch.tensor([0.8])
# Loss & optimizer
loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.8)
# Vòng lặp huấn luyện
for step in range(50):
# Forward
y_hat = model(x)
loss = loss_fn(y_hat, y)
# Backward — TỰ ĐỘNG tính gradient cho MỌI tham số
optimizer.zero_grad() # xóa gradient cũ
loss.backward() # ← đây là backprop
optimizer.step() # w <- w - lr * grad
if step % 10 == 0:
print(f"Step {step:3d} | loss = {loss.item():.5f}")
# Xem gradient của từng weight sau backward()
for name, param in model.named_parameters():
print(f"{name}: shape={tuple(param.shape)}, "
f"last grad norm={param.grad.norm().item():.4f}")Điểm khác biệt: với PyTorch, bạn không cần viết công thức đạo hàm cho sigmoid, linear layer, MSE,... Thư viện đã biết cách lấy đạo hàm mọi phép tính cơ bản, và tự xâu chuỗi chúng lại. Đây gọi là autograd (automatic differentiation). Nhưng bản chất nó đang làm chính xác những gì bạn vừa viết bằng tay ở NumPy ở trên.
Vì sao backprop là một cuộc cách mạng
Trước backprop, huấn luyện mạng đa tầng gần như bất khả thi — ta chỉ biết điều chỉnh trọng số ở lớp cuối (perceptron 1 lớp). Backprop mở khóa kiến trúc đa tầng và, khi kết hợp với GPU & lượng dữ liệu khổng lồ, dẫn trực tiếp đến Transformer, CNN, và cuối cùng là các mô hình ngôn ngữ lớn ngày nay.
- Backpropagation tính CHÍNH XÁC gradient (hướng + độ lớn) cho MỌI trọng số chỉ trong 1 lần duyệt ngược qua mạng.
- Công cụ toán học nền tảng là QUY TẮC CHUỖI (chain rule) — đạo hàm của hàm hợp = tích các đạo hàm trung gian.
- Forward pass lưu các activation (h, ŷ); backward pass dùng chúng để tính gradient qua sigmoid/ReLU.
- Cập nhật trọng số: w ← w − α · ∂L/∂w. Dấu TRỪ vì gradient chỉ hướng TĂNG của loss — ta muốn đi hướng ngược lại.
- Vanishing/Exploding gradient là thách thức chính ở mạng sâu — giải pháp: ReLU, BatchNorm, ResNet, LayerNorm, Gradient Clipping.
- Framework (PyTorch, TF, JAX) tự động làm backprop qua autograd, nhưng bản chất vẫn là chain rule đang chạy dưới mui xe.
Kiểm tra hiểu biết
Backpropagation dùng quy tắc toán học nào để tính gradient khi lan truyền ngược qua nhiều lớp?