Mục lục
- Recap — vì sao cần gộp precision và recall
- F1-Score — định nghĩa và công thức
- Vì sao là harmonic mean, không phải arithmetic mean
- F-beta Score — tổng quát hoá F1
- F1 và F-beta trong sklearn
- Multi-class F1 — macro, micro, weighted, per-class
- Khi nào dùng macro vs weighted vs micro
- Precision-Recall Curve và Average Precision
- F1 vs ROC-AUC — chọn metric nào
- Tối ưu threshold để maximize F1
- Cảnh báo phổ biến với F1
- Industry-specific — chọn beta theo domain
- Code Python — breast cancer và iris
- Bài tập thực hành
- Bài tiếp theo
Recap — vì sao cần gộp precision và recall
Bài 24 đã định nghĩa precision và recall:
\[ \text{Precision} = \frac{TP}{TP + FP}, \qquad \text{Recall} = \frac{TP}{TP + FN} \]Hai metric này thường trade-off: đẩy threshold lên thì precision tăng nhưng recall giảm, và ngược lại. Khi cần so sánh nhiều model hoặc tune hyperparameter trong GridSearchCV, một con số duy nhất tiện hơn 2 con số phải cân nhắc đồng thời.
F1-Score là cách gộp precision và recall thành một số duy nhất với một ràng buộc quan trọng: cả hai phải đủ tốt, không cho phép một bên cao bù cho bên kia cực thấp.
F1-Score — định nghĩa và công thức
F1-Score là harmonic mean của precision và recall:
\[ F_1 = \frac{2 \cdot \text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}} = \frac{2 \, TP}{2 \, TP + FP + FN} \]Dạng phải (qua confusion matrix) tiện khi tính nhanh từ counts mà không qua precision/recall trung gian.
Tính chất:
- Range \( [0, 1] \). Càng cao càng tốt.
- \( F_1 = 1 \) khi và chỉ khi precision = recall = 1 (perfect classification, không có FP và không có FN).
- \( F_1 = 0 \) khi precision = 0 hoặc recall = 0 (nghĩa là \( TP = 0 \)).
- Đối xứng giữa precision và recall — đổi vai hai bên không thay đổi giá trị F1.
Vì sao là harmonic mean, không phải arithmetic mean
Có 3 loại mean phổ biến (cho \( a, b > 0 \)):
\[ \text{AM} = \frac{a + b}{2}, \quad \text{GM} = \sqrt{a \, b}, \quad \text{HM} = \frac{2 \, a \, b}{a + b} \]Bất đẳng thức quen thuộc: \( \text{HM} \leq \text{GM} \leq \text{AM} \), với dấu bằng chỉ khi \( a = b \). Harmonic mean luôn nhỏ nhất, và đặc biệt nhạy với giá trị nhỏ.
Ví dụ cụ thể — model A có \( P = 1.0, R = 0 \) (predict 1 sample đúng, bỏ qua tất cả positive còn lại):
- Arithmetic mean: \( (1.0 + 0) / 2 = 0.5 \) — gợi ý model trung bình, sai bản chất vì model thực ra không có giá trị.
- Harmonic mean (F1): \( 2 \cdot 1.0 \cdot 0 / (1.0 + 0) = 0 \) — phản ánh đúng "model fail".
Tổng quát: harmonic mean phạt extreme. Một bên cực thấp kéo F1 về gần 0, không cho phép bên còn lại bù.
Bảng so sánh — cùng arithmetic mean \( = 0.5 \) nhưng F1 khác nhau:
- \( P = 0.5, R = 0.5 \Rightarrow F_1 = 0.50 \) (balance).
- \( P = 0.7, R = 0.3 \Rightarrow F_1 = 0.42 \) (hơi lệch).
- \( P = 0.9, R = 0.1 \Rightarrow F_1 \approx 0.18 \) (lệch nặng).
- \( P = 0.99, R = 0.01 \Rightarrow F_1 \approx 0.02 \) (extreme).
Đây là lý do F1 phù hợp với bài toán mà cả hai loại sai (FP và FN) đều phải kiểm soát: model chỉ được công nhận khi cân bằng được hai bên.
F-beta Score — tổng quát hoá F1
Khi bài toán ưu tiên một bên (precision hoặc recall) hơn bên kia, dùng F-beta:
\[ F_\beta = (1 + \beta^2) \cdot \frac{\text{Precision} \cdot \text{Recall}}{\beta^2 \cdot \text{Precision} + \text{Recall}} \]Tham số \( \beta > 0 \) điều chỉnh trọng số:
- \( \beta = 1 \) — chính là F1, cân bằng hai bên.
- \( \beta < 1 \) (vd \( F_{0.5} \)) — ưu tiên precision. Recall bị giảm trọng số, sai FP đắt hơn sai FN.
- \( \beta > 1 \) (vd \( F_2, F_3 \)) — ưu tiên recall. Sai FN (bỏ sót positive) đắt hơn sai FP.
Cách nhớ ý nghĩa \( \beta \): "recall quan trọng gấp \( \beta \) lần precision" trong nghĩa weighted harmonic mean. \( \beta = 2 \) → recall quan trọng gấp 2; \( \beta = 0.5 \) → precision quan trọng gấp 2.
Use case điển hình:
- \( F_2 \) — medical screening: bỏ sót bệnh (FN) tốn kém hơn báo nhầm (FP, có thể test lại). Recall quan trọng hơn.
- \( F_{0.5} \) — spam filter / fraud alert: report FP làm phiền user / chặn email thật quan trọng hơn lọt vài spam. Precision quan trọng hơn.
F1 và F-beta trong sklearn
from sklearn.metrics import f1_score, fbeta_score
y_true = [0, 1, 1, 0, 1, 1, 0, 1]
y_pred = [0, 1, 0, 0, 1, 1, 1, 1]
f1 = f1_score(y_true, y_pred) # beta = 1
f2 = fbeta_score(y_true, y_pred, beta=2) # ưu tiên recall
f05 = fbeta_score(y_true, y_pred, beta=0.5) # ưu tiên precision
print(f"F1 = {f1:.4f}") # 0.8000
print(f"F2 = {f2:.4f}") # 0.8065
print(f"F0.5 = {f05:.4f}") # 0.7937
Tham số average mặc định là "binary" — chỉ tính F1 cho class positive (label 1 mặc định). Với multi-class hoặc multi-label phải truyền average rõ ràng (mục 6).
Khi precision + recall = 0 (tức không có TP), sklearn raise UndefinedMetricWarning và trả về 0. Có thể tuỳ chỉnh bằng zero_division=0 hoặc 1 tuỳ ngữ cảnh.
Multi-class F1 — macro, micro, weighted, per-class
F1 vốn định nghĩa cho binary. Với multi-class, sklearn tính F1 từng class (one-vs-rest) rồi gộp lại theo một trong các kiểu sau:
- Macro — trung bình cộng F1 của các class, không weight: \[ F_1^{\text{macro}} = \frac{1}{K} \sum_{k=1}^K F_1^{(k)} \] Mọi class đều quan trọng như nhau. Nhạy với class hiếm — class hiếm performance kém kéo macro xuống.
- Micro — gộp TP, FP, FN của tất cả class trước, rồi tính F1 một lần: \[ F_1^{\text{micro}} = \frac{2 \, \sum_k TP_k}{2 \, \sum_k TP_k + \sum_k FP_k + \sum_k FN_k} \] Với multi-class single-label (mỗi sample 1 label), \( F_1^{\text{micro}} = \) accuracy. Class lớn chiếm trọng số nhiều.
- Weighted — trung bình có trọng số theo support (số sample mỗi class): \[ F_1^{\text{weighted}} = \frac{1}{N} \sum_{k=1}^K n_k \cdot F_1^{(k)} \] Reflect tổng thể nhưng vẫn cho phép class hiếm kéo metric xuống một phần.
- Per-class — trả về vector \( K \) phần tử, F1 của từng class. Quan trọng nhất khi report, không bị che dấu bởi aggregate.
from sklearn.metrics import f1_score, classification_report
f1_macro = f1_score(y_true, y_pred, average="macro")
f1_micro = f1_score(y_true, y_pred, average="micro")
f1_weighted = f1_score(y_true, y_pred, average="weighted")
f1_per_cls = f1_score(y_true, y_pred, average=None) # array per-class
# Báo cáo gọn nhất:
print(classification_report(y_true, y_pred, digits=4))
classification_report in cả precision, recall, F1, support cho từng class và 3 aggregate (macro, weighted, accuracy) — quy chuẩn nên dùng trong notebook khi đánh giá baseline.
Khi nào dùng macro vs weighted vs micro
- Macro: data imbalanced + muốn mọi class đều phải tốt. Vd phân loại bệnh hiếm — không thể để class hiếm có F1 = 0.1 chỉ vì class phổ biến có F1 = 0.95.
- Weighted: imbalanced + muốn số đo phản ánh tổng thể trải nghiệm user. Class nhiều sample tự nhiên ảnh hưởng nhiều hơn.
- Micro: với multi-class single-label, không thêm thông tin so với accuracy — không nên dùng để chẩn đoán imbalance. Với multi-label thì micro lại có nghĩa riêng (gộp toàn bộ TP/FP/FN trên tất cả label).
Quy tắc kinh nghiệm: với imbalanced data, luôn xem per-class F1 trước, rồi mới chọn macro hoặc weighted để báo cáo aggregate. Đừng chỉ in một con số micro/accuracy và kết luận model "tốt".
Ví dụ minh hoạ chênh lệch — dataset 3 class với phân bố 900/90/10 sample, model dự đoán đúng class lớn nhưng tệ với class hiếm:
- F1 per-class:
[0.97, 0.60, 0.10]. - F1 macro =
(0.97 + 0.60 + 0.10) / 3 = 0.557. - F1 weighted ≈
(900·0.97 + 90·0.60 + 10·0.10) / 1000 = 0.928. - Hai con số chênh nhau gần 0.4 — tuỳ business mà chọn cách report.
Precision-Recall Curve và Average Precision
F1 phụ thuộc threshold — model classifier như Logistic Regression cho ra probability, threshold mặc định 0.5 chỉ là một lựa chọn. Precision-Recall (PR) Curve visualize toàn bộ trade-off khi quét threshold từ 0 đến 1:
- Trục X: recall, trục Y: precision.
- Mỗi điểm trên đường cong = (recall, precision) ở một threshold.
- Threshold cao → ít prediction positive → precision cao, recall thấp (góc trái trên).
- Threshold thấp → nhiều prediction positive → recall cao, precision thấp (góc phải dưới).
- Model tốt: đường cong nằm sát góc phải-trên (cả precision và recall đều cao).
from sklearn.metrics import precision_recall_curve, average_precision_score
# y_score: probability của class positive, lấy từ predict_proba(X)[:, 1]
precision, recall, thresholds = precision_recall_curve(y_true, y_score)
# Mảng precision, recall có n+1 phần tử; thresholds có n phần tử
# (điểm cuối là precision=1, recall=0 cho threshold = +inf — không có threshold tương ứng)
ap = average_precision_score(y_true, y_score)
print(f"Average Precision = {ap:.4f}")
Average Precision (AP) là diện tích dưới PR curve — tổng kết toàn bộ curve thành một con số, threshold-independent. AP cao = PR curve sát góc phải-trên.
AP là alternative của ROC-AUC dành cho imbalanced data: PR curve không bị "kéo đẹp" bởi True Negative nhiều, trong khi ROC bị (Bài 26 sẽ phân tích).
F1 vs ROC-AUC — chọn metric nào
Bài 26 sẽ trình bày kỹ ROC-AUC. Tóm lược tiêu chí chọn:
- F1 (hoặc F-beta): cần một con số ở threshold cố định (vd 0.5). Khi deploy production và đã chốt threshold, F1 phản ánh đúng performance thật.
- ROC-AUC: cần metric threshold-independent — đánh giá khả năng rank positive trên negative của model. Tốt để so sánh model trước khi chốt threshold.
- Imbalanced data: ưu tiên F1 (hoặc Average Precision) hơn ROC-AUC. ROC-AUC bị "kéo đẹp" do nhiều True Negative — model dở vẫn ra AUC cao. PR curve / F1 không có hiệu ứng đó vì không dùng TN.
Quy tắc thực tiễn: với fraud detection, hiếm bệnh, anomaly — báo cáo PR-AUC và F1 thay vì ROC-AUC.
Tối ưu threshold để maximize F1
Threshold mặc định 0.5 không phải lúc nào cũng tối ưu — đặc biệt khi data imbalanced. Quy trình tìm threshold maximize F1:
- Lấy
y_score = model.predict_proba(X_val)[:, 1]. - Gọi
precision_recall_curveđể được mảng precision, recall, thresholds. - Tính F1 cho mỗi cặp (precision, recall).
- Chọn threshold tương ứng với F1 lớn nhất.
import numpy as np
from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_val, y_score)
# Bỏ phần tử cuối (precision=1, recall=0 — không có threshold tương ứng)
p, r = precision[:-1], recall[:-1]
# Tính F1 từng điểm; tránh chia 0
f1 = np.where((p + r) > 0, 2 * p * r / (p + r), 0)
best_idx = f1.argmax()
best_threshold = thresholds[best_idx]
best_f1 = f1[best_idx]
print(f"Best threshold = {best_threshold:.4f}, F1 = {best_f1:.4f}")
# Áp dụng threshold mới khi predict
y_pred_tuned = (model.predict_proba(X_test)[:, 1] >= best_threshold).astype(int)
Hai lưu ý:
- Tune threshold trên validation set, không trên test set — nếu không sẽ leak. Test set chỉ để báo cáo cuối.
- Threshold tối ưu F1 không nhất thiết là tối ưu business — F1 cân bằng FP và FN, business có thể đặt cost khác nhau. Khi cost rõ ràng, dùng F-beta hoặc tính trực tiếp expected cost cho mỗi threshold.
Cảnh báo phổ biến với F1
- F1 không "fix" class imbalance hoàn toàn. Với binary, F1 mặc định chỉ tính cho class positive (label 1) — vẫn cần xem support, accuracy, và F1 của class negative riêng nếu cần.
- F1 macro và weighted có thể chênh nhiều với imbalanced data (xem ví dụ mục 7). Báo cáo cả hai, đừng chỉ chọn con số đẹp hơn.
- F1 không phân biệt FP và FN. Nếu bài toán có cost không đối xứng (vd FN đắt gấp 10 FP), F1 không phản ánh — phải dùng F-beta phù hợp, hoặc tính expected cost trực tiếp.
- F1 không phải là probability. F1 = 0.85 không nghĩa "model đúng 85%". Để truyền đạt cho non-technical stakeholder, nên kèm precision, recall, và ví dụ confusion matrix.
- Đừng tối ưu F1 mà bỏ qua context business. F1 là proxy — một model F1 = 0.78 có thể tốt hơn model F1 = 0.82 nếu nó đúng ở các case quan trọng hơn (vd loại bệnh nguy hiểm hơn).
Industry-specific — chọn beta theo domain
- Information Retrieval / Search: F1 standard. Cân bằng giữa lấy đúng (precision) và lấy đủ (recall).
- Medical screening (cancer, sepsis): F2 hoặc cao hơn. Bỏ sót bệnh = hậu quả nghiêm trọng, false positive chỉ tốn thêm xét nghiệm xác minh.
- Email spam filter / SMS filter: F0.5. Spam lọt vào inbox phiền nhưng có thể xoá; email thật bị filter mất là thiệt hại lớn hơn.
- Fraud / payment risk: F1 hoặc F0.5 tuỳ chính sách. Block giao dịch thật quá nhiều mất khách; bỏ sót gian lận mất tiền.
- NLP — NER, intent classification, NLU: F1 micro hoặc macro tuỳ task. Báo cáo conference NLP gần như mặc định F1.
- Object detection: dùng mAP (mean Average Precision) — biến thể của AP cho multi-class với bounding box; F1 không trực tiếp áp dụng được.
Quy tắc: trước khi chốt metric, viết ra cost (hoặc impact) của FP và FN trong domain — chọn beta để phản ánh tỉ lệ cost đó.
Code Python — breast cancer và iris
Phần 1 — Breast cancer (binary): train Logistic Regression, compute F1, F2, F0.5.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score, fbeta_score, classification_report
X, y = load_breast_cancer(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
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
model = LogisticRegression(max_iter=2000).fit(X_train, y_train)
y_pred = model.predict(X_test)
f1 = f1_score(y_test, y_pred)
f2 = fbeta_score(y_test, y_pred, beta=2)
f05 = fbeta_score(y_test, y_pred, beta=0.5)
print(f"F1 = {f1:.4f}") # ~ 0.9846
print(f"F2 = {f2:.4f}") # ~ 0.9846 (precision ~= recall nên gần nhau)
print(f"F0.5 = {f05:.4f}") # ~ 0.9846
print(classification_report(y_test, y_pred, digits=4))
Phần 2 — Iris (multi-class): F1 macro, weighted, per-class.
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y
)
model = LogisticRegression(max_iter=2000).fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"F1 macro = {f1_score(y_test, y_pred, average='macro'):.4f}")
print(f"F1 weighted = {f1_score(y_test, y_pred, average='weighted'):.4f}")
print(f"F1 micro = {f1_score(y_test, y_pred, average='micro'):.4f}")
print(f"F1 per-class = {f1_score(y_test, y_pred, average=None)}")
# F1 macro ~ 0.9778
# F1 weighted ~ 0.9778
# F1 micro ~ 0.9778 (iris balanced -> macro = weighted = micro)
# F1 per-class ~ [1.0, 0.9474, 0.9863]
Iris balanced (50/50/50 sample mỗi class) nên 3 aggregate trùng nhau — đây là tình huống lý tưởng, không phản ánh được khi nào chọn macro vs weighted. Hiệu ứng khác nhau chỉ rõ với imbalanced data (mục 7).
Phần 3 — Quét threshold trên breast cancer maximize F1:
import numpy as np
from sklearn.metrics import precision_recall_curve
# Dùng validation split riêng để tránh leak vào test
X_tr, X_val, y_tr, y_val = train_test_split(
X_train, y_train, test_size=0.25, random_state=0, stratify=y_train
)
model = LogisticRegression(max_iter=2000).fit(X_tr, y_tr)
y_score_val = model.predict_proba(X_val)[:, 1]
precision, recall, thresholds = precision_recall_curve(y_val, y_score_val)
p, r = precision[:-1], recall[:-1]
f1_arr = np.where((p + r) > 0, 2 * p * r / (p + r), 0)
best_idx = f1_arr.argmax()
best_threshold = thresholds[best_idx]
print(f"Best threshold = {best_threshold:.4f}, val F1 = {f1_arr[best_idx]:.4f}")
# Áp dụng trên test set, so với threshold mặc định 0.5
y_score_test = model.predict_proba(X_test)[:, 1]
y_pred_default = (y_score_test >= 0.5).astype(int)
y_pred_tuned = (y_score_test >= best_threshold).astype(int)
print(f"Test F1 (threshold 0.5) = {f1_score(y_test, y_pred_default):.4f}")
print(f"Test F1 (threshold {best_threshold:.3f}) = {f1_score(y_test, y_pred_tuned):.4f}")
Phần 4 — Vẽ Precision-Recall curve (mô tả): dùng matplotlib.pyplot.plot(recall, precision), set xlabel("Recall"), ylabel("Precision"), thêm baseline horizontal ở mức positive_rate = y_test.mean() (no-skill classifier). Sklearn cũng có helper PrecisionRecallDisplay.from_estimator(model, X_test, y_test) để vẽ trực tiếp.
Bài tập thực hành
Bài 1 — Tính bằng tay. Cho \( P = 0.8, R = 0.6 \). Tính:
- F1.
- F2 (ưu tiên recall).
- F0.5 (ưu tiên precision).
Đáp án mong đợi: \( F_1 = 0.6857 \), \( F_2 = 0.6383 \), \( F_{0.5} = 0.7407 \). Verify bằng fbeta_score trên một cặp y_true, y_pred tự dựng cho ra precision/recall đúng như đề.
Bài 2 — Iris multi-class. Lặp lại Phần 2 mục 13 nhưng dùng RandomForestClassifier(n_estimators=20, random_state=0). In F1 macro, weighted, per-class. So sánh với Logistic Regression — model nào có F1 macro cao hơn?
Bài 3 — Threshold tối ưu trên breast cancer. Tái hiện Phần 3 mục 13. Yêu cầu thêm:
- In ra precision và recall tại threshold tối ưu.
- Vẽ PR curve và đánh dấu điểm threshold tối ưu (chấm tròn).
- So sánh với threshold tối ưu cho F2 (recall heavy) — threshold đó có khác không?
Bài 4 — Imbalanced data. Sinh dữ liệu bằng make_classification(n_samples=2000, weights=[0.95, 0.05], random_state=0) — class positive chỉ 5%. Train Logistic Regression. So sánh:
- Accuracy.
- F1 binary (default).
- F1 macro.
- F1 weighted.
- Average Precision.
Quan sát: accuracy có thể cao bất thường (vd 0.95+) chỉ vì luôn predict negative. F1 binary có thấp như mong đợi không? Nhận xét.
Bài 5 — F-beta cho spam filter giả lập. Cho ma trận nhầm lẫn dạng dict: {"TP": 80, "FP": 5, "FN": 20, "TN": 895}. Tính precision, recall, F1, F0.5, F2 từ counts. Argument bằng lập luận: spam filter này nên báo metric nào?
Bài tiếp theo
Bài 26: ROC Curve và AUC — metric threshold-independent cho binary classification: định nghĩa TPR/FPR, đường ROC, diện tích AUC, so sánh với PR-AUC khi data imbalanced, và cách dùng roc_curve / roc_auc_score trong sklearn.
Tài liệu tham khảo
- scikit-learn — Classification metrics
- scikit-learn — f1_score API
- scikit-learn — fbeta_score API
- scikit-learn — precision_recall_curve API
- scikit-learn — average_precision_score API
- scikit-learn — classification_report API
- scikit-learn — PrecisionRecallDisplay
- Wikipedia — F-score
- Wikipedia — Harmonic mean
- Wikipedia — Precision and recall
- George Forman (2003) — An Extensive Empirical Study of Feature Selection Metrics for Text Classification
