Mở đầu: câu hỏi không phải "bao nhiêu" mà là "có hay không"
Bài 1 dạy model đoán một con số (giá nhà, điểm thi). Bài 2 dạy nó tự học con số đó bằng cách lăn xuống loss landscape. Nhưng phần lớn quyết định quan trọng nhất trong đời sống lẫn công nghiệp không hỏi "bao nhiêu" — chúng hỏi "có hay không": email này có phải spam? khối u trên phim chụp có ác tính? giao dịch thẻ này có gian lận? Đưa thẳng bài toán này vào cỗ máy hồi quy tuyến tính của Bài 1 nghe hợp lý — cứ fit một đường thẳng qua điểm 0/1, rồi cắt ở 0,5. Bài này sẽ chứng minh bằng số rằng cách làm ngây thơ đó vỡ trận ngay khi có 1 điểm dữ liệu hơi lệch, và xây lại từ đầu công thức đúng: sigmoid nén đầu ra về xác suất, cross-entropy làm hàm mất mát, và một bộ thước đo khác hẳn MSE để biết model tốt tới đâu.
Cuối bài là bộ phân loại 2D kéo-thả tương tác — bạn tự tay đặt điểm đỏ/xanh, xem đường ranh giới quyết định (decision boundary) tự uốn theo, và thấy confusion matrix cùng precision/recall đổi số ngay khi kéo ngưỡng quyết định. Đây cũng là bài đầu tiên bạn tự hỏi "model chính xác 99%" có phải luôn là tin tốt hay không — câu trả lời sẽ khiến bạn nhìn mọi con số accuracy sau này bằng con mắt khác.
1. Vì sao đường thẳng + ngưỡng 0,5 thất bại
Thử cách ngây thơ trước: coi nhãn 0/1 (rớt/đậu) như một con số $y$, fit đường hồi quy tuyến tính $y = wx + b$ của Bài 1, rồi dự đoán lớp $1$ nếu $wx + b \geq 0{,}5$. Với 7 điểm "số giờ học" ($x$) → "đậu/rớt" ($y$) tách biệt rõ ràng:
| Giờ học $x$ | 1 | 2 | 3 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|
| Nhãn $y$ | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Nghiệm giải tích (công thức Bài 1) cho $w \approx 0{,}160$, $b \approx -0{,}252$ — đường cắt $0{,}5$ tại $x \approx 4{,}70$, đúng giữa khoảng trống $3$–$6$, phân loại đúng cả 7/7 điểm. Có vẻ ổn. Giờ thêm đúng một điểm THẬT (không nhập sai gì cả): một người học $x = 50$ giờ và vẫn đậu ($y=1$) — một outlier về độ lớn, không phải outlier về nhãn:
// Them 1 diem THAT (x=50, y=1) - khong sai nhan, chi o rat xa
const withOutlier = [...clean, { x: 50, y: 1 }];
const fit = fitLeastSquares(withOutlier); // ham y het Bai 1
// w ~ 0.0144, b ~ 0.470 -> nguong cat 0.5 tai x ~ 2.09 (!)
// x=3 (nhan that: RỚT) gio du doan: 0.0144*3 + 0.470 = 0.513 >= 0.5 -> "DAU" — SAI
Một điểm $x=50$ duy nhất kéo hệ số $w$ từ $0{,}160$ xuống còn $0{,}0144$ (giảm hơn 10 lần, vì đường phải "vươn" ra xa để đi qua điểm mới) và đẩy ngưỡng cắt $0{,}5$ từ $x \approx 4{,}70$ về $x \approx 2{,}09$ — lùi sâu vào giữa vùng đáng lẽ toàn là "rớt". Hệ quả: điểm $x=3$ (nhãn thật là $0$ — rớt) giờ được dự đoán $0{,}513 \geq 0{,}5$, tức bị đoán nhầm thành "đậu". Không có gì sai về mặt thuật toán — least-squares vẫn tối ưu đúng MSE của nó — nhưng bản thân việc dùng MSE + ngưỡng cứng cho bài toán 0/1 là sai công cụ.
Vấn đề gốc rễ: đường thẳng $wx+b$ không có giới hạn trên/dưới — nó có thể vọt lên $50$ hay xuống $-30$, trong khi nhãn chỉ có $0$ hoặc $1$. Cần một hàm nén mọi giá trị thực về khoảng $(0, 1)$ để đầu ra đọc được như xác suất. Hàm đó là sigmoid:
$$\sigma(z) = \frac{1}{1 + e^{-z}}$$
Giải nghĩa: $z$ (gọi là logit) là đầu ra tuyến tính thô $wx+b$, có thể là số thực bất kỳ. Sigmoid nén nó về $(0,1)$: $z \to +\infty$ thì $\sigma \to 1$; $z \to -\infty$ thì $\sigma \to 0$; và đẹp nhất, $\sigma(0) = 0{,}5$ — đúng điểm cân bằng. Vài mốc đáng nhớ (tính tay, khớp self-test): $\sigma(2) \approx 0{,}881$, $\sigma(-2) \approx 0{,}119$ — càng xa $0$ theo hướng nào, sigmoid càng bão hoà sát $1$ hoặc $0$ theo hướng đó, không bao giờ chạm hẳn biên. Model logistic bây giờ là $\hat{y} = \sigma(wx+b)$ — và $\hat{y}$ đọc được đúng nghĩa: "xác suất model tin điểm này thuộc lớp 1".
2. Cross-entropy: hàm mất mát phạt theo độ tự tin sai
Sigmoid giải quyết vế "đầu ra là xác suất". Vế còn lại: nếu vẫn dùng MSE $(\hat{y} - y)^2$ của Bài 1 để train, chuyện gì xảy ra? Hàm mất mát đúng cho phân loại là binary cross-entropy (BCE):
$$L = -\big[y \log(\hat{y}) + (1-y)\log(1-\hat{y})\big]$$
Giải nghĩa: với mỗi điểm, đúng một trong hai số hạng "sống" tuỳ nhãn thật — nếu $y=1$, số hạng thứ hai triệt tiêu ($1-y=0$), loss chỉ còn $-\log(\hat{y})$; nếu $y=0$, chỉ còn $-\log(1-\hat{y})$. Trực giác: loss = "phạt theo độ tự tin SAI". Bảng dưới tính $-\log(\hat{y})$ khi nhãn thật là $1$ (đối xứng cho nhãn $0$):
| Xác suất model đoán $\hat{y}$ (nhãn thật $y=1$) | Cross-entropy loss | Diễn giải |
|---|---|---|
| $0{,}99$ | $0{,}0101$ | Tự tin ĐÚNG — phạt gần như 0 |
| $0{,}5$ | $0{,}6931$ | Không chắc gì cả — phạt vừa |
| $0{,}1$ | $2{,}3026$ | Nghiêng sai — phạt tăng rõ |
| $0{,}01$ | $4{,}6052$ | Tự tin SAI — phạt nặng |
| $0{,}001$ | $6{,}9078$ | Cực kỳ tự tin SAI — phạt cực nặng |
Cùng một "khoảng cách" tới đáp án đúng ($1 - \hat{y}$ giảm dần đều: $0{,}01 \to 0{,}5 \to 0{,}9 \to 0{,}99 \to 0{,}999$) nhưng loss KHÔNG tăng đều — nó tăng vọt phi tuyến khi model càng tự tin mà càng sai. Đây chính xác là hành vi ta muốn: một model tự tin $99\%$ nhưng sai phải bị phạt nặng hơn nhiều một model lưỡng lự $50/50$.
Vì sao không dùng MSE cho phân loại? Lý do sâu hơn "công thức khác nhau" — nó nằm ở gradient. Đạo hàm loss theo $z$ (logit, trước sigmoid) của hai hàm mất mát:
$$\frac{dL_{MSE}}{dz} = 2(\sigma(z)-y)\cdot\sigma'(z) \qquad \frac{dL_{CE}}{dz} = \sigma(z) - y$$
Cross-entropy cho một kết quả đẹp đến bất ngờ: gradient của nó theo $z$ chính là "lỗi dự đoán" ($\hat{y} - y$) — không hề đi qua $\sigma'(z)$. MSE thì khác: gradient của nó nhân thêm $\sigma'(z) = \sigma(z)(1-\sigma(z))$, và số hạng này tự triệt tiêu khi $\sigma(z)$ tiến gần $0$ hoặc $1$ (bão hoà). Số liệu cụ thể tại $z=-6$ (model rất tự tin lớp $0$) khi nhãn thật là $y=1$ (tự tin SAI — đúng lúc cần gradient lớn nhất để sửa):
| Hàm mất mát | Gradient $dL/dz$ tại $z=-6$, $y=1$ | Ý nghĩa |
|---|---|---|
| MSE | $-0{,}00492$ | Gần như $0$ — gradient tiêu biến đúng lúc cần sửa gấp nhất |
| Cross-entropy | $-0{,}99753$ | Lớn, đúng dấu — cập nhật mạnh về phía đúng |
Chênh lệch hơn 200 lần. Dùng MSE cho phân loại, model tự tin sai gần như đứng im — không ai "sửa" nó nữa vì gradient quá nhỏ. Đây là lý do kỹ thuật thật sự, không phải quy ước tuỳ tiện, khiến cross-entropy là lựa chọn bắt buộc cho phân loại, không phải MSE.
Cross-entropy có $\log(\hat{y})$ và $\log(1-\hat{y})$ — cả hai đều nổ nếu tham số chạm đúng $0$ hoặc $1$. Số dấu phẩy động (float64) khiến chuyện này KHÔNG chỉ là lý thuyết:
- Tự tin SAI cực độ ($z=-750$, $y=1$): $\sigma(z)$ làm tròn về đúng $0{,}0$ (không còn số thực nào biểu diễn được nhỏ hơn) → $-\log(0) = \texttt{Infinity}$.
-
Tự tin ĐÚNG cực độ ($z=40$, $y=1$): $\sigma(z)$ làm tròn về đúng $1{,}0$ → số hạng
lẽ ra vô hại $(1-y)\log(1-\hat{y}) = 0 \times \log(0) = 0 \times \texttt{-Infinity} =$
NaNtrong số học dấu phẩy động — dù dự đoán hoàn toàn chính xác.
Cách khắc phục chuẩn công nghiệp: clip $\hat{y}$ vào khoảng $[\epsilon, 1-\epsilon]$
(thường $\epsilon = 10^{-12}$) trước khi lấy log. Với $z=-750$, sau clip loss $= 27{,}63$ — một số lớn
nhưng hữu hạn, gradient descent vẫn cập nhật được bình thường thay vì nhận Infinity/NaN
rồi phá hỏng mọi tham số phía sau (mọi phép cộng với NaN đều cho NaN).
3. Decision boundary tuyến tính & phân loại đa lớp
Với 2 đặc trưng $x_1, x_2$ (dữ liệu 2 chiều), model là $\hat{y} = \sigma(w_1x_1 + w_2x_2 + b)$. Model dự đoán lớp $1$ khi $\hat{y} \geq 0{,}5$ — mà $\sigma(0)=0{,}5$ đúng, nên điều kiện đó tương đương $w_1x_1+w_2x_2+b \geq 0$. Đường ranh giới (decision boundary) — nơi model "phân vân đúng 50/50" — là đường thẳng:
$$w_1x_1 + w_2x_2 + b = 0$$
Đây là sự thật hay bị bỏ qua: logistic regression luôn cho ranh giới là một đường/mặt phẳng THẲNG, bất kể sigmoid nghe "phi tuyến" thế nào — độ cong chỉ nằm ở cách xác suất chuyển tiếp mượt qua ranh giới đó (xa ranh giới thì $\hat{y}$ áp sát $0$ hoặc $1$; gần ranh giới thì lưỡng lự quanh $0{,}5$), bản thân đường biên vẫn thẳng tuyệt đối. Dữ liệu không tách được bằng 1 đường thẳng (ví dụ hình xoắn ốc) là bài toán logistic regression thuần không giải nổi — cần mạng neural nhiều lớp (Bài 6).
Với nhiều hơn 2 lớp (ví dụ 10 chữ số của MNIST — Bài 10), sigmoid không dùng được trực tiếp vì các xác suất phải cộng lại đúng bằng $1$. Hàm tổng quát hoá là softmax:
$$\text{softmax}(z)_i = \frac{e^{z_i}}{\sum_j e^{z_j}}$$
Mỗi lớp có một logit riêng, softmax biến cả vector logit thành một phân bố xác suất hợp lệ (mọi số $\geq
0$, tổng $=1$), rồi lấy lớp có xác suất cao nhất (argmax) làm dự đoán cuối. Ví dụ tính tay
(khớp self-test), 3 lớp với logit $[2{,}0,\ 1{,}0,\ 0{,}1]$:
function softmax(logits) {
const m = Math.max(...logits); // tru max truoc de tranh e^lon tran so
const exps = logits.map(z => Math.exp(z - m));
const s = exps.reduce((a, b) => a + b, 0);
return exps.map(e => e / s);
}
softmax([2.0, 1.0, 0.1]);
// -> [0.6590, 0.2424, 0.0986] (tong = 1.000000, dung sai < 1e-9)
// argmax = lop 0 — logit lon nhat luon thanh xac suat lon nhat
Chú ý dòng trừ m: không phải trang trí, mà là kỹ thuật ổn định số học — nếu một logit lớn (vd
$1000$), $e^{1000}$ tràn số thành Infinity ngay lập tức; trừ đi max trước khi lấy mũ giữ mọi
số hạng $\leq e^0 = 1$ mà không đổi kết quả cuối (tính chất toán học của softmax: cộng/trừ hằng số vào mọi
logit không đổi output).
m thì hỏng ngay ở logit nào?Infinity, và phép
chia Infinity / Infinity cho ra NaN cho cả 3 lớp — dù bài toán logit $1000$ so
với $1$ hoàn toàn không phải trường hợp cực đoan bất thường (một mạng neural huấn luyện dở có thể dễ
dàng sinh ra logit cỡ đó). Sau khi trừ max ($m=1000$): softmax trả về đúng $[1{,}0,\ 0{,}0,\ 0{,}0]$ —
lớp có logit vượt trội gần như tuyệt đối chiếm hết xác suất, không còn NaN nào.
4. Đo model đúng cách: accuracy có thể nói dối
Bài toán phân loại có bộ thước đo riêng, khác hẳn $R^2$ của Bài 1. Thước đo quen thuộc nhất — accuracy (tỉ lệ đoán đúng) — lại là cái dễ đánh lừa nhất khi dữ liệu lệch (một lớp áp đảo lớp kia). Ví dụ cụ thể: tầm soát một bệnh hiếm, $100$ người thì $99$ người âm tính, $1$ người dương tính. Một model "ngu" nhất có thể — luôn luôn đoán âm tính, không nhìn dữ liệu đầu vào — đạt:
| Chỉ số | Giá trị |
|---|---|
| Accuracy | $99\%$ |
| Số ca dương tính thật phát hiện được (Recall) | $0\%$ — bỏ sót người bệnh duy nhất |
$99\%$ nghe như một model xuất sắc — nó chỉ đơn giản phản ánh dữ liệu lệch, không nói gì về khả năng phát hiện bệnh. Cần thước đo tách riêng từng loại đúng/sai. Confusion matrix gọi tên chính xác 4 khả năng khi so dự đoán với nhãn thật:
| Dự đoán: Dương tính | Dự đoán: Âm tính | |
|---|---|---|
| Thật: Dương tính | TP | FN — bỏ sót |
| Thật: Âm tính | FP — báo động giả | TN |
Từ 4 con số đó, ba thước đo quan trọng nhất:
$$\text{Precision} = \frac{TP}{TP+FP} \qquad \text{Recall} = \frac{TP}{TP+FN} \qquad F_1 = \frac{2 \cdot \text{Precision} \cdot \text{Recall}}{\text{Precision}+\text{Recall}}$$
Giải nghĩa: Precision — trong các ca model báo dương tính, bao nhiêu phần trăm dương tính thật (đo "báo động giả có nhiều không"). Recall — trong các ca dương tính thật, model bắt được bao nhiêu phần trăm (đo "bỏ sót có nhiều không"). F1 là trung bình điều hoà của hai số trên — chỉ cao khi cả hai đều khá, không thể "ăn gian" bằng cách đẩy một số lên cực đoan. Hai thước đo này luôn đánh đổi nhau qua ngưỡng quyết định (mặc định $0{,}5$, nhưng chỉnh được): ngưỡng càng cao, model càng "kén" khi báo dương tính (precision tăng, recall giảm); ngưỡng càng thấp, model càng "rộng rãi" (recall tăng, precision giảm). Số liệu cụ thể trên cùng 1 bộ dự đoán (self-test đối chiếu):
| Ngưỡng quyết định | Precision | Recall | F1 |
|---|---|---|---|
| $0{,}3$ (rộng rãi) | $0{,}700$ | $1{,}000$ | $0{,}824$ |
| $0{,}5$ (mặc định) | $0{,}857$ | $0{,}857$ | $0{,}857$ |
| $0{,}7$ (kén chọn) | $1{,}000$ | $0{,}571$ | $0{,}727$ |
Precision tăng đơn điệu, recall giảm đơn điệu khi ngưỡng tăng — đúng như dự đoán; F1 đạt đỉnh ở ngưỡng giữa vì nó phạt cả hai cực đoan. Demo Mục 5 cho bạn tự kéo ngưỡng và nhìn 3 con số này đổi trực tiếp.
- Nếu dùng trung bình cộng $(P+R)/2 = (1{,}0 + 0{,}01)/2 = 0{,}505$ — nghe vẫn "được một nửa điểm", che giấu hoàn toàn việc model gần như vô dụng.
- Trung bình điều hoà (F1 thật): $2 \times 1{,}0 \times 0{,}01 / (1{,}0 + 0{,}01) \approx 0{,}0198$ — sụp xuống gần $0$, phản ánh đúng thực tế: một con số cực thấp đủ để kéo sập toàn bộ điểm số, bất kể con số còn lại đẹp cỡ nào.
5. Thực hành: bộ phân loại 2D — kéo điểm, xem ranh giới & confusion matrix đổi live
Toàn bộ phần "học" của model 2D chỉ là gradient descent (Bài 2) áp dụng đúng gradient đẹp $\hat{y}-y$ của Mục 2 cho từng trọng số — không có gì bí ẩn hơn vòng lặp dưới đây, chạy full-batch trên cả tập điểm mỗi bước:
function trainLogistic(points, lr, epochs) {
let w1 = 0, w2 = 0, b = 0; // khoi tao 0 — khong ngau nhien, tai lap duoc
const n = points.length;
for (let e = 0; e < epochs; e++) {
let g1 = 0, g2 = 0, gb = 0;
for (const p of points) {
const err = sigmoid(w1*p.x + w2*p.y + b) - p.label; // = dL/dz (Muc 2)
g1 += err * p.x; g2 += err * p.y; gb += err;
}
w1 -= lr * g1 / n; w2 -= lr * g2 / n; b -= lr * gb / n; // w = w - eta*grad
}
return { w1, w2, b };
}
// trainLogistic(12 diem mac dinh, lr=0.15, 400 buoc) -> 12/12 dung, w1~0.998 w2~-0.041 b~-4.572
Bấm vào khung để thêm điểm lớp đang chọn, kéo điểm có sẵn để di chuyển. Sau mỗi thay đổi, model huấn luyện lại từ đầu bằng đúng hàm trên (không ngẫu nhiên — tái lập được), vẽ lại đường ranh giới, và cập nhật confusion matrix theo ngưỡng đang chọn:
| Đoán: Lớp 1 | Đoán: Lớp 0 | |
|---|---|---|
| Thật: Lớp 1 | – | – |
| Thật: Lớp 0 | – | – |
Kéo một điểm xanh (lớp 1) sang bên trái đường ranh giới để tạo ra 1 False Negative, rồi kéo thanh trượt ngưỡng xuống thấp — xem model "rộng rãi" hơn kéo nó về đúng lớp và Recall tăng trở lại.
Đối chiếu công nghiệp: PyTorch không dùng sigmoid() + cross-entropy tách rời như
bài viết — nó gộp hai bước thành BCEWithLogitsLoss, nhận thẳng logit $z$ (chưa qua sigmoid).
Lý do là ổn định số học: tính $\sigma(z)$ trước rồi mới $\log$ có thể mất chính xác hoặc tràn số ở $z$ cực
trị (đúng cạm bẫy Mục 2) — công thức gộp dùng biến thể log-sum-exp tính trực tiếp từ $z$, tránh hoàn toàn
bước trung gian nguy hiểm:
# Doi chieu 1-1 voi demo 2D — cai dat: pytorch.org/get-started/locally
import torch
X = torch.tensor([[2.,2.],[2.,4.],[3.,3.],[4.,2.],[3.,5.],[4.,6.],
[7.,6.],[8.,7.],[6.,8.],[8.,5.],[7.,8.],[9.,6.]])
y = torch.tensor([0.,0.,0.,0.,0.,0., 1.,1.,1.,1.,1.,1.])
w = torch.zeros(2, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
opt = torch.optim.SGD([w, b], lr=0.15)
loss_fn = torch.nn.BCEWithLogitsLoss() # nhan LOGIT tho, tu lo sigmoid + log on dinh
for epoch in range(400):
opt.zero_grad()
z = X @ w + b # logit tho — KHONG goi sigmoid() truoc
loss = loss_fn(z, y) # gop sigmoid + cross-entropy, on dinh so hoc
loss.backward()
opt.step()
print(torch.sigmoid(X @ w + b)) # xac suat cuoi — khop demo 2D tren trang
Tóm lược
- ✅ Hồi quy + ngưỡng cứng vỡ trận khi có outlier về độ lớn: 1 điểm $x=50$ kéo ngưỡng cắt từ $x\approx4{,}70$ về $x\approx2{,}09$, làm $x=3$ bị đoán nhầm — cần hàm nén về xác suất, không phải đường thẳng vô hạn.
- ✅ Sigmoid $\sigma(z)=1/(1+e^{-z})$ nén mọi số thực về $(0,1)$, $\sigma(0)=0{,}5$ — model logistic là $\hat{y}=\sigma(wx+b)$.
- ✅ Cross-entropy phạt theo độ tự tin sai (tăng phi tuyến, không tuyến tính); gradient của nó $= \hat{y}-y$ KHÔNG tiêu biến, còn gradient MSE nhân thêm $\sigma'(z)$ có thể nhỏ hơn 200 lần ở vùng bão hoà — lý do kỹ thuật thật sự để không dùng MSE cho phân loại.
-
✅ $\log(0)$ có 2 kiểu hỏng: tự tin sai cực độ →
Infinity; tự tin ĐÚNG cực độ →NaNtừ số hạng thừa $0 \times \log(0)$ — clip xác suất vào $[\epsilon, 1-\epsilon]$ để tránh cả hai. -
✅ Decision boundary logistic luôn là đường thẳng; đa lớp dùng
softmax (tổng luôn $=1$) +
argmax. - ✅ Accuracy đánh lừa trên dữ liệu lệch — model "luôn đoán âm tính" đạt $99\%$ accuracy mà bỏ sót $100\%$ ca dương tính; precision/recall/F1 và lựa chọn ngưỡng mới phản ánh đúng loại sai nào đắt giá hơn trong bài toán cụ thể.
Tải file code thực hành minh họa bài học
File JavaScript gồm sigmoid, cross-entropy, softmax, confusion matrix và logistic regression 2D của
demo, với self-test đối chiếu đúng mọi con số trích dẫn trong bài (chạy
node logistic_regression.js):
Bình luận