iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

一個資安場景特有的不對稱

機器學習的標準做法是看 F1 score,也就是 Precision 跟 Recall 的調和平均。這個指標的前提是兩種錯誤同等重要

在個資防護裡,這個前提不成立。

False Positive(誤判):把不是個資的東西當成個資遮掉。後果是文件可讀性下降、模型效果變差、使用者抱怨。可以修復。

False Negative(漏抓):真的個資沒被抓到。後果是明文個資直接送出行內邊界。不可修復 —— 資料一旦送出去就收不回來,而且你不會知道它發生過。

所以在這個場景,F1 是誤導性的指標。應該用的是加權 F-beta,beta > 1 表示更重視 Recall:

def f_beta(precision, recall, beta=2.0):
    if precision + recall == 0:
        return 0.0
    b2 = beta ** 2
    return (1 + b2) * precision * recall / (b2 * precision + recall)

beta 取多少?我的建議是 2 到 3。beta=2 表示 Recall 的重要性是 Precision 的兩倍。

更直接的做法是設 Recall 底線,在底線之上再優化 Precision

直接識別碼類 Recall ≥ 0.99;特種個資類 Recall ≥ 0.95;在此前提下 Precision 越高越好。

這種寫法比「F1 ≥ 0.9」清楚得多,而且它逼你面對「哪一類個資可以容忍漏抓」這個問題。答案通常是:直接識別碼一個都不能漏。

評測集怎麼建

評測集的品質決定了你的數字有沒有意義。三個原則:

一、絕對不能用正式資料。 全部用合成。這一點沒有例外。

二、要涵蓋失效情境,不只是理想情境。 如果你的評測集全是格式標準的樣本,你會得到一個漂亮但沒有意義的 Recall。

我的評測集分成四個子集,分別報告數字:

子集 內容 用途
Clean 格式標準的個資 基準線
Noisy 全形、分隔符、跨行、OCR 錯字 韌性
Adversarial 刻意規避的樣態 紅隊
Negative 格式像個資但不是 誤判率

只報總體數字是不誠實的。 因為 Clean 子集通常佔多數,總體 Recall 會被它拉高,掩蓋 Noisy 和 Adversarial 的真實表現。

三、Adversarial 子集要持續成長。 每次發現一個新的漏抓樣態,就加進去。這一集是整個評測體系裡最有價值的資產,因為它是實戰經驗的累積。

逐類型報告,不要只看總數

from collections import defaultdict

def evaluate(gold, pred, iou_threshold=0.5):
    """gold/pred: list of {type, start, end}"""
    stats = defaultdict(lambda: {"tp": 0, "fp": 0, "fn": 0})
    matched = set()

    for g in gold:
        hit = None
        for i, p in enumerate(pred):
            if i in matched or p["type"] != g["type"]:
                continue
            if iou(g, p) >= iou_threshold:
                hit = i
                break
        if hit is not None:
            matched.add(hit)
            stats[g["type"]]["tp"] += 1
        else:
            stats[g["type"]]["fn"] += 1

    for i, p in enumerate(pred):
        if i not in matched:
            stats[p["type"]]["fp"] += 1

    result = {}
    for t, s in stats.items():
        prec = s["tp"] / (s["tp"] + s["fp"]) if s["tp"] + s["fp"] else 0
        rec = s["tp"] / (s["tp"] + s["fn"]) if s["tp"] + s["fn"] else 0
        result[t] = {
            "precision": round(prec, 4),
            "recall": round(rec, 4),
            "f2": round(f_beta(prec, rec, 2.0), 4),
            **s,
        }
    return result

def iou(a, b):
    inter = max(0, min(a["end"], b["end"]) - max(a["start"], b["start"]))
    union = max(a["end"], b["end"]) - min(a["start"], b["start"])
    return inter / union if union else 0.0

逐類型報告會揭露一件事:整體 Recall 0.95 可能來自「身分證字號 0.99、健康資訊 0.62」的平均。 而後者正是後果最嚴重的那一類。

部分匹配的處理

一個常被忽略的問題:偵測到了但範圍不完整,算對還是錯?

原文:  身分證字號 A123456789
標準答案: [A123456789]
偵測結果: [123456789]     ← 少了首字母

從 ML 的角度,IoU = 0.9,算命中。從資安的角度,這是一次洩漏——首字母 A 洩漏出去了,而首字母帶有戶籍地資訊。

所以在資安評測裡,我對直接識別碼類採嚴格匹配:範圍必須完全覆蓋標準答案,才算 TP。範圍更大(多遮了)算 TP 但計入過度遮罩統計;範圍更小(少遮了)一律算 FN。

def strict_match(gold, pred):
    """預測範圍必須完全覆蓋標準答案"""
    return pred["start"] <= gold["start"] and pred["end"] >= gold["end"]

對敘述性的特種個資,這個標準要放寬,因為「哪裡開始哪裡結束」本身就有主觀性。改用 IoU 0.5 加上人工複核。

把評測變成 CI

評測不是上線前做一次的事。規則會改、模型會更新、新的文件類型會加進來,每一次改動都可能造成回歸。

# 概念性的 CI 步驟
- name: PII detection regression
  run: |
    python -m eval.run \
      --dataset eval/sets/ \
      --min-recall-direct 0.99 \
      --min-recall-sensitive 0.95 \
      --max-fp-rate 0.05 \
      --fail-on-regression

門檻沒過就擋合併。這件事在 D26 會擴展成完整的自動化紅隊回歸。

一個誠實的收尾

不管你的數字多好看,都要記住:評測集是你自己設計的,它衡量的是「你想到的失效情境」。 你沒想到的那些,數字裡不會出現。

所以 Recall 0.99 的正確解讀不是「99% 的個資會被抓到」,而是「在我設計的測試分布下,99% 會被抓到」。真實分布跟測試分布的差距,是這個架構永遠存在的殘餘風險。

這也是為什麼架構不能只靠偵測——後面的 Vault 隔離、輸出側檢查、fail-closed 設計,都是在承認「偵測一定會漏」的前提下建立的第二道防線。


明天進去識別化:三種手法,以及為什麼只有一種可逆。


關於作者

我是 Fngi,專注在 AI 資安、LLM 紅隊與 AI 治理框架落地。
IG:@aid3fend

有想討論的架構細節或不同意見,留言或私訊都歡迎。



上一篇
Day 7|Sensitive Data Protection 與中文情境的自訂 infoType
下一篇
Day 9|遮罩、概化、Token 化:只有一種可以回來
系列文
《30 天為金融法務部門打造 LLM 個資防護閘:去識別化、可控還原與紅隊驗證》14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言