⭐ Vision Engineer · Chặng 10Bài 5/12Bài học~75 phút

Loss function và optimizer: SGD, Momentum, RMSProp, Adam

MAE, MSE, Huber, cross-entropy; batch, mini-batch SGD; Momentum, Adagrad, RMSProp, Adam với công thức đúng và thí nghiệm PyTorch kiểm chứng.

Loss function và optimizer: SGD, Momentum, RMSProp, Adam

Học xong bài này, bạn sẽ

  • Chọn hàm loss đúng cho hồi quy và phân loại
  • Hiểu mini-batch SGD và learning rate
  • So sánh Momentum, Adagrad, RMSProp, Adam

Nên học trước: PyTorch cơ bản: tensor, autograd, vòng lặp huấn luyện

Nội dung bài viết
  1. Loss function: đo mức độ sai
  2. Gradient: hướng dốc nhất
  3. Gradient Descent: xuống núi trong sương mù
  4. Ba phiên bản Gradient Descent
  5. Những thách thức của Gradient Descent
  6. Momentum: thêm quán tính
  7. Adagrad: learning rate riêng cho từng tham số
  8. RMSProp: Adagrad có "trí nhớ ngắn hạn"
  9. Adam: Momentum + RMSProp
  10. So sánh và chọn optimizer
  11. Tóm tắt
  12. Tài liệu tham khảo

Ở bài Mạng nơ-ron và CNN hoạt động thế nào, mạng nơ-ron đã biết "đoán" bằng forward pass; bài Gradient descent từ đầu với NumPy đã cho thấy cách giảm loss cho một mô hình tuyến tính. Nhưng với trọng số ngẫu nhiên, nó đoán bừa. Để mạng học, ta cần ba thứ: một con số đo mức độ sai (loss function), hướng sửa sai (gradient), và chiến lược bước đi (optimizer). Bài này đi qua cả ba, kèm công thức chính xác và code PyTorch kiểm chứng.

Số trang (ví dụ "trang 81") trong bài chỉ tới slide của bài giảng Deep Learning – Computer Vision – NLP (Thang Nguyen, Viet Nguyen); bài này giải thích chi tiết, kiểm chứng và đính chính các slide đó.

Loss function: đo mức độ sai

Hàm mất mát (loss function, còn gọi cost/objective function) so sánh dự đoán \(\hat{y}\) (y_pred) với giá trị đúng \(y\) (target) và trả về một con số: càng nhỏ càng tốt.

\[ \text{Loss} = J(\hat{y}, y), \qquad \text{Huấn luyện:}\ \ \min_{W}\ \frac{1}{N}\sum_{i=1}^{N} J\big(f(x_i; W),\, y_i\big) \]

Chọn loss phù hợp với loại bài toán: hồi quy (dự đoán số thực: toạ độ box, tuổi, giá) hay phân loại (dự đoán lớp).

Loss cho bài toán hồi quy

LossCông thứcĐặc điểmPyTorch
MAE (L1)\(\frac{1}{n}\sum |y_i-\hat y_i|\)Bền với ngoại lai (outlier); gradient có độ lớn không đổi, không khả vi tại 0.nn.L1Loss()
MSE (L2)\(\frac{1}{n}\sum (y_i-\hat y_i)^2\)Phạt nặng sai số lớn (bình phương), mượt, nhưng nhạy với ngoại lai.nn.MSELoss()
Huberxem bên dướiGiống MSE khi sai số nhỏ, giống MAE khi sai số lớn: lấy ưu điểm của cả hai.nn.HuberLoss(delta=1.0)
\[ \text{Huber}_\delta(y,\hat y) = \begin{cases} \tfrac12 (y-\hat y)^2 & \text{nếu } |y-\hat y| \le \delta \\[2pt] \delta\left(|y-\hat y| - \tfrac12\delta\right) & \text{nếu } |y-\hat y| > \delta \end{cases} \]

Ví dụ với \(\delta = 1\), hai sai số 0.5 và 3: phần thứ nhất \(\tfrac12\cdot0.5^2 = 0.125\), phần thứ hai \(1\cdot(3-0.5) = 2.5\), trung bình \(1.3125\). Nếu dùng MSE, sai số 3 đóng góp tới \(9\) và "kéo" cả mô hình về phía điểm ngoại lai. Biến thể nn.SmoothL1Loss (dùng trong Faster R-CNN để hồi quy bounding box) trùng với Huber khi \(\delta=\beta=1\).

import torch
import torch.nn as nn

pred   = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)

mae = nn.L1Loss()(pred, target)
mse = nn.MSELoss()(pred, target)
hub = nn.HuberLoss(delta=1.0)(pred, target)
mse.backward()                 # tính gradient của loss theo pred
print(mae.item(), mse.item(), hub.item())

Loss cho bài toán phân loại: Softmax + Cross-Entropy

Mạng cho ra các điểm số thô gọi là logits (có thể âm, tổng không bằng 1). Hàm softmax biến chúng thành phân phối xác suất:

\[ p_i = \text{softmax}(z)_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}} \]

Cross-entropy đo khoảng cách giữa phân phối dự đoán \(p\) và nhãn đúng \(t\) (dạng one-hot):

\[ L_{CE} = -\sum_{i=1}^{K} t_i \log p_i = -\log p_{\text{lớp đúng}} \]

Ví dụ ở trang 25: ảnh chó, logits \([3.2,\ 1.3,\ 0.2,\ 0.8]\) cho các lớp Dog, Cat, Horse, Cheetah.

LớpLogit\(e^{z}\)Xác suấtNhãn t
Dog3.224.530.7751
Cat1.33.670.1160
Horse0.21.220.0390
Cheetah0.82.230.0700

Loss \(= -\log(0.775) \approx 0.255\). Nếu mô hình chỉ cho Dog xác suất 0.01, loss \(= -\log 0.01 \approx 4.6\): đoán sai một cách tự tin bị phạt rất nặng.

Mẹo kiểm tra nhanh (sanity check)Mô hình mới khởi tạo đoán gần đều \(1/K\) cho mọi lớp, nên loss ban đầu xấp xỉ \(\log K\). Với CIFAR-10, loss epoch đầu phải quanh \(\ln 10 \approx 2.30\). Nếu thấy 15 hay 0.01 ngay từ đầu, gần như chắc chắn có lỗi (chuẩn hoá dữ liệu, nhãn, hay softmax hai lần).
logits = torch.tensor([[3.2, 1.3, 0.2, 0.8]])
target = torch.tensor([0])                      # chỉ số lớp đúng (Dog), KHÔNG phải one-hot

loss = nn.CrossEntropyLoss()(logits, target)
manual = -torch.log_softmax(logits, dim=1)[0, 0]
print(loss.item(), manual.item())               # 0.2547 0.2547
Lỗi rất phổ biếnnn.CrossEntropyLoss đã bao gồm sẵn log-softmax. Đầu vào phải là logits thô, nhãn là chỉ số lớp kiểu torch.long. Đừng thêm nn.Softmax ở cuối mô hình rồi đưa vào loss: mô hình vẫn chạy nhưng học chậm và kém hơn. Với bài toán nhị phân hay đa nhãn (một ảnh nhiều nhãn), dùng nn.BCEWithLogitsLoss.

Gradient: hướng dốc nhất

Gradient của hàm nhiều biến là vector các đạo hàm riêng:

\[ \nabla f(x, y, z) = \left\langle \frac{\partial f}{\partial x},\ \frac{\partial f}{\partial y},\ \frac{\partial f}{\partial z} \right\rangle \]
  • Gradient là một vector.
  • Nó chỉ theo hướng tăng nhanh nhất của hàm; độ lớn của nó là tốc độ tăng lớn nhất.
  • Vậy muốn giảm loss nhanh nhất, ta đi ngược hướng gradient.

Với mạng nơ-ron, "biến" chính là hàng triệu trọng số; gradient \(\partial L/\partial W\) cho biết tăng nhẹ từng trọng số thì loss thay đổi ra sao. Cách tính gradient hiệu quả cho mạng nhiều lớp là chủ đề của bài Backpropagation.

Gradient Descent: xuống núi trong sương mù

Hình dung bạn đứng trên sườn núi trong sương mù dày, chỉ cảm nhận được độ dốc dưới chân. Chiến lược: dò hướng dốc nhất đi xuống, bước một bước, lặp lại cho đến khi xuống thung lũng. Đó là gradient descent:

\[ W_{\text{new}} = W_{\text{old}} - \eta\, \frac{\partial L}{\partial W_{\text{old}}} \]

\(\eta > 0\) là learning rate (tốc độ học), siêu tham số quan trọng nhất:

  • Quá nhỏ: cần rất nhiều bước mới tới đáy, huấn luyện lâu.
  • Vừa phải: tới đáy nhanh và ổn định.
  • Quá lớn: bước nhảy vọt qua đáy, loss dao động hoặc tăng vọt (phân kỳ), có thể thành NaN.

Với linear regression + MSE, mặt loss là một "cái bát" lồi (convex): chỉ có một đáy, gradient descent chắc chắn tới nơi. Với neural network, mặt loss không lồi, gồ ghề với vô số đồi, hố, yên ngựa (trang 31, 34 của tài liệu). Kỳ lạ là trên thực tế gradient descent vẫn tìm được lời giải rất tốt cho mạng sâu.

Ba phiên bản Gradient Descent

Phiên bảnMỗi lần cập nhật dùngƯu điểmNhược điểm
Batch GDToàn bộ dữ liệuGradient chính xác, đường đi mượtRất chậm, không vừa bộ nhớ với dữ liệu lớn
Stochastic GD1 mẫuCập nhật rất nhanh, nhiễu giúp thoát hố nôngĐường đi zigzag mạnh, không tận dụng được GPU
Mini-batch GDN mẫu (vd. 32–256)Cân bằng: tận dụng GPU, nhiễu vừa phảiThêm siêu tham số batch size

Trong thực tế "SGD" hầu như luôn có nghĩa là mini-batch SGD. Ba thuật ngữ cần phân biệt (trang 58): với 1.000 mẫu và batch size = 100, cần 10 iteration (lần cập nhật) để đi hết dữ liệu một lượt, tức 1 epoch.

Những thách thức của Gradient Descent

  • Cực tiểu địa phương (local minima): gradient bằng 0 ở một hố không phải sâu nhất; GD thuần dừng lại ở đó.
  • Điểm yên ngựa (saddle point): gradient bằng 0, theo hướng này là đáy nhưng hướng kia là đỉnh. Ở không gian hàng triệu chiều, điểm yên ngựa và vùng bằng phẳng (plateau) phổ biến hơn cực tiểu địa phương nhiều (Dauphin et al., 2014), làm huấn luyện chững lại.
  • Khe hẹp (ravine): mặt loss dốc đứng theo một hướng nhưng thoải theo hướng khác. GD dao động qua lại giữa hai vách thay vì lao thẳng xuống lòng khe.
  • Một learning rate chung cho mọi tham số, dù có tham số cần bước lớn, có tham số cần bước nhỏ.

Các optimizer dưới đây sinh ra để giải quyết lần lượt những vấn đề này.

Momentum: thêm quán tính

Thay vì bước theo gradient hiện tại, ta tích luỹ một "vận tốc" giống quả bóng lăn xuống dốc:

\[ v_t = \gamma\, v_{t-1} + \eta \nabla_w L(w_t), \qquad w_{t+1} = w_t - v_t \]

\(\gamma\) (thường 0.9) là hệ số quán tính. Hiệu quả:

  • Ở khe hẹp, các thành phần gradient đổi dấu liên tục sẽ triệt tiêu nhau, còn thành phần dọc lòng khe cộng dồn: bớt zigzag, tăng tốc.
  • Quán tính giúp "lăn vượt" qua các hố nông và điểm yên ngựa.

Thí nghiệm ở trang 39: tối thiểu hoá \(f(x) = x^2 + 10\sin x\) từ \(x_0 = 5\), \(\eta = 0.1\). Mình chạy thử với PyTorch:

import torch

def f(x):
    return x**2 + 10 * torch.sin(x)

def run(**kw):
    x = torch.tensor(5.0, requires_grad=True)
    opt = torch.optim.SGD([x], **kw)
    for _ in range(100):
        opt.zero_grad()
        f(x).backward()
        opt.step()
    return x.item(), f(x).item()

print(run(lr=0.1))                 # (3.837, 8.316)  -> kẹt ở cực tiểu địa phương
print(run(lr=0.1, momentum=0.9))   # (-1.31, -7.946) -> tới cực tiểu toàn cục

GD thường dừng ở hố \(x \approx 3.84\); có momentum, "quả bóng" vượt đồi và tới đáy thật \(x \approx -1.31\).

Công thức trong PyTorch hơi kháctorch.optim.SGD cài đặt \(v_t = \mu v_{t-1} + g_t\), \(w \leftarrow w - \eta v_t\) (learning rate nhân ở ngoài). Khi \(\eta\) không đổi, hai cách viết tương đương. Tham số nesterov=True bật biến thể Nesterov: "nhìn trước" vị trí sắp tới rồi mới tính gradient.

Adagrad: learning rate riêng cho từng tham số

\[ \theta_{t+1,i} = \theta_{t,i} - \frac{\eta}{\sqrt{G_{t,ii} + \epsilon}}\, g_{t,i}, \qquad G_{t,ii} = \sum_{\tau=1}^{t} g_{\tau,i}^2 \]
  • \(g_{t,i}\): gradient của tham số \(\theta_i\) tại bước \(t\).
  • \(G_t\): ma trận đường chéo, phần tử \((i,i)\) là tổng bình phương mọi gradient trong quá khứ của \(\theta_i\).
  • \(\epsilon \approx 10^{-8}\): tránh chia cho 0.

Tham số nào đã nhận gradient lớn thường xuyên sẽ bị giảm bước; tham số hiếm khi được cập nhật (đặc trưng thưa) được bước lớn hơn. Nhược điểm chí mạng: \(G\) chỉ tăng, không giảm, nên learning rate hiệu dụng teo dần về 0 và việc học dừng sớm. (PyTorch đặt \(\epsilon\) ngoài căn: \(\eta/(\sqrt{G}+\epsilon)\); khác biệt không đáng kể.)

RMSProp: Adagrad có "trí nhớ ngắn hạn"

RMSProp (Hinton) thay tổng tích luỹ bằng trung bình động hàm mũ của bình phương gradient, nên quá khứ xa dần bị quên:

\[ E[g^2]_t = \gamma\, E[g^2]_{t-1} + (1-\gamma)\, g_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{E[g^2]_t} + \epsilon}\, g_t \]

Giá trị khuyến nghị: \(\gamma = 0.9\), \(\eta = 10^{-3}\) (PyTorch mặc định alpha=0.99, lr=0.01).

Đính chính công thức ở trang 45Slide ghi mẫu số là \(\sqrt{(1-\gamma)g_{t-1}^2 + \gamma g_t + \varepsilon}\): vừa đảo vai trò của \(\gamma\), vừa thiếu bình phương ở \(g_t\), và chỉ dùng gradient của bước trước thay vì trung bình động tích luỹ. Hãy dùng công thức ở trên (theo bài giảng gốc của Hinton và tài liệu PyTorch).

Adam: Momentum + RMSProp

Adam (Kingma & Ba, 2015) kết hợp trung bình động của gradient (moment bậc 1, như momentum) và của bình phương gradient (moment bậc 2, như RMSProp), kèm hiệu chỉnh độ lệch cho các bước đầu:

\[ m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t, \qquad v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 \] \[ \hat m_t = \frac{m_t}{1-\beta_1^t}, \quad \hat v_t = \frac{v_t}{1-\beta_2^t}, \qquad \theta_{t+1} = \theta_t - \frac{\eta\, \hat m_t}{\sqrt{\hat v_t} + \epsilon} \]

Mặc định: \(\eta = 10^{-3}\), \(\beta_1 = 0.9\), \(\beta_2 = 0.999\), \(\epsilon = 10^{-8}\). AdamW tách weight decay ra khỏi bước cập nhật gradient (Loshchilov & Hutter, 2019) và là lựa chọn mặc định cho rất nhiều mô hình hiện đại (ViT, DETR, ConvNeXt…).

So sánh và chọn optimizer

OptimizerÝ tưởngKhi nào dùngPyTorch
SGD + MomentumQuán tínhHuấn luyện CNN phân loại từ đầu; thường tổng quát hoá rất tốt khi đi kèm lịch giảm learning rateSGD(params, lr=0.1, momentum=0.9, weight_decay=5e-4)
AdagradLR riêng, tích luỹ mãiDữ liệu thưa; ít dùng cho CNNAdagrad(params, lr=0.01)
RMSPropLR riêng, trung bình độngMột số mô hình (MobileNet, EfficientNet gốc), RLRMSprop(params, lr=1e-3)
Adam / AdamWMomentum + RMSPropMặc định "chạy được ngay", fine-tuning, TransformerAdamW(params, lr=1e-3, weight_decay=0.01)

Learning rate hiếm khi giữ nguyên suốt quá trình. Các scheduler phổ biến trong torch.optim.lr_scheduler: StepLR (giảm 10 lần sau mỗi số epoch cố định), CosineAnnealingLR (giảm mượt theo hình cos), OneCycleLR; thường kèm vài epoch warmup tăng dần learning rate lúc đầu.

model = torch.nn.Linear(10, 2)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)

for epoch in range(100):
    # ... train một epoch: zero_grad -> forward -> loss -> backward -> optimizer.step()
    scheduler.step()          # cập nhật learning rate sau mỗi epoch

Tóm tắt

  • Hồi quy: MAE, MSE, Huber. Phân loại: softmax + cross-entropy (đưa logits vào nn.CrossEntropyLoss).
  • Gradient chỉ hướng tăng nhanh nhất; GD đi ngược nó với bước \(\eta\).
  • Mini-batch SGD là chuẩn; phân biệt batch size, iteration, epoch.
  • Momentum chống zigzag và thoát hố nông; Adagrad/RMSProp cho learning rate riêng từng tham số; Adam gộp cả hai.

Còn một mảnh ghép: tính \(\partial L/\partial W\) cho hàng triệu trọng số thế nào cho nhanh? Mời đọc tiếp Backpropagation qua ví dụ tính tay.

Tài liệu tham khảo

  • Sebastian Ruder (2016). An overview of gradient descent optimization algorithms (arXiv:1609.04747).
  • Kingma & Ba (2015). Adam: A Method for Stochastic Optimization; Loshchilov & Hutter (2019). Decoupled Weight Decay Regularization.
  • Geoffrey Hinton, bài giảng Coursera Neural Networks for Machine Learning, Lecture 6e (RMSProp).
  • Dauphin et al. (2014). Identifying and attacking the saddle point problem in high-dimensional non-convex optimization.
  • Tài liệu PyTorch: torch.nn (loss functions) và torch.optim.

Bài tập thực hành

  1. Chạy thí nghiệm f(x) = x² + 10·sin(x) từ x = 5 với SGD, SGD + momentum và Adam; ghi lại điểm dừng của từng optimizer và giải thích.

Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.