⭐ Vision Engineer · Chặng 5Bài 4/6Bài học~45 phút

Precision, Recall, F1 và ma trận nhầm lẫn cho trạm kiểm tra lỗi

Ma trận nhầm lẫn với NG là lớp dương, precision, recall, F1 bằng tay và scikit-learn, dịch sang escape, overkill, ppm và bẫy accuracy.

Precision, Recall, F1 và ma trận nhầm lẫn cho trạm kiểm tra lỗi

Học xong bài này, bạn sẽ

  • Tính các chỉ số từ ma trận nhầm lẫn
  • Chuyển sang ngôn ngữ nhà máy
  • Không bị đánh lừa bởi accuracy

Nên học trước: Tiêu chí OK/NG, escape và overkill

Nội dung bài viết
  1. 1. Ma trận nhầm lẫn: NG là lớp dương
  2. 2. Các chỉ số và tên gọi trong nhà máy
  3. 3. Tính bằng scikit-learn và cái bẫy thứ tự nhãn
  4. 4. Nghịch lý accuracy khi dữ liệu mất cân bằng
  5. 5. Precision phụ thuộc tỉ lệ lỗi: phòng thí nghiệm khác dây chuyền
  6. 6. Từ điểm số đến quyết định: đường cong precision–recall
  7. 7. Ma trận nhầm lẫn nhiều lớp
  8. 8. Báo cáo cho nhà máy: số tuyệt đối và độ bất định
  9. 9. Sai lầm thường gặp
  10. 10. Bài tập
  11. Tóm tắt

"Mô hình đạt độ chính xác 99%" nghe rất ấn tượng, cho tới khi bạn biết dây chuyền chỉ có 1% hàng lỗi: một chương trình luôn trả lời "OK" cũng đạt 99%. Bài trước, "Tiêu chí OK/NG, escape và overkill", đã dạy ngôn ngữ của nhà máy: lọt lỗi, loại nhầm, ppm, chi phí mỗi ca. Bài này dạy ngôn ngữ của người làm học máy và thống kê: ma trận nhầm lẫn, precision, recall, F1, specificity, và quan trọng nhất là cách dịch qua lại giữa hai ngôn ngữ đó. Bạn sẽ cần kỹ năng này mỗi khi đọc báo cáo của nhà cung cấp phần mềm AI, so sánh hai phương án, hoặc viết tiêu chí nghiệm thu ở bài sau, "Đặc tả kỹ thuật và nghiệm thu FAT/SAT".

Mục tiêu cuối bàiLập được ma trận nhầm lẫn với NG là lớp dương; tính bằng tay và bằng scikit-learn accuracy, precision, recall, specificity, F1; dịch sang escape, overkill, ppm lọt, tỉ lệ hàng tốt trong thùng NG; giải thích bằng số vì sao accuracy đánh lừa khi dữ liệu mất cân bằng và vì sao precision đo trong phòng thí nghiệm khác hẳn precision ngoài dây chuyền; đọc được ma trận nhầm lẫn nhiều lớp.

1. Ma trận nhầm lẫn: NG là lớp dương

Trong thống kê, lớp dương (positive) là thứ ta đang đi tìm: bệnh trong xét nghiệm, thư rác trong bộ lọc, và hàng lỗi (NG) trong trạm kiểm tra. "Dương" không có nghĩa là "tốt". Quy ước này quyết định tên gọi của mọi chỉ số, nên hãy viết rõ trong mọi báo cáo: lớp dương = NG.

Trạm báo NG (dự đoán dương)Trạm báo OK (dự đoán âm)
Thật sự NGTP (true positive): bắt đúng lỗiFN (false negative): lọt lỗi, escape
Thật sự OKFP (false positive): loại nhầm, overkillTN (true negative): cho qua đúng

Cách nhớ: chữ thứ hai (P/N) là điều trạm nói, chữ thứ nhất (T/F) cho biết trạm nói đúng hay sai. FN = trạm nói "âm" (OK) và nói sai, tức là một sản phẩm lỗi đã đi qua.

2. Các chỉ số và tên gọi trong nhà máy

Chỉ sốCông thứcCâu hỏi nó trả lờiTên trong nhà máy
Accuracy(TP + TN) / tổngBao nhiêu phần quyết định là đúng?"Độ chính xác" (dễ gây hiểu lầm)
Recall, sensitivity, TPRTP / (TP + FN)Trong các sản phẩm NG thật, bắt được bao nhiêu?Tỉ lệ phát hiện; escape = 1 − recall
Specificity, TNRTN / (TN + FP)Trong các sản phẩm OK thật, cho qua đúng bao nhiêu?overkill = 1 − specificity = FP / (FP + TN)
Precision, PPVTP / (TP + FP)Trong các sản phẩm bị báo NG, bao nhiêu là NG thật?1 − precision = tỉ lệ hàng tốt trong thùng NG (khối lượng kiểm lại)
NPVTN / (TN + FN)Trong các sản phẩm được cho qua, bao nhiêu là OK thật?ppm lọt = (1 − NPV) × 10⁶ = FN / (FN + TN) × 10⁶
F12·P·R / (P + R) = 2TP / (2TP + FP + FN)Một con số gộp precision và recallHầu như không dùng khi báo cáo cho sản xuất

Escape và overkill ở đây được tính theo đúng định nghĩa của bài "Tiêu chí OK/NG, escape và overkill": escape theo số NG thật, overkill theo số OK thật. Có nhà máy tính overkill theo tổng sản lượng (FP / tổng); hai cách cho con số gần nhau khi tỉ lệ lỗi nhỏ, nhưng phải thống nhất trước khi ký.

Áp dụng cho bài thực hành của trang: một trạm kiểm 10.000 sản phẩm, có 50 lỗi thật, phát hiện được 45, báo nhầm 120 sản phẩm tốt. Bước đầu tiên luôn là điền đủ bốn ô: TP = 45, FN = 50 − 45 = 5, FP = 120, TN = (10.000 − 50) − 120 = 9.830.

def chi_so(TP, FN, FP, TN):
    """Mọi chỉ số từ 4 ô của ma trận nhầm lẫn (lớp dương = NG)."""
    tong = TP + FN + FP + TN
    return {
        "accuracy": (TP + TN) / tong,
        "recall (tỉ lệ bắt lỗi)": TP / (TP + FN),
        "specificity": TN / (TN + FP),
        "precision": TP / (TP + FP),
        "F1": 2 * TP / (2 * TP + FP + FN),
        "escape = FN/(TP+FN)": FN / (TP + FN),
        "overkill = FP/(FP+TN)": FP / (FP + TN),
        "overkill theo tổng sản lượng": FP / tong,
        "hàng tốt trong thùng NG": FP / (TP + FP),
    }


TP, FN, FP = 45, 5, 120
TN = 10_000 - (TP + FN) - FP
print(f"TP = {TP}, FN = {FN}, FP = {FP}, TN = {TN}")
for ten, gia_tri in chi_so(TP, FN, FP, TN).items():
    print(f"{ten:30s} {gia_tri:8.2%}")
print(f"ppm lọt theo hàng được giao: {FN / (FN + TN) * 1e6:.0f} ppm ({FN} lỗi trong {FN + TN} sản phẩm giao)")
TP = 45, FN = 5, FP = 120, TN = 9830
accuracy                         98.75%
recall (tỉ lệ bắt lỗi)           90.00%
specificity                      98.79%
precision                        27.27%
F1                               41.86%
escape = FN/(TP+FN)              10.00%
overkill = FP/(FP+TN)             1.21%
overkill theo tổng sản lượng      1.20%
hàng tốt trong thùng NG          72.73%
ppm lọt theo hàng được giao: 508 ppm (5 lỗi trong 9835 sản phẩm giao)

Đọc cho quản lý sản xuất, không cần nhắc tới F1:

  • Trạm bắt 45/50 lỗi (recall 90%), tức escape 10%: 5 sản phẩm lỗi đã đi tiếp, khoảng 508 ppm trên hàng được giao.
  • Trạm loại nhầm 120 sản phẩm tốt, overkill khoảng 1,2%. Trong thùng NG có 165 sản phẩm, 73% là hàng tốt (precision chỉ 27%): người kiểm lại phải xem 165 sản phẩm để tìm 45 lỗi thật.
  • Accuracy 98,75% nghe rất tốt nhưng không nói lên điều nào trong hai điều trên.

3. Tính bằng scikit-learn và cái bẫy thứ tự nhãn

Trong dự án thật bạn có hai danh sách: nhãn thật (do QC xác nhận) và kết quả của trạm, mỗi phần tử một sản phẩm. scikit-learn tính mọi thứ từ hai danh sách đó, với điều kiện bạn nói rõ lớp dương:

import numpy as np
from sklearn.metrics import (accuracy_score, classification_report, confusion_matrix,
                             f1_score, precision_score, recall_score)

# Dựng lại 10.000 sản phẩm của bài thực hành (thứ tự không quan trọng)
y_that = np.array(["NG"] * 50 + ["OK"] * 9950)
y_tram = np.array(["NG"] * 45 + ["OK"] * 5 + ["NG"] * 120 + ["OK"] * 9830)

cm = confusion_matrix(y_that, y_tram, labels=["NG", "OK"])     # hàng = thật, cột = dự đoán
print("Ma trận nhầm lẫn (hàng: thật NG, OK; cột: báo NG, OK):")
print(cm)
(TP, FN), (FP, TN) = cm
print(f"TP={TP} FN={FN} FP={FP} TN={TN}")
print(f"accuracy    {accuracy_score(y_that, y_tram):.4f}")
print(f"precision   {precision_score(y_that, y_tram, pos_label='NG'):.4f}")
print(f"recall      {recall_score(y_that, y_tram, pos_label='NG'):.4f}")
print(f"specificity {recall_score(y_that, y_tram, pos_label='OK'):.4f}   (recall của lớp OK)")
print(f"F1          {f1_score(y_that, y_tram, pos_label='NG'):.4f}")
print(classification_report(y_that, y_tram, labels=["NG", "OK"], digits=4))
Ma trận nhầm lẫn (hàng: thật NG, OK; cột: báo NG, OK):
[[  45    5]
 [ 120 9830]]
TP=45 FN=5 FP=120 TN=9830
accuracy    0.9875
precision   0.2727
recall      0.9000
specificity 0.9879   (recall của lớp OK)
F1          0.4186
              precision    recall  f1-score   support

          NG     0.2727    0.9000    0.4186        50
          OK     0.9995    0.9879    0.9937      9950

    accuracy                         0.9875     10000
   macro avg     0.6361    0.9440    0.7061     10000
weighted avg     0.9959    0.9875    0.9908     10000

Kết quả khớp với phần tính tay. Lưu ý classification_report in cả hai lớp: dòng NG là precision/recall với NG là lớp dương; dòng OK cho specificity (recall của OK) và NPV (precision của OK). Hai dòng macro avg và weighted avg sẽ được giải thích ở mục 7.

Bẫy thứ tự nhãn khi dùng 0/1Nếu nhãn là số 0 (OK) và 1 (NG) và bạn không truyền labels, scikit-learn sắp nhãn tăng dần, nên ma trận có dạng [[TN, FP], [FN, TP]], ngược với bảng ở mục 1. Còn precision_score mặc định coi nhãn 1 là lớp dương: nếu nhóm của bạn lỡ quy ước 1 = OK, mọi con số "precision, recall" trong báo cáo đều là của lớp OK, thường rất cao và rất sai lệch. Luôn truyền labels và pos_label một cách tường minh.
from sklearn.metrics import confusion_matrix, precision_score

y_that = [1, 1, 0, 0, 0, 1, 0, 0]          # 1 = NG, 0 = OK
y_tram = [1, 0, 0, 1, 0, 1, 0, 0]
print(confusion_matrix(y_that, y_tram))     # mặc định: thứ tự [0, 1]
tn, fp, fn, tp = confusion_matrix(y_that, y_tram).ravel()
print(f"tn={tn} fp={fp} fn={fn} tp={tp}")
print(confusion_matrix(y_that, y_tram, labels=[1, 0]))   # thứ tự như bảng mục 1: NG trước
print("precision NG:", round(precision_score(y_that, y_tram, pos_label=1), 3),
      "| precision nếu lỡ coi OK là lớp dương:", round(precision_score(y_that, y_tram, pos_label=0), 3))
[[4 1]
 [1 2]]
tn=4 fp=1 fn=1 tp=2
[[2 1]
 [1 4]]
precision NG: 0.667 | precision nếu lỡ coi OK là lớp dương: 0.8

4. Nghịch lý accuracy khi dữ liệu mất cân bằng

Dây chuyền thật có rất ít lỗi: 0,1–2% là phổ biến. Khi lớp NG hiếm, accuracy gần như chỉ phản ánh khả năng cho qua hàng OK. So sánh ba "trạm" trên 20.000 sản phẩm có 0,5% lỗi:

import numpy as np
from sklearn.metrics import accuracy_score, balanced_accuracy_score, matthews_corrcoef, recall_score

N, N_NG = 20_000, 100
y_that = np.array([1] * N_NG + [0] * (N - N_NG))          # 1 = NG


def tram(bat_duoc, loai_nham):
    """Kết quả một trạm: bắt `bat_duoc` NG đầu tiên, loại nhầm `loai_nham` OK đầu tiên."""
    y = np.zeros(N, int)
    y[:bat_duoc] = 1
    y[N_NG:N_NG + loai_nham] = 1
    return y


cac_tram = {"Luôn báo OK": tram(0, 0),
            "Trạm A (nhạy)": tram(95, 398),
            "Trạm B (dè dặt)": tram(60, 20)}
print("trạm             | accuracy | recall | overkill | balanced acc | MCC")
for ten, y in cac_tram.items():
    ovk = ((y == 1) & (y_that == 0)).sum() / (y_that == 0).sum()
    print(f"{ten:16s} | {accuracy_score(y_that, y):8.2%} | {recall_score(y_that, y, zero_division=0):6.0%} |"
          f" {ovk:8.2%} | {balanced_accuracy_score(y_that, y):12.3f} | {matthews_corrcoef(y_that, y):5.3f}")
trạm             | accuracy | recall | overkill | balanced acc | MCC
Luôn báo OK      |   99.50% |     0% |    0.00% |        0.500 | 0.000
Trạm A (nhạy)    |   97.98% |    95% |    2.00% |        0.965 | 0.423
Trạm B (dè dặt)  |   99.70% |    60% |    0.10% |        0.799 | 0.669
  • Chương trình "luôn báo OK" đạt accuracy 99,5% mà không bắt được lỗi nào.
  • Trạm B có accuracy cao hơn trạm A nhưng để lọt 40% lỗi; trạm A lọt 5% và đổi lại loại nhầm khoảng 2% hàng tốt. Chọn theo accuracy là chọn sai trong hầu hết nhà máy.
  • Balanced accuracy = (recall + specificity) / 2 và MCC (hệ số tương quan Matthews, từ −1 đến 1, bằng 0 với trạm đoán bừa hoặc luôn báo một lớp) không bị đánh lừa bởi mất cân bằng: cả hai cho chương trình "luôn báo OK" điểm của một trạm vô dụng (0,5 và 0).
  • Nhưng để ý: balanced accuracy xếp trạm A trên, còn MCC xếp trạm B trên, vì MCC tính cả precision (trạm A loại nhầm 398 sản phẩm tốt để bắt 95 lỗi). Không có một con số "đúng" cho mọi nhà máy: mỗi chỉ số gộp ngầm định một tỉ lệ chi phí giữa escape và overkill. Chỉ chi phí thật (bài trước) mới quyết định được A hay B; báo cáo cho sản xuất vẫn nên là cặp escape/overkill kèm số lượng tuyệt đối.

5. Precision phụ thuộc tỉ lệ lỗi: phòng thí nghiệm khác dây chuyền

Recall và specificity là tính chất của trạm: chúng chỉ phụ thuộc trạm đối xử với một sản phẩm NG (hoặc OK) ra sao. Precision thì phụ thuộc thêm tỉ lệ lỗi của đầu vào. Bộ ảnh thử của nhà cung cấp thường có số NG ngang số OK, nên precision trong báo cáo rất đẹp; đưa ra dây chuyền có 0,3% lỗi thì precision sụp đổ dù trạm không thay đổi gì:

RECALL, OVERKILL = 0.98, 0.01            # tính chất của trạm, đo được trên bộ thử
print("tỉ lệ lỗi | trên 10.000 sp: NG bắt được | OK bị loại nhầm | precision | hàng tốt trong thùng NG")
for ti_le in (0.5, 0.10, 0.02, 0.005, 0.001):
    ng = 10_000 * ti_le
    tp, fp = RECALL * ng, OVERKILL * (10_000 - ng)
    print(f"{ti_le:9.1%} | {tp:27.0f} | {fp:15.0f} | {tp / (tp + fp):9.1%} | {fp / (tp + fp):10.1%}")
tỉ lệ lỗi | trên 10.000 sp: NG bắt được | OK bị loại nhầm | precision | hàng tốt trong thùng NG
    50.0% |                        4900 |              50 |     99.0% |       1.0%
    10.0% |                         980 |              90 |     91.6% |       8.4%
     2.0% |                         196 |              98 |     66.7% |      33.3%
     0.5% |                          49 |             100 |     33.0% |      67.0%
     0.1% |                          10 |             100 |      8.9% |      91.1%

Cùng một trạm (bắt 98% lỗi, loại nhầm 1% hàng tốt): trên bộ thử 50/50 precision 99%; ở dây chuyền 0,1% lỗi, cứ 11 sản phẩm bị loại thì 10 là hàng tốt. Hai hệ quả thực tế:

  • Khi đọc báo cáo "precision 99%", hỏi ngay: bộ thử có tỉ lệ NG bao nhiêu? Hãy yêu cầu recall và overkill (hoặc specificity), rồi tự tính precision theo tỉ lệ lỗi thật của dây chuyền.
  • Khối lượng kiểm lại thùng NG tính theo precision ở tỉ lệ lỗi thật. Một trạm "tốt" trên giấy vẫn có thể khiến người kiểm lại chán nản và bỏ qua thùng NG, như bài trước đã cảnh báo.

6. Từ điểm số đến quyết định: đường cong precision–recall

Mô hình học máy (và nhiều công cụ rule-based) không trả lời thẳng OK/NG mà cho một điểm số (score), rồi ngưỡng biến điểm số thành quyết định. Mỗi ngưỡng cho một ma trận nhầm lẫn khác nhau; đường cong precision–recall vẽ tất cả các cặp đó. Bài trước đã chọn ngưỡng theo chi phí; ở đây ta chọn theo một ràng buộc thường gặp trong đặc tả: "recall ≥ 99% trên tập kiểm định", rồi xem cái giá phải trả. Dữ liệu là điểm số mô phỏng:

import numpy as np
from sklearn.metrics import average_precision_score, precision_recall_curve, roc_auc_score

rng = np.random.default_rng(3)


def tao_bo(n_ok, n_ng):
    """Điểm số mô phỏng: hàng OK quanh 0,25, hàng NG quanh 0,70 (1 = chắc chắn NG)."""
    diem = np.concatenate([rng.normal(0.25, 0.10, n_ok), rng.normal(0.70, 0.12, n_ng)])
    return np.r_[np.zeros(n_ok, int), np.ones(n_ng, int)], np.clip(diem, 0, 1)


y_val, s_val = tao_bo(5000, 200)              # tập kiểm định: dùng để CHỌN ngưỡng
y_test, s_test = tao_bo(5000, 200)            # tập thử: chỉ dùng để BÁO CÁO
print(f"AP (diện tích dưới đường PR) = {average_precision_score(y_val, s_val):.3f},"
      f" ROC AUC = {roc_auc_score(y_val, s_val):.4f}")

p, r, nguong = precision_recall_curve(y_val, s_val)
ok = np.where(r[:-1] >= 0.99)[0]              # các ngưỡng còn giữ recall ≥ 99%
T = nguong[ok[-1]]                            # ngưỡng cao nhất thỏa ràng buộc → ít loại nhầm nhất
print(f"Ngưỡng chọn trên tập kiểm định: {T:.3f}")
for ten, y, s in (("kiểm định", y_val, s_val), ("thử", y_test, s_test)):
    bao_ng = s >= T
    tp, fn = (bao_ng & (y == 1)).sum(), (~bao_ng & (y == 1)).sum()
    fp = (bao_ng & (y == 0)).sum()
    print(f"  tập {ten:9s}: bắt {tp}/{tp + fn} NG (recall {tp / (tp + fn):.1%}),"
          f" loại nhầm {fp}/5000 OK (overkill {fp / 5000:.2%}), precision {tp / (tp + fp):.1%}")
AP (diện tích dưới đường PR) = 0.979, ROC AUC = 0.9983
Ngưỡng chọn trên tập kiểm định: 0.443
  tập kiểm định: bắt 198/200 NG (recall 99.0%), loại nhầm 138/5000 OK (overkill 2.76%), precision 58.9%
  tập thử      : bắt 197/200 NG (recall 98.5%), loại nhầm 137/5000 OK (overkill 2.74%), precision 59.0%
  • AP (average precision) và ROC AUC tóm tắt cả đường cong thành một con số, tiện để so sánh hai mô hình. Nhưng dây chuyền chạy ở một ngưỡng; thứ cần ghi vào đặc tả là recall và overkill tại ngưỡng đó.
  • Ràng buộc recall ≥ 99% có giá: overkill khoảng 2,7%, precision chỉ khoảng 59% dù AUC gần 1. Ngưỡng chọn trên tập kiểm định thường cho kết quả hơi khác trên tập thử độc lập: ở đây recall trên tập thử chỉ còn 98,5%, dưới ràng buộc. Báo cáo con số của tập thử, không phải con số của tập đã dùng để chọn ngưỡng. Với 200 NG, mỗi NG bị lọt là 0,5% recall: số lượng mẫu NG giới hạn độ chính xác của mọi con số (mục 8).

7. Ma trận nhầm lẫn nhiều lớp

Khi trạm phân loại cả loại lỗi (xước, móp, bẩn), ma trận nhầm lẫn có một hàng và một cột cho mỗi lớp. Đường chéo là phân loại đúng; ô ngoài đường chéo cho biết lớp nào bị nhầm sang lớp nào:

import numpy as np
from sklearn.metrics import classification_report, confusion_matrix

LOP = ["OK", "xuoc", "mop", "ban"]
# (thật, dự đoán, số lượng): kết quả trên bộ thử 1.000 ảnh
DEM = [("OK", "OK", 880), ("OK", "ban", 15), ("OK", "xuoc", 5),
       ("xuoc", "xuoc", 34), ("xuoc", "ban", 4), ("xuoc", "OK", 2),
       ("mop", "mop", 25), ("mop", "xuoc", 3), ("mop", "OK", 2),
       ("ban", "ban", 27), ("ban", "xuoc", 3)]
y_that = np.array([t for t, d, n in DEM for _ in range(n)])
y_du_doan = np.array([d for t, d, n in DEM for _ in range(n)])

cm = confusion_matrix(y_that, y_du_doan, labels=LOP)
print("thật \\ dự đoán " + "".join(f"{l:>6s}" for l in LOP))
for ten, hang in zip(LOP, cm):
    print(f"{ten:15s}" + "".join(f"{v:6d}" for v in hang))
print(classification_report(y_that, y_du_doan, labels=LOP, digits=3, zero_division=0))

# Gộp về câu hỏi của nhà máy: OK hay NG (mọi loại lỗi đều là NG)
that_ng, bao_ng = y_that != "OK", y_du_doan != "OK"
tp, fn = (that_ng & bao_ng).sum(), (that_ng & ~bao_ng).sum()
fp, tn = (~that_ng & bao_ng).sum(), (~that_ng & ~bao_ng).sum()
print(f"Gộp OK/NG: TP={tp} FN={fn} FP={fp} TN={tn} → escape {fn / (tp + fn):.1%}, overkill {fp / (fp + tn):.1%}")
print(f"Nhầm loại lỗi nhưng vẫn báo NG: {(that_ng & bao_ng & (y_that != y_du_doan)).sum()} ảnh")
thật \ dự đoán     OK  xuoc   mop   ban
OK                880     5     0    15
xuoc                2    34     0     4
mop                 2     3    25     0
ban                 0     3     0    27
              precision    recall  f1-score   support

          OK      0.995     0.978     0.987       900
        xuoc      0.756     0.850     0.800        40
         mop      1.000     0.833     0.909        30
         ban      0.587     0.900     0.711        30

    accuracy                          0.966      1000
   macro avg      0.834     0.890     0.852      1000
weighted avg      0.974     0.966     0.968      1000

Gộp OK/NG: TP=96 FN=4 FP=20 TN=880 → escape 4.0%, overkill 2.2%
Nhầm loại lỗi nhưng vẫn báo NG: 10 ảnh
  • Đọc theo hàng: 2 vết xước và 2 vết móp bị báo OK, đó là 4 escape thật sự. Đọc theo cột: 15 ảnh OK bị báo "bẩn", nguồn overkill lớn nhất; nên xem lại các ảnh này (bụi, vết dầu, phản xạ?).
  • 10 ảnh bị nhầm loại lỗi (xước thành bẩn…) nhưng vẫn bị loại. Với quyết định OK/NG chúng vô hại; chúng chỉ quan trọng nếu loại lỗi dùng để truy nguyên nhân hoặc chọn thùng tái chế.
  • macro avg lấy trung bình các lớp như nhau; weighted avg lấy trung bình theo số mẫu mỗi lớp nên bị lớp OK (900 mẫu) chi phối, luôn trông đẹp hơn. Với kiểm tra lỗi, hãy xem từng lớp và con số gộp OK/NG, đừng chỉ nhìn weighted avg.

8. Báo cáo cho nhà máy: số tuyệt đối và độ bất định

"Recall 90%" tính trên 50 mẫu NG khác hẳn "recall 90%" tính trên 5.000 mẫu. Hãy luôn ghi phân số (45/50) và, khi có thể, khoảng tin cậy. Khoảng Wilson là một cách tính đơn giản và ổn định cho tỉ lệ:

import math


def wilson(k, n, z=1.96):
    """Khoảng tin cậy Wilson (mặc định 95%) cho tỉ lệ k/n."""
    p = k / n
    tam = (p + z * z / (2 * n)) / (1 + z * z / n)
    nua = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
    return tam - nua, tam + nua


for ten, k, n in (("recall bài thực hành", 45, 50), ("cùng recall, 10 lần mẫu", 450, 500),
                  ("overkill bài thực hành", 120, 9950)):
    lo, hi = wilson(k, n)
    print(f"{ten:24s}: {k}/{n} = {k / n:6.2%}, khoảng tin cậy 95%: {lo:6.2%} – {hi:6.2%}")
recall bài thực hành    : 45/50 = 90.00%, khoảng tin cậy 95%: 78.64% – 95.65%
cùng recall, 10 lần mẫu : 450/500 = 90.00%, khoảng tin cậy 95%: 87.06% – 92.33%
overkill bài thực hành  : 120/9950 =  1.21%, khoảng tin cậy 95%:  1.01% –  1.44%

Với 50 mẫu NG, recall thật có thể chỉ khoảng 79% hoặc cao tới khoảng 96%: hai trạm có recall 88% và 92% trên 50 mẫu là không phân biệt được. Khi kết quả là 0 lần lọt, dùng quy tắc số 3 đã học ở bài trước (escape < 3/n với 95% tin cậy). Mẫu báo cáo ngắn gọn cho một đợt thử:

Bộ thử: 10.000 sản phẩm (50 NG đã xác nhận bởi QC, 9.950 OK), ngưỡng phiên bản v1.3, ngày …
Lớp dương: NG
Bắt lỗi:  45/50 (recall 90,0%; KTC 95%: 78,6–95,7%)  → escape 5/50 = 10,0%
Loại nhầm: 120/9.950 (overkill 1,21%)                → thùng NG: 165 sp, trong đó 120 sp tốt (73%)
Lọt ra hàng giao: 5/9.835 ≈ 508 ppm
Ghi chú: 5 sản phẩm lọt đều là lỗi loại … nằm ở vùng …; ảnh đính kèm.

9. Sai lầm thường gặp

  • Không nói lớp dương là gì, hoặc dùng mặc định của thư viện với quy ước 1 = OK (mục 3).
  • Chỉ báo accuracy hoặc chỉ báo F1. Sản xuất cần escape và overkill (và số lượng tuyệt đối).
  • Mang precision từ bộ thử cân bằng ra dây chuyền (mục 5).
  • Chọn ngưỡng và báo cáo trên cùng một tập dữ liệu: kết quả lạc quan hơn thực tế (mục 6).
  • Lẫn đơn vị đếm: đếm theo ảnh, theo vùng ROI, theo lỗi hay theo sản phẩm? Một sản phẩm chụp bởi 4 camera là NG nếu bất kỳ ảnh nào báo NG, nên overkill theo sản phẩm lớn hơn overkill theo ảnh (bài tập 3). Ghi rõ đơn vị đếm trong đặc tả.
  • Nhãn thật không đáng tin: nếu QC dán nhãn sai 2% bộ thử, mọi chỉ số đều sai theo. Mẫu sát giới hạn phải được kiểm và ký bởi người có thẩm quyền.

10. Bài tập

Bài 1 (thực hành của bài). Một trạm kiểm 10.000 sản phẩm, 50 lỗi thật, phát hiện 45, báo nhầm 120. Tính recall, precision, tỉ lệ escape và overkill (theo số OK thật và theo tổng sản lượng). Viết hai câu báo cáo cho quản lý sản xuất.

Xem đáp án
TP, FN, FP = 45, 50 - 45, 120
TN = 10_000 - 50 - FP
print(f"recall    = {TP}/{TP + FN} = {TP / (TP + FN):.1%}")
print(f"precision = {TP}/{TP + FP} = {TP / (TP + FP):.1%}")
print(f"escape    = {FN}/{TP + FN} = {FN / (TP + FN):.1%}")
print(f"overkill  = {FP}/{FP + TN} = {FP / (FP + TN):.2%} (theo OK thật), {FP / 10_000:.2%} (theo tổng)")
recall    = 45/50 = 90.0%
precision = 45/165 = 27.3%
escape    = 5/50 = 10.0%
overkill  = 120/9950 = 1.21% (theo OK thật), 1.20% (theo tổng)

Báo cáo: "Trạm bắt được 45/50 sản phẩm lỗi; 5 sản phẩm lỗi đã lọt (escape 10%). Trạm loại nhầm 120 sản phẩm tốt (khoảng 1,2%), nên trong 165 sản phẩm ở thùng NG chỉ 45 là lỗi thật; cần người kiểm lại thùng NG cho tới khi giảm được overkill."

Bài 2. Nhà cung cấp AI báo: trên bộ thử 500 OK và 500 NG, mô hình bắt 490 NG và loại nhầm 10 OK, "precision 98%, recall 98%". Dây chuyền của bạn có 0,3% lỗi và chạy 30.000 sản phẩm mỗi ca. Mỗi ca dự kiến có bao nhiêu sản phẩm trong thùng NG, bao nhiêu là hàng tốt, và precision thực tế bao nhiêu?

Xem đáp án
recall, overkill = 490 / 500, 10 / 500        # tính chất của mô hình, không phụ thuộc tỉ lệ lỗi
sp_ca, ti_le = 30_000, 0.003
ng = sp_ca * ti_le
tp, fp = recall * ng, overkill * (sp_ca - ng)
print(f"recall {recall:.0%}, overkill {overkill:.0%}")
print(f"Mỗi ca: {ng:.0f} NG thật, thùng NG có {tp + fp:.0f} sp ({tp:.0f} lỗi thật + {fp:.0f} hàng tốt)")
print(f"Precision thực tế: {tp / (tp + fp):.1%}")
recall 98%, overkill 2%
Mỗi ca: 90 NG thật, thùng NG có 686 sp (88 lỗi thật + 598 hàng tốt)
Precision thực tế: 12.8%

Overkill 2% trên 29.910 sản phẩm tốt là khoảng 600 sản phẩm tốt bị loại mỗi ca, gần gấp 7 lần số lỗi thật; precision thực tế chỉ khoảng 13%. Precision 98% trong báo cáo chỉ đúng với bộ thử 50/50. Cần đàm phán lại theo overkill (ví dụ ≤ 0,2%) chứ không theo precision.

Bài 3. Một sản phẩm được 4 camera chụp 4 mặt; sản phẩm là NG nếu bất kỳ ảnh nào báo NG. Mỗi camera có overkill 0,5% trên ảnh của sản phẩm tốt (giả sử độc lập). Overkill theo sản phẩm là bao nhiêu? Một lỗi chỉ nhìn thấy ở một mặt, camera mặt đó có recall 97%: recall theo sản phẩm là bao nhiêu?

Xem đáp án

Overkill theo sản phẩm = 1 − (1 − 0,005)⁴ ≈ 1,99%, gần gấp 4 lần overkill mỗi camera. Recall theo sản phẩm với lỗi chỉ thấy ở một mặt vẫn là 97% (cộng thêm một chút nếu camera khác loại nhầm đúng sản phẩm đó do overkill ngẫu nhiên, khoảng 0,03 × 0,015 ≈ 0,05%, không đáng kể). Bài học: ghép nhiều camera làm overkill cộng dồn còn recall không tăng; đặc tả phải nói rõ con số tính theo ảnh hay theo sản phẩm.

Tóm tắt

  • Lớp dương = NG. Bốn ô: TP (bắt đúng), FN (escape), FP (overkill), TN (cho qua đúng); luôn điền đủ bốn ô trước khi tính.
  • Recall = TP/(TP+FN), escape = 1 − recall; specificity = TN/(TN+FP), overkill = 1 − specificity; precision = TP/(TP+FP), 1 − precision = phần hàng tốt trong thùng NG; ppm lọt = FN/(FN+TN) × 10⁶.
  • Dùng scikit-learn với labels và pos_label tường minh; nhãn 0/1 mặc định cho ma trận [[TN, FP], [FN, TP]].
  • Accuracy vô nghĩa khi lỗi hiếm; precision phụ thuộc tỉ lệ lỗi đầu vào, còn recall và overkill thì không. Balanced accuracy, MCC tốt hơn để so sánh mô hình.
  • Chọn ngưỡng trên tập kiểm định, báo cáo trên tập thử; ghi số tuyệt đối, khoảng tin cậy và đơn vị đếm (ảnh hay sản phẩm). Ma trận nhiều lớp: xem từng hàng/cột rồi gộp về OK/NG.

Bài tiếp theo: Đặc tả kỹ thuật và nghiệm thu FAT/SAT, nơi các chỉ số trong bài này trở thành tiêu chí nghiệm thu có chữ ký của hai bên.

Bài tập thực hành

  1. Một trạm kiểm 10.000 sản phẩm, 50 lỗi thật, phát hiện 45, báo nhầm 120: tính recall, precision, tỉ lệ escape và overkill.

Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.