Model Evaluation Metrics: Đừng để Accuracy "Lừa" Bạn

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: "Bảng Nhầm Lẫn" Không Hề Nhầm Lẫn

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.

Cấu trúc Confusion Matrix

                    Predicted
                 Negative  Positive
Actual  Negative    TN       FP
        Positive    FN       TP

4 thành phần:

  1. True Positive (TP): Predict Positive, thực tế Positive ✅
    VD: Predict có ung thư, thực tế có ung thư

  2. True Negative (TN): Predict Negative, thực tế Negative ✅
    VD: Predict không ung thư, thực tế không ung thư

  3. 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ả

  4. 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!

Ví dụ Thực tế

Email Spam Detection:

                    Predicted
                 Not Spam   Spam
Actual  Not Spam   900       50      = 950 legitimate emails
        Spam        20       30      = 50 spam emails
                   ----     ----
                   920      80
  • TN = 900: Legitimate emails được classify đúng
  • FP = 50: Legitimate emails bị nhầm là spam (người dùng miss emails quan trọng!)
  • FN = 20: Spam emails lọt vào inbox (annoying nhưng không critical)
  • TP = 30: Spam được detect đúng

Trade-off ở đây:

  • FP cao → người dùng miss emails quan trọng (VD: job offer, client email) → rất tệ
  • FN cao → spam lọt vào inbox → annoying nhưng chấp nhận được

→ Với spam detection, minimize FP quan trọng hơn minimize FN.

Accuracy, Precision, Recall: The Holy Trinity

Accuracy: "Tỷ lệ Đúng Tổng thể"

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:

  • Classes balanced (50-50 hoặc gần đó)
  • Cost của FP và FN tương đương nhau

Khi nào KHÔNG nên dùng Accuracy:

  • Imbalanced data (99% class A, 1% class B)
  • FP và FN có impact khác nhau (ung thư detection: FN nguy hiểm hơn FP rất nhiều)

Precision: "Trong số dự đoán Positive, bao nhiêu là đúng?"

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:

  • Cost of FP cao: Spam filtering (FP = miss important email), fraud detection alerts (FP = waste investigator time)
  • Muốn minimize false alarms

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)

Recall (Sensitivity, True Positive Rate): "Trong số thực tế Positive, bao nhiêu được detect?"

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:

  • Cost of FN cao: Disease diagnosis (FN = miss disease case), fraud detection (FN = miss fraud transaction)
  • Muốn không bỏ sót positive cases

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)

Precision-Recall Trade-off: Không Thể Có Cả Hai?

Mâu thuẫn cốt lõi:

  • Tăng Precision (careful predictions) → Giảm Recall (miss cases)
  • Tăng Recall (aggressive predictions) → Giảm Precision (false alarms)

Ví dụ: Cancer Detection

Model A (Conservative):

  • Chỉ predict cancer khi confidence >95%
  • Precision = 90% (hầu hết predictions đúng)
  • Recall = 50% (miss nhiều cases) ← NGUY HIỂM!

Model B (Aggressive):

  • Predict cancer khi confidence >30%
  • Precision = 40% (nhiều false alarms → thêm tests không cần thiết)
  • Recall = 95% (detect hầu hết cases) ← AN TOÀN hơn

→ Với medical diagnosis, Recall ưu tiên hơn Precision (better safe than sorry).

Với Spam Detection:

Model A (Conservative):

  • Precision = 95% (ít legitimate emails bị nhầm)
  • Recall = 60% (nhiều spam lọt qua inbox)

Model B (Aggressive):

  • Precision = 70% (nhiều legitimate emails bị filter nhầm)
  • Recall = 90% (ít spam lọt qua)

→ Precision ưu tiên hơn (miss important email tệ hơn là có spam trong inbox).

F1-Score: "Best of Both Worlds"

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:

  • Classes moderately imbalanced
  • Cần balance giữa Precision và Recall
  • Không biết nên ưu tiên cái nào

Variants:

  • F2-Score: Weight Recall gấp đôi Precision (dùng khi Recall quan trọng hơn)
  • F0.5-Score: Weight Precision gấp đôi Recall (dùng khi Precision quan trọng hơn)
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)

ROC Curve & AUC: Đánh giá Model Across All Thresholds

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 (Receiver Operating Characteristic)

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:

  • Perfect model: ROC curve đi qua góc trên-trái (TPR=1, FPR=0)
  • Random guessing: Đường chéo 45° (AUC = 0.5)
  • Model càng gần góc trên-trái càng tốt

AUC (Area Under the Curve)

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ụ:

  • AUC = 0.9 → 90% trường hợp, model assign higher probability cho actual positive sample so với actual negative sample

Khi nào dùng AUC:

  • So sánh models independent of threshold
  • Classes moderately imbalanced (AUC robust hơn accuracy)
  • Muốn metric tổng quát về model performance

Limitation:

  • Không phù hợp với highly imbalanced data (nên dùng Precision-Recall curve thay thế)
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()

Precision-Recall Curve: Cho Imbalanced Data

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:

  • Highly imbalanced data (1:99, 1:999)
  • Positive class quan trọng hơn (fraud, disease)
  • Muốn focus vào performance on minority class

Metrics cho Regression

Với regression (predict continuous values), metrics khác hoàn toàn.

Mean Absolute Error (MAE)

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:

  • Dễ interpret (cùng đơn vị với target)
  • Robust với outliers (không square errors)

Nhược điểm:

  • Không penalize large errors nặng hơn small errors

Mean Squared Error (MSE)

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:

  • Penalize large errors nhiều hơn: Error 20k được punish gấp 4 lần error 10k
  • Smooth, differentiable (tốt cho optimization)

Nhược điểm:

  • Đơn vị là squared → khó interpret ($² là gì?)
  • Very sensitive với outliers

Root Mean Squared Error (RMSE)

Công thức:

RMSE = √MSE = √[(1/n) * Σ(y_actual - y_predicted)²]

Ví dụ trên:

RMSE = √250 ≈ 15.8k

Ưu điểm:

  • Cùng đơn vị với target (dễ interpret hơn MSE)
  • Vẫn penalize large errors

Nhược điểm:

  • Vẫn sensitive với outliers

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:

  • MAE: Muốn treat tất cả errors equally, có nhiều outliers
  • RMSE: Muốn penalize large errors, outliers ít

R² (R-squared, Coefficient of Determination)

Công thức:

R² = 1 - (SS_residual / SS_total)

SS_residual = Σ(y_actual - y_predicted)²
SS_total = Σ(y_actual - y_mean)²

Ý nghĩa:

  • R² = 1: Perfect predictions (model explains 100% variance)
  • R² = 0: Model không tốt hơn việc predict mean
  • R² < 0: Model tệ hơn baseline (worse than just predicting mean!)

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:

  • Normalized [0, 1] (hoặc âm nếu model tệ)
  • Dễ compare models trên different scales

Nhược điểm:

  • Có thể misleading với non-linear relationships
  • Always tăng khi thêm features (ngay cả garbage features) → Dùng Adjusted R² để penalize model complexity
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}")

Cross-Validation: Đánh giá Model Một Cách Tin Cậy

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.

K-Fold Cross-Validation

Workflow:

  1. Chia data thành K folds (thường k=5 hoặc 10)
  2. Train K lần:
    • Iteration 1: Train trên folds {2,3,4,5}, test trên fold 1
    • Iteration 2: Train trên folds {1,3,4,5}, test trên fold 2
    • ...
    • Iteration K: Train trên folds {1,2,3,4}, test trên fold K
  3. Average metrics từ K iterations
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 giá robust hơn (mỗi sample được test 1 lần)
  • Phát hiện overfitting (nếu train score >> CV score)
  • Sử dụng toàn bộ data (không "waste" data cho test set)

Nhược điểm:

  • Training time tăng K lần
  • Không phù hợp với very large datasets

Stratified K-Fold

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.

Time Series Cross-Validation

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

Choosing the Right Metric: Decision Framework

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

Key Takeaways

  • Accuracy is NOT always the answer - especially với imbalanced data
  • Confusion Matrix là foundation - hiểu TP, TN, FP, FN
  • Precision vs Recall: Trade-off không thể tránh - chọn based on cost of errors
  • F1-Score: Balance khi không biết ưu tiên gì
  • ROC-AUC: Tổng quan performance across thresholds, tốt cho balanced data
  • PR Curve: Thay thế ROC cho highly imbalanced data
  • Regression metrics:
    • MAE: Robust với outliers
    • RMSE: Penalize large errors
    • R²: Percentage variance explained
  • Cross-validation: Đánh giá robust hơn single train/test split
  • Always stratify cho imbalanced classification
  • Respect temporal order cho time series

Next steps:

  • Thực hành calculate metrics manually để hiểu sâu
  • Học threshold tuning để optimize cho business metrics cụ thể
  • Explore advanced metrics: Cohen's Kappa, Matthews Correlation Coefficient

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