Bài 2 xong, bạn đã có linear algebra đủ để hiểu forward pass: input vào, nhân ma trận, ra logits, softmax, chọn token. Mô hình đã “chạy”. Nhưng câu hỏi còn bỏ ngỏ: mô hình đã học thế nào? Ai đặt ra mấy trăm triệu con số trong embedding matrix, trong attention, trong MLP?

Câu trả lời là training. Và training = điều chỉnh hàng tỷ con số nhỏ dần. Điều chỉnh theo hướng nào? Cần calculus để trả lời.

Nhưng dev không cần giải tích cổ điển — không cần nhớ công thức tích phân hay định lý Taylor. Chỉ cần 3 khái niệm: derivative, gradient, chain rule. Và từ đó suy ra backpropagation là gì.

Mental model tổng quát: training loop

Trước khi đi vào từng thứ, đây là vòng lặp training:

for each batch:
    1. FORWARD:  input → weights → prediction → loss (1 số duy nhất)
    2. BACKWARD: từ loss, tính gradient cho TẤT CẢ weights
    3. UPDATE:   weights -= learning_rate * gradient

Ba bước, lặp hàng triệu lần. Loss là một con số đo độ sai của mô hình — càng nhỏ càng tốt. Bước backward (backprop) là lõi phức tạp nhất. Calculus là công cụ để thực hiện bước đó.

Nếu chỉ nhớ một câu: gradient cho biết mỗi weight nên thay đổi theo hướng nào để loss giảm.

Phần 1: Derivative — đo độ dốc

Derivative (đạo hàm) của hàm f tại điểm x là một số cho biết: nếu x thay đổi một chút, f(x) thay đổi nhanh bao nhiêu và theo chiều nào?

Ví dụ đơn giản nhất:

f(x) = x²
f'(x) = 2x

Tại x = 3:  f'(3) = 6  →  hàm đang tăng dốc
Tại x = 0:  f'(0) = 0  →  hàm đang ở đáy phẳng
Tại x = -2: f'(-2) = -4 →  hàm đang giảm

Dấu của derivative nói lên chiều: dương thì hàm đang tăng theo x, âm thì đang giảm. Giá trị tuyệt đối nói lên tốc độ: càng lớn thì độ dốc càng cao.

Muốn giảm f(x), bạn đi ngược chiều derivative: nếu f'(x) > 0, giảm x; nếu f'(x) < 0, tăng x.

Dev không cần giải đạo hàm bằng tay. Có thể tính số gần đúng bằng numerical derivative:

def numerical_derivative(f, x, h=1e-5):
    return (f(x + h) - f(x)) / h

f = lambda x: x ** 2

print(numerical_derivative(f, x=3.0))   # ~6.0
print(numerical_derivative(f, x=0.0))   # ~0.0
print(numerical_derivative(f, x=-2.0))  # ~-4.0

Công thức (f(x+h) - f(x)) / h là định nghĩa toán học của derivative được hiện thực hoá bằng code. h là số rất nhỏ (1e-5). PyTorch dùng automatic differentiation thay vì numerical để chính xác hơn và nhanh hơn — nhưng intuition là y hệt.

Phần 2: Gradient — đạo hàm cho hàm nhiều biến

Trong LLM thực tế, loss không phụ thuộc vào một biến x mà phụ thuộc vào hàng tỷ weights. Cần derivative theo từng weight một.

Gradient là vector chứa tất cả các partial derivative đó:

Loss phụ thuộc vào: w1, w2, w3, ..., w_N

∇L = [∂L/∂w1,  ∂L/∂w2,  ∂L/∂w3,  ...,  ∂L/∂w_N]

Ký hiệu ∂L/∂w1 đọc là “partial derivative của L theo w1” — tức là: giữ nguyên mọi thứ, chỉ thay đổi w1 một chút, loss thay đổi bao nhiêu?

Gradient ∇L (đọc “nabla L”) là một vector cùng chiều với weights. Nếu mô hình có 8 tỷ weights, gradient cũng là vector 8 tỷ phần tử.

Tính chất quan trọng nhất của gradient: nó chỉ hướng mà loss tăng nhanh nhất. Suy ra: đi ngược hướng gradient sẽ giảm loss nhanh nhất.

Đây là analogy quen thuộc: bạn đứng trên sườn núi, trời sương mù, muốn xuống thung lũng. Không nhìn thấy gì ngoài mặt đất dưới chân. Cảm nhận độ dốc bằng chân — hướng nào dốc lên nhất (gradient), rồi bước đúng hướng ngược lại. Từng bước nhỏ. Đó là gradient descent.

w_new = w_old - learning_rate * ∂L/∂w

learning_rate (thường là 1e-4 đến 3e-3) quyết định bước nhảy dài bao nhiêu. Quá lớn thì “nhảy” qua thung lũng, quá nhỏ thì hội tụ chậm.

Phần 3: Chain rule — trái tim của backprop

Vấn đề thực tế: loss không phụ thuộc trực tiếp vào weight đầu tiên. Phải đi qua rất nhiều hàm trung gian.

w1 → layer 1 output → layer 2 output → ... → prediction → loss

Chain rule giải quyết chuyện này. Với hàm hợp y = f(g(x)):

dy/dx = (dy/dg) * (dg/dx)

Đạo hàm của hàm hợp bằng tích các đạo hàm cục bộ tại từng bước.

Analogy để nhớ: giả sử bạn đổi tiền USD sang VND, rồi dùng VND để mua vàng. Bạn muốn biết: “nếu USD tăng 1 đô, số vàng tôi mua được thay đổi bao nhiêu?”

USD → VND:   1 USD = 25,000 VND  (tỷ lệ A)
VND → vàng:  1 chỉ = 8,500,000 VND  (tỷ lệ B, đảo ngược)

dVàng/dUSD = (dVàng/dVND) * (dVND/dUSD)
           = (1/8,500,000) * 25,000
           ≈ 0.00294 chỉ vàng per USD

Không cần biết toàn bộ chuỗi — chỉ cần nhân các tỷ lệ cục bộ lại với nhau.

Với neural network 2 layers đơn giản:

x1 → [w1, b1] → x2 = relu(w1*x1 + b1) → [w2, b2] → y = w2*x2 + b2 → Loss = (y - target)²

Muốn tính dL/dw1 (gradient của loss theo weight đầu tiên):

dL/dw1 = (dL/dy) * (dy/dx2) * (dx2/dw1)
        = chain qua loss → layer 2 → layer 1

Mỗi bước chỉ cần tính đạo hàm cục bộ tại điểm đó. Chain rule nối chúng lại.

Phần 4: Backpropagation — chain rule có hệ thống

Backprop không phải thuật toán mới — nó chỉ là cách áp dụng chain rule có hệ thống từ output về input, đi qua từng node trong computation graph.

FORWARD PASS (tính loss):
x1 → [layer 1] → x2 → [layer 2] → y → [loss fn] → L

BACKWARD PASS (tính gradient):
L → [dloss/dy] → y → [dy/dx2] → x2 → [dx2/dw1] → w1

Tại mỗi node:

  1. Nhận incoming gradient từ node phía sau (từ output về)
  2. Tính local gradient của node đó (đạo hàm cục bộ)
  3. Nhân chúng lại và pass tiếp sang node phía trước
grad_w1 = incoming_grad * local_grad_w1
grad_x1 = incoming_grad * local_grad_x1

“Back” trong backprop vì đi ngược chiều forward pass: forward đi input → loss, backward đi loss → weights.

LLM có hàng trăm layers, nhưng quy trình hoàn toàn giống: PyTorch xây computation graph trong forward pass, rồi gọi .backward() để chạy toàn bộ chain rule tự động, một lần duy nhất, cho tất cả weights.

# PyTorch lo toàn bộ
loss = criterion(predictions, targets)
loss.backward()       # <-- backprop chạy ở đây
optimizer.step()      # <-- update weights
optimizer.zero_grad() # <-- reset gradient cho batch tiếp theo

Ba dòng này là vòng lặp training của mọi mô hình PyTorch, từ MLP nhỏ đến GPT-4.

Phần 5: Tự code backprop cho 1 neuron

Dưới đây là backprop bằng tay cho một neuron, không dùng PyTorch, để thấy chain rule hoạt động thực tế ra sao.

import math

# --- Hàm kích hoạt ---
def sigmoid(z):
    return 1.0 / (1.0 + math.exp(-z))

def sigmoid_prime(z):
    s = sigmoid(z)
    return s * (1 - s)

# --- Setup: 1 neuron đơn giản ---
# y_hat = sigmoid(w * x + b)
# Loss  = (y_hat - target)^2

x      = 2.0    # input (cố định)
target = 1.0    # nhãn đúng
w      = 0.1    # weight khởi tạo ngẫu nhiên
b      = 0.0    # bias
lr     = 0.5    # learning rate

for step in range(10):
    # --- Forward pass ---
    z     = w * x + b          # pre-activation
    y_hat = sigmoid(z)          # prediction
    loss  = (y_hat - target)**2 # mean squared error

    # --- Backward pass (chain rule) ---
    # dL/dy_hat = 2 * (y_hat - target)
    dL_dyhat = 2 * (y_hat - target)

    # dy_hat/dz = sigmoid'(z)
    dyhat_dz = sigmoid_prime(z)

    # dz/dw = x,  dz/db = 1
    dz_dw = x
    dz_db = 1.0

    # Chain rule
    dL_dw = dL_dyhat * dyhat_dz * dz_dw
    dL_db = dL_dyhat * dyhat_dz * dz_db

    # --- Update ---
    w = w - lr * dL_dw
    b = b - lr * dL_db

    print(f"step {step:2d} | loss={loss:.4f} | w={w:.4f} | b={b:.4f}")

Output (loss giảm dần):

step  0 | loss=0.2891 | w=0.4153 | b=0.2076
step  1 | loss=0.1950 | w=0.6594 | b=0.3297
step  2 | loss=0.1241 | w=0.8490 | b=0.3945
step  3 | loss=0.0748 | w=0.9773 | b=0.4136
step  4 | loss=0.0434 | w=1.0579 | b=0.4030
step  5 | loss=0.0247 | w=1.1023 | b=0.3722
step  6 | loss=0.0138 | w=1.1226 | b=0.3312
step  7 | loss=0.0077 | w=1.1276 | b=0.2875
step  8 | loss=0.0042 | w=1.1232 | b=0.2452
step  9 | loss=0.0023 | w=1.1134 | b=0.2062

Loss giảm từ 0.29 xuống 0.002 sau 10 bước. Không có magic — chỉ là chain rule tính đạo hàm, rồi trừ gradient nhỏ. Scale lên hàng tỷ weights và hàng triệu bước thì đó là training GPT.

Sau khi đọc bài này, xem video “The spelled-out intro to neural networks and backpropagation: building micrograd” của Andrej Karpathy (1.5 tiếng). Ông làm y hệt nhưng với computation graph tổng quát hơn. Bạn sẽ hiểu gần như toàn bộ video chỉ với 3 khái niệm đã học ở đây.

Phần 6: Optimizer — vượt qua plain gradient descent

SGD (Stochastic Gradient Descent) vanilla là:

w = w - lr * gradient

Đơn giản nhưng có vấn đề: learning rate cố định cho mọi weight, trong khi weights khác nhau cần step size khác nhau. Weight nào đang “lắc” nhiều nên bước nhỏ lại; weight nào đang hội tụ chậm nên bước lớn hơn.

Adam (và AdamW — biến thể cho LLM) giải quyết chuyện này bằng hai trick:

  1. Momentum: nhớ hướng gradient của các bước trước, không thay đổi hướng đột ngột (như quả cầu lăn xuống dốc — không dừng ngay khi mặt đất phẳng ra)
  2. Adaptive learning rate: mỗi weight có lr riêng, tự điều chỉnh dựa trên lịch sử gradient của nó

Dev không cần hiểu math chi tiết của Adam. Cần biết:

OptimizerKhi nào dùng
SGD + momentumTraining từ đầu khi muốn kiểm soát
AdamDefault cho hầu hết task, hội tụ nhanh
AdamWDefault cho LLM training (có weight decay)

Hyperparameters quan trọng cần biết tên (dù chưa cần tune ngay):

  • learning_rate: thường 1e-4 đến 3e-4 cho LLM fine-tuning
  • batch_size: số samples mỗi bước update — lớn hơn thì gradient ổn hơn nhưng tốn GPU
  • epochs: số lần đi qua toàn bộ training data
  • weight_decay (trong AdamW): regularization, tránh overfit

Bộ mặc định tốt để bắt đầu fine-tune LLM nhỏ: lr=2e-4, AdamW, batch_size=8, weight_decay=0.01.

Cheatsheet

Khái niệmÝ nghĩaVí dụ
DerivativeĐộ dốc của hàm tại 1 điểmf(x)=x², f'(3)=6
GradientVector đạo hàm hàm nhiều biến∇L = [∂L/∂w1, ∂L/∂w2, ...]
Chain ruleĐạo hàm hàm hợp = tích đạo hàm cục bộdy/dx = (dy/dg) * (dg/dx)
BackpropChain rule từ loss ngược về weightsloss.backward() trong PyTorch
Gradient descentCập nhật weight ngược chiều gradientw -= lr * grad

Năm điểm phải nhớ:

  • Derivative = độ dốc. Dương tức hàm đang tăng, âm tức đang giảm.
  • Gradient = vector derivative theo từng weight. Chỉ hướng loss tăng nhanh nhất.
  • Đi ngược gradient → loss giảm. Đây là gradient descent.
  • Chain rule = cách tính gradient xuyên qua nhiều layer bằng cách nhân đạo hàm cục bộ.
  • Backprop = áp dụng chain rule từ output về input, có hệ thống, tự động.

Lời kết

Ba bài đầu đã xây nền: bài 1 là pipeline LLM end-to-end, bài 2 là linear algebra, bài 3 này là calculus. Bạn đã có đủ công cụ để hiểu tại sao training hoạt động — không phải magic, mà là chain rule chạy hàng tỷ lần.

Bài 4 là Probability cho LLM: tại sao output của model là xác suất, softmax hoạt động thế nào, cross-entropy loss là gì, và perplexity đo cái gì. Đây là mảnh ghép cuối của math foundation trước khi bắt tay code neural network thực sự.

Còn bây giờ: mở video Karpathy “The spelled-out intro to neural networks and backpropagation: building micrograd” trên YouTube. Sau bài này bạn sẽ hiểu được phần lớn video đó — và ông ấy build backprop từ zero trong Python thuần, không khác gì đoạn code 30 dòng ở Phần 5 nhưng tổng quát hơn nhiều.