Bỏ qua để đến nội dung

Gradient Descent

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

  • Hiểu gradient descent (GD) tìm đáy của hàm mất mát thế nào, và vì sao đi ngược gradient là đi xuống dốc.
  • Viết được quy tắc cập nhật θ←θ−η∇L(θ)\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta\nabla\mathcal{L}(\boldsymbol{\theta}) cho MSE ở dạng vectơ, và cài đặt nó bằng NumPy.
  • Giải thích được vì sao tốc độ học (learning rate) η\eta nhỏ thì chậm, lớn thì dao động hoặc phân kỳ (diverge), qua ngưỡng η<2/L\eta < 2/L; vì sao chuẩn hóa đặc trưng (feature scaling) giúp GD nhanh hơn, và khi nào nên dừng.
  • Phân biệt batch GD, SGD và mini-batch, biết epoch là gì, và dùng được SGDRegressor của scikit-learn.

Bạn đứng trên sườn núi trong sương mù dày và muốn xuống thung lũng. Bạn không thấy đáy ở đâu, chỉ cảm nhận được mặt đất dưới chân đang dốc về phía nào. Cách làm hợp lý: bước một bước ngắn về phía dốc xuống, đứng lại, cảm nhận độ dốc lần nữa, rồi bước tiếp. Khi mặt đất dưới chân phẳng, bạn đã tới đáy.

Đó là toàn bộ ý tưởng của gradient descent (đôi khi dịch là “hạ gradient”, viết tắt GD). “Ngọn núi” là hàm mất mát, “vị trí” là bộ tham số, còn “độ dốc” là đạo hàm.

Bài trước giải thẳng bằng phương trình chuẩn, nhưng nhiều mô hình (hồi quy logistic, mạng nơ-ron) không có công thức nghiệm, và khi số đặc trưng nn rất lớn, giải hệ n+1n + 1 ẩn tốn cỡ n3n^3 phép tính. GD chỉ cần độ dốc tại điểm đang đứng. Bài này dùng nó giải lại bài toán 10 căn hộ, nên kiểm tra được từng bước.

Giá thuê theo diện tích của 10 căn hộ. Số liệu tự tạo cho bài học, không phải giá thị trường.
Diện tích (m²)Giá thuê (triệu đồng/tháng)
256.7
306.5
368.8
428.8
489.4
5512.2
6012.0
6814.1
7513.9
8516.8

Bắt đầu từ một dự đoán rất tệ: w=0w = 0, b=0b = 0, tức căn nào cũng đoán giá 0 đồng. MSE là 129.75. Chưa biết đáy ở đâu, bạn thử nhích từng tham số một chút rồi tính lại MSE:

ThửMSE
Xuất phát: w=0w = 0, b=0b = 0129.75
Tăng ww thêm 0.01117.42
Tăng bb thêm 0.01129.53

Nhích tham số nào lên MSE cũng giảm, nên cả hai nên tăng. Nhưng tăng ww làm MSE giảm 12.33, còn tăng bb chỉ làm MSE giảm 0.22, kém khoảng 50 lần. Lý do: tăng ww thêm 0.01 làm giá dự đoán của một căn cỡ trung bình, khoảng 52 m², tăng 0.52 triệu, còn tăng bb thêm 0.01 chỉ làm mọi giá dự đoán tăng 0.01 triệu. Độ dốc cho biết nên đi về phía nào, và mạnh cỡ nào. Còn chênh lệch giữa hai hướng sẽ thành rắc rối ở mục chuẩn hóa đặc trưng.

Bài toán của bài này là: từ một điểm xuất phát bất kỳ, lặp lại “đo độ dốc, bước một bước ngắn xuống dốc” cho tới đáy của MSE, và chọn độ dài bước sao cho tới nơi nhanh mà không văng ra khỏi lòng chảo.

Gradient: gom các độ dốc thành một vectơ

Phần tiêu đề “Gradient: gom các độ dốc thành một vectơ”

Như bài trước, gộp tham số thành vectơ θ=[w,b]⊤\boldsymbol{\theta} = [w, b]^\top. Gradient của L\mathcal{L} tại θ\boldsymbol{\theta} là vectơ các đạo hàm riêng, ký hiệu ∇L(θ)\nabla\mathcal{L}(\boldsymbol{\theta}) (đọc là “nabla L”):

∇L(θ)=[∂L/∂w∂L/∂b]\nabla\mathcal{L}(\boldsymbol{\theta}) = \begin{bmatrix} \partial\mathcal{L}/\partial w \\ \partial\mathcal{L}/\partial b \end{bmatrix}

Với nn đặc trưng, gradient có n+1n + 1 phần tử. Bài trước đã dẫn hai đạo hàm riêng của MSE, với y^(i)=wx(i)+b\hat{y}^{(i)} = wx^{(i)} + b:

∂L∂w=2m∑i=1mx(i)(y^(i)−y(i))∂L∂b=2m∑i=1m(y^(i)−y(i))\begin{aligned} \frac{\partial\mathcal{L}}{\partial w} &= \frac{2}{m}\sum_{i=1}^{m}x^{(i)}\left(\hat{y}^{(i)} - y^{(i)}\right) \\ \frac{\partial\mathcal{L}}{\partial b} &= \frac{2}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right) \end{aligned}

Viết bằng ma trận thiết kế X\mathbf{X} (cột diện tích, rồi cột toàn số 1): vectơ Xθ−y\mathbf{X}\boldsymbol{\theta} - \mathbf{y} chứa các hiệu y^(i)−y(i)\hat{y}^{(i)} - y^{(i)}. Nhân nó với X⊤\mathbf{X}^\top, hàng đầu (cột diện tích) cho tổng thứ nhất, hàng thứ hai (cột số 1) cho tổng thứ hai. Nhân thêm 2m\frac{2}{m} là được đúng hai đạo hàm riêng:

∇L(θ)=2mX⊤(Xθ−y)\nabla\mathcal{L}(\boldsymbol{\theta}) = \frac{2}{m}\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right)

Công thức này đúng với mọi nn. Cho gradient bằng 0\mathbf{0} thì được X⊤(Xθ−y)=0\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right) = \mathbf{0}, chính là phương trình chuẩn. Vậy chỗ GD dừng lại đúng là nghiệm của bài trước.

Tại (0,0)(0, 0), gradient là [−1264.08,−21.84]⊤[-1264.08, -21.84]^\top. Nhân với 0.01 được −12.64-12.64 và −0.22-0.22, gần đúng mức giảm trong bảng ở mục Trực giác.

Một biến trước. Nhìn thật gần một điểm ww, đồ thị của L\mathcal{L} gần như trùng với tiếp tuyến (tangent line) của nó, đường thẳng có độ dốc L′(w)\mathcal{L}'(w). Nên với một bước nhỏ Δ\Delta:

L(w+Δ)≈L(w)+L′(w) Δ\mathcal{L}(w + \Delta) \approx \mathcal{L}(w) + \mathcal{L}'(w)\,\Delta

Chọn Δ=−η L′(w)\Delta = -\eta\,\mathcal{L}'(w) với η>0\eta > 0 nhỏ, thay vào:

L(w+Δ)≈L(w)−η(L′(w))2\mathcal{L}(w + \Delta) \approx \mathcal{L}(w) - \eta\left(\mathcal{L}'(w)\right)^2

Bình phương không âm, nên L\mathcal{L} giảm (trừ khi L′(w)=0\mathcal{L}'(w) = 0, tức đã ở chỗ phẳng). Đọc bằng lời: dốc lên về bên phải (L′>0\mathcal{L}' > 0) thì bước sang trái, và ngược lại.

Nhiều biến. Nhích tham số một đoạn Δ=[Δw,Δb]⊤\boldsymbol{\Delta} = [\Delta_w, \Delta_b]^\top. Mỗi tham số góp phần thay đổi bằng độ dốc theo nó nhân với đoạn nhích, và tổng các phần là một tích vô hướng:

L(θ+Δ)≈L(θ)+∂L∂wΔw+∂L∂bΔb=L(θ)+∇L(θ)⊤Δ\begin{aligned} \mathcal{L}(\boldsymbol{\theta} + \boldsymbol{\Delta}) &\approx \mathcal{L}(\boldsymbol{\theta}) + \frac{\partial\mathcal{L}}{\partial w}\Delta_w + \frac{\partial\mathcal{L}}{\partial b}\Delta_b \\ &= \mathcal{L}(\boldsymbol{\theta}) + \nabla\mathcal{L}(\boldsymbol{\theta})^\top\boldsymbol{\Delta} \end{aligned}

Chọn Δ=−η∇L(θ)\boldsymbol{\Delta} = -\eta\nabla\mathcal{L}(\boldsymbol{\theta}), số hạng cuối thành −η∥∇L(θ)∥2-\eta\lVert\nabla\mathcal{L}(\boldsymbol{\theta})\rVert^2, luôn không dương. Hơn nữa, trong mọi bước cùng độ dài, tích vô hướng ∇L⊤Δ=∥∇L∥ ∥Δ∥cos⁡α\nabla\mathcal{L}^\top\boldsymbol{\Delta} = \lVert\nabla\mathcal{L}\rVert\,\lVert\boldsymbol{\Delta}\rVert\cos\alpha nhỏ nhất khi góc α\alpha giữa hai vectơ là 180°. Vậy gradient chỉ hướng đi lên dốc nhất, và ngược gradient là hướng đi xuống dốc nhất. Trên bản đồ đường mức (contour line), tức các đường nối những điểm cùng MSE, hướng này vuông góc với đường mức đi qua điểm đang đứng.

Chữ “xấp xỉ” là điểm mấu chốt: tiếp tuyến chỉ giống đồ thị ở gần điểm đang đứng, nên bước quá dài thì L\mathcal{L} có thể tăng.

Ý tưởng này có từ năm 1847. Để giải các hệ phương trình trong bài toán tính quỹ đạo thiên thể, Augustin-Louis Cauchy gom chúng thành một tổng bình phương, rồi làm tổng đó nhỏ dần bằng cách dịch mỗi ẩn ngược dấu đạo hàm riêng của nó.

Gradient descent lặp lại một bước duy nhất:

θt+1=θt−η∇L(θt)\boldsymbol{\theta}_{t+1} = \boldsymbol{\theta}_t - \eta\nabla\mathcal{L}(\boldsymbol{\theta}_t)

Ở đây t=0,1,2,…t = 0, 1, 2, \dots là số thứ tự của bước, θ0\boldsymbol{\theta}_0 là điểm xuất phát (thường là 0\mathbf{0}), và η>0\eta > 0 là tốc độ học, một siêu tham số (hyperparameter) do bạn chọn. Với MSE, thay gradient vào:

θt+1=θt−η⋅2mX⊤(Xθt−y)\boldsymbol{\theta}_{t+1} = \boldsymbol{\theta}_t - \eta \cdot \frac{2}{m}\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta}_t - \mathbf{y}\right)

Mỗi bước dùng cả mm mẫu để tính gradient, nên cách này gọi là batch gradient descent. Hai điều đáng để ý:

  • Bước tự ngắn lại gần đáy, vì độ dài bước là η∥∇L∥\eta\lVert\nabla\mathcal{L}\rVert mà gradient nhỏ dần. Không cần giảm η\eta theo thời gian.
  • MSE của hồi quy tuyến tính là một cái bát, gọi là hàm lồi (convex function): chỉ có một đáy, không có hố phụ nào để mắc kẹt. Với mạng nơ-ron thì không còn đảm bảo này.

Xét cái bát đơn giản nhất, một parabol:

f(w)=L2(w−w∗)2+cf(w) = \frac{L}{2}\left(w - w^\ast\right)^2 + c

w∗w^\ast là đáy, cc là giá trị nhỏ nhất, còn L>0L > 0 là độ cong (curvature): đạo hàm bậc hai của ff, cho biết thành bát dựng đứng cỡ nào. Đừng nhầm chữ LL này với hàm mất mát L\mathcal{L}.

Đạo hàm là f′(w)=L(w−w∗)f'(w) = L\left(w - w^\ast\right). Thay vào quy tắc cập nhật wt+1=wt−ηf′(wt)w_{t+1} = w_t - \eta f'(w_t), rồi trừ w∗w^\ast ở cả hai vế:

wt+1−w∗=(wt−w∗)−ηL(wt−w∗)=(1−ηL)(wt−w∗)\begin{aligned} w_{t+1} - w^\ast &= \left(w_t - w^\ast\right) - \eta L\left(w_t - w^\ast\right) \\ &= \left(1 - \eta L\right)\left(w_t - w^\ast\right) \end{aligned}

Dòng thứ hai đặt wt−w∗w_t - w^\ast ra ngoài. Vậy sau mỗi bước, khoảng cách tới đáy bị nhân với cùng một số q=1−ηLq = 1 - \eta L. Sau tt bước, nó là qt(w0−w∗)q^t\left(w_0 - w^\ast\right). Mọi thứ phụ thuộc vào qq:

η\etaq=1−ηLq = 1 - \eta LGD làm gì
0<η<1/L0 < \eta < 1/L0<q<10 < q < 1Tiến dần về đáy từ một phía. η\eta càng nhỏ, qq càng gần 1, càng chậm
η=1/L\eta = 1/Lq=0q = 0Tới đáy sau đúng một bước
1/L<η<2/L1/L < \eta < 2/L−1<q<0-1 < q < 0Nhảy qua lại hai bên đáy, lần sau ngắn hơn lần trước: dao động nhưng vẫn hội tụ (converge)
η=2/L\eta = 2/Lq=−1q = -1Nhảy qua lại mãi, không gần thêm
η>2/L\eta > 2/Lq<−1q < -1Mỗi lần nhảy xa hơn: phân kỳ

Thử với f(w)=w2f(w) = w^2, tức L=2L = 2, đáy w∗=0w^\ast = 0, ngưỡng 2/L=12/L = 1, xuất phát từ w0=1w_0 = 1:

η\etaqqw0,w1,…,w5w_0, w_1, \dots, w_5
0.10.81, 0.8, 0.64, 0.512, 0.4096, 0.3277
0.501, 0, 0, 0, 0, 0
0.9−0.81, −0.8, 0.64, −0.512, 0.4096, −0.3277
1.1−1.21, −1.2, 1.44, −1.728, 2.0736, −2.4883

Để ý hàng 0.1 và hàng 0.9: khoảng cách tới đáy giống hệt nhau ở mọi bước, chỉ khác dấu. Bước dài gấp 9 lần mà không nhanh hơn chút nào.

MSE theo (w,b)(w, b) cũng là một cái bát bậc hai, nhưng cong nhiều theo hướng này, ít theo hướng kia. Độ cong đo bằng các đạo hàm riêng bậc hai: lấy đạo hàm của ∂L/∂w\partial\mathcal{L}/\partial w theo ww được 2m∑i(x(i))2\frac{2}{m}\sum_i\left(x^{(i)}\right)^2, theo bb được 2m∑ix(i)\frac{2}{m}\sum_i x^{(i)}; đạo hàm của ∂L/∂b\partial\mathcal{L}/\partial b theo bb là 2. Xếp vào một ma trận, gọi là ma trận Hessian (Hessian matrix):

H=[2m∑i(x(i))22m∑ix(i)2m∑ix(i)2]=2mX⊤X\mathbf{H} = \begin{bmatrix} \frac{2}{m}\sum_i \left(x^{(i)}\right)^2 & \frac{2}{m}\sum_i x^{(i)} \\ \frac{2}{m}\sum_i x^{(i)} & 2 \end{bmatrix} = \frac{2}{m}\mathbf{X}^\top\mathbf{X}

Dấu bằng cuối đúng vì các phần tử của X⊤X\mathbf{X}^\top\mathbf{X} là ∑i(x(i))2\sum_i \left(x^{(i)}\right)^2, ∑ix(i)\sum_i x^{(i)} và mm. H\mathbf{H} không phụ thuộc θ\boldsymbol{\theta}: cái bát cong như nhau ở mọi chỗ.

Bài này không chứng minh, nhưng với mọi bát bậc hai, có hai hướng vuông góc với nhau (hai vectơ riêng, eigenvector, của H\mathbf{H}) mà dọc theo mỗi hướng, GD cư xử đúng như ví dụ một chiều, với độ cong riêng của hướng đó. Hai độ cong ấy là hai trị riêng (eigenvalue) của H\mathbf{H}. Gọi LL là trị riêng lớn nhất (hướng dốc nhất), μ\mu là trị riêng nhỏ nhất (hướng phẳng nhất). Theo hai hướng, khoảng cách tới đáy lần lượt bị nhân với 1−ηL1 - \eta L và 1−ημ1 - \eta\mu mỗi bước:

  • Hướng dốc nhất quyết định ngưỡng. GD hội tụ khi η<2/L\eta < 2/L. Vượt ngưỡng thì văng ra theo hướng này.
  • Hướng có hệ số gần ±1\pm 1 nhất quyết định tốc độ. Khi η\eta còn xa 2/L2/L, đó là hướng phẳng: 1−ημ1 - \eta\mu rất gần 1 khi μ\mu nhỏ. Khi η\eta sát ngưỡng, 1−ηL1 - \eta L gần −1-1: hướng dốc nhảy qua lại và tắt rất chậm.

Tỉ số κ=L/μ\kappa = L/\mu gọi là số điều kiện (condition number). κ\kappa lớn nghĩa là bát dẹt: đường mức là những elip dài và hẹp, và không có η\eta nào tốt cho cả hai hướng.

Với diện tích thô của bảng căn hộ:

H=[6197.6104.8104.82],L≈6199.4,μ≈0.228\mathbf{H} = \begin{bmatrix} 6197.6 & 104.8 \\ 104.8 & 2 \end{bmatrix}, \qquad L \approx 6199.4, \quad \mu \approx 0.228

κ≈27 200\kappa \approx 27\,200, đúng con số “khoảng 27 nghìn” bạn đã gặp ở bài Hồi quy tuyến tính: số điều kiện của X⊤X\mathbf{X}^\top\mathbf{X}. Hướng dốc nhất gần như trùng trục ww, hướng phẳng gần như trùng trục bb: lòng bát là một khe hẹp và rất dài. Thành khe dốc ép η<2/L≈3.2×10−4\eta < 2/L \approx 3.2 \times 10^{-4}. Với η=3×10−4\eta = 3 \times 10^{-4}, khoảng cách dọc khe chỉ bị nhân với 1−3×10−4×0.228≈0.999931 - 3 \times 10^{-4} \times 0.228 \approx 0.99993 mỗi bước: sau 100 bước, bb mới nhích từ 0 lên 0.017, trong khi đáp án là 2.04.

Cách chữa là chuẩn hóa đặc trưng: đưa đặc trưng về thang đo cỡ 1, để đổi ww không còn mạnh hơn đổi bb hàng nghìn lần. Bài này dùng chuẩn hóa min-max (min-max scaling), đưa diện tích từ đoạn 25–85 m² về đoạn [0,1][0, 1]:

x~=x−2585−25=x−2560\tilde{x} = \frac{x - 25}{85 - 25} = \frac{x - 25}{60}

Ở bài trước, dấu ngã trong x~(i)\tilde{x}^{(i)} là độ lệch khỏi trung bình. Trong bài này, x~\tilde{x} là diện tích đã chuẩn hóa.

Chỉ chuẩn hóa diện tích, giữ nguyên giá thuê yy, để MSE vẫn là MSE của bài trước. Mô hình mới là y^=w~x~+b~\hat{y} = \tilde{w}\tilde{x} + \tilde{b}, và hai tham số có nghĩa rất đời thường:

  • b~\tilde{b} là giá dự đoán cho căn nhỏ nhất (25 m²), vì ở đó x~=0\tilde{x} = 0;
  • w~\tilde{w} là chênh lệch giá dự đoán giữa căn lớn nhất (85 m²) và căn nhỏ nhất, vì x~\tilde{x} đi từ 0 tới 1.

Thay cột diện tích của X\mathbf{X} bằng cột x~\tilde{x} rồi tính lại:

H=[0.6130.9130.9132],L≈2.4533,μ≈0.160\mathbf{H} = \begin{bmatrix} 0.613 & 0.913 \\ 0.913 & 2 \end{bmatrix}, \qquad L \approx 2.4533, \quad \mu \approx 0.160

Ngưỡng thành 2/L≈0.8152/L \approx 0.815 và κ≈15\kappa \approx 15: từ khe hẹp 27 200 xuống một cái bát bầu dục, dài gấp khoảng 4 lần rộng (κ≈3.9\sqrt{\kappa} \approx 3.9). Hướng dốc nhất giờ chủ yếu là b~\tilde{b}, tức nâng hay hạ cả đường. Hướng phẳng nhất là xoay đường quanh khoảng giữa bảng.

Tại (0,0)(0, 0), gradient giờ là [−11.97,−21.84]⊤[-11.97, -21.84]^\top; với η=0.3\eta = 0.3, bước đầu tới (3.59,6.55)(3.59, 6.55), MSE từ 129.75 xuống 12.05. Cứ thế, GD tới đáy ở w~≈10.17\tilde{w} \approx 10.17, b~≈6.28\tilde{b} \approx 6.28. Để đổi về đơn vị gốc, thay x~=(x−25)/60\tilde{x} = (x - 25)/60 vào mô hình rồi gom hệ số của xx:

w~ x−2560+b~=w~60 x+(b~−25 w~60)⇒w=w~60,b=b~−25w\tilde{w}\,\frac{x - 25}{60} + \tilde{b} = \frac{\tilde{w}}{60}\,x + \left(\tilde{b} - 25\,\frac{\tilde{w}}{60}\right) \quad\Rightarrow\quad w = \frac{\tilde{w}}{60}, \qquad b = \tilde{b} - 25w

Ra w=10.17/60≈0.1695w = 10.17/60 \approx 0.1695 và b≈6.28−25×0.1695≈2.04b \approx 6.28 - 25 \times 0.1695 \approx 2.04, đúng đáp án của bài trước.

Cách chuẩn hóa phổ biến nhất là z-score (StandardScaler, bài Ridge, Lasso, Elastic Net): trừ trung bình, chia độ lệch chuẩn. Với một đặc trưng, nó làm bát tròn hẳn (κ=1\kappa = 1): tốt cho tính toán, nhưng không còn thung lũng để quan sát, nên bài này dùng min-max. Cách nào cũng vậy, tham số chuẩn hóa chỉ học từ tập train.

GD không tự biết mình đã tới đáy. Bạn cần tiêu chí dừng (stopping criterion), thường dùng kết hợp:

  1. Số bước tối đa. Luôn nên có, để vòng lặp không chạy mãi khi phân kỳ.
  2. Gradient đủ nhỏ: ∥∇L∥<ε\lVert\nabla\mathcal{L}\rVert < \varepsilon, ví dụ 10−610^{-6}. Mặt đất gần như phẳng.
  3. Mất mát gần như không giảm nữa. Tham số tol của scikit-learn làm theo kiểu này, và có thể dừng quá sớm (hộp thứ hai ở mục Lỗi hay gặp).

Với mm = 10 triệu, mỗi bước của batch GD phải duyệt hết 10 triệu mẫu. Ý tưởng: tính gradient trên một phần dữ liệu, có nhiễu nhưng rẻ hơn nhiều.

  • SGD (stochastic gradient descent, gradient descent ngẫu nhiên): mỗi bước dùng một mẫu chọn ngẫu nhiên.
  • Mini-batch gradient descent: mỗi bước dùng một nhóm BB mẫu, gradient là 2BXB⊤(XBθ−yB)\frac{2}{B}\mathbf{X}_B^\top\left(\mathbf{X}_B\boldsymbol{\theta} - \mathbf{y}_B\right), với XB\mathbf{X}_B, yB\mathbf{y}_B là các hàng của nhóm đó. Đây là lựa chọn mặc định khi huấn luyện mạng nơ-ron.
  • Epoch: một lượt đi qua toàn bộ tập train, gồm m/Bm/B bước. Nên xáo trộn dữ liệu ở đầu mỗi epoch.

Cùng một epoch, SGD cập nhật mm lần, batch GD chỉ một lần. Đổi lại, SGD lắc lư: mỗi bước kéo θ\boldsymbol{\theta} về phía một mẫu, nên với η\eta cố định nó cứ nhấp nhô quanh đáy. Cách chữa là giảm η\eta dần, gọi là lịch tốc độ học (learning rate schedule). SGD có tiền thân là phương pháp xấp xỉ ngẫu nhiên (stochastic approximation) của Robbins và Monro (1951). Các cải tiến như Momentum và Adam để dành cho bài Các thuật toán tối ưu nâng cao.

GD cần chọn η\eta và chuẩn hóa cho đúng, chỉ chắc chắn tới cực tiểu toàn cục (global minimum) khi hàm lồi, và với ít đặc trưng thì phương trình chuẩn vẫn tiện hơn; nó đáng giá khi nn hoặc mm rất lớn, hoặc khi không có nghiệm dạng đóng, như ở bài Hồi quy Logistic và bài Mạng nơ-ron nhân tạo.

Hình dưới vẽ đường mức của MSE theo (w~,b~)(\tilde{w}, \tilde{b}), dấu × là đáy (10.17,6.28)(10.17, 6.28), GD xuất phát ở (0,0)(0, 0). Nút Bước đi một bước, Chạy đi tới tối đa 100 bước, Đặt lại về đầu, thanh trượt chọn một trong năm mức η\eta. Biểu đồ nhỏ vẽ MSE theo số bước (thang log), đường đứt là MSE nhỏ nhất 0.3615. Ngưỡng là 2/L≈0.8152/L \approx 0.815. Hãy thử:

  1. η=0.30\eta = 0.30: bấm Bước vài lần. MSE đi 129.75 → 12.05 → 3.5960: hai bước đầu lao xuống lòng thung lũng, rồi đường đi rẽ ngoặt, men theo đáy và chậm hẳn. MSE vào trong 1% của đáy ở bước 69.
  2. η=0.05\eta = 0.05: bấm Chạy. Các bước ngắn dần, hết 100 bước MSE vẫn còn 1.0023.
  3. η=0.70\eta = 0.70: đường đi zigzag hai bên thung lũng, hẹp dần, và vào trong 1% của đáy ở bước 29, nhanh nhất trong năm mức.
  4. η=0.80\eta = 0.80 rồi 0.850.85: sát ngưỡng, đường đi nhảy gần hết bề ngang thung lũng, hết 100 bước còn 0.4240; vượt ngưỡng, MSE tăng 151.40 → 177.24 → 207.95 → 244.32 và văng khỏi khung ở bước 4.
Mỗi bước đi ngược hướng gradient của MSE. Chọn tốc độ học η rồi bấm Bước hoặc Chạy. Diện tích đã chuẩn hóa về [0, 1], nên hai trục là w̃ và b̃. Với dữ liệu này, 2/L ≈ 0.815.
  • Đường mức của MSE
  • Đường đi của gradient descent
  • Điểm bắt đầu (0, 0)
  • Vị trí hiện tại
  • Đáy (phương trình chuẩn)
Số bước0 / 100
MSE129.75
(w̃, b̃)(0.00, 0.00)
Đổi về đơn vị gốcŷ = 0.0000x + 0.00
  • MSE nhỏ nhất (0.3615)

Bấm Bước để đi một bước, hoặc Chạy để đi tối đa 100 bước.

Các đường mức là những elip dẹt, nghiêng, cùng tâm ở đáy (10.17, 6.28), nơi MSE nhỏ nhất bằng 0.3615. Đường đi bắt đầu ở (0, 0), nơi MSE bằng 129.75.

Đường đứt là MSE nhỏ nhất, 0.3615. Mỗi vạch trên trục MSE lớn gấp 10 lần vạch ngay dưới nó.

Ba ô dưới đây chạy độc lập, ô nào cũng tự tạo bảng căn hộ. Ô thứ hai và thứ ba lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm Matplotlib và scikit-learn.

Ô đầu tiên cài đặt batch GD: chuẩn hóa min-max, xuất phát từ (0,0)(0, 0) với η=0.3\eta = 0.3, dừng khi ∥∇L∥<10−6\lVert\nabla\mathcal{L}\rVert < 10^{-6} hoặc khi đủ 10 000 bước, rồi đổi nghiệm về đơn vị gốc và so với phương trình chuẩn.

Batch gradient descent bằng NumPy

import numpy as np
# Bảng 10 căn hộ (dữ liệu minh họa, tự tạo cho bài học).
# area: diện tích (m²), rent: giá thuê (triệu đồng/tháng).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
m = len(area)
# Chuẩn hóa min-max: diện tích về đoạn [0, 1], giữ nguyên giá thuê.
lo, span = area.min(), area.max() - area.min() # 25 và 60
x = (area - lo) / span
X = np.column_stack([x, np.ones(m)]) # thêm cột số 1 cho b
eta = 0.3 # tốc độ học
theta = np.zeros(2) # điểm xuất phát: w~ = 0, b~ = 0
for step in range(10_000): # tiêu chí dừng 1: số bước tối đa
grad = 2 / m * X.T @ (X @ theta - rent) # gradient của MSE
if np.linalg.norm(grad) < 1e-6: # tiêu chí dừng 2: gradient rất nhỏ
break
theta = theta - eta * grad # quy tắc cập nhật
w_s, b_s = theta # w~, b~ trên diện tích đã chuẩn hóa
print(f"stopped after {step} steps")
print(f"scaled: w~ = {w_s:.4f}, b~ = {b_s:.4f}")
# Đổi về đơn vị gốc: w~ (x - 25) / 60 + b~ = w x + b.
w = w_s / span
b = b_s - lo * w
mse = np.mean((w * area + b - rent) ** 2)
print(f"GD: w = {w:.4f}, b = {b:.4f}, MSE = {mse:.4f}")
# Đối chiếu với phương trình chuẩn của bài trước, trên diện tích thô.
A = np.column_stack([area, np.ones(m)])
w_ne, b_ne = np.linalg.solve(A.T @ A, A.T @ rent)
mse_ne = np.mean((w_ne * area + b_ne - rent) ** 2)
print(f"normal: w = {w_ne:.4f}, b = {b_ne:.4f}, MSE = {mse_ne:.4f}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

GD dừng sau 282 bước với w~=10.1686\tilde{w} = 10.1686, b~=6.2763\tilde{b} = 6.2763, tức w=0.1695w = 0.1695, b=2.0394b = 2.0394, MSE =0.3615= 0.3615: khớp phương trình chuẩn. MSE đã vào trong 1% của đáy từ bước 69; hơn 200 bước còn lại là để gradient nhỏ tới 10−610^{-6}. Hãy sửa eta thành 0.05 hoặc 0.7 rồi chạy lại.

Ô thứ hai chạy 100 bước GD với vài mức η\eta, trên diện tích thô (bên trái) và đã chuẩn hóa (bên phải), rồi vẽ MSE theo số bước, trục tung theo thang log. Số in ra là MSE sau 100 bước.

Tốc độ học, có và không chuẩn hóa

import matplotlib.pyplot as plt
import numpy as np
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
m = len(area)
x = (area - area.min()) / (area.max() - area.min()) # min-max
# Chạy GD từ (0, 0). Trả về MSE ở bước 0, 1, ..., steps.
def mse_curve(feature, eta, steps):
X = np.column_stack([feature, np.ones(m)])
theta = np.zeros(2)
curve = [np.mean((X @ theta - rent) ** 2)]
for _ in range(steps):
theta = theta - eta * 2 / m * X.T @ (X @ theta - rent)
curve.append(np.mean((X @ theta - rent) ** 2))
return curve
runs = [("raw", area, [3e-4, 3.3e-4]), # trái: diện tích thô (m²)
("scaled", x, [0.05, 0.3, 0.7, 0.8, 0.85])] # phải: min-max
fig, axes = plt.subplots(1, 2, figsize=(9, 3.6), sharey=True)
for ax, (name, feature, etas) in zip(axes, runs):
for eta in etas:
curve = mse_curve(feature, eta, 100)
ax.plot(curve, label=f"eta = {eta:g}")
v = curve[-1] # MSE sau 100 bước; số quá lớn thì in dạng 1.234e+05
text = f"{v:.4f}" if v < 1e4 else f"{v:.3e}"
print(f"{name}, eta = {eta:g}: {text}")
ax.axhline(0.3615, color="k", ls=":") # MSE nhỏ nhất
ax.set(title=name, xlabel="Step", yscale="log", ylim=(0.1, 1e3))
ax.legend()
axes[0].set_ylabel("MSE (log scale)")
fig.tight_layout()
long_run = mse_curve(area, 3e-4, 10_000) # dữ liệu thô, 10 000 bước
print(f"raw, eta = 0.0003, 10000 steps: {long_run[-1]:.4f}")
plt.show()

Lần chạy đầu cần tải Python, khoảng 10 MB.

  • Diện tích đã chuẩn hóa: các số khớp phần Minh họa. Để ý η=0.8\eta = 0.8: đường cong mất mát (loss curve) trông như giảm đều, dù tham số đang nhảy qua lại. η=0.85\eta = 0.85 phân kỳ tới 1.635×1091.635 \times 10^{9}.
  • Diện tích thô: ngưỡng là 2/L≈3.226×10−42/L \approx 3.226 \times 10^{-4}. η=3×10−4\eta = 3 \times 10^{-4} giảm nhanh rồi nằm ngang ở 0.8273: đã xuống đáy khe, giờ bò dọc khe. Sau 10 000 bước còn 0.4819 (dòng in cuối); tới bước 35 650 mới vào trong 1% của đáy. η=3.3×10−4\eta = 3.3 \times 10^{-4}, chỉ hơn ngưỡng một chút, đã phân kỳ tới 9.987×1059.987 \times 10^{5}.

Các mức phân kỳ chỉ chạy 100 bước: chạy lâu hơn thì số tràn và NumPy báo RuntimeWarning.

Ô thứ ba so ba cỡ batch bằng một vòng lặp tự viết, cùng η=0.1\eta = 0.1 và 100 epoch, rồi dùng SGDRegressor của scikit-learn, lớp cài đặt SGD.

SGD, mini-batch và SGDRegressor

import numpy as np
from sklearn.linear_model import SGDRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import MinMaxScaler
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
m, lo, span = len(area), area.min(), area.max() - area.min()
X = np.column_stack([(area - lo) / span, np.ones(m)]) # min-max, cột 1
def minibatch_gd(batch_size, eta=0.1, epochs=100, seed=0):
rng = np.random.default_rng(seed)
theta = np.zeros(2)
for _ in range(epochs): # mỗi epoch: xáo trộn, chia m // batch_size nhóm
for idx in np.array_split(rng.permutation(m), m // batch_size):
Xb, yb = X[idx], rent[idx]
theta = theta - eta * 2 / len(idx) * Xb.T @ (Xb @ theta - yb)
w = theta[0] / span # đổi về đơn vị gốc
return np.mean((X @ theta - rent) ** 2), w, theta[1] - lo * w
for size in [10, 2, 1]: # 10 = m: batch GD; 1: SGD
mse, w, b = minibatch_gd(size)
print(f"batch {size:2d}: MSE = {mse:.4f}, w = {w:.4f}, b = {b:.2f}")
A = area.reshape(-1, 1) # scikit-learn cần bảng 2 chiều
sgd = SGDRegressor(random_state=0).fit(A, rent) # mặc định, diện tích thô
mse = np.mean((sgd.predict(A) - rent) ** 2)
print(f"default, raw area: MSE = {mse:.1e}, n_iter_ = {sgd.n_iter_}")
sgd = SGDRegressor(penalty=None, learning_rate="constant", eta0=0.05,
max_iter=1000, tol=None, random_state=0)
scaler = MinMaxScaler()
model = make_pipeline(scaler, sgd).fit(A, rent)
w = sgd.coef_[0] * scaler.scale_[0] # đổi về đơn vị gốc
b = sgd.intercept_[0] + sgd.coef_[0] * scaler.min_[0]
mse = np.mean((model.predict(A) - rent) ** 2)
print(f"min-max + constant: MSE = {mse:.4f}, w = {w:.4f}, b = {b:.2f}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đích là w=0.1695w = 0.1695, b=2.04b = 2.04, MSE 0.3615.

  • Batch 10 (batch GD) chỉ cập nhật 100 lần, MSE còn 0.4885: chưa tới.
  • Batch 2 (mini-batch) cập nhật 500 lần, gần đáy nhất: MSE 0.3632.
  • Batch 1 (SGD) cập nhật 1 000 lần nhưng kết thúc xa đáy hơn: MSE 0.3766, b=2.15b = 2.15, vì nó cứ lắc lư quanh đáy.

SGDRegressor mặc định trên diện tích thô phân kỳ tới MSE 3.6×10243.6 \times 10^{24} sau 6 epoch: eta0=0.01 mặc định quá lớn so với thang đo của diện tích thô. Cấu hình cuối có MinMaxScaler, penalty=None (tắt phạt L2 bật sẵn), η\eta cố định 0.05 và tol=None (chạy đủ 1000 epoch). MSE ra 0.3615, nhưng b=2.05b = 2.05 vẫn lệch 0.01: với η\eta cố định, SGD dừng ở một điểm nào đó trong lúc đang nhấp nhô quanh đáy.

Câu 1/4 Hàm mất mát một chiều L(w)=2(w−3)2\mathcal{L}(w) = 2(w - 3)^2. Bạn chạy gradient descent với η=0.4\eta = 0.4, xuất phát từ w=0w = 0. Điều gì xảy ra?
Câu 2/4 Tại θ=[w,b]⊤=[1,2]⊤\boldsymbol{\theta} = [w, b]^\top = [1, 2]^\top, gradient của L\mathcal{L} là [−4,2]⊤[-4, 2]^\top. Sau một bước gradient descent với η=0.1\eta = 0.1, θ\boldsymbol{\theta} bằng bao nhiêu?
Câu 3/4 Bạn chạy GD trên diện tích thô (m², không chuẩn hóa) với một η\eta đang hội tụ tốt. Đồng nghiệp đổi cột diện tích sang dm² (nhân 100) và giữ nguyên η\eta. Nhiều khả năng điều gì xảy ra?
Câu 4/4 Tập train có m=1000m = 1000 mẫu. Bạn huấn luyện 3 epoch bằng mini-batch cỡ 50. Tham số được cập nhật bao nhiêu lần?

Ý chính:

  • Gradient descent lặp lại θ←θ−η∇L(θ)\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta\nabla\mathcal{L}(\boldsymbol{\theta}), với ∇L=2mX⊤(Xθ−y)\nabla\mathcal{L} = \frac{2}{m}\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right) cho MSE, và dừng ở đúng nghiệm của phương trình chuẩn.
  • GD hội tụ khi 0<η<2/L0 < \eta < 2/L, với LL là trị riêng lớn nhất của Hessian 2mX⊤X\frac{2}{m}\mathbf{X}^\top\mathbf{X}: η\eta nhỏ thì chậm, trên 1/L1/L thì dao động, trên 2/L2/L thì phân kỳ.
  • Chuẩn hóa min-max x~=(x−25)/60\tilde{x} = (x - 25)/60 đưa κ\kappa từ 27 200 về khoảng 15, nên số bước tới 1% của đáy giảm từ 35 650 (η=3×10−4\eta = 3 \times 10^{-4}) xuống 69 (η=0.3\eta = 0.3); đổi về đơn vị gốc bằng w=w~/60w = \tilde{w}/60, b=b~−25wb = \tilde{b} - 25w.
  • Batch GD dùng cả mm mẫu mỗi bước, mini-batch dùng BB mẫu, SGD dùng 1 mẫu, và đường mất mát nằm ngang chưa chắc đã tới đáy.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu