跳到主要內容

LV.14

試煉場:評估

漏掉一個病人姓名,比多遮一個字嚴重

BOSS
漏網之 PHI
時間
約 60 分鐘
建議先過
LV.13

先看 Boss:漏網之 PHI

第一次看到評估報告時,你可能會被一個漂亮數字吸引。但在去識別化這件事上,真正的問題永遠是:漏了什麼? Boss 是藏在英文句子裡的病人姓名,例如「Pt 王小明 reported chest tightness」,它躲在中英夾雜的縫隙中,最容易漏網。

指標

作者的 synth/evaluate.py 會計算以下指標。先看定義:

span recall(片段召回率,主指標):標準答案中每一個 PHI 片段,若其原文字串出現在模型預測的清單中,就算抓到。為什麼用「字串出現在清單中」就算?因為遮蔽程式會把清單中的字串在整份病歷裡全部取代,所以只要字串在清單裡,同一字串的所有出現位置都會被遮到。

類比:它就是這個任務的 sensitivity(敏感度)。漏掉一個 PHI = 偽陰性。

precision(unique):模型預測的不重複字串中,有多少是真的 PHI。類似陽性預測值,用來確認模型沒有亂標一大堆。

F1:recall 與 precision 的調和平均,只當參考;本任務更在意 recall。

clean_note_rate:整份病歷的 PHI 全部抓到的比例,可理解為 note 層級的「全對率」。它比片段層級嚴格得多:一份病歷 20 個 PHI 漏 1 個,recall 是 95%,但這份病歷不算 clean。

valid_json_rate:輸出能被解析成合法 JSON 的比例。格式錯誤,遮蔽程式無法使用,等同於全部漏掉。

recall_by_type:各類型(NAME、DOCTOR、MRN、IDNO、DATE、PHONE、ADDRESS、HOSPITAL)各自的 recall,用來找出弱點在哪裡。

bootstrap 95% CI:測試集只有一批病歷,recall 會因為抽到哪些病歷而有變動。bootstrap 的做法是:以「病歷」為單位,有放回地重抽樣 2000 次,每次重算 recall,取第 2.5 與 97.5 百分位當區間。報告 recall 時宜附上區間;樣本小時區間會很寬。

重點程式如下(節錄 evaluate.py 的 score):

caught = [s["text"] in ptexts for s in g["phi"]]
...
for _ in range(2000):
    sample = [per_note[rng.randrange(len(per_note))] for _ in per_note]
    boots.append(sum(a for a, _ in sample) / max(1, sum(b for _, b in sample)))
boots.sort()
ci = [boots[49], boots[1949]]

逐行解釋:

  • caught 對每個標準答案片段,判斷其原文是否在預測字串集合 ptexts 內。
  • per_note 記錄每份病歷「抓到幾個 / 共幾個」,bootstrap 以整份病歷為抽樣單位。
  • 迴圈 2000 次:每次從病歷中有放回抽出與原本一樣多份,重新計算總 recall。
  • 排序後取第 50 與第 1950 個值,約為 2.5% 與 97.5% 位置,即 95% 區間。

三個對手:base、LoRA、regex

比較要公平:同一份 test 集,三種做法:

  1. base(prompt-only):不微調的同一模型,只用 system prompt 說明任務。這是檢驗「微調是否值得」的基準線。
  2. LoRA:你在 LV13 訓練的 adapter。
  3. regex(規則式):用正規表示式寫的規則,抓電話、身分證、病歷號、日期、醫院名等有格式的字串。

規則式不是稻草人。文獻中有研究在胸部 CT 報告的匿名化比較了離線的 LLaMa-2 與規則式,規則式在日期、病歷號等有固定格式的項目表現較好(Eur Radiol,PMID 39480533)。所以 regex 要認真寫,才算公平的對手。

冒煙測試的初步觀察(非正式,n = 29 份)

下面的數字來自作者的冒煙測試(29 份病歷,prompt-only 在關閉思考模式下)。樣本小,而且這份測試集與冒煙訓練集共用骨架(這只影響 LoRA 的數字,所以 LoRA 不列入),僅用來示範怎麼讀報告,不是正式結果:

方法span recall(95% CI)valid JSON rate
base(prompt-only,關閉 thinking)0.37(0.22–0.52)0.45
regex0.37(0.30–0.45)1.00

怎麼讀這張表:

  • 兩者 point estimate 相近,但 95% CI 有重疊。在這 29 份上未顯示明確差異,不能寫成兩者一樣。
  • base 若沒有關閉 thinking,作者觀察到 valid JSON 為 0%(思考過程用光 500 個 token)。可見 prompt 與 chat template 的設定本身就會大幅影響結果。
  • regex 的 recall_by_type:HOSPITAL 與 PHONE 為 1.00,MRN 約 0.62,DATE 約 0.58,IDNO 0.40,而 NAME、DOCTOR、ADDRESS 皆為 0。regex 抓不到人名,因為人名沒有格式可循。
  • 冒煙測試中 LoRA 的 0.995 因資料洩漏(測試集與訓練集共用骨架)不引用。下一節的正式結果改以骨架切分的 test 集評估,LoRA 的 span recall 為 0.978,兩者不能直接比較。

正式實驗結果(作者在 M5 32GB 的實測)

正式實驗使用公開資料集 v1.0(train 700 / valid 100 / test 200 份,以骨架切分),test 集共 1,349 個 PHI span。span recall 的 95% CI 以「病歷」為單位 bootstrap 2000 次。模型為 Gemma 4 E2B,LoRA 設定見 LV13。

方法span recall(95% CI)precisionclean notevalid JSON
regex0.548(0.530–0.564)0.9170.0%—
Gemma 4 E2B prompt-only0.439(0.370–0.501)0.95917.5%46%
Gemma 4 E2B + LoRA0.978(0.956–0.993)0.99593.0%99%
prompt-only + regex(hybrid)0.750(0.714–0.783)0.91635.5%—
LoRA + regex(hybrid)0.994(0.985–1.000)0.94798.5%—

各類型 recall 的重點:

  • regex:NAME、DOCTOR、ADDRESS 皆為 0;DATE 0.98、IDNO 0.99;MRN、PHONE、HOSPITAL 為 1.0。
  • LoRA:各類型 recall 介於 0.95–0.99。
  • hybrid(LoRA + regex):DATE、IDNO、MRN、PHONE、HOSPITAL 補到 1.0,代價是 precision 由 0.995 降到 0.947。

怎麼解讀這張表:

  • LoRA(0.978,95% CI 0.956–0.993)與 prompt-only(0.439,95% CI 0.370–0.501)的 CI 沒有重疊,在這份 test 集上 LoRA 的 span recall 明顯較高。這是同一份 test 集、同一組設定下的觀察,不代表其他資料也會有同樣差距。
  • regex 的 HOSPITAL 為 1.0,是因為合成資料中假院名的後綴固定,屬於資料的偏差;真實院名的寫法更多樣,不宜據此推論 regex 在真實病歷也能抓到院名。
  • 合成資料的分布單純,分數偏高,不代表真實病歷的表現。高 recall 也不等於零洩漏:LoRA 的 clean note 為 93.0%,表示仍有約 7% 的病歷至少漏掉一個 PHI。
  • 與冒煙測試對照:冒煙的 LoRA 0.995 受資料洩漏影響,不引用;正式的 0.978 是以骨架切分後的結果,才是可以引用的數字。
  • 評估推論時間(200 份,batch 16):base 294 秒、LoRA 234 秒。

這張圖給你的設計啟發是 hybrid:電話、身分證這類有格式的交給規則(又快又穩),人名、地址這類語意性強的交給模型,兩者聯集後再遮蔽。

統計措辭

寫報告時:

  • recall 一律附 95% CI。
  • 差異的 CI 或區間有重疊時,寫「在本測試集上未顯示明確差異」,不寫「沒有差別」或「效果一樣」。要宣稱「相當」需要 non-inferiority 設計與預先設定的 margin。
  • 單次實驗、單一測試集、單一隨機種子,不要寫「證明」。
  • 不要把 fine-tune 前後的 point estimate 差距寫成「進步了 X%」,除非區間不重疊或有適當的檢定。

誠實揭露:合成資料的分數偏高

本站所有評估都在合成資料上進行。合成病歷由模板與假名單填成,格式規律、PHI 位置明確,比真實病歷容易。因此:

  • 分數只代表在合成資料上的表現。
  • 不能外推到真實病歷。真實病歷有縮寫、錯字、手寫轉錄、不規則格式等。
  • 文獻中的高分例子也是如此。Bordeaux 急診病歷研究(JMIR AI,PMID 40605780)報告 Mistral 7B 的 PII-level F1 為 0.9673,3000 份中仍有 4 份姓名未完全去除。高 F1 不等於零洩漏。
  • 任何真實病歷的處理,無論在不在本機,都需要院方核准與 IRB;本站全程只用合成資料。

一般能力有沒有退化

微調可能讓模型在其他任務變差(常稱為災難性遺忘,catastrophic forgetting)。可以用 TMMLU+ 的醫學相關科目做對照檢查,只當作「一般能力有沒有明顯退化」的粗略觀察,不是主線評估,也不當訓練資料。這部分的做法與數字【實測數據待補】。進階讀者可以參考 lm-evaluation-harness 這類通用評估工具,本站只做概念介紹。

另外,若要使用 LLM 當評審(LLM-as-judge),只適合開放式輸出(LV16),而且要搭配人工抽樣。

🍎 Mac 與 ☁️ Colab 實作

🍎 Mac:用 evaluate.py 分三次執行:--model 不帶 --adapter 跑 base,帶 --adapter 跑 LoRA,--regex 跑規則式。輸出的 json 檔與逐筆預測 .preds.jsonl 可以拿來找漏網 PHI。另可用 mlx_lm lora --test 看 perplexity(只是參考)。

☁️ Colab:notebook 內已內建同一套 parse、score,先跑 fine-tune 前、再跑 fine-tune 後,並印出各類型 recall 與 CI 是否重疊的提示。

本關重點

  • 主指標是 span recall(類似 sensitivity);次要看 precision、clean_note_rate、valid_json_rate,並附 bootstrap 95% CI。
  • 比較對象要有 base(prompt-only)、LoRA、regex;regex 對有格式的 PHI 不弱,但抓不到人名。
  • CI 有重疊時寫「未顯示明確差異」,不寫「一樣」。
  • 合成資料分數偏高,不能外推到真實病歷;高 F1 也不等於零洩漏。
  • 正式實驗(合成資料、test 200 份):LoRA span recall 0.978(0.956–0.993),prompt-only 0.439(0.370–0.501),CI 沒有重疊;冒煙測試的 0.995 因資料洩漏不引用。

BOSS 戰:漏網之 PHI

HP0/5
  1. 在去識別化任務中,為什麼本站把 span recall 當作主指標?

  2. 某次評估中,fine-tuned 模型的 span recall 95% CI 與 base 模型的 95% CI 有重疊。下列哪個寫法最嚴謹?

  3. regex 在這個任務上對哪類 PHI 最可能完全抓不到?

  4. 本站的評估結果「偏高」的主要原因及處理方式是?

  5. 文獻中有研究報告 PII-level F1 為 0.9673,但 3000 份中仍有 4 份有姓名未完全去除。這個例子想教的是?