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.
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
- Loss function: đo mức độ sai
- Gradient: hướng dốc nhất
- Gradient Descent: xuống núi trong sương mù
- Ba phiên bản Gradient Descent
- Những thách thức của Gradient Descent
- Momentum: thêm quán tính
- Adagrad: learning rate riêng cho từng tham số
- RMSProp: Adagrad có "trí nhớ ngắn hạn"
- Adam: Momentum + RMSProp
- So sánh và chọn optimizer
- Tóm tắt
- 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.
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
| Loss | Công thức | Đặc điểm | PyTorch |
|---|---|---|---|
| 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() |
| Huber | xem bên dưới | Giố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) |
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ớp | Logit | \(e^{z}\) | Xác suất | Nhãn t |
|---|---|---|---|---|
| Dog | 3.2 | 24.53 | 0.775 | 1 |
| Cat | 1.3 | 3.67 | 0.116 | 0 |
| Horse | 0.2 | 1.22 | 0.039 | 0 |
| Cheetah | 0.8 | 2.23 | 0.070 | 0 |
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.
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
nn.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ản | Mỗi lần cập nhật dùng | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Batch GD | Toàn bộ dữ liệu | Gradient chính xác, đường đi mượt | Rất chậm, không vừa bộ nhớ với dữ liệu lớn |
| Stochastic GD | 1 mẫu | Cậ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 GD | N mẫu (vd. 32–256) | Cân bằng: tận dụng GPU, nhiễu vừa phải | Thê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\).
torch.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).
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ưởng | Khi nào dùng | PyTorch |
|---|---|---|---|
| SGD + Momentum | Quán tính | Huấ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 rate | SGD(params, lr=0.1, momentum=0.9, weight_decay=5e-4) |
| Adagrad | LR riêng, tích luỹ mãi | Dữ liệu thưa; ít dùng cho CNN | Adagrad(params, lr=0.01) |
| RMSProp | LR riêng, trung bình động | Một số mô hình (MobileNet, EfficientNet gốc), RL | RMSprop(params, lr=1e-3) |
| Adam / AdamW | Momentum + RMSProp | Mặc định "chạy được ngay", fine-tuning, Transformer | AdamW(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
- 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.


