Danh sách bài viết

Bài 10: Loss function cho classification: Cross-Entropy

Cross-Entropy loss cho classification: Binary Cross-Entropy (BCE), Categorical Cross-Entropy (CCE), Negative Log Likelihood. Vì sao MSE không phù hợp cho probability. Gradient gọn \( \hat{p} - y \) khi pair với sigmoid / softmax. Label smoothing, class weights, ignore_index, Focal Loss, KL Divergence. PyTorch API nhận LOGITS — không apply softmax / sigmoid thủ công.

24/05/2026
14 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Hiểu vì sao MSE không phù hợp khi output là probability — gradient saturate cùng sigmoid / softmax.
  • Viết được Binary Cross-Entropy (BCE) cho 2 class và Categorical Cross-Entropy (CCE) cho \( K \) class.
  • Chứng minh gradient gọn \( \hat{p} - y \) khi pair sigmoid + BCE hoặc softmax + CCE.
  • Phân biệt CCE với one-hot label và Negative Log Likelihood (NLL).
  • Hiểu label smoothing — vì sao thay one-hot bằng phân phối "mềm".
  • Dùng đúng PyTorch API: nn.CrossEntropyLossnn.BCEWithLogitsLoss nhận LOGITS, không phải probability.
  • Biết Focal Loss và KL Divergence ở mức intuition.

Bài này kế tiếp Bài 9 — Loss cho regression và là loss-pair cho softmax (Bài 8) / sigmoid (Bài 5). Series 2 Bài 22 (Logistic Regression) đã giới thiệu BCE; ở đây mở rộng cho multi-class và đi sâu API PyTorch.

2

Vì sao không dùng MSE cho classification

Output của binary classification thường là \( \hat{p} = \sigma(z) \), của multi-class là \( \hat{\mathbf{p}} = \text{softmax}(\mathbf{z}) \). Cả hai đều ép giá trị về \( (0, 1) \).

Nếu lấy MSE giữa probability và label:

\[ L_{\text{MSE}} = \frac{1}{2}(\hat{p} - y)^2 \]

Backprop qua sigmoid: \( \frac{\partial L}{\partial z} = (\hat{p} - y) \cdot \sigma'(z) = (\hat{p} - y) \cdot \hat{p}(1 - \hat{p}) \). Khi \( \hat{p} \) gần 0 hoặc 1 — vùng saturate của sigmoid (B5) — thừa số \( \hat{p}(1-\hat{p}) \to 0 \) làm gradient triệt tiêu, kể cả khi prediction sai bét. Ví dụ \( y = 1 \), \( \hat{p} = 0.01 \): error lớn \( (\hat{p} - y) = -0.99 \) nhưng \( \hat{p}(1 - \hat{p}) = 0.0099 \) → gradient \( \approx -0.0098 \), rất nhỏ. Network học cực chậm.

Vấn đề thứ hai: MSE coi mọi sai số như nhau theo bình phương. Một sai số \( |y - \hat{p}| = 0.5 \) ở giữa và một sai số \( = 0.5 \) ở rìa (predict 1.0 khi đúng là 0.5) đóng góp cùng giá trị — không phản ánh "tự tin sai" tệ hơn "lưỡng lự".

Cross-entropy giải hai vấn đề cùng lúc:

  • Gradient pair với sigmoid / softmax rút gọn về \( \hat{p} - y \) — không có thừa số \( \sigma'(z) \) làm triệt tiêu.
  • Phạt cực mạnh khi model "tự tin sai", phạt vừa khi "lưỡng lự".

Đó là lý do cross-entropy là mặc định cho classification — cả lý thuyết (maximum likelihood của Bernoulli / Categorical distribution) lẫn thực hành.

3

Binary Cross-Entropy

Cho bài toán binary classification: nhãn \( y \in \{0, 1\} \), prediction \( \hat{p} \in (0, 1) \) là output của sigmoid. Binary Cross-Entropy (BCE), còn gọi log loss:

\[ L_{\text{BCE}} = -\frac{1}{n} \sum_{i=1}^n \left[ y_i \log \hat{p}_i + (1 - y_i) \log(1 - \hat{p}_i) \right] \]

Cách đọc nhanh — chỉ một trong hai số hạng "sống":

  • Khi \( y_i = 1 \): loss của sample \( i \) là \( -\log \hat{p}_i \). Predict 1.0 đúng → loss = 0. Predict gần 0 → loss \( \to +\infty \).
  • Khi \( y_i = 0 \): loss là \( -\log(1 - \hat{p}_i) \). Predict 0.0 đúng → loss = 0. Predict gần 1 → loss \( \to +\infty \).

BCE phát sinh tự nhiên từ maximum likelihood cho Bernoulli distribution. Mỗi sample là một phép thử với xác suất thành công \( \hat{p}_i \); log-likelihood của dữ liệu là \( \sum_i [y_i \log \hat{p}_i + (1 - y_i) \log(1 - \hat{p}_i)] \). Minimize negative log-likelihood = minimize BCE. Series 2 Bài 22 (Logistic Regression) đã chứng minh chi tiết.

Bảng giá trị tham chiếu (loss cho 1 sample, \( y = 1 \)):

  • \( \hat{p} = 0.99 \): \( -\log 0.99 \approx 0.010 \).
  • \( \hat{p} = 0.9 \): \( -\log 0.9 \approx 0.105 \).
  • \( \hat{p} = 0.5 \): \( -\log 0.5 \approx 0.693 \) (entropy của fair coin).
  • \( \hat{p} = 0.1 \): \( -\log 0.1 \approx 2.303 \).
  • \( \hat{p} = 0.01 \): \( -\log 0.01 \approx 4.605 \).
  • \( \hat{p} = 0.001 \): \( -\log 0.001 \approx 6.908 \).
4

Tính chất của BCE

  • Không âm: \( L_{\text{BCE}} \ge 0 \), bằng 0 khi và chỉ khi mọi prediction khớp tuyệt đối với label.
  • Convex theo \( \hat{p} \): với một sample cố định, \( -\log \hat{p} \) và \( -\log(1 - \hat{p}) \) đều convex. Tổ hợp tuyến tính của hai hàm convex vẫn convex. Lưu ý: convex theo \( \hat{p} \) không có nghĩa convex theo weight \( W \) của network (chuỗi composition phi tuyến qua nhiều layer làm loss surface non-convex).
  • Phạt cực mạnh "tự tin sai": predict 0.99 nhưng \( y = 0 \) → loss \( -\log 0.01 \approx 4.6 \). Predict 0.5 khi \( y = 0 \) → loss chỉ \( \approx 0.69 \). Tỷ lệ phạt tăng phi tuyến theo độ tự tin.
  • Loss \( \to +\infty \) khi \( \hat{p} \to 0 \) và \( y = 1 \) (hoặc ngược lại). Trong code phải clip \( \hat{p} \) khỏi 0 và 1 hoặc dùng phiên bản nhận logit để tránh \( \log 0 \).
  • Đối xứng theo class: đổi vai trò class 0 ↔ class 1 (cùng đổi \( \hat{p} \to 1 - \hat{p} \) và \( y \to 1 - y \)) thì loss không đổi.
5

Gradient gọn khi pair sigmoid + BCE

Gọi \( z \) là logit (pre-activation), \( \hat{p} = \sigma(z) \). Lấy đạo hàm loss của 1 sample theo \( z \):

\[ L = -\big[ y \log \hat{p} + (1 - y) \log(1 - \hat{p}) \big] \]

\[ \frac{\partial L}{\partial \hat{p}} = -\frac{y}{\hat{p}} + \frac{1 - y}{1 - \hat{p}} = \frac{\hat{p} - y}{\hat{p}(1 - \hat{p})} \]

Kết hợp với \( \sigma'(z) = \hat{p}(1 - \hat{p}) \) (đạo hàm sigmoid, B5):

\[ \frac{\partial L}{\partial z} = \frac{\partial L}{\partial \hat{p}} \cdot \sigma'(z) = \frac{\hat{p} - y}{\hat{p}(1 - \hat{p})} \cdot \hat{p}(1 - \hat{p}) = \hat{p} - y \]

Hai thừa số \( \hat{p}(1 - \hat{p}) \) triệt tiêu — gradient theo logit chỉ còn residual \( \hat{p} - y \). Không có yếu tố saturate. Predict tự tin sai (\( |\hat{p} - y| \to 1 \)) thì gradient cũng tiến tới 1 — network học mạnh ngay cả khi sigmoid saturate. Đây là lý do thực dụng để cross-entropy thay MSE.

So sánh nhanh với MSE pair sigmoid:

  • MSE + sigmoid: \( \partial L / \partial z = (\hat{p} - y) \cdot \hat{p}(1 - \hat{p}) \) — gradient triệt tiêu khi sigmoid saturate.
  • BCE + sigmoid: \( \partial L / \partial z = \hat{p} - y \) — gradient luôn có scale tương ứng với độ sai.
6

Categorical Cross-Entropy

Mở rộng cho \( K \) class. Label \( \mathbf{y}_i \in \{0, 1\}^K \) là one-hot (đúng một vị trí bằng 1, còn lại 0). Prediction \( \hat{\mathbf{p}}_i = \text{softmax}(\mathbf{z}_i) \in (0, 1)^K \), \( \sum_k \hat{p}_{i,k} = 1 \).

Categorical Cross-Entropy (CCE):

\[ L_{\text{CCE}} = -\frac{1}{n} \sum_{i=1}^n \sum_{k=1}^K y_{i,k} \log \hat{p}_{i,k} \]

Lưu ý sự đối ứng với BCE: BCE là CCE cho \( K = 2 \) với class 0 và class 1 được parameterize qua \( \hat{p} \) và \( 1 - \hat{p} \). Hai công thức tương đương về mặt toán; PyTorch tách hai API vì numerical implementation khác nhau (sigmoid 1 logit vs softmax \( K \) logits).

7

NLL — dạng rút gọn cho hard label

Vì \( \mathbf{y}_i \) one-hot chỉ có một vị trí bằng 1 — gọi vị trí đó là \( c_i \) (true class index) — tổng \( \sum_k y_{i,k} \log \hat{p}_{i,k} \) rút gọn về \( \log \hat{p}_{i, c_i} \). CCE viết lại:

\[ L = -\frac{1}{n} \sum_{i=1}^n \log \hat{p}_{i, c_i} \]

Tức là chỉ lấy log probability của true class rồi đảo dấu. Tên gọi: Negative Log Likelihood (NLL). Đây cũng là maximum likelihood của Categorical distribution: minimize NLL = maximize \( \prod_i \hat{p}_{i, c_i} \) — xác suất mà model gán cho đúng label.

Ví dụ với \( K = 3 \), \( \mathbf{y} = [0, 1, 0] \), \( \hat{\mathbf{p}} = [0.1, 0.7, 0.2] \): chỉ class index 1 đóng góp, \( L = -\log 0.7 \approx 0.357 \).

Trong PyTorch, nn.NLLLoss nhận đầu vào là log_softmax (đã log) và target là integer class index — đúng dạng rút gọn ở trên.

8

Gradient gọn khi pair softmax + CCE

Softmax: \( \hat{p}_k = e^{z_k} / \sum_j e^{z_j} \). Jacobian softmax có dạng:

\[ \frac{\partial \hat{p}_k}{\partial z_m} = \hat{p}_k (\delta_{km} - \hat{p}_m) \]

(\( \delta_{km} = 1 \) khi \( k = m \), ngược lại bằng 0). Riêng dạng tổng quát này phức tạp vì là ma trận \( K \times K \). Nhưng khi pair với CCE, kết quả lại đơn giản đáng kinh ngạc.

Loss cho 1 sample \( L = -\sum_k y_k \log \hat{p}_k \). Đạo hàm theo logit \( z_m \) bằng chain rule:

\[ \frac{\partial L}{\partial z_m} = -\sum_k y_k \frac{1}{\hat{p}_k} \cdot \hat{p}_k (\delta_{km} - \hat{p}_m) = -\sum_k y_k (\delta_{km} - \hat{p}_m) \]

\[ = -y_m + \hat{p}_m \sum_k y_k = \hat{p}_m - y_m \]

(dùng \( \sum_k y_k = 1 \) vì \( \mathbf{y} \) là phân phối). Viết gọn dưới dạng vector:

\[ \frac{\partial L}{\partial \mathbf{z}} = \hat{\mathbf{p}} - \mathbf{y} \]

Cùng dạng "residual" như BCE pair sigmoid. Implementation backward không cần chạy qua Jacobian \( K \times K \) — chỉ trừ vector. Đây cũng là lý do PyTorch gộp LogSoftmax + NLLLoss thành CrossEntropyLoss: backward path nhanh và stable hơn nhiều so với softmax + nll riêng.

9

Label smoothing

One-hot label "khắt khe": muốn \( \hat{p}_{c} \to 1 \) tuyệt đối cho true class. Để đạt được điều đó, logit của true class phải \( \to +\infty \) (do softmax) — weight bị đẩy ra rất lớn, model overconfident, dễ overfit.

Label smoothing (Szegedy và cộng sự, 2016) thay one-hot bằng phân phối "mềm":

\[ y_{\text{smooth}, k} = (1 - \alpha) \cdot y_k + \frac{\alpha}{K} \]

Với \( \alpha \in [0, 1] \) là hệ số smoothing. Ví dụ \( K = 10 \), \( \alpha = 0.1 \):

  • True class: \( (1 - 0.1) \cdot 1 + 0.1/10 = 0.91 \).
  • Mỗi class còn lại: \( (1 - 0.1) \cdot 0 + 0.1/10 = 0.01 \).

Hệ quả:

  • Loss tối thiểu (khi prediction = label smoothed) không còn bằng 0 — vẫn có một mức "entropy" sàn, vì \( y_{\text{smooth}} \) có hỗ trợ trên mọi class.
  • Logit không cần \( \to \infty \) → weight giữ ở scale hợp lý → regularization implicit.
  • Giảm overconfident: probability output ít khi bị "ép" về 0/1, calibration tốt hơn.

Label smoothing là default trong nhiều SOTA: training Transformer gốc dùng \( \alpha = 0.1 \) (Vaswani và cộng sự, 2017); training ImageNet classification dùng \( \alpha = 0.1 \) phổ biến (Inception-v3 trở đi). PyTorch hỗ trợ trực tiếp qua tham số label_smoothing trong nn.CrossEntropyLoss từ phiên bản 1.10.

10

PyTorch API — nhận LOGITS

Phần quan trọng nhất của bài. PyTorch có 4 loss liên quan đến cross-entropy; nhầm input là lỗi phổ biến nhất với người mới.

Loss Input Target Dùng khi
nn.CrossEntropyLoss Logits (raw output, KHÔNG softmax) Integer class index (hoặc probability tensor từ 1.10+) Multi-class classification (mặc định)
nn.BCEWithLogitsLoss Logits (raw output, KHÔNG sigmoid) Float trong [0, 1] (thường là 0/1) Binary / multi-label classification (mặc định)
nn.BCELoss Probability (đã sigmoid) Float trong [0, 1] Hiếm — chỉ khi đã có probability sẵn
nn.NLLLoss Log-probability (đã log_softmax) Integer class index Khi muốn tách rõ log_softmax và NLL

Quy tắc vàng: output thô của model (logits), không apply softmax / sigmoid thủ công trước loss. Dùng CrossEntropyLoss cho multi-class, BCEWithLogitsLoss cho binary / multi-label. Cả hai cộng LogSoftmax / sigmoid vào bên trong bằng log-sum-exp trick — chính xác và stable hơn implementation manual.

import torch
import torch.nn as nn

# Multi-class: 3 class, batch 4
logits = torch.randn(4, 3)              # output thô của model
target = torch.tensor([0, 2, 1, 0])     # class index, dtype=long

loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(logits, target)
print(loss.item())

Sai lầm phổ biến — apply softmax trước rồi gọi CrossEntropyLoss:

# SAI — softmax 2 lần, gradient lệch
probs = torch.softmax(logits, dim=1)
loss = nn.CrossEntropyLoss()(probs, target)   # nhận tensor như "logit"

# ĐÚNG
loss = nn.CrossEntropyLoss()(logits, target)

Lúc inference mới apply softmax / sigmoid để đọc probability:

with torch.no_grad():
    logits = model(x)
    probs = torch.softmax(logits, dim=1)
    pred = probs.argmax(dim=1)

Soft label (PyTorch 1.10+): CrossEntropyLoss chấp nhận target là probability tensor cùng shape với logits — dùng cho knowledge distillation, mixup, label smoothing thủ công.

target_soft = torch.tensor([
    [0.9, 0.05, 0.05],
    [0.05, 0.05, 0.9],
])
logits = torch.randn(2, 3)
loss = nn.CrossEntropyLoss()(logits, target_soft)   # OK trên 1.10+
11

Reduction

Mọi loss function trong PyTorch nhận tham số reduction với 3 giá trị:

  • "mean" (mặc định): trung bình loss qua batch. Gradient không phụ thuộc batch size — learning rate giữ được khi đổi batch.
  • "sum": tổng loss. Gradient scale theo batch size.
  • "none": trả về tensor loss per-sample, shape giống target. Dùng khi cần weight thủ công từng sample (vd: curriculum learning).
per_sample = nn.CrossEntropyLoss(reduction="none")(logits, target)
# per_sample.shape == torch.Size([4])
print(per_sample)

Khi chuyển từ "mean" sang "sum", nhớ giảm learning rate hoặc chia batch size để giữ scale gradient — bỏ qua bước này là nguyên nhân thường gặp khi reproduce paper không khớp.

12

Class weights và ignore_index

Class weights — handle imbalance. Tham số weight nhân vào loss của từng class:

# 3 class, class 1 hiếm → cho weight cao hơn
class_weight = torch.tensor([1.0, 5.0, 1.0])
loss_fn = nn.CrossEntropyLoss(weight=class_weight)

Heuristic phổ biến: \( w_k = N / (K \cdot n_k) \) với \( n_k \) là số sample của class \( k \) và \( N \) là tổng — class hiếm nhận weight lớn hơn. Series 2 Bài 42 (Imbalanced Classification) đã thảo luận chi tiết các chiến lược.

ignore_index — bỏ qua một class index khi tính loss. Use case kinh điển: padding token trong NLP. Câu ngắn được pad bằng token đặc biệt (vd: index -100 trong Hugging Face Transformers); loss tại vị trí pad không nên đóng góp vào gradient.

loss_fn = nn.CrossEntropyLoss(ignore_index=-100)
target = torch.tensor([0, 2, -100, 1])   # token thứ 3 bị bỏ qua

PyTorch convention: target value bằng ignore_index không tham gia mean hay sum — chia trung bình cũng không tính sample đó.

13

Focal Loss — preview

Class weights chỉ tái cân bằng trung bình theo class. Khi imbalance cực đoan (object detection: nền chiếm 99%+ pixel, foreground 1%), class weights vẫn không đủ — easy negative chiếm áp đảo gradient.

Focal Loss (Lin và cộng sự, 2017 — bài RetinaNet) thêm thừa số down-weight các sample dễ:

\[ L_{\text{focal}} = -\alpha_t (1 - \hat{p}_t)^\gamma \log \hat{p}_t \]

Trong đó \( \hat{p}_t \) là probability của true class. So với BCE chuẩn (\( -\log \hat{p}_t \)), Focal Loss có thêm:

  • \( (1 - \hat{p}_t)^\gamma \): khi \( \hat{p}_t \) đã cao (sample dễ), thừa số gần 0 — loss bị "tắt". Khi \( \hat{p}_t \) thấp (sample khó), thừa số gần 1 — giữ nguyên loss.
  • \( \alpha_t \): class weight cổ điển.
  • \( \gamma \) (focusing parameter): \( \gamma = 0 \) là BCE thường; paper dùng \( \gamma = 2 \).

Phổ biến trong object detection (RetinaNet, FCOS), segmentation imbalanced, fraud detection. PyTorch không có Focal Loss built-in nhưng tự viết 10 dòng — sẽ gặp lại ở module CNN cho object detection.

14

Numerical pitfall

Tính BCE thủ công trên probability sau sigmoid dễ overflow / NaN:

# NGUY HIỂM — không stable
p = torch.sigmoid(logits)
loss = -(y * torch.log(p) + (1 - y) * torch.log(1 - p)).mean()

Khi \( z \) âm rất lớn: \( \sigma(z) \) bị làm tròn thành 0 trong float32 → \( \log 0 = -\infty \) → loss = NaN, training sập. Ngay cả khi clip \( p \in [\epsilon, 1 - \epsilon] \), gradient vẫn lệch tại vùng saturate.

Cách stable — gộp sigmoid và log thành log-sum-exp trick:

\[ -\log \sigma(z) = \log(1 + e^{-z}) = \text{softplus}(-z) \]

Với \( z \) lớn dương: \( \log(1 + e^{-z}) \approx e^{-z} \), không overflow. Với \( z \) lớn âm: dùng đẳng thức \( \log(1 + e^{-z}) = -z + \log(1 + e^{z}) \), tránh tính \( e^{-z} \) khổng lồ. PyTorch's BCEWithLogitsLoss dùng công thức tổng quát:

\[ L = \max(z, 0) - z y + \log(1 + e^{-|z|}) \]

Tương đương về toán nhưng stable trên toàn miền \( z \in \mathbb{R} \). CrossEntropyLoss dùng cùng kỹ thuật cho softmax (trừ max(z) trước khi exp trong LogSumExp).

Bottom line: luôn dùng BCEWithLogitsLoss / CrossEntropyLoss và truyền logits. Không tự viết log(sigmoid(...)) hoặc log(softmax(...)).

15

KL Divergence — generalize

Kullback-Leibler divergence giữa hai phân phối rời rạc \( p \) và \( q \) trên cùng tập \( K \) outcome:

\[ D_{\text{KL}}(p \,\|\, q) = \sum_k p_k \log \frac{p_k}{q_k} \]

Khai triển:

\[ D_{\text{KL}}(p \,\|\, q) = \underbrace{\sum_k p_k \log p_k}_{-H(p)} - \underbrace{\sum_k p_k \log q_k}_{-\text{CCE}(p, q)} \]

Tức là \( D_{\text{KL}}(p \,\|\, q) = H(p, q) - H(p) \), trong đó \( H(p, q) = -\sum_k p_k \log q_k \) là cross-entropy và \( H(p) = -\sum_k p_k \log p_k \) là entropy của \( p \).

Khi \( p \) là phân phối thật cố định (label phân phối), \( H(p) \) là hằng số. Minimize \( D_{\text{KL}}(p \,\|\, q) \) tương đương minimize cross-entropy. Đó là lý do BCE / CCE và KL Divergence cho ra cùng optimizer trong hầu hết bài toán supervised.

Khi nào dùng KL Divergence trực tiếp:

  • Knowledge distillation (Hinton và cộng sự, 2015): teacher phân phối \( p \) là output của big model, student \( q \) học match \( p \). Khi đó \( H(p) \) không hằng (vì \( p \) thay đổi theo input), nên minimize KL chính xác hơn CCE.
  • Variational Autoencoder (B43): regularization KL giữa posterior \( q(z|x) \) và prior \( p(z) \).
  • Reinforcement Learning: PPO penalty KL giữa policy mới và cũ.

PyTorch: nn.KLDivLoss(reduction="batchmean", log_target=False). Input là log-probability của \( q \), target là probability của \( p \). Convention hơi ngược với CrossEntropyLoss — đọc docs kỹ trước khi dùng.

16

Code Python

Implement BCE và CCE bằng NumPy theo dạng stable (nhận logits), so sánh với output PyTorch:

import numpy as np


def bce_with_logits(logits, y):
    # L = max(z, 0) - z*y + log(1 + exp(-|z|))
    z = logits
    return np.mean(np.maximum(z, 0) - z * y + np.log1p(np.exp(-np.abs(z))))


def cross_entropy_with_logits(logits, y_idx):
    # logits: (n, K), y_idx: (n,)
    z = logits
    z_max = z.max(axis=1, keepdims=True)
    log_sum_exp = np.log(np.exp(z - z_max).sum(axis=1, keepdims=True)) + z_max
    log_probs = z - log_sum_exp                # (n, K)
    n = z.shape[0]
    nll = -log_probs[np.arange(n), y_idx]
    return nll.mean()


# Demo BCE penalty khi confident sai
y = np.array([1.0, 0.0, 1.0])
logits_good = np.array([2.0, -2.0, 1.5])      # confident và đúng
logits_bad  = np.array([-3.0, 4.0, -2.5])     # confident và sai

print(f"BCE good = {bce_with_logits(logits_good, y):.4f}")
print(f"BCE bad  = {bce_with_logits(logits_bad,  y):.4f}")

Output:

BCE good = 0.1366
BCE bad  = 4.2879

"Confident sai" bị phạt nặng hơn "lưỡng lự" hàng chục lần — đặc trưng của cross-entropy.

Kiểm chứng với PyTorch:

import torch
import torch.nn as nn

logits = torch.tensor([[2.0, 1.0, 0.1],
                       [0.1, 2.0, 1.0]])
target = torch.tensor([0, 1])

# CrossEntropyLoss
ce = nn.CrossEntropyLoss()(logits, target)
print(f"CrossEntropyLoss     = {ce.item():.4f}")

# Tương đương: LogSoftmax + NLLLoss
logp = nn.LogSoftmax(dim=1)(logits)
nll = nn.NLLLoss()(logp, target)
print(f"LogSoftmax + NLLLoss = {nll.item():.4f}")

# So với NumPy
print(f"NumPy CE             = {cross_entropy_with_logits(logits.numpy(), target.numpy()):.4f}")

Cả ba in ra cùng một con số.

17

Train MLP iris với CrossEntropyLoss

Iris: 3 class, 4 feature, 150 sample — bài toán multi-class classification kinh điển. Code đầy đủ với PyTorch 2.x, dùng CrossEntropyLoss:

import torch
import torch.nn as nn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Load và chuẩn bị
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
X_train = StandardScaler().fit_transform(X_train)
X_test  = StandardScaler().fit(X_train).transform(X_test)

X_train = torch.tensor(X_train, dtype=torch.float32)
X_test  = torch.tensor(X_test,  dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.long)
y_test  = torch.tensor(y_test,  dtype=torch.long)

# Model — KHÔNG có softmax ở cuối
model = nn.Sequential(
    nn.Linear(4, 16),
    nn.ReLU(),
    nn.Linear(16, 3),                    # logits, KHÔNG apply softmax
)

loss_fn = nn.CrossEntropyLoss()           # nhận logits trực tiếp
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

for epoch in range(100):
    logits = model(X_train)
    loss = loss_fn(logits, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if epoch % 20 == 0:
        print(f"Epoch {epoch:3d}  loss = {loss.item():.4f}")

# Inference — apply softmax để đọc probability
with torch.no_grad():
    logits = model(X_test)
    probs = torch.softmax(logits, dim=1)
    pred = probs.argmax(dim=1)
    acc = (pred == y_test).float().mean()
    print(f"Test acc = {acc.item():.4f}")

Bật label smoothing chỉ cần thêm tham số:

loss_fn = nn.CrossEntropyLoss(label_smoothing=0.1)

Quan sát: với iris (dataset nhỏ, 3 class cân bằng), label smoothing chưa cho lợi ích rõ rệt. Lợi ích thực sự xuất hiện trên dataset lớn (ImageNet, MS COCO) và model dễ overfit (Transformer). Thử nghiệm có-không là bài tập 3.

18

Bài tập

  1. Tính tay BCE với \( \mathbf{y} = [1, 0, 1] \), \( \hat{\mathbf{p}} = [0.9, 0.1, 0.6] \). Sau đó tính lại nếu \( \hat{p}_3 \) tụt còn 0.05 — quan sát loss thay đổi bao nhiêu.
  2. Tính tay CCE cho 1 sample với \( \mathbf{y}_{\text{onehot}} = [0, 1, 0] \), \( \hat{\mathbf{p}} = [0.1, 0.7, 0.2] \). Lặp lại với \( \hat{\mathbf{p}} = [0.1, 0.05, 0.85] \) và quan sát.
  3. Train MLP iris (mục 17) trong 200 epoch với và không có label_smoothing=0.1. Plot train loss và test accuracy. Có khác biệt rõ không?
  4. Chứng minh đẳng thức ở mục 14: \( -\log \sigma(z) = \max(z, 0) - z + \log(1 + e^{-|z|}) \) trong trường hợp \( y = 1 \). Gợi ý: chia trường hợp \( z \ge 0 \) và \( z < 0 \).
  5. Implement Focal Loss bằng PyTorch (\( \gamma = 2 \), \( \alpha = 0.25 \)) cho bài toán binary. So sánh gradient của Focal Loss và BCE khi \( \hat{p} = 0.9 \) (sample dễ, đúng) và \( \hat{p} = 0.1 \) (sample khó, sai). Xác minh sample dễ có gradient nhỏ hơn.
  6. Tại sao gọi nn.CrossEntropyLoss()(torch.softmax(logits, dim=1), target) (truyền probability thay vì logits) lại "chạy không lỗi" nhưng kết quả sai? Phân tích bằng cách viết loss tay.
  7. Tính \( D_{\text{KL}}(p \,\|\, q) \) với \( p = [0.5, 0.5] \), \( q = [0.9, 0.1] \). Sau đó tính \( D_{\text{KL}}(q \,\|\, p) \). KL có đối xứng không?
Đáp án ngắn
  1. BCE = \( -[1 \cdot \log 0.9 + 0 \cdot \log 0.1 + 1 \cdot \log 0.6]/3 = -[\log 0.9 + \log 0.6]/3 \approx -[-0.1054 - 0.5108]/3 \approx 0.2054 \). Nếu \( \hat{p}_3 = 0.05 \): \( \approx -[-0.1054 - 2.9957]/3 \approx 1.034 \). Loss tăng \( \approx 5 \) lần vì 1 sample "confident sai".
  2. Sample 1: \( L = -\log 0.7 \approx 0.357 \). Sample 2: \( L = -\log 0.05 \approx 2.996 \). Predict đúng class với confidence thấp ăn loss vừa; predict sai class với confidence cao ăn loss nặng.
  3. Với iris, khác biệt nhỏ. Label smoothing giúp khi overfit rõ; iris quá đơn giản để thấy lợi ích.
  4. Trường hợp \( z \ge 0 \): \( \max(z,0) - z + \log(1+e^{-z}) = z - z + \log(1+e^{-z}) = \log(1+e^{-z}) = -\log \sigma(z) \). Trường hợp \( z < 0 \): \( \max(z,0) - z + \log(1+e^{z}) = 0 - z + \log(1+e^{z}) = \log(e^{-z}(1+e^{z})) = \log(1+e^{-z}) \cdot e^{-z}/e^{-z} \) — viết lại thành \( -z + \log(1+e^{z}) = -\log \sigma(z) \). OK.
  5. BCE gradient theo logit (cho 1 sample): \( \hat{p} - y \). Focal Loss có thêm thừa số \( (1 - \hat{p}_t)^\gamma \) và đạo hàm \( \gamma (1 - \hat{p}_t)^{\gamma-1} \cdot (-1) \cdot \log \hat{p}_t \). Với \( \hat{p}=0.9, y=1 \): \( (1 - 0.9)^2 = 0.01 \) — gradient gần như tắt. Với \( \hat{p}=0.1, y=1 \): \( (1 - 0.1)^2 = 0.81 \) — gần BCE.
  6. CrossEntropyLoss nội bộ apply LogSoftmax lên input — truyền probability vào tức là làm \( \text{softmax}(\text{softmax}(z)) \) rồi log. Sai về toán, không sai về dtype nên không raise error.
  7. \( D_{\text{KL}}(p \,\|\, q) = 0.5 \log(0.5/0.9) + 0.5 \log(0.5/0.1) \approx 0.5 \cdot (-0.5878) + 0.5 \cdot 1.6094 \approx 0.5108 \). \( D_{\text{KL}}(q \,\|\, p) = 0.9 \log(0.9/0.5) + 0.1 \log(0.1/0.5) \approx 0.9 \cdot 0.5878 + 0.1 \cdot (-1.6094) \approx 0.3681 \). KL không đối xứng — đó là lý do gọi "divergence" thay vì "distance".
19

Tóm tắt

  • MSE không phù hợp cho classification vì gradient triệt tiêu khi pair với sigmoid / softmax saturate; cross-entropy cho gradient \( \hat{p} - y \) không saturate.
  • BCE: \( L = -[y \log \hat{p} + (1-y) \log(1-\hat{p})] \). Phạt rất nặng "confident sai", loss \( \to \infty \) khi predict 0 cho true 1.
  • CCE: \( L = -\sum_k y_k \log \hat{p}_k \). Với one-hot label rút gọn thành NLL \( L = -\log \hat{p}_{c} \).
  • Pair softmax + CCE: gradient theo logit là \( \hat{\mathbf{p}} - \mathbf{y} \) — không cần Jacobian softmax.
  • Label smoothing thay one-hot bằng phân phối mềm \( y_{\text{smooth}} = (1-\alpha) y + \alpha/K \) — chống overconfident, default trong Transformer.
  • PyTorch nn.CrossEntropyLoss nhận logits và integer class index (hoặc probability cho soft label). Không apply softmax thủ công.
  • nn.BCEWithLogitsLoss nhận logits; tránh dùng BCELoss + sigmoid manual vì numerical instability.
  • reduction = "mean" | "sum" | "none". weight handle class imbalance, ignore_index skip padding token.
  • Focal Loss = BCE \( \times (1 - \hat{p}_t)^\gamma \) — down-weight easy sample, dùng trong object detection imbalanced extreme.
  • KL Divergence \( D_{\text{KL}}(p \,\|\, q) = H(p, q) - H(p) \). Khi \( p \) cố định, KL và CCE chỉ khác hằng số. Dùng KL khi target không cố định (distillation, VAE, RL).