~3-4 giờ · cần xong bài 3
Đích của bài này: hiểu vì sao gradient là hướng dốc nhất — và thấy rằng câu trả lời không nằm trong giải tích, nó nằm ở tích vô hướng của môn 2 bài 1. Đây là chỗ hai môn khoá vào nhau.
⚡ 30 phút đầu — làm trước, đọc sau
Đừng đọc hết file rồi mới gõ. Mở terminal ngay:
import numpy as np
f = lambda p: p[0]**2 + p[1]**2 # cái bát
P = np.array([3.0, 4.0])
# 1. tự viết đạo hàm riêng bằng central difference: (f(p+h·e) − f(p−h·e)) / 2h
# đích: ∂f/∂x = 6 ∂f/∂y = 8 grad = (6, 8) |grad| = 10
# 2. tự viết đạo hàm theo hướng: đo THẲNG dọc theo u, KHÔNG dùng gradient
# đích: u=(1,0) → 6 u=(0,1) → 8
# u=(0.6,0.8) → 10 u=(-0.8,0.6) → 0 u=(-0.6,-0.8) → -10
# 3. quét 3600 hướng đơn vị quanh vòng tròn, tìm hướng cho đạo hàm LỚN NHẤT
# đích: max ≈ 10.0 tại góc = 53.1° (lưới 3600 hướng → bước 0.1°, chỉ đọc được tới 1 chữ số thập phân)
# → so với arctan2(8, 6) = 53.1301° — lệch 0.0301°, tức nhỏ hơn nửa bước lưới (0.05°)
Ra đúng ba nhóm số → đọc tiếp §1-3 để hiểu vì sao hướng max lại đúng bằng hướng gradient,
và vì sao giá trị max lại đúng bằng |grad|. Hai trùng khớp đó không phải ngẫu nhiên.
Ra sai ở bước 2 → gần như chắc chắn bạn quên chuẩn hoá u. Xem 🪤 Bẫy 1.
1. Đạo hàm riêng — đóng băng mọi biến trừ một
Bài 1-3 làm việc với f(x): một đầu vào, một đầu ra. Bề mặt loss thật thì không vậy — nó là
f(w₁, w₂, ..., w_n), hàng triệu đầu vào, một số ra.
Câu hỏi tự nhiên: "đạo hàm theo cái gì?" Câu trả lời đầu tiên là: theo từng biến một.
Đạo hàm riêng
∂f/∂x= đóng băng tất cả biến khác, coiflà hàm một biến củax, rồi lấy đạo hàm.
Ký hiệu ∂ (chữ "d cong") chỉ để nhắc bạn rằng có những biến khác đang bị giữ yên.
Ví dụ tính tay — f(x, y) = x²y tại điểm (3, 2):
∂f/∂x : coi y = 2 là hằng số → f = 2x² → ∂f/∂x = 4x → tại x=3 : 12
(công thức tổng quát: ∂f/∂x = 2xy = 2·3·2 = 12)
∂f/∂y : coi x = 3 là hằng số → f = 9y → ∂f/∂y = 9
(công thức tổng quát: ∂f/∂y = x² = 3² = 9)
Kiểm bằng central difference (h = 1e-6): ra đúng 12.0 và 9.0.
Ẩn dụ backend: đạo hàm riêng chính là A/B test một tham số. Bạn đo p99 latency, đổi
thread_pool_size lên một nấc, giữ nguyên mọi config khác, xem latency nhích bao nhiêu. Đó là
∂latency/∂thread_pool. Đổi cả 5 config cùng lúc thì không đo được cái gì cả — và toán cũng vậy.
Bề mặt loss
Từ giờ hãy giữ hình ảnh này: f(x, y) là một bề mặt địa hình. (x, y) là toạ độ trên bản đồ,
f là độ cao. Đường đồng mức (level set / contour) là tập điểm có cùng độ cao — đúng như bản đồ
địa hình, hoặc như biểu đồ heatmap của Grafana.
f = x² + y²là một cái bát (paraboloid), đường đồng mức là các vòng tròn.f = x² + 10y²cũng là bát, nhưng dẹt: đường đồng mức là ellipse dẹt. Nhớ hàm này — nó là ngôi sao của mục 🪤.
2. Gradient là một VECTOR, không phải một số
Gom tất cả đạo hàm riêng lại thành một vector:
∇f(x, y) = ( ∂f/∂x , ∂f/∂y )
Với f = x²y tại (3, 2):
∇f = (12, 9) |∇f| = √(144 + 81) = √225 = 15
hướng đơn vị = (12, 9)/15 = (0.8, 0.6)
(Số đẹp có chủ ý: 9-12-15 là tam giác 3-4-5 phóng to 3 lần.)
Đây là bước nhảy khái niệm của cả bài. Đạo hàm một biến là một SỐ (độ dốc). Đạo hàm nhiều biến là một VECTOR — và vector đó sống trong đúng không gian mà tham số đang sống.
Hệ quả rất thực dụng: nếu bạn có n tham số thì ∇f cũng có n thành phần, cùng shape với tham số.
Đó là lý do dòng cập nhật w ← w − lr·∇f(w) cộng trừ được — hai vế cùng loại, cùng chiều. Nếu shape
không khớp thì bạn đã tính sai gradient, không phải "cần reshape".
Và ba tính từ của vector đều có nghĩa:
Thuộc tính của ∇f |
Nghĩa trên bề mặt |
|---|---|
| Hướng | Hướng đi lên dốc nhất tại điểm đó |
Độ dài |∇f| |
Độ dốc lớn nhất tại điểm đó |
∇f = 0 |
Mặt đất phẳng — điểm dừng (§4) |
Ba dòng trên nghe như định nghĩa. Chúng không phải định nghĩa — chúng là định lý, và §3 chứng minh chúng trong ba dòng.
3. ⭐ Đạo hàm theo hướng — chỗ môn 2 và môn 3 khoá vào nhau
Đây là mục trung tâm. Nếu bài này chỉ được giữ lại một mục, giữ mục này.
3.1. Câu hỏi
Đạo hàm riêng chỉ trả lời cho hai hướng đặc biệt: dọc trục x, dọc trục y. Nhưng bạn có thể
bước theo bất kỳ hướng nào. Câu hỏi đúng phải là:
Đứng ở
p, bước theo hướng đơn vịu.fthay đổi nhanh cỡ nào?
Định nghĩa thẳng, không mượn gradient:
D_u f(p) = lim [ f(p + h·u) − f(p) ] / h với |u| = 1
h→0
Đây là đạo hàm một biến của hàm g(t) = f(p + t·u)
tại t = 0 — tức "cắt bề mặt bằng một mặt phẳng đứng dọc hướng u, rồi lấy độ dốc của đường cắt".
Hàm sai phân trung tâm ở phần ⚡ xấp xỉ đạo hàm này khi hàm đủ trơn. Chỉ có giới hạn đối xứng
chưa đủ chứng minh đạo hàm tồn tại: |t| tại 0 là phản ví dụ.
3.2. Định lý — một dòng
D_u f(p) = ∇f(p) · u ← TÍCH VÔ HƯỚNG (môn 2, bài 1)
Vì sao đúng: f(p + t·u) thay đổi do x nhích t·u₁ và y nhích t·u₂. Đóng góp của x là
(∂f/∂x)·u₁, của y là (∂f/∂y)·u₂. Cộng lại — đó là ∇f · u. (Đây chính là chain rule của
bài 3, viết cho hai đường vào thay vì một.)
Kiểm bằng số — f = x² + y² tại (3, 4), ∇f = (2x, 2y) = (6, 8), |∇f| = 10:
u (đơn vị) |
∇f · u tính tay |
central difference | Đọc là |
|---|---|---|---|
(1, 0) |
6·1 + 8·0 = 6 |
6.000000 | dốc dọc trục x |
(0, 1) |
6·0 + 8·1 = 8 |
8.000000 | dốc dọc trục y |
(0.6, 0.8) |
3.6 + 6.4 = 10 |
10.000000 | ⭐ LỚN NHẤT |
(−0.8, 0.6) |
−4.8 + 4.8 = 0 |
0.000000 | đi ngang, không lên không xuống |
(−0.6, −0.8) |
−3.6 − 6.4 = −10 |
−10.000000 | ⭐ NHỎ NHẤT |
Chú ý (0.6, 0.8) = (6, 8)/10 — đúng bằng gradient đã chuẩn hoá.
3.3. Vì sao gradient LÀ hướng dốc nhất
Giờ dùng lại công thức của môn 2 bài 1:
∇f · u = |∇f| · |u| · cos θ = |∇f| · cos θ (vì |u| = 1)
|∇f| là một hằng số tại điểm đó. Thứ duy nhất bạn điều khiển được là cos θ, và cos θ ∈ [−1, 1].
Vậy:
θ giữa u và ∇f |
cos θ |
D_u f |
Ý nghĩa |
|---|---|---|---|
0° — cùng hướng ∇f |
1 | +|∇f| = 10 |
dốc lên nhất có thể |
90° — vuông góc |
0 | 0 | đi dọc đường đồng mức |
180° — ngược ∇f |
−1 | −|∇f| = −10 |
dốc xuống nhất có thể |
Quét 100.000 hướng đơn vị quanh vòng tròn bằng máy: max = 10.0 tại u = (0.600018, 0.799986),
min = −10.0. Khớp ∇f/|∇f| = (0.6, 0.8) và |∇f| = 10.
Điểm cần khắc sâu, đọc chậm: Gradient không được định nghĩa là "hướng dốc nhất". Nó được định nghĩa là "gom đạo hàm riêng lại thành vector" — một thao tác hành chính, chả có gì hình học.
Việc nó trở thành hướng dốc nhất là một hệ quả của tích vô hướng:
a·blớn nhất khibcùng hướnga. Bạn đã biết sự thật đó từ môn 2 bài 1, ở dạng "tích vô hướng đo mức độ cùng hướng".Đó là toàn bộ lý do gradient descent đi theo
−∇f. Không phải quy ước, không phải may mắn.
3.4. Ba món quà đi kèm
(a) −∇f là hướng dốc xuống nhất. Đây là dòng w ← w − lr·∇f. Dấu trừ có nguồn gốc, không phải cho vui.
(b) Gradient vuông góc với đường đồng mức. Đi dọc đường đồng mức nghĩa là f không đổi, tức
D_u f = 0, tức ∇f · u = 0 — mà môn 2 bài 1 dạy rồi: tích vô hướng bằng 0 là vuông góc.
Kiểm: tại (3,4) trên f = x²+y², đường đồng mức là vòng tròn bán kính 5; tiếp tuyến của nó là
(−0.8, 0.6). Máy trả ∇f · (−0.8, 0.6) = −8.88e-16 — bằng 0 tới sai số máy.
(c) D_u f chính là độ dài cái bóng. Nhớ phép chiếu ở môn 2 bài 1: a·b/|b| là độ dài bóng của
a trên b. Vì |u| = 1, ∇f · u chính là độ dài bóng của ∇f chiếu lên hướng u. Gradient
là "toàn bộ độ dốc"; đạo hàm theo hướng là "phần độ dốc chiếu được xuống hướng bạn chọn". Chọn hướng
vuông góc → bóng bằng 0 → không có độ dốc nào chiếu xuống được.
4. Điểm dừng: cực tiểu, cực đại — và YÊN NGỰA
∇f = 0 là điều kiện CẦN để có cực trị. Không phải điều kiện ĐỦ. Đây là chỗ trực giác 1 chiều
phản bội bạn.
Ví dụ tính tay: f(x, y) = x² − y² tại (0, 0).
∇f = (2x, −2y) → ∇f(0,0) = (0, 0) ✓ điểm dừng
f(0, 0) = 0
Nhưng nhìn hai bên:
f( 0.1, 0 ) = 0.1² − 0² = +0.01 ← đi theo trục x thì LÊN
f(-0.1, 0 ) = (−0.1)² − 0² = +0.01 (cả hai phía đều lên)
f( 0 , 0.1 ) = 0² − 0.1² = −0.01 ← đi theo trục y thì XUỐNG
f( 0 ,−0.1 ) = 0² −(−0.1)² = −0.01 (cả hai phía đều xuống)
Quét toàn bộ vòng tròn bán kính 0.1 quanh gốc: min = −0.01, max = +0.01. Tức là vừa có hướng
lên vừa có hướng xuống. Đúng 50% số hướng làm f giảm, 50% làm f tăng (đo trên 360.000 hướng:
0.500006 / 0.499994).
Điểm yên ngựa (saddle point):
∇f = 0, nhưng là cực tiểu theo hướng này và cực đại theo hướng kia. Hình ảnh: cái yên ngựa. Hoặc: cái đèo giữa hai đỉnh núi — đứng ở đèo, đi dọc sống núi thì lên, đi ngang xuống hai thung lũng thì xuống.
Ẩn dụ backend: một cấu hình vừa là tốt nhất nếu bạn chỉ chỉnh batch_size, vừa là tệ nhất nếu
bạn chỉ chỉnh pool_size. Bạn thử từng chiều một, thấy chiều nào cũng "không cải thiện được nữa", và
kết luận sai rằng đã tối ưu.
Vậy làm sao phân biệt cực tiểu với yên ngựa khi cả hai đều có ∇f = 0? Cần đạo hàm bậc hai.
5. Hessian — đạo hàm bậc hai, và eigenvalue quyết định tất cả
Lấy đạo hàm riêng của từng thành phần gradient, theo từng biến. Kết quả là một ma trận n×n:
H = [ ∂²f/∂x² ∂²f/∂x∂y ]
[ ∂²f/∂y∂x ∂²f/∂y² ]
Hessian luôn đối xứng (định lý Clairaut/Schwarz với hàm đủ trơn): ∂²f/∂x∂y = ∂²f/∂y∂x.
Kiểm bằng số với f = x³y² + sin(xy) tại (1.3, 0.7): cả hai đều ra 6.99329736, lệch đúng 0.0.
Đối xứng là một tin rất lớn: môn 2 bài 7 nói ma trận đối xứng thì eigenvalue luôn thực và eigenvector luôn trực giao. Nghĩa là bề mặt luôn có các "trục chính" vuông góc nhau.
Ví dụ tính tay — hai hàm của bài này:
f = x² + y² f = x² − y²
∂²/∂x² = 2 ∂²/∂x² = 2
∂²/∂y² = 2 ∂²/∂y² = −2
∂²/∂x∂y = 0 ∂²/∂x∂y = 0
H = [ 2 0 ] H = [ 2 0 ]
[ 0 2 ] [ 0 −2 ]
eig = (2, 2) eig = (−2, 2)
Bảng phân loại — chỉ nhìn DẤU của eigenvalue
Dấu các eigenvalue của H |
Kết luận | Hình ảnh |
|---|---|---|
| Tất cả > 0 | CỰC TIỂU địa phương | cái bát, cong lên mọi hướng |
| Tất cả < 0 | CỰC ĐẠI địa phương | cái vòm, cong xuống mọi hướng |
| Trái dấu | YÊN NGỰA | cong lên hướng này, xuống hướng kia |
| Có eigenvalue = 0, không có cả hai dấu | phép thử bậc hai chưa kết luận được | có hướng phẳng, phải xét thêm |
Kết quả máy chạy:
| Hàm | H |
eigenvalue | Kết luận |
|---|---|---|---|
x² + y² |
[[2,0],[0,2]] |
2, 2 |
CỰC TIỂU |
−x² − y² |
[[−2,0],[0,−2]] |
−2, −2 |
CỰC ĐẠI |
x² − y² |
[[2,0],[0,−2]] |
−2, 2 |
YÊN NGỰA |
x² + 10y² |
[[2,0],[0,20]] |
2, 20 |
CỰC TIỂU (nhưng dẹt — xem 🪤) |
x² (rãnh máng) |
[[2,0],[0,0]] |
0, 2 |
không kết luận được |
Nối thẳng về eigenvector (môn 2 bài 7)
Hai hàm trên có H chéo nên nhìn phát biết ngay. Đổi sang hàm mà trục chính không trùng trục toạ độ:
f = 0.5x² + 2xy + 0.5y²
H = [ 1 2 ] eigenvalue = (−1, 3)
[ 2 1 ] eigenvector = (−0.7071, 0.7071) ứng với −1 ← hướng (1,−1), cong XUỐNG
( 0.7071, 0.7071) ứng với 3 ← hướng (1, 1), cong LÊN
Nhìn vào bảng số [[1,2],[2,1]] thì không thấy gì. Nhìn vào eigen thì thấy ngay: đây là một yên
ngựa xoay 45°. Hai eigenvector chính là hai "trục" của yên ngựa; hai eigenvalue là độ cong dọc
hai trục đó.
Đây là môn 2 bài 7 trả cổ tức. Eigenvector của Hessian = các trục chính của bề mặt loss tại điểm đó. Eigenvalue = độ cong dọc trục đó. Dấu → lên hay xuống. Độ lớn → cong gắt hay thoai thoải. Toàn bộ hình dạng địa phương của bề mặt nằm trong phổ của
H.
Và một cách nhìn gọn: Hessian là Jacobian của gradient (§7). Kiểm bằng số với f = x² + 10y²:
lấy Jacobian số của hàm ∇f = (2x, 20y) tại (1,1) → ra đúng [[2, 0], [0, 20]].
6. Vì sao ở chiều CAO, yên ngựa áp đảo cực tiểu
Đây là câu hỏi "tận cùng" số 4 của môn 3, và nó có câu trả lời bằng một câu:
Để là cực tiểu, cần TẤT CẢ
neigenvalue cùng dương. Chỉ cần một eigenvalue âm → yên ngựa. Ởnlớn, "tất cảncái cùng dấu" là một điều kiện cực kỳ khắt khe.
Ước lượng thô kiểu tung đồng xu: nếu mỗi eigenvalue độc lập dương/âm 50-50, xác suất là cực trị
(toàn dương hoặc toàn âm) là 2·(1/2)ⁿ → n=10 cho 0.2%, n=20 cho 0.0002%.
Mô phỏng thật — sinh ma trận đối xứng ngẫu nhiên S = (A + Aᵀ)/√2 với A ~ N(0,1), đếm dấu
eigenvalue. N = 1000 ma trận mỗi n, seed 42:
n |
toàn dương (cực tiểu) | toàn âm (cực đại) | trái dấu (yên ngựa) |
|---|---|---|---|
| 1 | 50.2% | 49.8% | 0.0% |
| 2 | 13.8% | 15.5% | 70.7% |
| 3 | 2.4% | 2.9% | 94.7% |
| 5 | 0.0% | 0.0% | 100.0% |
| 10 | 0.0% | 0.0% | 100.0% |
| 20 | 0.0% | 0.0% | 100.0% |
Từ n = 5 trở lên, 1000 mẫu không bắt được một cực trị nào. Chạy lại với N = 200.000 (seed 7)
để ước lượng phần đuôi:
n |
P(là cực trị) đo được | ước lượng đồng xu 2·(1/2)ⁿ |
thực tế hiếm hơn |
|---|---|---|---|
| 2 | 0.292770 | 0.50 | 1.7× |
| 3 | 0.049130 | 0.25 | 5.1× |
| 4 | 0.004980 | 0.125 | 25× |
| 5 | 0.000290 | 0.0625 | 215× |
Với n = 50 và n = 100 (200 mẫu mỗi loại): 0/200, đúng như dự đoán.
Chú ý cột cuối: thực tế còn hiếm hơn ước lượng đồng xu, và khoảng cách giãn nhanh theo n. Lý do:
eigenvalue của ma trận ngẫu nhiên đẩy nhau và trải đều quanh 0 — chúng không độc lập. Bạn không
cần chứng minh điều đó ở môn này; chỉ cần thấy chiều của kết quả.
Điều này đổi cách nghĩ thế nào
"Mắc kẹt ở local minimum" là nỗi sợ nhập từ trực giác 2 chiều. Với mạng có hàng triệu tham số, cần xét cả yên ngựa, hướng phẳng và cực tiểu. Thí nghiệm ma trận ngẫu nhiên ở trên minh hoạ độ khó của điều kiện "mọi hướng cùng cong lên", không đo tần suất các điểm mà một mạng thật gặp khi huấn luyện.
Tin tốt: yên ngựa thoát được — theo định nghĩa nó có ít nhất một hướng đi xuống. Tin xấu: thoát rất chậm, vì quanh yên ngựa
|∇f|nhỏ → bước đi ngắn → loss nằm ngang hàng nghìn vòng lặp. Đó là "plateau" bạn thấy trên đường loss, và nó thường bị chẩn đoán nhầm là "đã hội tụ".
Đo tốc độ thoát bằng số — f = x² − y², gradient descent lr = 0.1, xuất phát lệch khỏi yên ngựa
đúng 1e-6 theo trục y (thành phần y nhân 1.2 mỗi bước, x nhân 0.8):
| sau bước | y |
|---|---|
| 10 | 6.19e-06 |
| 30 | 2.37e-04 |
| 60 | 5.63e-02 |
| 100 | 82.8 |
60 bước đầu gần như không thấy gì xảy ra, rồi bung ra. Và nếu xuất phát đúng tại (0,0):
chạy 100.000 bước, vị trí vẫn là (0, 0) — đứng yên vĩnh viễn. Trong thực tế nhiễu ngẫu nhiên của
SGD chính là thứ đá bạn ra khỏi mép đó; đây là một trong những lý do SGD hoạt động tốt hơn gradient
descent toàn phần.
7. Jacobian — sơ lược, chỉ cần biết nó là gì
Gradient dùng cho hàm trả về một số. Khi hàm trả về một vector (một lớp mạng nơ-ron chẳng hạn:
vào n chiều, ra m chiều), ta xếp gradient của từng thành phần đầu ra thành các hàng của một ma trận:
J[i, j] = ∂F_i / ∂x_j J có kích thước m × n
Ví dụ tính tay — F(x, y) = (x²y, x + y) tại (1, 2):
F(1,2) = (2, 3)
J = [ ∂(x²y)/∂x ∂(x²y)/∂y ] = [ 2xy x² ] = [ 4 1 ]
[ ∂(x+y)/∂x ∂(x+y)/∂y ] [ 1 1 ] [ 1 1 ]
Kiểm bằng sai phân số: khớp chính xác.
Chain rule nhiều biến = NHÂN Jacobian. Thêm G(a, b) = (a + b, ab), G(2,3) = (5,6),
J_G(2,3) = [[1,1],[3,2]]:
J_{G∘F}(1,2) = J_G(F(1,2)) · J_F(1,2) = [ 1 1 ] · [ 4 1 ] = [ 5 2 ]
[ 3 2 ] [ 1 1 ] [ 14 5 ]
Sai phân số cho đúng [[5, 2], [14, 5]]. Nhân ma trận, đúng thứ tự phải-sang-trái như môn 2 bài 2.
Bảng gộp — bốn tầng, một ý tưởng
| Hàm | Đạo hàm là gì | Kích thước |
|---|---|---|
f: ℝ → ℝ |
một SỐ | 1×1 |
f: ℝⁿ → ℝ |
gradient — một VECTOR | n (= Jacobian một hàng) |
F: ℝⁿ → ℝᵐ |
Jacobian — một MA TRẬN | m×n |
| gradient của gradient | Hessian = Jacobian của ∇f |
n×n, đối xứng |
Đây là teaser cho bài kế: backpropagation qua
Llớp = nhânLma trận Jacobian. Và ngay từ bảng này bạn đã đoán được vì sao gradient vanishing/exploding tồn tại — nhân một chuỗi dài ma trận thì kết quả hoặc teo về 0 hoặc nổ, hiếm khi giữ nguyên độ lớn.
8. Một bề mặt loss thật — MSE của hồi quy tuyến tính
Đủ hàm đồ chơi. Lấy hàm loss thật, số vẫn tính tay được:
X = [ 1 2 ] y = ( 5, 6, 8 ) f(w) = |X·w − y|²
[ 3 1 ]
[ 0 4 ]
Tại w = (1, 1):
X·w = (1+2, 3+1, 0+4) = (3, 4, 4)
r = X·w−y = (−2, −2, −4)
f(w) = 4 + 4 + 16 = 24
∇f = 2·Xᵀ·r
Xᵀ·r = ( 1(−2)+3(−2)+0(−4) , 2(−2)+1(−2)+4(−4) ) = (−8, −22)
∇f = (−16, −44) ✓ khớp sai phân số
Hessian:
XᵀX = [ 1+9+0 2+3+0 ] = [ 10 5 ] H = 2·XᵀX = [ 20 10 ]
[ 2+3+0 4+1+16 ] [ 5 21 ] [ 10 42 ]
eig(H) = (16.1339, 45.8661) → cả hai DƯƠNG → cực tiểu
số điều kiện κ = 45.8661/16.1339 = 2.8428
nghiệm tối ưu w* = (1.313514, 1.972973), loss* = 0.086486
Nhận xét đáng giá nhất của mục này:
H = 2XᵀXkhông phụ thuộc vàow— bề mặt là một paraboloid duy nhất, cùng độ cong ở mọi nơi. VàXᵀXluôn nửa xác định dương (mọi eigenvalue ≥ 0) vìvᵀXᵀXv = |Xv|² ≥ 0.Hồi quy tuyến tính KHÔNG CÓ điểm yên ngựa và không có cực tiểu địa phương giả. Mọi điểm dừng đều là cực tiểu toàn cục. Đó là vì sao least squares "dễ" — và vì sao mạng nơ-ron, nơi
Hđổi theowvà trái dấu tứ tung, thì không.
🪤 BẪY
Bẫy 1 — quên chuẩn hoá u: code chạy, số sai lặng lẽ
D_u f = ∇f · u chỉ đúng khi |u| = 1. Đưa vào một u chưa chuẩn hoá thì kết quả bị nhân lên
đúng |u| lần — không crash, không cảnh báo, chỉ là một số sai.
tại (3,4), ∇f = (6,8):
u = (0.6, 0.8) |u| = 1 → ∇f · u = 10 ✓ ĐÚNG
u = (3, 4) |u| = 5 → ∇f · u = 50 ✗ SAI, gấp đúng 5 lần
Hai u này cùng hướng. Cùng hướng nhưng khác kết quả — đó là dấu hiệu chắc chắn rằng bạn đang tính
sai đại lượng. Chống bẫy: chuẩn hoá bên trong hàm (u = u/|u|), đừng tin người gọi.
Bẫy 2 — gradient chỉ dốc nhất TẠI ĐIỂM ĐÓ, không phải hướng tới đích
Đây là bẫy quan trọng nhất của bài. Gradient là thông tin cục bộ: nó biết mặt đất nghiêng thế nào dưới chân bạn, và không biết gì về vị trí cực tiểu.
Lấy f = x² + c·y² (bát dẹt), xuất phát (1, 1), cực tiểu ở (0,0). So hướng −∇f với hướng
thẳng tới cực tiểu:
c |
−∇f chuẩn hoá |
hướng thẳng tới đích | góc lệch |
|---|---|---|---|
| 1 | (−0.7071, −0.7071) |
(−0.7071, −0.7071) |
0° |
| 2 | (−0.4472, −0.8944) |
(−0.7071, −0.7071) |
18.43° |
| 10 | (−0.0995, −0.9950) |
(−0.7071, −0.7071) |
39.29° |
| 100 | (−0.0100, −0.9999) |
(−0.7071, −0.7071) |
44.43° |
Chỉ khi đường đồng mức là vòng tròn hoàn hảo (c = 1) thì gradient mới chỉ thẳng vào đích. Càng dẹt,
gradient càng chỉ gần như vuông góc với trục dài — tức gần như bỏ qua hướng thật sự cần đi.
Hậu quả: gradient descent zig-zag. Chạy với learning rate tối ưu cho từng c (lr = 1/(1+c)),
dừng khi |p| < 1e-3:
c (= số điều kiện κ) |
lr tối ưu |
số bước | độ dài đường đi | so với đường thẳng √2 = 1.414 |
|---|---|---|---|---|
| 1 | 0.500000 | 1 | 1.4142 | 1.0× |
| 2 | 0.333333 | 7 | 2.2350 | 1.6× |
| 10 | 0.090909 | 37 | 10.0439 | 7.1× |
| 100 | 0.009901 | 363 | 99.9347 | 70.7× |
Sáu điểm đầu với c = 10 — nhìn dấu của y để thấy zig-zag:
(1.000000, 1.000000)
(0.818182, -0.818182) ← vọt qua bên kia
(0.669421, 0.669421) ← vọt ngược lại
(0.547708, -0.547708)
(0.448125, 0.448125)
(0.366648, -0.366648)
y đổi dấu mỗi bước, x bò từng chút. Toàn bộ ngân sách bước đi bị đốt vào việc dao động theo
hướng dốc, trong khi hướng cần đi là hướng thoai thoải.
Ẩn dụ backend: gradient descent là greedy routing — mỗi bước chọn hop rẻ nhất kế tiếp. Nó không phải Dijkstra. Không có gì đảm bảo tổng đường đi ngắn nhất, và với topology xấu (bề mặt dẹt) nó tệ hơn đường thẳng cả trăm lần.
Ba con số nữa cùng chủ đề, cùng c = 10 (λ_max = 20):
- Ngưỡng phân kỳ là
lr = 2/λ_max = 0.1. Sau 50 bước:lr=0.09→|p| = 5.1e-05(hội tụ);lr=0.0999→0.905;lr=0.1→1.0đứng yên, dao động mãi;lr=0.1001→1.105;lr=0.11→9.1e+03(nổ). Ranh giới sắc như dao. lrbị chặn bởi eigenvalue lớn nhất, còn tốc độ hội tụ bị quyết định bởi eigenvalue nhỏ nhất. Đó chính là lý doκ = λ_max/λ_minlà con số quyết định — và là lý do normalization tồn tại.- Nếu được dùng Hessian:
p − H⁻¹·∇fvớiH⁻¹ = [[0.5, 0], [0, 0.05]]cho(0, 0)trong đúng 1 bước, từ mọi điểm xuất phát. Bước Newton "sửa lại tỉ lệ" từng trục theo độ cong của nó. Adam là một xấp xỉ rẻ tiền của ý tưởng đó, và giờ bạn biết nó xấp xỉ cái gì.
Bẫy 3 — h quá nhỏ trong sai phân: càng nhỏ càng SAI
Trực giác nói h → 0 thì càng chính xác. Số thực dấu phẩy động nói không. f(p+h) − f(p−h) là hiệu
hai số gần bằng nhau → triệt tiêu tai hại (catastrophic cancellation), rồi chia cho 2h khuếch đại
sai số lên.
Đo trên f = x²+y² tại (3,4), u = (0.6, 0.8), đáp án đúng là 10:
h |
kết quả | sai số |
|---|---|---|
1e-1 |
10.0000000000 | 0 |
1e-3 |
10.0000000000 | 2.1e-13 |
1e-5 |
9.9999999998 | 2.0e-10 |
1e-8 |
10.0000001169 | 1.2e-07 |
1e-10 |
10.0000008274 | 8.3e-07 |
1e-12 |
9.9991126490 | 8.9e-04 |
1e-14 |
9.9475983006 | 5.2e-02 |
Ở bảng này sai số chỉ đi lên khi h nhỏ dần — và có một lý do rất cụ thể, đừng bỏ qua nó:
x²+y² là hàm bậc hai, nên số hạng cắt cụt h²·f‴/6 bằng đúng 0 (f‴ = 0). Central difference
ở đây chính xác về mặt toán học với mọi h; thứ duy nhất còn lại là sai số làm tròn, mà nó chỉ tăng
khi h giảm. Đó là vì sao h = 1e-1 cho sai số bằng 0 chằn chặn.
Với hàm không phải bậc hai thì mới thấy đủ hai nhánh. Đo trên g = eˣ·sin(y) tại (0.7, 1.1),
cùng u = (0.6, 0.8), đáp án giải tích = 1.8075470746:
h |
kết quả | sai số |
|---|---|---|
1e-1 |
1.8052823947 | 2.3e-03 |
1e-3 |
1.8075468483 | 2.3e-07 |
1e-5 |
1.8075470746 | 2.3e-11 ← đáy |
1e-6 |
1.8075470748 | 1.5e-10 |
1e-8 |
1.8075470898 | 1.5e-08 |
1e-10 |
1.8075485553 | 1.5e-06 |
1e-14 |
1.8096635301 | 2.1e-03 |
Đây mới là hình chữ U: h lớn thì sai số cắt cụt (~h²) áp đảo, h nhỏ thì sai số làm tròn
(~ε/h) áp đảo. Vùng ngọt của central difference nằm khoảng 1e-5 … 1e-6 — đúng chỗ hai nhánh giao nhau.
Quay lại bảng đầu: 1e-14 cho sai 0.5% — đủ để một bài gradient-check "thất bại" mà bạn đi sửa nhầm chỗ.
Bẫy 4 — thấy ∇f ≈ 0 rồi kết luận "đã hội tụ"
Sau §6 thì rõ: ở chiều cao, ∇f ≈ 0 gần như chắc chắn là yên ngựa hoặc plateau, không phải đích.
Muốn biết thật thì phải hỏi dấu eigenvalue của Hessian — mà với hàng triệu tham số thì H là ma trận
10⁶ × 10⁶, không dựng nổi. Đó là lý do trong thực tế người ta không kiểm; người ta chỉ chờ thêm,
hoặc dùng nhiễu SGD để đá ra khỏi mép.
Code — tự cài, KHÔNG dùng hàm có sẵn
Chạy được ngay, mọi khẳng định trong bài đều có assert.
import numpy as np
# ---------- 1. bốn nguyên thuỷ, tự cài ----------
def partial(f, p, i, h=1e-5):
"""Đạo hàm riêng theo biến thứ i — đóng băng các biến khác."""
e = np.zeros_like(p); e[i] = h
return (f(p + e) - f(p - e)) / (2 * h)
def gradient(f, p, h=1e-5):
"""Gradient = gom tất cả đạo hàm riêng thành VECTOR."""
return np.array([partial(f, p, i, h) for i in range(len(p))])
def dir_deriv(f, p, u, h=1e-5):
"""Đạo hàm theo hướng — đo THẲNG dọc u, KHÔNG đi qua gradient.
Chuẩn hoá u bên trong: đây là chỗ chặn 🪤 Bẫy 1."""
u = u / np.linalg.norm(u)
return (f(p + h * u) - f(p - h * u)) / (2 * h)
def hessian(f, p, h=1e-4):
"""Hessian bằng sai phân bậc hai."""
n = len(p); H = np.zeros((n, n))
for i in range(n):
for j in range(n):
ei = np.zeros(n); ei[i] = h
ej = np.zeros(n); ej[j] = h
H[i, j] = (f(p+ei+ej) - f(p+ei-ej) - f(p-ei+ej) + f(p-ei-ej)) / (4*h*h)
return H
def classify(H):
"""Phân loại điểm dừng CHỈ bằng dấu eigenvalue — §5."""
w = np.linalg.eigvalsh(H) # eigvalsh: dành cho ma trận đối xứng
if np.all(w > 1e-8): return "CỰC TIỂU", w
if np.all(w < -1e-8): return "CỰC ĐẠI", w
if np.all(np.abs(w) > 1e-8): return "YÊN NGỰA", w
return "KHÔNG KẾT LUẬN", w
bowl = lambda p: p[0]**2 + p[1]**2
saddle = lambda p: p[0]**2 - p[1]**2
# ---------- 2. §3: D_u f = ∇f · u, và MAX ở đúng hướng gradient ----------
P = np.array([3.0, 4.0])
g = gradient(bowl, P)
assert np.allclose(g, [6.0, 8.0], atol=1e-6)
assert abs(np.linalg.norm(g) - 10.0) < 1e-6
for u, want in [([1,0], 6.0), ([0,1], 8.0), ([0.6,0.8], 10.0),
([-0.8,0.6], 0.0), ([-0.6,-0.8], -10.0)]:
u = np.array(u, dtype=float)
assert abs(dir_deriv(bowl, P, u) - want) < 1e-5 # đo thẳng
assert abs(float(g @ (u/np.linalg.norm(u))) - want) < 1e-6 # qua tích vô hướng
print("D_u f = grad · u ✓")
# quét 200.000 hướng: MAX phải bằng |grad| và ở đúng hướng grad
th = np.linspace(0, 2*np.pi, 200_000, endpoint=False)
U = np.stack([np.cos(th), np.sin(th)])
vals = g @ U
assert abs(vals.max() - 10.0) < 1e-6
assert np.allclose(U[:, int(np.argmax(vals))], g/np.linalg.norm(g), atol=1e-4)
print("MAX =", round(float(vals.max()), 6), "đúng tại u = grad/|grad| ✓")
# ---------- 3. §4: điểm yên ngựa ----------
O = np.array([0.0, 0.0])
assert np.allclose(gradient(saddle, O), [0.0, 0.0], atol=1e-9) # grad = 0
assert saddle(np.array([0.1, 0.0])) > 0 > saddle(np.array([0.0, 0.1])) # nhưng không cực trị
print("x²−y² tại (0,0): grad = 0 mà KHÔNG phải cực trị ✓")
# ---------- 4. §5: Hessian + phân loại ----------
Hb, Hs = hessian(bowl, O), hessian(saddle, O)
assert np.allclose(Hb, [[2,0],[0, 2]], atol=1e-4)
assert np.allclose(Hs, [[2,0],[0,-2]], atol=1e-4)
assert classify(Hb)[0] == "CỰC TIỂU" and classify(Hs)[0] == "YÊN NGỰA"
print("Hessian:", classify(Hb), classify(Hs))
# ---------- 5. ĐỐI CHIẾU scipy — bắt buộc ----------
from scipy.optimize import approx_fprime
import scipy.linalg as sla
assert np.allclose(gradient(bowl, P), approx_fprime(P, bowl, 1e-6), atol=1e-4)
assert np.allclose(np.sort(sla.eigvalsh(Hs)), [-2.0, 2.0], atol=1e-4)
print("khớp scipy ✓")
# ---------- 6. §6: mô phỏng — yên ngựa áp đảo ở chiều cao ----------
rng = np.random.default_rng(42)
print("\n n | toàn dương | toàn âm | yên ngựa")
for n in [1, 2, 3, 5, 10, 20]:
pos = neg = mix = 0
for _ in range(1000):
A = rng.normal(size=(n, n)); S = (A + A.T) / np.sqrt(2) # ma trận đối xứng
w = np.linalg.eigvalsh(S)
if np.all(w > 0): pos += 1
elif np.all(w < 0): neg += 1
else: mix += 1
print(f"{n:2d} | {pos/10:9.1f}% | {neg/10:6.1f}% | {mix/10:7.1f}%")
# ---------- 7. 🪤 Bẫy 2: zig-zag trên bát dẹt ----------
def gd(gfn, p, lr, tol=1e-3, itmax=100_000):
path = [p.copy()]
for _ in range(itmax):
p = p - lr * gfn(p); path.append(p.copy())
if np.linalg.norm(p) < tol: break
return np.array(path)
print("\n c | lr tối ưu | số bước | độ dài đường đi (đường thẳng = 1.4142)")
for c in [1, 10, 100]:
gf = lambda p, c=c: np.array([2*p[0], 2*c*p[1]])
path = gd(gf, np.array([1.0, 1.0]), 1.0/(1.0+c))
L = float(np.sum(np.linalg.norm(np.diff(path, axis=0), axis=1)))
print(f"{c:3d} | {1/(1+c):9.6f} | {len(path)-1:7d} | {L:8.4f}")
# ---------- 8. 🪤 Bẫy 1: quên chuẩn hoá → sai lặng lẽ, không crash ----------
u_bad = np.array([3.0, 4.0]) # cùng hướng (0.6,0.8) nhưng |u| = 5
assert abs(float(g @ u_bad) - 50.0) < 1e-4 # ra 50 thay vì 10
assert abs(dir_deriv(bowl, P, u_bad) - 10.0) < 1e-5 # hàm có chuẩn hoá thì vẫn đúng
print("\nbẫy chuẩn hoá: grad @ (3,4) = 50 thay vì 10 ✓ → chỉ assert mới bắt được")
print("\nTẤT CẢ ASSERT PASS")
Quy tắc đang được luyện: khi gradient tự tính lệch approx_fprime, nguyên nhân hầu như luôn là
h (Bẫy 3) hoặc chuẩn hoá u (Bẫy 1) — không phải hiểu sai toán. Ghi vào nhật ký lỗi.
Nối vào AI
| Khái niệm bài này | Chỗ dùng trong AI |
|---|---|
Đạo hàm riêng ∂L/∂w_ij |
Độ nhạy của loss với một trọng số — đơn vị nguyên tử của mọi cập nhật |
| Gradient là vector cùng shape với tham số | Vì sao w ← w − lr·∇L cộng trừ được. Optimizer state (momentum, Adam) cũng cùng shape |
D_u f = ∇f · u |
Nền tảng của gradient descent: −∇f là hướng giảm nhanh nhất. Cũng là nền của gradient clipping (chặn |∇f|) và cosine similarity giữa các gradient trong multi-task learning |
| Gradient ⊥ đường đồng mức | Cách đọc mọi hình minh hoạ contour trong bài báo tối ưu |
| Điểm yên ngựa | Plateau trên đường loss. Vì sao SGD (có nhiễu) thoát tốt hơn gradient descent toàn phần |
| Hessian, eigenvalue | Độ cong bề mặt loss. lr tối đa = 2/λ_max — nền của learning-rate finder |
Số điều kiện κ = λ_max/λ_min |
Vì sao cần BatchNorm / LayerNorm và chuẩn hoá đầu vào: chúng làm bề mặt bớt dẹt, κ giảm, ít zig-zag |
H⁻¹∇f (bước Newton) |
Ý tưởng gốc của Adam / RMSProp: xấp xỉ rẻ tiền việc chia mỗi trục cho độ cong của nó |
| Jacobian | Một lớp mạng ℝⁿ → ℝᵐ. Backprop = nhân chuỗi Jacobian |
H = 2XᵀX luôn ≥ 0 |
Vì sao hồi quy tuyến tính/logistic (bài toán lồi) dễ, mạng sâu (không lồi) khó |
Chú ý mạch nối: môn 2 bài 1 dạy tích vô hướng đo mức độ cùng hướng → bài này dùng đúng sự thật đó để chứng minh gradient là hướng dốc nhất. Môn 2 bài 7 dạy eigenvector là trục bất biến → bài này dùng đúng sự thật đó để đọc hình dạng bề mặt loss. Môn 3 không phát minh gì mới về hình học; nó chỉ áp môn 2 lên bề mặt.
Tự kiểm — trả lời được mới sang bài sau
∇fđược định nghĩa thế nào? Và vì sao nó lại trở thành hướng dốc nhất? Trong câu trả lời phải xuất hiện chữ tích vô hướng và chữcos θ. Nếu không, bạn đang học thuộc.- Vì sao
|D_u f| ≤ |∇f|với mọiuđơn vị? Và đẳng thức xảy ra khi nào? (Đây là bất đẳng thức Cauchy–Schwarz, đội lốt.) - Chứng minh trong hai dòng rằng
∇fvuông góc với đường đồng mức. Bắt đầu từ "đi dọc đường đồng mức thìfkhông đổi". ∇f = 0tạip. Nêu ba khả năng. Bạn phân biệt chúng bằng cái gì, và cần đại lượng nào?- Vì sao ở
nchiều, yên ngựa nhiều hơn cực tiểu địa phương theo cấp số nhân? Nêu con số cụ thể chon = 5. Rồi trả lời: điều đó khiến câu "model bị kẹt ở local minimum" trở nên đúng hơn hay sai hơn? f = x² + 100y².Hlà gì?λ_max,λ_min,κbằng bao nhiêu?lrlớn nhất còn hội tụ là bao nhiêu, và vì sao nó bị chặn bởiλ_maxchứ không phảiλ_min?- ⭐ Câu khó nhất. Với
f = x² + 100y²xuất phát từ(1,1): hướng−∇flệch 44.43° so với hướng thẳng tới cực tiểu. Giải thích vì sao không hề mâu thuẫn với việc−∇flà hướng dốc nhất. (Gợi ý: "dốc nhất" là câu trả lời cho câu hỏi nào, và "tới đích nhanh nhất" là câu trả lời cho câu hỏi nào? Hai câu hỏi đó khác nhau ở chữ nào?) - Bạn có
f: ℝ¹⁰⁰⁰ → ℝ. Gradient là gì (kích thước bao nhiêu)? Hessian là gì? Vì sao trong thực tế không ai dựng Hessian cho mạng nơ-ron — và Adam thay thế nó bằng cách nào?
Câu 7 là ranh giới thật của bài này. Nó tách người biết công thức gradient khỏi người hiểu gradient là thông tin cục bộ. Trả lời được câu 7 thì toàn bộ chương optimizer ở môn 7 (momentum, Adam, learning rate schedule) sẽ đọc như hệ quả chứ không phải như danh sách thủ thuật cần nhớ.
Câu 1 là câu phải trả lời bằng lời, không viết công thức. Nếu bạn buộc phải mở lại §3.3 thì nghĩa là chưa xong.
Bài tập về nhà trước bài sau
1. Xem — 3Blue1Brown
| Series / Tập | Vì sao |
|---|---|
| Essence of Calculus, ch.10 — Higher order derivatives | Đạo hàm bậc hai = độ cong. Đây là nửa trực giác của Hessian, ở 1 chiều. Xem xong quay lại đọc §5 |
| Neural Networks, ch.2 — Gradient descent, how neural networks learn | Bề mặt loss và quả bóng lăn xuống dốc, bằng hình động. Chính là §1 và §3 của bài này, ở quy mô thật |
Phần đa biến 3B1B không nằm trong Essence of Calculus. Grant Sanderson làm hẳn một series Multivariable Calculus trên Khan Academy — đó mới là nơi có video về partial derivatives, gradient, directional derivatives, quadratic approximation, saddle points và Jacobian. Nếu muốn xem hình cho đúng bài này thì tìm ở đó, không phải trên kênh YouTube của 3B1B.
Nguồn chính vẫn là MML book ch.5 (Vector Calculus): §5.2 đạo hàm riêng & gradient, §5.3 gradient của hàm vector (Jacobian), §5.7 đạo hàm bậc cao & Hessian. Thêm MML §7.1 cho phần lr/κ.
Bỏ qua ở môn 3, và lý do — đừng bỏ vì lười, bỏ vì đúng:
| Bỏ | Vì sao |
|---|---|
| Calculus ch.8, 9 (tích phân, định lý cơ bản) | Lộ trình này không dùng tích phân. Nó cần cho xác suất liên tục (môn 6) và VAE/diffusion — học khi tới đó, không sớm hơn |
| Calculus ch.11 (Taylor series) | Chỉ cần biết nó tồn tại, ở đúng một dạng: f(p+d) ≈ f(p) + ∇f·d + ½·dᵀHd. Đó chính là §5 — Hessian là hệ số bậc hai của khai triển Taylor. Chuỗi vô hạn, bán kính hội tụ, phần dư: không dùng |
| Calculus ch.12 (cách nhìn khác về đạo hàm) | Hay, nhưng là bonus trực giác. Không có khái niệm nào ở đó chảy tiếp vào backprop |
| Neural Networks ch.5, 6 | Transformer/attention — thuộc môn 8. Xem bây giờ chỉ tốn buổi tối |
2. Làm — ba việc, làm mới tính
(a) Tính tay rồi kiểm bằng máy. f(x, y) = x³ − 3xy + y³.
Tìm tất cả điểm dừng (giải ∇f = 0), dựng H tại từng điểm, phân loại bằng dấu eigenvalue.
Đích để đối chiếu — chỉ mở ra sau khi đã tự làm:
điểm dừng: (0,0) và (1,1)
(0,0): H = [[0,−3],[−3,0]] eig = (−3, 3) → YÊN NGỰA
(1,1): H = [[6,−3],[−3,6]] eig = ( 3, 9) → CỰC TIỂU, f(1,1) = −1
Rồi kiểm bằng số: quét vòng tròn bán kính 0.05 quanh mỗi điểm.
Quanh (0,0): min = −0.00383839, max = +0.00375234 — hai dấu → yên ngựa ✓
Quanh (1,1): min trên vòng = −0.99633839 > f(1,1) = −1 — mọi hướng đều cao hơn → cực tiểu ✓
(b) Tự chạy lại mô phỏng §6 và đẩy xa hơn. Thêm n = 50 và n = 100 (200 mẫu mỗi loại,
np.linalg.eigvalsh đủ nhanh). Trả lời bằng một câu: nếu mạng của bạn có 1 triệu tham số, xác suất
một điểm ∇L = 0 bất kỳ là cực tiểu địa phương thật, cỡ bao nhiêu?
(c) Vẽ. Dùng matplotlib.contour vẽ đường đồng mức của x² + 10y², rồi vẽ chồng lên đó quỹ đạo
gradient descent với lr = 0.09 xuất phát (1,1). Nhìn tận mắt cái zig-zag của 🪤 Bẫy 2.
Rồi đổi sang lr = 0.11 và xem nó nổ. Một hình này đáng giá hơn cả mục 🪤 viết bằng chữ.
Bài sau sẽ là: gradient descent — biến §3 ("−∇f là hướng dốc xuống nhất") thành một thuật toán
có vòng lặp, và trả lời câu hỏi mà bài này cố tình để treo: bước bao xa mỗi lần? Toàn bộ 🪤 Bẫy 2
là phần mở đầu của bài đó.