Skip to content
Phạm Khánh Minh Mẫn / syllabus

MÔN 03 / Bài 04

Gradient và điểm yên ngựa

Từ lát cắt và đường đồng mức đến gradient, Hessian và những hướng cong của bề mặt loss.

PDF ghi tay

Bản cô đọng: hình trước, công thức sau. Nền trắng, chữ đen.

Trong bài này ↓

~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, coi f là hàm một biến của x, 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. f thay đổ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·b lớn nhất khi b cùng hướng a. 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Ả n eigenvalue cùng dương. Chỉ cần một eigenvalue âm → yên ngựa. Ở n lớn, "tất cả n cá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 L lớp = nhân L ma 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ᵀX không phụ thuộc vào w — bề mặt là một paraboloid duy nhất, cùng độ cong ở mọi nơi. Và XᵀX luô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 theo w và 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.
  • lr bị 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/λ_min là con số quyết định — và là lý do normalization tồn tại.
  • Nếu được dùng Hessian: p − H⁻¹·∇f với H⁻¹ = [[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

  1. ∇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.
  2. Vì sao |D_u f| ≤ |∇f| với mọi u đơn vị? Và đẳng thức xảy ra khi nào? (Đây là bất đẳng thức Cauchy–Schwarz, đội lốt.)
  3. Chứng minh trong hai dòng rằng ∇f vuông góc với đường đồng mức. Bắt đầu từ "đi dọc đường đồng mức thì f không đổi".
  4. ∇f = 0 tại p. 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?
  5. Vì sao ở n chiề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ể cho n = 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?
  6. f = x² + 100y². H là gì? λ_max, λ_min, κ bằng bao nhiêu? lr lớn nhất còn hội tụ là bao nhiêu, và vì sao nó bị chặn bởi λ_max chứ không phải λ_min?
  7. ⭐ Câu khó nhất. Với f = x² + 100y² xuất phát từ (1,1): hướng −∇f lệ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 −∇f là 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?)
  8. 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 đó.

Vị trí đọc được lưu trên trình duyệt này khi cho phép lưu trữ; không đồng bộ giữa các thiết bị.