先看 Boss:資料洩漏寄生蟲
上一關你造出了一批合成病歷。這一關要把它們轉成模型能「讀」的教材。但在動手前,先認識本關的 Boss:資料洩漏寄生蟲(data leakage)。
它的把戲是:偷偷讓期末考的題目混進平常練習。學生分數很漂亮,換一份新題目就露餡。
這不是假想。作者在做冒煙測試(smoke test,先用很小的資料量確認整條流程能跑通)時,就被它咬過一口。LoRA 微調後,在 29 份測試病歷上 span recall(PHI 片段召回率,下一關詳述)為 0.995,看起來近乎完美。回頭檢查才發現:測試集與訓練集來自同一批骨架,只是填入的假名字不同。這個 0.995 在本站標為不可信,正式實驗會重新切分。
什麼是 chat 格式 jsonl
微調資料最常見的格式是 jsonl(JSON Lines):一行就是一筆完整的 JSON,行與行之間沒有逗號。好處是可以一行一行讀,檔案再大也不怕。
chat 格式的每一筆有一個 messages 陣列,裡面是依序的對話:
system:給模型的任務規則,像是開刀前的標準作業程序。user:使用者輸入,在我們的專案裡就是一份合成病歷。assistant:標準答案,也就是模型應該產出的內容。
mlx-lm 除了 chat 之外,還支援 tools、completions、text 等格式(見 mlx-lm LORA.md 文件)。本站只用 chat,因為它最接近你之後實際對模型說話的方式。
為什麼不能隨便拼字串,而要用 chat 格式?因為每個模型家族有自己的 chat template(對話模板),規定角色標記、換行、結束符號長什麼樣。套錯模板,模型會像看到格式不對的病歷表單,答非所問。轉換成 chat 格式後,由 tokenizer 套用該家族的模板,細節交給工具處理。
設計決定:target 只輸出 PHI 清單
這是整個專案最重要的設計選擇之一。我們有兩種做法:
- 讓模型直接輸出遮蔽後的整份病歷。
- 讓模型只輸出 PHI 清單,遮蔽交給程式。
本站選第二種,理由有四個:
- 輸出短:一份病歷可能上千字,PHI 清單只有幾十個 token,訓練與推論都快。
- 好計分:清單可以直接和標準答案逐項比對,不必判斷「兩段長文有多像」。
- 不傷內容:改寫整份病歷時,模型可能順手更動檢驗數值、藥名或診斷。由程式做字串取代,非 PHI 的部分一個字都不會動。
- 可稽核:出錯時你能看到模型漏了哪一項,而不是只拿到一份看不出問題的長文。
類比:資深護理師在病歷上用螢光筆標出個資,之後由行政人員照螢光筆處理。標記和處理分開,責任清楚。
下面是 synth/build_sft.py 的核心邏輯(精簡版):
SYSTEM = (
"你是病歷去識別化助手。找出病歷中所有個人可識別資訊(PHI),"
"類型限 NAME, DOCTOR, MRN, IDNO, DATE, PHONE, ADDRESS, HOSPITAL。"
"只輸出 JSON:{\"phi\": [{\"type\": 類型, \"text\": 原文字串}]},依出現順序,重複出現也要列出。"
)
def target(rec):
return json.dumps(
{"phi": [{"type": p["type"], "text": p["text"]} for p in rec["phi"]]},
ensure_ascii=False,
)
def to_chat(rec):
return {"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": rec["text"]},
{"role": "assistant", "content": target(rec)},
]}
逐段解釋:
SYSTEM是任務說明,規定 8 種 PHI 類型與輸出格式。訓練和之後推論都要用同一段文字,否則等於換了考試規則。target(rec)把這筆資料已知的 PHI 清單轉成 JSON 字串。因為病歷是我們用程式合成的,PHI 的位置與內容都是精確已知,不需要人工標註。ensure_ascii=False讓中文保持原樣,不會變成一串跳脫碼。to_chat(rec)把三個角色組成一筆 chat 資料。
切分:以「骨架」為單位
回到 Boss。上一關的兩段式生成法,是先由 LLM 產生帶佔位符的骨架,再由程式填入不同假資料。所以同一個骨架可以長出多份病歷,它們共享句型、病程描述與診斷,只有 PHI 不同。
如果把這些兄弟姊妹隨機分到不同 split,test 裡的病歷在 train 中就有「幾乎一模一樣的兄弟」。模型只要記住句型就能得高分。這正是冒煙測試的狀況。
正確做法是以骨架為單位切分,同一個骨架產生的病歷只能進入同一個 split。類比:盲測的診斷試驗驗證組,應該是訓練診斷規則時完全沒看過的另一批病人,不是「同一位病人的另一次門診」。
本站公開資料集由 synth/build_release.py 組裝,用的是更簡單的做法:每個骨架只填一次假資料,一個骨架恰好對應一份病歷,根本不會有兄弟病歷。流程是:只取 AI 品管判定「保留」的骨架 → 去掉內容完全相同的重複骨架 → 用固定種子抽出需要的份數並洗牌 → 依序切成 70 / 10 / 20 → 最後才逐一填值。核心邏輯(精簡版):
rng = random.Random(2026)
chosen = sorted(rng.sample(uniq, n)) # uniq: kept, de-duplicated skeleton indices
rng.shuffle(chosen)
n_tr, n_va = int(n * 0.7), int(n * 0.1)
splits = {"train": chosen[:n_tr],
"validation": chosen[n_tr:n_tr + n_va],
"test": chosen[n_tr + n_va:]}
# each skeleton index is then filled exactly once: one note per skeleton
逐行解釋:
uniq:通過品管、而且去除重複後的骨架編號。rng.sample抽出n個,排序後再用同一個固定種子(2026)洗牌,讓每次執行得到一樣的切法,結果可重現。- 前 70% 是 train、接著 10% 是 validation、最後 20% 是 test:train 用來學、validation 用來在訓練中隨堂小考、test 是期末考。被切的是「骨架編號」,所以切分單位就是骨架。
- 之後每個骨架只填值一次,寫成
train.jsonl、validation.jsonl、test.jsonl(含 PHI 位置),同時輸出 SFT 用的sft/train.jsonl、sft/valid.jsonl、sft/test.jsonl。
如果你用 LV10 的 fill_phi.py 自己產生 notes.jsonl,它同樣是每個骨架填一次;build_sft.py 會先依全文去重,再隨機切成 70 / 10 / 20,並另存一份 .gold.jsonl(每份病歷的原始文字與 PHI 位置,評估時會用到)。要留意:兩個內容相同的骨架填入不同假資料後全文不同,會逃過全文去重,所以先在骨架層級去重(build_release.py 的做法)比較穩。若你想讓同一個骨架填很多次來擴充資料,就要保留骨架編號,並以骨架為單位分組切分。
test 切出來之後就鎖住:訓練期間不看、不調參、不因為分數不好就回頭換資料。想調整的話,用 valid。
分量與品質
資料多不等於比較好。文獻中有例子顯示,結構化抽取任務每個資料集只用不超過 100 份訓練報告,就達到與人類標註者 non-inferior 的結果(Sci Rep 2025,PMID 41286063)。這不代表你的任務也只要 100 份,但提醒我們:先確保資料乾淨、標註正確、覆蓋多樣情境,再談數量。
🍎 Mac 與 ☁️ Colab 實作
🍎 Mac:用 python synth/build_sft.py --in data/notes.jsonl --out data/sft 把 notes.jsonl 轉成 train.jsonl、valid.jsonl、test.jsonl(以及對應的 .gold.jsonl),放在同一個資料夾;若用公開資料集,直接用其中 sft/ 資料夾的三個檔。mlx-lm 會依檔名自動讀取這三個檔。
☁️ Colab:用 datasets 的 load_dataset 載入同一份資料,套用同樣的 to_chat,再用 Unsloth 的 get_chat_template(tokenizer, chat_template="gemma-4") 套上 Gemma 4 的模板,印出第一筆確認格式(對應本站 notebook 的「步驟 3 到 6」)。注意 Colab 版的驗證集 split 名稱是 validation,Mac 版檔名是 valid.jsonl。
資料集放在 GitHub(或本站下載),notebook 可下載後上傳到 Colab 開啟。再次提醒:本站全程只使用合成資料;真實病歷即使在本機處理,也需要院方核准與 IRB(人體研究倫理審查),「本地」不等於「合法」。
本關重點
- chat 格式 jsonl:每行一筆,
messages內有 system / user / assistant;套用 chat template 的工作交給 tokenizer。 - target 只輸出 PHI 清單 JSON,遮蔽交給程式:輸出短、好計分、不改動醫療內容、可稽核。
- 以骨架為單位切分 train / valid / test,同骨架的兄弟病歷不可跨 split。
- 冒煙測試 0.995 是資料洩漏的反例,不可引用;test set 切出後鎖住。
- 資料先求乾淨與多樣,再求數量。