Mở đầu: bịt mắt thả xuống sườn núi
Tưởng tượng bạn bị bịt mắt thả xuống một sườn núi trong sương mù và phải tìm đường xuống thung lũng. Bạn không nhìn thấy toàn cảnh — nhưng bàn chân cảm nhận được độ nghiêng ngay tại chỗ đứng. Chiến lược hiển nhiên: bước một bước nhỏ về phía dốc xuống, rồi cảm nhận lại, rồi bước tiếp. Không cần bản đồ, không cần biết thung lũng ở đâu — chỉ cần độ dốc tại một điểm, lặp đủ nhiều lần. Đó, nguyên văn, là gradient descent (GD — "đi xuống theo độ dốc") — thuật toán đang huấn luyện mọi mạng neural trên đời, từ bộ lọc spam tới GPT.
Bài 1 kết thúc bằng một món nợ: nghiệm giải tích "cho đạo hàm bằng 0" chỉ tồn tại cho model tuyến tính + MSE — model phi tuyến hay triệu tham số thì không có công thức nào để giải. Bài này trả món nợ đó: hiểu đạo hàm bằng hình học (không cần epsilon-delta), tính tay từng bước quy tắc cập nhật, khám phá 3 vùng hành vi của learning rate với ngưỡng chính xác (bò, zigzag, văng), phân biệt batch/mini-batch/SGD, và kết bằng demo thả hòn bi lăn trên địa hình loss — nơi bạn tận mắt thấy nó kẹt ở điểm yên ngựa rồi thoát ra nhờ nhiễu.
Tài nguyên ngoài: Khan Academy — Differential calculus, 3Blue1Brown — Essence of Calculus (trực quan hoá đạo hàm đẹp nhất internet).
1. Đạo hàm là "độ dốc tại một điểm"
Quên định nghĩa giới hạn trong sách giáo khoa một lát. Với mục đích của toàn bộ series này, chỉ cần một câu: đạo hàm của hàm $L$ tại điểm $w$ là độ dốc của đồ thị ngay tại điểm đó — phóng to đồ thị đủ gần quanh $w$, đường cong nào cũng trông như đường thẳng, và độ dốc của đường thẳng ấy chính là đạo hàm $\frac{dL}{dw}$. Dấu của nó cho biết hướng: đạo hàm dương nghĩa là đi sang phải thì $L$ tăng; đạo hàm âm nghĩa là đi sang phải thì $L$ giảm. Độ lớn cho biết dốc gắt cỡ nào.
Ví dụ cụ thể dùng suốt bài: $L(w) = (w - 3)^2$ — một parabol đáy tại $w = 3$, chính là dạng loss MSE của Bài 1 khi chỉ còn 1 tham số. Đạo hàm của nó là:
$$\frac{dL}{dw} = 2(w - 3)$$
Giải nghĩa: tại $w = 0$ (bên trái đáy), đạo hàm $= -6$ — âm, tức đi sang phải thì loss giảm, và dốc khá gắt. Tại $w = 2{,}9$ (sát đáy), đạo hàm $= -0{,}2$ — vẫn âm nhưng gần như phẳng. Tại đúng đáy $w = 3$, đạo hàm $= 0$ — hết dốc, đứng ở điểm thấp nhất. Nhận xét then chốt: đạo hàm tự nhỏ dần khi tới gần đáy — sự thật này sẽ làm gradient descent tự giảm tốc mà không cần ai ra lệnh.
Khi model có nhiều tham số ($w_1, w_2, \ldots$), mỗi tham số có đạo hàm riêng (đạo hàm riêng phần), và vector gom tất cả lại gọi là gradient, ký hiệu $\nabla L$. Sự thật hình học quan trọng nhất bài: gradient luôn chỉ về hướng làm $L$ TĂNG nhanh nhất tại điểm đang đứng. Muốn giảm loss? Đi ngược hướng gradient — đó là chữ "descent" (đi xuống) trong tên thuật toán. Và đây là điều đẹp nhất: ta không cần giải phương trình nào cả — chỉ cần biết hướng dốc tại chỗ đứng hiện tại, lặp đủ nhiều, trọng lực toán học lo phần còn lại.
2. Thuật toán Gradient Descent: một dòng công thức
Toàn bộ thuật toán là một quy tắc cập nhật lặp đi lặp lại:
$$w \leftarrow w - \eta \cdot \nabla L(w)$$
Giải nghĩa từng ký hiệu: $w$ là tham số hiện tại; $\nabla L(w)$ là gradient của loss tại đó (với 1 tham số thì chính là $\frac{dL}{dw}$); $\eta$ (eta) là learning rate — kích thước bước chân, con số quan trọng nhất bạn sẽ chỉnh trong cả sự nghiệp ML; mũi tên $\leftarrow$ nghĩa là "gán giá trị mới". Dấu trừ là linh hồn: gradient chỉ hướng tăng, ta trừ đi để đi xuống.
Tính tay 3 bước với $L(w) = (w-3)^2$, xuất phát $w_0 = 0$, learning rate $\eta = 0{,}25$ (mọi con số đều nằm trong self-test của file tải về):
| Bước | $w$ hiện tại | Gradient $2(w-3)$ | Cập nhật $w - 0{,}25 \cdot \text{grad}$ | Loss mới $(w-3)^2$ |
|---|---|---|---|---|
| 1 | $0$ | $-6$ | $0 - 0{,}25 \cdot (-6) = 1{,}5$ | $2{,}25$ |
| 2 | $1{,}5$ | $-3$ | $1{,}5 + 0{,}75 = 2{,}25$ | $0{,}5625$ |
| 3 | $2{,}25$ | $-1{,}5$ | $2{,}25 + 0{,}375 = 2{,}625$ | $0{,}140625$ |
Nhìn cột gradient: $-6 \to -3 \to -1{,}5$ — mỗi bước nhỏ đi một nửa, nên bước chân tự co lại khi gần đáy, đúng như Mục 1 dự báo. Loss rơi từ $9$ xuống $0{,}14$ chỉ sau 3 bước. Code hoá nguyên văn:
function gd1d(w0, lr, steps) {
let w = w0;
for (let i = 0; i < steps; i++) {
const grad = 2 * (w - 3); // dL/dw cua (w-3)² — Bai 7 se TU DONG tinh cai nay
w = w - lr * grad; // quy tac cap nhat: nguoc huong gradient
}
return w;
}
// gd1d(0, 0.25, 3) → 2.625 (khop bang tinh tay)
// gd1d(0, 0.25, 20) → 2.999997 ≈ 3
Giờ tới nhân vật chính: learning rate. Với parabol này, mỗi bước có công thức đóng $w_{t+1} - 3 = (1 - 2\eta)(w_t - 3)$ — khoảng cách tới đáy bị nhân với hệ số $k = 1 - 2\eta$ sau mỗi bước, nên hành vi suy ra được chính xác:
| Learning rate $\eta$ | Hệ số $k = 1 - 2\eta$ | Hành vi (số liệu từ self-test) |
|---|---|---|
| $0{,}1$ (quá nhỏ) | $0{,}8$ | Bò — sau 10 bước mới tới $w = 2{,}678$, còn cách đáy cả quãng |
| $0{,}25$ (vừa) | $0{,}5$ | Mượt — mỗi bước gần đáy gấp đôi, 20 bước là $\approx 3{,}0$ |
| $0{,}75$ (lớn) | $-0{,}5$ | Zigzag — nhảy qua lại 2 bên đáy: $0 \to 4{,}5 \to 2{,}25 \to 3{,}375 \to 2{,}81$… nhưng vẫn hội tụ |
| $1{,}05$ (quá lớn) | $-1{,}1$ | Văng — khoảng cách tới đáy TĂNG $10\%$ mỗi bước: $3 \to 3{,}3 \to 3{,}6 \to 4{,}0$…, loss nổ |
Ranh giới ba vùng ($\eta < 0{,}5$ mượt, $0{,}5 < \eta < 1$ zigzag, $\eta > 1$ văng) là đặc sản của đúng parabol này — loss thật không cho công thức đóng như vậy. Nhưng ba kiểu hành vi thì phổ quát: mọi người làm ML đều từng gặp cả ba, và demo Mục 5 cho bạn kéo slider để tự tạo ra từng kiểu.
gd1d(0, 0.5, 1) trả về
đúng $3{,}0$ — chạm đáy chỉ sau 1 bước duy nhất, thay vì 20 bước như $\eta = 0{,}25$.
Đây không phải may mắn: với loss bậc hai $L(w) = a(w - w^*)^2$ bất kỳ, learning rate lý tưởng luôn là
$\eta = 1/(2a)$ — chọn đúng nó thì gradient descent suy biến thành nghiệm giải tích của Bài 1 sau đúng
một bước. Tiếc là loss của mạng neural thật không phải một parabol đơn giản có "điểm ngọt" tính sẵn được
như vậy — đây chính là lý do learning rate luôn phải dò bằng thực nghiệm (Bài 9 bàn kỹ hơn).
gd1d(0, 1.0, 6) cho ra dãy $0 \to 6 \to 0 \to 6 \to 0 \to 6$ — dao động
vĩnh viễn giữa hai điểm cách đều đáy, không bao giờ tiến gần hơn cũng không bao giờ
văng xa hơn (loss giữ nguyên $9$ ở mọi bước chẵn). Đây là ranh giới toán học chính xác giữa hội tụ và
phân kỳ — trong thực tế gần như vô nghĩa (chọn đúng $\eta$ bằng chính xác ranh giới là chuyện hiếm),
nhưng hiểu nó giúp thấy rõ "văng" không phải bật/tắt đột ngột mà là một phổ liên tục quanh $|k| = 1$.
Phản xạ tự nhiên khi thấy loss tăng qua từng epoch là "code sai rồi" — và người mới thường
mất cả buổi soi lại công thức gradient. Nhưng như bảng trên chứng minh bằng số:
thuật toán hoàn toàn đúng vẫn phân kỳ khi $\eta$ quá lớn — mỗi bước nhảy xa hơn qua
đáy, khoảng cách nhân $1{,}1$ lần mỗi bước, loss tăng dần rồi nổ thành
Infinity/NaN. Quy trình chẩn đoán đúng thứ tự:
- Giảm learning rate 10 lần (vd $0{,}1 \to 0{,}01$) rồi chạy lại;
- Loss chịu giảm → nguyên nhân là $\eta$, tăng dần lại tới mức nhanh mà chưa nổ;
- Loss vẫn tăng ngay cả khi $\eta$ tí hon → giờ mới đáng nghi code (thường là dấu: cộng gradient thay vì trừ).
Thử ngay trong demo Mục 5: kéo $\eta$ lên vùng đỏ và nhìn hòn bi văng khỏi thung lũng.
3. Batch, mini-batch và SGD: tính gradient trên bao nhiêu dữ liệu?
Loss của Bài 1 là trung bình trên toàn bộ dữ liệu — nên gradient chuẩn cũng phải tính trên toàn bộ dữ liệu. Với 4 điểm thì chuyện nhỏ; với 10 triệu ảnh thì mỗi một bước cập nhật phải cày hết 10 triệu ảnh — quá đắt. Ba chiến lược, khác nhau đúng một chỗ: mỗi bước dùng bao nhiêu mẫu để ước lượng gradient (chiến lược thứ hai mang tên SGD — gradient descent "ngẫu nhiên", vì gradient ước lượng từ mẫu bốc ngẫu nhiên):
| Chiến lược | Mẫu/bước | Gradient | Đánh đổi |
|---|---|---|---|
| Batch GD | Toàn bộ dataset | Chính xác, mượt | Mỗi bước cực đắt; ngốn bộ nhớ; quỹ đạo tất định |
| SGD thuần | 1 mẫu | Rất nhiễu | Bước siêu rẻ nhưng loss nhảy loạn xạ, khó song song hoá |
| Mini-batch (mặc định công nghiệp) | 32–512 mẫu | Nhiễu vừa phải | Cân bằng tốc độ/ổn định; tận dụng phần cứng song song |
// Batch GD: 1 buoc = gradient trung binh tren TOAN BO data
for (let epoch = 0; epoch < E; epoch++) {
const g = avgGradient(data, w); // cay het N mau moi duoc 1 buoc
w = w - lr * g;
}
// SGD thuan: 1 buoc = gradient cua DUNG 1 mau (nhieu nhat)
for (let epoch = 0; epoch < E; epoch++) {
for (const sample of shuffle(data)) {
w = w - lr * gradient(sample, w); // N buoc re moi epoch
}
}
// Mini-batch (mac dinh cong nghiep): 1 buoc = trung binh 1 LO nho
for (let epoch = 0; epoch < E; epoch++) {
for (const batch of chunks(shuffle(data), 64)) {
w = w - lr * avgGradient(batch, w); // nhieu vua phai, chay song song tot
}
}
Trực giác dễ nhầm ở đây: nhiễu nghe như khuyết điểm phải chịu đựng để đổi lấy tốc độ. Sự thật thú vị hơn — nhiễu của mini-batch đôi khi là điều tốt. Gradient nhiễu nghĩa là thỉnh thoảng bước lệch khỏi hướng dốc nhất, và cú lệch ngẫu nhiên đó có thể đá hòn bi văng khỏi những vùng địa hình xấu — đặc biệt là điểm yên ngựa mà Mục 4 sắp mô tả — nơi batch GD tất định sẽ nằm im vĩnh viễn. Demo Mục 5 chứng minh điều này bằng một nút bấm.
4. Địa hình loss: local minimum, saddle point, plateau
Parabol của Mục 2 là địa hình trong mơ: một đáy duy nhất, đi xuống là chắc chắn tới. Loss của mạng neural thật (từ Bài 6 trở đi) là địa hình triệu chiều gồ ghề, với 3 dạng địa hình xấu kinh điển:
| Địa hình | Là gì | Vì sao gây khó |
|---|---|---|
| Local minimum | Đáy thung lũng con — thấp hơn mọi điểm lân cận nhưng không phải thấp nhất toàn cục | Gradient = 0, GD dừng lại dù còn thung lũng sâu hơn ở nơi khác |
| Saddle point (điểm yên ngựa) | Gradient = 0 nhưng KHÔNG phải đáy: dốc xuống theo hướng này, dốc lên theo hướng khác | GD tất định đứng im hoặc bò cực chậm quanh nó |
| Plateau (cao nguyên) | Vùng gần phẳng — gradient tí hon nhưng chưa tới đáy nào | Bước chân co theo gradient nên tiến độ gần như đóng băng |
Trực giác 2D khiến local minimum nghe đáng sợ nhất — nhìn địa hình đồi núi ai chẳng sợ kẹt nhầm thung lũng. Nhưng trong không gian triệu chiều, bức tranh đảo ngược:
loss.backward() bí ẩn trong snippet PyTorch cuối bài — Bài 7 ta sẽ tự xây nó từ số 0.
5. Thực hành: thả hòn bi lên loss landscape
Hai demo, đúng các hàm đã học. Demo A — parabol $L(w) = (w-3)^2$ của Mục 2: kéo slider learning rate, bấm ▶ xem hòn bi lăn; tự tái tạo đủ 3 kiểu hành vi trong bảng (thử đúng các mốc $0{,}1$ / $0{,}75$ / $1{,}05$):
Mốc đáng thử: η = 0.1 (bò) · 0.25 (mượt) · 0.75 (zigzag qua lại 2 bên đáy) · trên 1.0 (văng — loss tăng dần, đúng cạm bẫy Mục 2). Bi văng khỏi khung là hành vi đúng của phân kỳ, không phải lỗi.
Demo B — địa hình 2 đáy $f(x, y) = (x^2-1)^2 + y^2$ nhìn từ trên xuống, vẽ bằng cách tô
từng pixel theo độ cao qua ImageData (kỹ thuật của
Series Canvas Bài 11; màu đậm = thấp): hai thung lũng
tại $(\pm 1, 0)$, một điểm yên ngựa ngay giữa tại $(0, 0)$. Phần lõi của mỗi bước GD 2 chiều — kèm nhiễu
seeded mô phỏng mini-batch — chỉ vài dòng:
// f(x,y) = (x²-1)² + y² — dao ham rieng theo tung truc
function grad2d(x, y) {
return { gx: 4 * x * (x * x - 1), gy: 2 * y };
}
// 1 buoc GD; noise() la PRNG seed CO DINH (mulberry32) — bat len de
// mo phong gradient nhieu cua mini-batch, tai lap duoc moi lan chay
function step(p, lr, noise) {
const { gx, gy } = grad2d(p.x, p.y);
const nx = noise ? (noise() - 0.5) * 0.6 : 0;
const ny = noise ? (noise() - 0.5) * 0.6 : 0;
return { x: p.x - lr * (gx + nx), y: p.y - lr * (gy + ny) };
}
// Xuat phat (0, 0.8) khong nhieu → ket saddle (0, 0) vinh vien
// CUNG cho do + nhieu seed 42 → thoat ve day (-0.999, 0.004)
Bấm lên bản đồ để chọn điểm xuất phát. Xuất phát đúng trên sống núi $x = 0$ (bấm nút 🎯): gradient theo $x$ bằng 0 tuyệt đối, hòn bi trượt thẳng xuống… rồi nằm im vĩnh viễn tại yên ngựa. Bật "nhiễu SGD" và chạy lại từ cùng chỗ — cú lệch ngẫu nhiên đầu tiên đá nó rơi vào một thung lũng:
Hòn bi đứng im tại (0, 0) khi xuất phát trên sống núi KHÔNG phải demo hỏng — đó chính là điểm yên ngựa: gradient bằng 0 đúng theo hướng cần thoát. Bật nhiễu rồi chạy lại từ cùng chỗ để thấy vì sao "nhiễu là điều tốt" (Mục 3).
Đối chiếu công nghiệp: PyTorch làm đúng vòng lặp Mục 2, chỉ khác nó tự tính gradient. Bản 1-1 với ví dụ parabol (đã kiểm chứng bằng mô phỏng lại trong Node — 20 bước, $\eta = 0{,}25$, hội tụ về $2{,}999997$):
# Doi chieu 1-1 voi vi du parabol — cai dat: pytorch.org/get-started/locally
import torch
w = torch.tensor(0.0, requires_grad=True) # w0 = 0, bao PyTorch theo doi gradient
opt = torch.optim.SGD([w], lr=0.25) # eta = 0.25 — dung bang tinh tay
for step in range(20):
opt.zero_grad() # xoa gradient cu (Bai 7 giai thich vi sao)
loss = (w - 3) ** 2 # L(w) = (w-3)²
loss.backward() # TU DONG tinh dL/dw — Bai 7 se tu xay
opt.step() # w ← w − lr·grad
print(w.item()) # ≈ 3.0 (2.999997 — khop mo phong Node)
Tóm lược
Món nợ "phương pháp lặp" của Bài 1 đã trả xong:
- ✅ Đạo hàm = độ dốc tại một điểm; gradient chỉ hướng loss TĂNG nhanh nhất → đi ngược để giảm — không cần giải phương trình, chỉ cần hướng dốc tại chỗ.
- ✅ Một dòng công thức $w \leftarrow w - \eta \nabla L$: gradient tự nhỏ dần gần đáy nên bước chân tự co; tính tay được từng bước ($0 \to 1{,}5 \to 2{,}25 \to 2{,}625$).
- ✅ Learning rate có 3 vùng số phận: bò – zigzag – văng; "loss tăng dần" gần như luôn là $\eta$ quá lớn, giảm 10 lần trước khi nghi code sai.
- ✅ Mini-batch là mặc định công nghiệp: gradient nhiễu vừa phải đổi lấy tốc độ — và chính nhiễu đó giúp thoát điểm yên ngựa.
- ✅ Triệu chiều đảo ngược trực giác: saddle point/plateau mới là kẻ thù chính, local minimum ít đáng sợ hơn hình dung 2D.
Tải file code thực hành minh họa bài học
File JavaScript gồm GD 1 tham số + địa hình 2 đáy (kèm nhiễu seeded) của cả 2 demo, với self-test đối
chiếu đúng bảng tính tay và các ngưỡng learning rate (chạy node gradient_descent.js):
Bình luận