Python - Bài 29: Gradient Descent

 Bài 29: Gradient Descent — Cho máy tự bước xuống đáy

Chặng 7 · Toán & Tư duy ML (Bài 26 → 30) · đây là bài 4/5 của chặng.

 Mục tiêu: ráp ba bài trước thành một thuật toán chạy được, và tận mắt thấy con số w tự bò về đúng giá trị đúng mà không ai mách nó.

 Đối tượng: học sinh cấp 2–3, sinh viên, người mới học Python. Cần biết trước: Bài 27 (đạo hàm) và Bài 28 (hàm mất mát).

 Cách đọc: bài này dài hơn thường lệ và cố ý như vậy. Nếu thấy rối ở đâu, dừng lại ở Lớp A của phần đó rồi đọc bảng tính tay ở phần 2 — bảng đó giải thích được gần hết mọi thắc mắc.

Sau bài này bạn sẽ:

  • Viết được công thức cập nhật tham số và giải thích được vì sao có dấu trừ trong đó.
  • Tính tay được ba bước đầu tiên của thuật toán, không cần máy tính.
  • Tự viết vòng lặp huấn luyện bằng Python, đúng 6 dòng.
  • Biết chọn tốc độ học và nhận ra ngay khi chọn sai.

Bạn đang ở đâu. Ba bài vừa rồi mỗi bài cho một mảnh, và tới hôm nay mới ráp lại được:

  • Bài 26 dạy máy biểu diễn dữ liệu bằng vector và ma trận.
  • Bài 27 cho chiếc la bàn: đạo hàm cho biết đang dốc về phía nào.
  • Bài 28 vẽ ra địa hình: hàm mất mát tạo thành chiếc bát, đáy bát là chỗ mô hình ít sai nhất.

Còn thiếu đúng một thứ: đôi chân. Có la bàn, có bản đồ, nhưng chưa ai bảo máy phải bước thế nào. Bài này là đôi chân đó, và tên của nó là gradient descent — nghĩa đen là “đi xuống theo độ dốc”.

 Cả bài này nằm gọn trong một dòng:

w mới = w cũ − (tốc độ học) × (đạo hàm tại w cũ)

Mọi thứ còn lại của bài chỉ là giải thích dòng này: dấu trừ ở đó để làm gì, tốc độ học là cái gì, và vì sao lặp lại nhiều lần thì tới đích.

Một vòng học của máy: bốn nhịp, lặp mãi 1. Đoán thử dùng w hiện có để tính ra y mô hình – Bài 27 2. Chấm điểm sai bao nhiêu: tính Loss hàm mất mát – Bài 28 3. Xem dốc đạo hàm của Loss theo w la bàn – Bài 27 4. Bước một bước chỉnh w ngược hướng dốc đôi chân – Bài 29 lặp lại hàng nghìn lần Không có phép màu nào ở đây. Chỉ là bốn việc này, lặp đi lặp lại.

Hình 1 — Toàn bộ “trí tuệ” của một mô hình học máy nằm trong vòng lặp bốn nhịp này. Ba nhịp đầu bạn đã học rồi; bài này chỉ thêm nhịp thứ tư.

1. Một bước đi là gì?

 Lớp A · Trực giác

Hình dung bạn đứng trong lòng chảo, bị bịt mắt, phải tìm xuống chỗ thấp nhất. Bạn không nhìn thấy đáy, nhưng chân bạn cảm được mặt đất đang nghiêng về phía nào.

Cách đi hợp lý nhất: nghiêng sang phải thì bước sang trái, nghiêng sang trái thì bước sang phải. Nói gọn: đi ngược hướng dốc lên. Đó là lý do trong công thức có dấu trừ.

Còn bước dài bao nhiêu? Hai quy tắc tự nhiên:

  • Chỗ nào dốc đứng thì bước dài — còn xa đáy lắm.
  • Chỗ nào gần bằng phẳng thì bước ngắn — sắp tới nơi rồi, bước mạnh là vọt qua.

May thay, đạo hàm đã tự lo cả hai: nó vừa cho biết hướng bằng dấu, vừa cho biết độ dốc bằng độ lớn. Nên chỉ cần lấy đạo hàm nhân với một hệ số nhỏ là ra đúng bước chân cần đi.

 Lớp B · Cơ chế

Ta dùng đúng một bộ dữ liệu cho cả bài, cố ý chọn thật nhỏ để bạn tính tay theo được:

x      = [1, 2]
y_that = [2, 4]        # quy luat that: y = 2x, nhung may CHUA biet
mô hình: y_doan = w * x   # chi co dung mot num van la w

Từ Bài 28, hàm mất mát của bộ này rút gọn được thành một biểu thức rất gọn:

Loss(w) = trung bình của (w*x - y)**2
        = 2.5 * (w - 2)**2       # chiec bat, day tai w = 2

đạo hàm: dLoss/dw = 5 * (w - 2)

Và đây là công thức cập nhật — trái tim của cả bài:

w = w - lr * dao_ham(w)

Trong đó lr là tốc độ học (learning rate), một số dương nhỏ do ta chọn. Ta lấy lr = 0.1 và bắt đầu từ w = 0.

Thử ngay bước đầu tiên bằng tay:

tại w = 0:  đạo hàm = 5 * (0 - 2) = -10     # am -> dang doc xuong ben phai
w mới = 0 - 0.1 * (-10) = 0 + 1 = 1

Chú ý cái hay của dấu trừ: đạo hàm âm, trừ đi một số âm thành cộng, nên w tăng lên. Đúng ý ta muốn — đáy bát ở bên phải nên phải đi sang phải. Nếu w đang ở bên phải đáy thì đạo hàm sẽ dương, và cùng công thức đó sẽ tự động kéo w giảm xuống. Một công thức lo cả hai chiều, không cần câu lệnh if nào.

 Lớp C · Nghiên cứu

Với mô hình có nhiều tham số, ta lấy đạo hàm theo từng tham số rồi gom lại thành một vector — đó chính là gradient, và cũng chính là vector của Bài 26. Công thức không đổi hình dạng, chỉ là mọi thứ trở thành vector:

W = W - lr * gradient(W)

Một mạng nơ-ron 10 triệu tham số cũng dùng đúng dòng này, chỉ khác là W có 10 triệu phần tử. Đây là lý do Bài 26 phải học vector trước: không có nó thì không viết nổi dòng này cho quy mô thật.

Tên gọi cũng dễ hiểu ra: gradient là độ dốc, descent là đi xuống. Nếu đổi dấu trừ thành dấu cộng, ta được gradient ascent — leo lên đỉnh, dùng khi muốn tối đa hóa thay vì tối thiểu hóa. Cùng một thuật toán, chỉ khác một dấu.

2. Chạy vòng lặp: xem w bò về đích

 Lớp A · Trực giác

Một bước thì chưa tới đâu. Nhưng cứ lặp lại mãi thì sao? Hãy nhìn bảng dưới đây — nó là phần quan trọng nhất của cả bài. Mọi số trong bảng đều tính tay được, bạn hãy kiểm lại vài dòng cho chắc.

Bướcw hiện tạiLossĐạo hàmTính w mới
0010−100 − 0,1×(−10) = 1
112,5−51 − 0,1×(−5) = 1,5
21,50,625−2,51,5 − 0,1×(−2,5) = 1,75
31,750,15625−1,251,75 − 0,1×(−1,25) = 1,875
41,8750,0390625−0,625→ 1,9375

Nhìn cột w: 0 → 1 → 1,5 → 1,75 → 1,875 → 1,9375. Mỗi bước đi được đúng một nửa quãng đường còn lại tới số 2.

Nhìn cột Loss: 10 → 2,5 → 0,625 → 0,156. Mỗi bước sai số chỉ còn một phần tư. Hợp lý thôi: khoảng cách giảm một nửa mà Loss thì bình phương khoảng cách, nên giảm bốn lần.

Nhìn cột đạo hàm: −10 → −5 → −2,5 → −1,25. Càng gần đáy thì độ dốc càng thoải, nên bước chân tự động ngắn lại. Không ai lập trình việc đó cả — nó là hệ quả của công thức.

Và câu hỏi mà nhiều người thắc mắc: vậy có bao giờ tới đúng 2 không? Về mặt toán học thì không, chỉ ngày càng gần. Nhưng sau vài chục bước thì w đã là 1,9999999 — đủ chính xác cho mọi mục đích thực tế.

 Lớp B · Cơ chế

Bảng ở trên viết thành Python là đúng sáu dòng. Đây là toàn bộ thuật toán, không giấu gì cả:

w  = 0.0          # doan bua mot gia tri ban dau
lr = 0.1          # toc do hoc

for buoc in range(20):
    grad = 5 * (w - 2)   # 3. xem doc
    w    = w - lr * grad  # 4. buoc mot buoc
    print(buoc, w)

Ba dòng trong vòng lặp ứng đúng với ba nhịp cuối ở Hình 1. Nhịp “đoán thử” và “chấm điểm” đang nằm ẩn bên trong công thức 5*(w-2), vì ta đã rút gọn sẵn bằng tay.

Trong phòng thực hành phía dưới, ta sẽ viết lại đầy đủ cả bốn nhịp, không rút gọn, để bạn thấy rõ từng nhịp một.

 Lớp C · Nghiên cứu

Cách dừng vòng lặp là một quyết định thật sự. Ở đây ta chạy đủ 20 bước rồi dừng — đơn giản nhưng thô. Trong thực tế người ta thường dừng khi Loss gần như không giảm nữa, hoặc khi độ lớn của gradient nhỏ hơn một ngưỡng. Số vòng chạy hết toàn bộ dữ liệu một lượt gọi là một epoch.

Còn một chi tiết lớn mà bài này cố tình giấu: ở đây mỗi bước ta dùng toàn bộ dữ liệu để tính gradient — gọi là batch gradient descent. Với dữ liệu vài triệu dòng thì làm vậy quá chậm, nên người ta lấy ngẫu nhiên một nhúm nhỏ mỗi lần, gọi là mini-batch hay SGD. Hướng đi khi đó hơi loạng choạng vì chỉ nhìn một phần dữ liệu, nhưng nhanh hơn rất nhiều và thường lại tránh được bẫy kẹt ở hố cạn.

Các thuật toán hiện đại như Adam hay RMSProp vẫn dựa trên đúng công thức của bài này, chỉ thêm phần tự điều chỉnh độ dài bước cho từng tham số riêng. Hiểu được dòng w = w - lr*grad là đã hiểu cái lõi của tất cả.

3. Tốc độ học — cái núm khó chịu nhất

Cùng một chiếc bát, ba cách bước chân còn rất xa đáy lr quá nhỏ đúng hướng nhưng quá chậm tốn hàng nghìn bước tới đáy sau vài bước lr vừa đẹp bước dài khi còn xa tự ngắn lại khi tới gần càng nhảy càng cao lr quá lớn vọt qua đáy sang bên kia sai số phình to, hỏng hẳn

Hình 2 — Tốc độ học không đổi hình dạng chiếc bát, nó chỉ đổi độ dài mỗi bước chân. Quá nhỏ thì bò mãi không tới; quá lớn thì nhảy vọt qua đáy và văng ngày một xa.

 Lớp A · Trực giác

Tốc độ học chính là độ dài sải chân. Đạo hàm đã chỉ đúng hướng rồi, nhưng bước bao xa theo hướng đó thì do bạn quyết.

Bước quá ngắn: đi đúng đường nhưng mất cả nghìn bước mới tới. Không sai, chỉ phí thời gian.

Bước quá dài: từ sườn bên trái nhảy vọt qua đáy sang tận sườn bên phải, mà lại còn cao hơn chỗ vừa đứng. Lần sau nhảy ngược lại còn cao hơn nữa. Sai số phình to dần thay vì nhỏ đi — mô hình hỏng hẳn.

Dấu hiệu nhận biết rất rõ khi chạy code: nếu thấy Loss tăng qua từng bước, hoặc hiện ra nan, gần như chắc chắn là tốc độ học đang quá lớn. Cứ chia đôi nó rồi chạy lại.

 Lớp B · Cơ chế

Với bộ dữ liệu của bài này có thể tính ra chính xác. Đặt e = w − 2 là khoảng cách còn lại tới đáy. Thay vào công thức cập nhật:

w mới = w - lr * 5 * (w - 2)
=> e mới = e * (1 - 5*lr)

Nghĩa là sau mỗi bước, khoảng cách còn lại được nhân với hệ số 1 − 5×lr. Chỉ cần nhìn hệ số này là biết ngay chuyện gì xảy ra:

Tốc độ họcHệ số co lạiKết quả
0,020,90Co chậm — đúng hướng nhưng lê thê
0,100,50Mỗi bước rút ngắn một nửa — rất tốt
0,200,00Tới đáy sau đúng một bước
0,45−1,25Nhân với số lớn hơn 1 → càng lúc càng xa

Quy luật rút ra: an toàn khi |1 − 5×lr| < 1, tức là lr nằm giữa 0 và 0,4. Vượt 0,4 là hỏng.

 Lớp C · Nghiên cứu

Trường hợp lr = 0,2 tới đích sau đúng một bước trông như phép màu, nhưng chỉ là may mắn của bài toán một tham số với hàm bậc hai. Con số 0,4 ở trên chính là 2 chia cho hệ số 5 trong đạo hàm — trong sách vở, hệ số đó liên quan tới độ cong của hàm mất mát. Bát càng hẹp và dốc thì tốc độ học an toàn càng phải nhỏ.

Với mô hình thật, không ai tính được ngưỡng đó, nên tốc độ học trở thành thứ phải dò bằng tay. Đây là ví dụ điển hình của siêu tham số (hyperparameter): thứ ta chọn trước khi huấn luyện, khác với tham số w mà máy tự học trong lúc huấn luyện. Phân biệt hai khái niệm này rất quan trọng — Chặng 8 sẽ quay lại kỹ.

Mẹo phổ biến nhất trong thực tế: bắt đầu với tốc độ học lớn cho đi nhanh, rồi giảm dần theo thời gian để về cuối bước ngắn lại mà đáp cho êm. Kỹ thuật đó gọi là learning rate schedule, và gần như mọi mô hình lớn hiện nay đều dùng.

⚠️ Ba lỗi khiến người mới bế tắc

Một: viết nhầm dấu trừ thành dấu cộng. Chỉ sai một ký tự nhưng máy sẽ leo lên thay vì xuống, Loss tăng vùn vụt. Nếu Loss cứ tăng đều, hãy kiểm tra dấu trước khi nghi ngờ mọi thứ khác.

Hai: cập nhật w trước khi tính xong gradient. Với nhiều tham số, phải tính hết gradient của tất cả rồi mới cập nhật đồng loạt. Cập nhật lần lượt sẽ khiến tham số sau nhìn vào tham số trước đã đổi, và hướng đi sai lệch.

Ba: tưởng Loss phải giảm đều tăm tắp. Trong bài này thì có, vì địa hình là chiếc bát trơn. Với dữ liệu thật và mini-batch, đường Loss luôn nhấp nhô — chỉ cần xu hướng chung đi xuống là ổn, đừng hoảng khi thấy nó nhích lên vài bước.

 Đối chiếu thuật ngữ

Phần lớn cảm giác rối ở giai đoạn này đến từ từ vựng chứ không phải từ ý tưởng. Bảng này để bạn tra khi đọc tài liệu tiếng Anh.

Tiếng ViệtTiếng AnhTrong bài này là gì
Tham sốparameter, weightChính là w — thứ máy tự học
Siêu tham sốhyperparameterChính là lr — thứ ta chọn trước
Hàm mất mátloss / cost functionChiếc bát ở Bài 28
Độ dốc, gradientgradientĐạo hàm của Loss theo tham số
Tốc độ họclearning rateĐộ dài mỗi sải chân
Một vòng dữ liệuepochChạy hết dữ liệu đúng một lượt
Hội tụconvergeLoss thôi giảm, đã tới đáy
Phân kỳdivergeLoss phình to, thuật toán hỏng

 Bảng ghi nhớ

Việc cần làmCách viết
Công thức cập nhậtw = w - lr * grad
Vì sao có dấu trừĐi ngược hướng dốc lên thì mới xuống được đáy
Gradient của bài này5 * (w - 2), tổng quát là (2/n)*sum(x*(w*x - y))
Bước tự ngắn lại khi gần đáyVì gradient nhỏ dần, không cần lập trình thêm
Loss tăng dầnGần như luôn là do lr quá lớn — chia đôi rồi chạy lại
Kết quả cần giữ lạiGiá trị w cuối cùng — đó chính là mô hình đã học

✎ Kiểm tra nhanh

Câu 1. Trong công thức w = w − lr × grad, dấu trừ có vai trò gì?

  • Làm cho w luôn giảm dần qua từng bước
  • Bắt máy đi ngược hướng dốc lên, tức là đi xuống phía đáy
  • Ngăn w nhận giá trị âm
  • Chỉ là quy ước, đổi thành dấu cộng cũng cho kết quả như nhau
Gradient chỉ hướng làm Loss tăng. Muốn giảm thì phải đi ngược lại, và dấu trừ làm đúng việc đó. Phương án đầu sai vì w có thể tăng — như trong bài, w đi từ 0 lên 2. Đổi thành dấu cộng là biến thuật toán thành leo lên đỉnh.

Câu 2. Tại w = 0 ta có đạo hàm bằng −10 và lr = 0,1. Giá trị w sau bước này là bao nhiêu?

  • −1
  • −10
  • 1
  • 10
w = 0 − 0,1 × (−10) = 0 + 1 = 1. Chỗ dễ nhầm nhất là quên rằng trừ đi một số âm thì thành cộng. Chính nhờ vậy mà w tăng lên đúng như ta muốn.

Câu 3. Nếu tại một điểm nào đó đạo hàm bằng đúng 0, bước tiếp theo w sẽ thế nào?

  • Tăng lên một lượng bằng lr
  • Giảm xuống một lượng bằng lr
  • Nhảy về 0
  • Không đổi — thuật toán đã dừng lại ở đó
w = w − lr × 0 = w. Đây chính là cách thuật toán tự biết mình đã tới đáy: không còn dốc thì không còn lý do để bước. Với địa hình phức tạp, điều này cũng lý giải vì sao mô hình có thể mắc kẹt ở một hố cạn.

Câu 4. Chạy vòng lặp mà thấy Loss cứ tăng qua từng bước. Nguyên nhân khả dĩ nhất là gì?

  • Tốc độ học quá lớn nên bước chân vọt qua đáy sang bên kia, ngày một xa
  • Dữ liệu quá ít
  • Chưa chạy đủ số bước
  • Giá trị khởi tạo của w quá nhỏ
Đây là dấu hiệu kinh điển của tốc độ học quá lớn. Cách xử lý đơn giản: chia đôi lr rồi chạy lại, lặp cho tới khi Loss chịu đi xuống. Chạy thêm bước chỉ làm nó phình to hơn, còn giá trị khởi tạo thì không gây ra chuyện này.

Câu 5. Vì sao càng gần đáy thì bước chân càng ngắn lại, dù lr không hề thay đổi?

  • Vì trong code có câu lệnh giảm lr theo thời gian
  • Vì gần đáy thì địa hình thoải, gradient nhỏ đi, mà bước chân tỉ lệ với gradient
  • Vì Python tự làm tròn số khi giá trị quá nhỏ
  • Vì hàm mất mát đổi hình dạng trong quá trình chạy
Độ dài bước bằng lr × grad. Gần đáy, độ dốc thoải nên grad nhỏ, kéo theo bước ngắn. Đây là món quà miễn phí của công thức — không ai phải lập trình thêm. Nhìn cột đạo hàm trong bảng: −10, −5, −2,5, −1,25.

Câu 6. Trong bảng tính tay, w đi 0 → 1 → 1,5 → 1,75 → 1,875. Quy luật ở đây là gì?

  • Mỗi bước cộng thêm một lượng cố định
  • Mỗi bước tăng gấp đôi giá trị cũ
  • Mỗi bước đi được đúng một nửa quãng đường còn lại tới 2
  • Không có quy luật, các số phụ thuộc dữ liệu
Từ 0 còn cách 2, đi 1 rồi còn cách 1; từ 1 còn cách 1, đi 0,5 rồi còn cách 0,5. Đúng một nửa mỗi lần. Đó là vì lr = 0,1 cho hệ số co lại bằng 1 − 5×0,1 = 0,5. Cũng vì thế mà về mặt toán học w không bao giờ chạm đúng 2, chỉ ngày càng gần.

Câu 7. Sau khi huấn luyện xong, thứ gì được giữ lại để về sau đem ra dự đoán?

  • Toàn bộ dữ liệu huấn luyện
  • Giá trị tốc độ học đã dùng
  • Danh sách Loss của mọi bước
  • Giá trị w cuối cùng
w chính là thứ mô hình đã học được, và cũng là thứ duy nhất cần giữ. Có w rồi thì tính y = w × x cho dữ liệu mới, không cần dữ liệu cũ nữa. Tốc độ học và lịch sử Loss chỉ phục vụ quá trình huấn luyện.

 Thực hành: viết vòng lặp học đầu tiên của bạn

▶ Cách dùng: bấm Khởi động Python một lần (lần đầu mất khoảng 10–20 giây), đợi báo sẵn sàng rồi bấm Chạy code. Phần 1 in ra đúng bảng tính tay ở trên — hãy đối chiếu vài dòng để tin rằng máy và bạn làm cùng một việc.

 Thử thách (làm được cả bốn là bạn đã thật sự nắm bài):

  1. Đổi lr = 0.2 ở phần 1 rồi chạy lại. Sau đúng một bước, w bằng bao nhiêu? Đối chiếu với bảng hệ số co lại ở Lớp B phần 3 để giải thích.
  2. Đổi lr = 0.5. Cột w diễn biến ra sao? Nó nhảy qua nhảy lại quanh số nào, và biên độ tăng hay giảm?
  3. Sửa dấu trừ trong w_moi = w - lr * g thành dấu cộng rồi chạy. Loss đi lên hay đi xuống? Điều này khớp với lỗi thứ nhất trong hộp cảnh báo chứ?
  4. Đổi dữ liệu thành y_that = np.array([3.0, 6.0]). Máy sẽ hội tụ về giá trị w nào? Bạn đoán trước rồi hãy chạy để kiểm tra.

Bài tiếp theo — đóng chặng. Đến đây bạn đã có đủ cả bốn mảnh: vector để biểu diễn, đạo hàm để định hướng, hàm mất mát để chấm điểm, gradient descent để bước đi. Bài 30 sẽ dùng cả bốn cùng lúc trong một dự án hoàn chỉnh: khớp một đường thẳng vào dữ liệu thật từ con số không, tự học cả độ dốc lẫn điểm cắt trục, không dùng thư viện máy học nào. Đó cũng chính là thứ mà scikit-learn làm bên trong — và ở Chặng 8, bạn sẽ gọi nó bằng một dòng lệnh, nhưng lần này là với hiểu biết đầy đủ về những gì đang diễn ra bên dưới.