⭐ Vision Engineer · Chặng 10Bài 6/12Bài học~75 phút

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.

Backpropagation: lan truyền ngược qua ví dụ tính tay

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
  1. Vì sao không tính gradient "thủ công" từng trọng số?
  2. Nền tảng: quy tắc chuỗi (chain rule)
  3. Đồ thị tính toán và ví dụ f = (x + y)·z
  4. Quy tắc vàng: upstream × local
  5. Ví dụ thứ hai: một neuron sigmoid
  6. Backprop cho cả một lớp Linear (dạng ma trận)
  7. Tự cài backprop bằng NumPy: mạng 2 lớp học XOR
  8. Toàn cảnh quá trình học của neural network
  9. Parameters và Hyperparameters
  10. Tóm tắt
  11. 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\).

x-2-4 y5-4 z-43 + q3-4 × f-121 xanh: giá trị forward · cam: gradient ∂f/∂(·) lan ngược
Forward đi từ trái sang phải, backward đi từ phải sang trái (trang 49–55 của tài liệu)

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ướcGradientCách tínhGiá 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ổngForwardBackward
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ơiCộ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 \]
Mẹo nhớ: khớp kích thướcGradient theo một biến luôn có cùng kích thước với biến đó. \(\partial L/\partial W\) phải là \(K\times D\); chỉ có một cách nhân \(dY\) (\(N\times K\)) với \(X\) (\(N\times D\)) ra \(K\times D\) là \(dY^T X\). Không cần thuộc công thức, chỉ cần ghép cho khớp chiều.

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:

  1. Forward propagation: đưa batch dữ liệu qua mạng, được dự đoán \(\hat y\).
  2. Loss: so \(\hat y\) với nhãn \(y\).
  3. Backward propagation: tính gradient của loss theo mọi trọng số.
  4. Optimizer: cập nhật trọng số (SGD, Adam…).
  5. 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ìnhCác lựa chọn cấu hình do người huấn luyện đặt
Ai quyết địnhHọc tự động bằng gradient descentCon người (thử nghiệm, grid/random search, Optuna…)
Ví dụ (deep learning)Trọng số Conv, Linear, \(\gamma, \beta\) của BatchNormLearning 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

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

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.