Tưởng tượng bạn build một model phát hiện ung thư từ X-quang. Model của bạn đạt 95% accuracy. Sounds great, right?
Nhưng đợi đã... nếu trong thực tế chỉ có 5% người bị ung thư, và model của bạn predict tất cả mọi người đều KHÔNG bị ung thư, thì accuracy vẫn là 95%! Nhưng model này hoàn toàn vô dụng - nó miss 100% trường hợp ung thư.
Đây chính là lý do tại sao accuracy không phải lúc nào cũng là metric tốt, và tại sao bạn cần hiểu sâu về các evaluation metrics khác.
Confusion Matrix là nền tảng để hiểu tất cả classification metrics. Nó cho bạn biết model predict đúng/sai ở đâu.
Predicted
Negative Positive
Actual Negative TN FP
Positive FN TP
4 thành phần:
True Positive (TP): Predict Positive, thực tế Positive ✅
VD: Predict có ung thư, thực tế có ung thư
True Negative (TN): Predict Negative, thực tế Negative ✅
VD: Predict không ung thư, thực tế không ung thư
False Positive (FP): Predict Positive, thực tế Negative ❌ (Type I Error)
VD: Predict có ung thư, thực tế KHÔNG có → báo động giả
False Negative (FN): Predict Negative, thực tế Positive ❌ (Type II Error)
VD: Predict KHÔNG ung thư, thực tế CÓ → miss case nguy hiểm!
Email Spam Detection:
Predicted
Not Spam Spam
Actual Not Spam 900 50 = 950 legitimate emails
Spam 20 30 = 50 spam emails
---- ----
920 80
Trade-off ở đây:
→ Với spam detection, minimize FP quan trọng hơn minimize FN.
Công thức:
Accuracy = (TP + TN) / (TP + TN + FP + FN)
= Số predictions đúng / Tổng số predictions
Ví dụ trên:
Accuracy = (900 + 30) / 1000 = 93%
Khi nào Accuracy hữu ích:
Khi nào KHÔNG nên dùng Accuracy:
Công thức:
Precision = TP / (TP + FP)
= True Positives / All Predicted Positives
Câu hỏi mà Precision trả lời:
"Khi model nói 'có ung thư', tôi tin được bao nhiêu %?"
Ví dụ:
Precision = 30 / (30 + 50) = 30 / 80 = 37.5%
Trong 80 emails model predict là spam, chỉ 30 emails (37.5%) thực sự là spam. 50 emails legitimate bị nhầm!
Khi nào Precision quan trọng:
Trade-off:
Nếu model cực kỳ conservative (chỉ predict Positive khi chắc chắn 100%):
→ TP ít, FP rất ít → Precision cao
→ Nhưng FN nhiều (miss nhiều positive cases)
Công thức:
Recall = TP / (TP + FN)
= True Positives / All Actual Positives
Câu hỏi mà Recall trả lời:
"Trong số người thực sự có ung thư, model detect được bao nhiêu %?"
Ví dụ:
Recall = 30 / (30 + 20) = 30 / 50 = 60%
Trong 50 spam emails thực tế, model chỉ detect được 30 (60%). 20 emails spam lọt qua!
Khi nào Recall quan trọng:
Trade-off:
Nếu model aggressive (predict Positive khi có chút nghi ngờ):
→ TP nhiều, FN ít → Recall cao
→ Nhưng FP nhiều (nhiều false alarms)
Mâu thuẫn cốt lõi:
Ví dụ: Cancer Detection
Model A (Conservative):
Model B (Aggressive):
→ Với medical diagnosis, Recall ưu tiên hơn Precision (better safe than sorry).
Với Spam Detection:
Model A (Conservative):
Model B (Aggressive):
→ Precision ưu tiên hơn (miss important email tệ hơn là có spam trong inbox).
Khi bạn cần balance giữa Precision và Recall?
Công thức:
F1 = 2 * (Precision * Recall) / (Precision + Recall)
F1-Score là harmonic mean của Precision và Recall.
Tại sao harmonic mean, không phải arithmetic mean?
Precision = 100%, Recall = 10%
Arithmetic mean = (100 + 10) / 2 = 55% ← misleading!
Harmonic mean (F1) = 2 * (100 * 10) / (100 + 10) = 18.2% ← more realistic
Harmonic mean "punish" khi có metric quá thấp → buộc cả hai phải cao.
Ví dụ:
Precision = 37.5%, Recall = 60%
F1 = 2 * (0.375 * 0.6) / (0.375 + 0.6) = 0.46 = 46%
Khi nào dùng F1:
Variants:
from sklearn.metrics import fbeta_score
# F2: Recall quan trọng hơn (medical diagnosis)
f2 = fbeta_score(y_true, y_pred, beta=2)
# F0.5: Precision quan trọng hơn (spam filtering)
f05 = fbeta_score(y_true, y_pred, beta=0.5)
Hầu hết classifiers không predict hard 0/1, mà predict probability:
Sample 1: P(Spam) = 0.95 → Confident spam
Sample 2: P(Spam) = 0.55 → Slightly lean spam
Sample 3: P(Spam) = 0.30 → Likely not spam
Bạn cần chọn threshold để convert probability → binary prediction:
If P(Spam) >= threshold → Predict Spam
Else → Predict Not Spam
Vấn đề: Threshold khác nhau → Precision/Recall khác nhau. Chọn threshold nào?
ROC curve vẽ True Positive Rate (TPR) vs False Positive Rate (FPR) across tất cả thresholds.
Definitions:
TPR (True Positive Rate) = Recall = TP / (TP + FN)
→ Tỷ lệ actual positives được detect
FPR (False Positive Rate) = FP / (FP + TN)
→ Tỷ lệ actual negatives bị nhầm là positive
Ví dụ:
Threshold TPR (Recall) FPR
0.9 0.3 0.01 ← Conservative
0.7 0.6 0.05
0.5 0.8 0.15
0.3 0.95 0.40 ← Aggressive
0.1 1.0 0.80
Plot (TPR, FPR) cho mỗi threshold → ROC curve:
TPR
1.0 | •──────•
| •
0.8 | • Perfect model
0.6 | • (TPR=1, FPR=0)
0.4 | •
0.2 |•
0.0 └──────────────────── FPR
0 0.2 0.4 0.6 0.8 1.0
Random guessing = đường chéo
Interpretation:
AUC = Diện tích dưới ROC curve
AUC = 1.0 → Perfect model
AUC = 0.9 → Excellent
AUC = 0.8 → Good
AUC = 0.7 → Fair
AUC = 0.5 → Random guessing (useless)
AUC < 0.5 → Worse than random (flip predictions!)
Ý nghĩa thực tế của AUC:
AUC = xác suất model rank một random positive sample cao hơn một random negative sample.
Ví dụ:
Khi nào dùng AUC:
Limitation:
from sklearn.metrics import roc_auc_score, roc_curve
import matplotlib.pyplot as plt
# Calculate ROC curve
fpr, tpr, thresholds = roc_curve(y_true, y_pred_proba)
# Calculate AUC
auc = roc_auc_score(y_true, y_pred_proba)
# Plot
plt.plot(fpr, tpr, label=f'AUC = {auc:.2f}')
plt.plot([0, 1], [0, 1], 'k--', label='Random')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend()
plt.show()
Khi data highly imbalanced (99% negative, 1% positive), ROC curve có thể misleading vì FPR sẽ rất thấp ngay cả khi có nhiều FP.
Ví dụ:
Data: 9900 negatives, 100 positives
FP = 500 (nhiều!)
FPR = 500 / 9900 = 0.05 (5% - trông có vẻ OK)
Nhưng thực tế: 500 false alarms trong 600 positive predictions!
Precision = 100 / 600 = 16.7% (rất tệ)
→ Precision-Recall curve sensitive hơn với imbalanced data.
Plot Precision vs Recall across thresholds:
Precision
1.0 |•
| \
0.8 | \
| •
0.6 | \
| •
0.4 | \
| •────
0.2 |
0.0 └──────────────────── Recall
0 0.2 0.4 0.6 0.8 1.0
Đường cong lý tưởng: Cao và phẳng (high precision across all recall levels)
Average Precision (AP): Diện tích dưới PR curve (analog của AUC)
from sklearn.metrics import average_precision_score, precision_recall_curve
# Calculate PR curve
precision, recall, thresholds = precision_recall_curve(y_true, y_pred_proba)
# Calculate AP
ap = average_precision_score(y_true, y_pred_proba)
# Plot
plt.plot(recall, precision, label=f'AP = {ap:.2f}')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.legend()
plt.show()
Khi nào dùng PR Curve thay vì ROC:
Với regression (predict continuous values), metrics khác hoàn toàn.
Công thức:
MAE = (1/n) * Σ|y_actual - y_predicted|
Ví dụ: Predict house prices
Actual: [100k, 200k, 300k, 400k]
Predicted: [110k, 190k, 320k, 380k]
Errors: [10k, 10k, 20k, 20k]
MAE = (10 + 10 + 20 + 20) / 4 = 15k
Interpretation: Trung bình, predictions sai lệch 15k so với actual.
Ưu điểm:
Nhược điểm:
Công thức:
MSE = (1/n) * Σ(y_actual - y_predicted)²
Ví dụ trên:
MSE = (10² + 10² + 20² + 20²) / 4
= (100 + 100 + 400 + 400) / 4
= 250
Ưu điểm:
Nhược điểm:
Công thức:
RMSE = √MSE = √[(1/n) * Σ(y_actual - y_predicted)²]
Ví dụ trên:
RMSE = √250 ≈ 15.8k
Ưu điểm:
Nhược điểm:
So sánh MAE vs RMSE:
Predictions: [100, 100, 100, 500] (500 là outlier)
Actual: [100, 100, 100, 100]
MAE = (0 + 0 + 0 + 400) / 4 = 100
RMSE = √[(0 + 0 + 0 + 160000) / 4] = √40000 = 200
→ RMSE cao gấp đôi MAE vì punish outlier nặng hơn
Khi nào dùng:
Công thức:
R² = 1 - (SS_residual / SS_total)
SS_residual = Σ(y_actual - y_predicted)²
SS_total = Σ(y_actual - y_mean)²
Ý nghĩa:
Ví dụ:
Actual: [100, 200, 300, 400]
Mean: 250
Predicted: [110, 190, 310, 390]
SS_total = (100-250)² + (200-250)² + (300-250)² + (400-250)²
= 22500 + 2500 + 2500 + 22500 = 50000
SS_residual = (100-110)² + (200-190)² + (300-310)² + (400-390)²
= 100 + 100 + 100 + 100 = 400
R² = 1 - (400 / 50000) = 1 - 0.008 = 0.992 = 99.2%
Interpretation: Model giải thích được 99.2% variance trong data.
Ưu điểm:
Nhược điểm:
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
mae = mean_absolute_error(y_true, y_pred)
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_true, y_pred)
print(f"MAE: {mae:.2f}")
print(f"RMSE: {rmse:.2f}")
print(f"R²: {r2:.3f}")
Chia data một lần 80/20 có thể lucky or unlucky - test set có thể quá dễ hoặc quá khó.
Cross-Validation giải quyết vấn đề này bằng cách test model nhiều lần trên các splits khác nhau.
Workflow:
Fold 1: [Test] [Train] [Train] [Train] [Train]
Fold 2: [Train] [Test] [Train] [Train] [Train]
Fold 3: [Train] [Train] [Test] [Train] [Train]
Fold 4: [Train] [Train] [Train] [Test] [Train]
Fold 5: [Train] [Train] [Train] [Train] [Test]
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
# 5-fold cross-validation
scores = cross_val_score(model, X, y, cv=5, scoring='f1')
print(f"F1 scores: {scores}")
print(f"Mean F1: {scores.mean():.3f} (+/- {scores.std():.3f})")
# Output:
# F1 scores: [0.85, 0.88, 0.82, 0.87, 0.86]
# Mean F1: 0.856 (+/- 0.021)
Ưu điểm:
Nhược điểm:
Vấn đề với K-Fold thường:
Data: 90% class 0, 10% class 1
Random fold có thể:
Fold 1: 95% class 0, 5% class 1
Fold 2: 85% class 0, 15% class 1
→ Inconsistent evaluation
Stratified K-Fold: Đảm bảo mỗi fold giữ nguyên tỷ lệ classes như original data.
from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, test_idx in skf.split(X, y):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# Check class distribution
print(f"Train: {y_train.mean():.2%}, Test: {y_test.mean():.2%}")
# Output: Train: 10.00%, Test: 10.00% (consistent!)
Best practice: Luôn dùng Stratified K-Fold cho classification với imbalanced data.
Vấn đề với standard K-Fold cho time series:
Data có temporal order → không thể train trên future data để predict past!
❌ WRONG (K-Fold):
[Test-2020] [Train-2021] [Train-2022] ← Cheating!
Time Series Split: Train luôn trước Test (respecting temporal order).
Split 1: [Train-Jan~Mar] | [Test-Apr]
Split 2: [Train-Jan~Apr] | [Test-May]
Split 3: [Train-Jan~May] | [Test-Jun]
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# Train on past, test on future
Classification:
Is data balanced?
├─ Yes
│ ├─ FP and FN equally bad? → Accuracy
│ └─ FP vs FN different costs?
│ ├─ FP worse → Precision
│ └─ FN worse → Recall
│
└─ No (Imbalanced)
├─ Need balance? → F1-Score
├─ FN critical (medical)? → Recall
├─ FP critical (spam)? → Precision
└─ Compare models? → AUC or Average Precision
Regression:
Do you have outliers?
├─ Yes, many → MAE (robust)
└─ No, or want to penalize outliers → RMSE
Need normalized metric? → R²
Need interpretable error? → MAE or RMSE
Next steps:
Trong bài tiếp theo, chúng ta sẽ đi sâu vào Neural Networks Fundamentals - kiến trúc của Deep Learning và cách chúng "học" từ data.
Bài viết thuộc series "From Zero to AI Engineer" - Module 4: ML Foundations