跳到主要內容

LV.10

合成病歷工坊

造一群不存在的病人

BOSS
偽病人製造機失控
時間
約 75 分鐘
建議先過
LV.08 、LV.09

這一關要打倒什麼?

Boss 是「偽病人製造機失控」:你請模型寫一百份病歷,結果一百份都是「54 歲男性,胸悶,STEMI」,連過敏史都一模一樣;或是它順手寫了一組看起來很真的電話號碼,你分不清那是亂編的還是記憶裡的真資料。

這一關要自己造一批合成病歷(synthetic notes),並且自動得到精確的 PHI 標註。這就是主線專案「病歷守門人」的訓練資料來源。

全站原則:所有病歷都是虛構的合成資料。真實病歷即使在本機處理,也需要院方核准與倫理審查(IRB);本地不等於合法。

先講為什麼:為什麼要自己造?

要訓練模型找出 PHI,需要「病歷 + 每個 PHI 的標準答案位置」。取得真實資料有兩個問題:

  1. 公開的臨床資料集(n2c2、MIMIC、PhysioNet)多半需要簽署資料使用協議(DUA)或通過 credentialed 存取,無法直接放進免費教學站。
  2. 作者在 Hugging Face 上沒有找到「公開、免申請、授權清楚的繁中或中英夾雜合成病歷」資料集。

所以我們自己造。造法的重點是:讓標準答案是「建構出來的」,不是「事後猜出來的」。

兩段式生成法(本站設計)

類比:護理站要準備一份示範用的交班單,你不會抄真病人,而是先做一份有空格的範本(「___ 先生,___ 歲,於 ___ 入院」),再拿一份虛構的人名與日期表去填空。每填一格,就順便在旁邊記下「這格是人名」。

第一段:LLM 寫骨架(skeleton)

請模型寫一份「完全虛構」的病歷,但所有可識別資訊一律用佔位符。本站定義了 9 種:

佔位符意義
{NAME}病人姓名
{RELATIVE}家屬或聯絡人姓名(標註時仍算 NAME)
{DOCTOR}臨床人員姓名
{MRN}病歷號
{IDNO}身分證字號
{DATE}任何日期
{PHONE}電話
{ADDRESS}地址
{HOSPITAL}醫院或診所名稱

提示詞中還規定:年齡、性別、症狀、檢驗數值、藥名與劑量照常寫(這些不是 PHI);診斷、檢查與用藥要互相吻合;左右側要對應(左側 MCA 梗塞對應右側無力);不要同時開兩種同類藥;使用台灣的醫界用語。

骨架長這樣(節錄示意,內容為虛構):

{NAME},68 歲男性,{DATE} 因 chest tightness 至 {HOSPITAL} 急診。
BP 160/90, HR 102。CXR 無明顯 infiltrate。病歷號 {MRN},聯絡電話 {PHONE}。

上面的示意只用來說明佔位符,不代表模型實際輸出;實際骨架請看專案裡的 skeletons.jsonl。

多樣性控制:為了避免一百份都長一樣,程式先隨機抽樣「條件」再交給模型:

  • 20 個科別,每個科別各有自己的疾病清單(例如心臟內科有 NSTEMI、心房顫動合併快速心室反應等),避免全部集中在少數熱門診斷。
  • 6 種病歷類型(急診、入院、出院摘要、病程紀錄、會診單、門診紀錄)。
  • 3 種書寫風格:電報式(大量英文縮寫)、中英夾雜、較完整的中文句子。
  • 年齡與性別的合理搭配(例如婦產科、攝護腺相關主題會固定性別;新生兒黃疸年齡為 0)。
  • 長度、至少要出現幾個佔位符、哪幾種佔位符需要固定出現。

第二段:Python 填值並記下位置

骨架拿到後,不再用 LLM。程式掃描每個 {XXX},用假資料填入,並同步記錄「填了什麼、從第幾個字元到第幾個字元」。

這裡有幾個關鍵設計:

  • 假姓名用 Faker 的 zh_TW:Faker 是產生假資料的 Python 套件;zh_TW 地區設定會產生台灣風格的姓名與地址。性別從骨架開頭判斷後,選男名或女名。
  • 同一份病歷內,人物保持一致:除了 {DATE} 與 {RELATIVE},同一種佔位符({NAME}、{DOCTOR}、{MRN} 等)有約七成機率沿用同一份病歷中第一次填過的值(同一位病人、同一位臨床人員);{RELATIVE} 則刻意與病人不同。
  • 身分證字號刻意不通過檢查碼:台灣身分證字號有檢查碼規則。程式會一直重新產生,直到檢查碼不通過為止,這樣它就不可能是任何真實有效的證號,降低巧合對上真人的風險。
  • 醫院名稱用假字組合:像「青嵐綜合醫院」這種由刻意挑過的字詞拼出來的名稱,並避開真實存在的院名(privacy_check.py 另有一份真實院名清單做最後檢查)。
  • 同一份病歷內,日期遞增:每份病歷先決定一個起始日期與一種主要日期格式(ISO、斜線、民國年、月/日、英文月份),之後每出現一個 {DATE} 就往後推進 0 到 90 天不等,並且約八成沿用同一種格式。這樣不會出現「出院日比入院日早」。
  • 病歷號與電話:用隨機數字產生,電話混合手機與市話格式。

簡化版程式(僅示意,完整版見專案 fill_phi.py):

import re
PH_RE = re.compile(r"\{([A-Z]+)\}")
TYPE_OF = {"RELATIVE": "NAME"}   # family names are labelled as NAME

def fill(skeleton, make_value):
    out, spans, pos, cursor = [], [], 0, 0
    for m in PH_RE.finditer(skeleton):
        before = skeleton[cursor:m.start()]
        out.append(before)
        pos += len(before)
        value = make_value(m.group(1))
        spans.append({"type": TYPE_OF.get(m.group(1), m.group(1)), "text": value,
                      "start": pos, "end": pos + len(value)})
        out.append(value)
        pos += len(value)
        cursor = m.end()
    out.append(skeleton[cursor:])
    text = "".join(out)
    assert all(text[s["start"]:s["end"]] == s["text"] for s in spans)
    return text, spans

逐行解釋:

  • PH_RE:用正規表示式找出所有 {大寫字母} 形式的佔位符。TYPE_OF 把 {RELATIVE} 標成 NAME,所以標註只有 8 種類型。
  • out:已經組好的文字片段;pos 是目前在「填值後文字」中的位置;cursor 是在「骨架」中處理到哪裡。
  • for m in ...:逐個處理佔位符。
  • before:從上一個佔位符之後、到這個佔位符之前的普通文字,原樣放進去,並把 pos 往後推。
  • value = make_value(...):向假資料產生器要一個值(人名、日期等)。
  • spans.append(...):記下這個 PHI 的類型、內容、起始與結束位置。位置是在填值後文字中的位置,這就是標準答案。
  • 結尾的 assert:自我檢查,確保每個記錄的位置切出來的字,真的等於當初填進去的值。這一行讓標註的正確性有程式保證。

完整版還會同時輸出一份把 PHI 換成 [NAME]、[DATE] 之類標記的去識別化文字,LV11 會用到。

生成之後:清理與剔除

模型的輸出不能直接用,需要兩道處理。

第一道:清理

  • 去掉思考過程(<think> 或 Gemma 4 的 thinking channel)與 markdown 殘留。
  • 統一佔位符格式(例如把 {{NAME}} 改成 {NAME})。
  • OpenCC 簡轉繁:小模型常混入簡體字,用 OpenCC(s2twp 設定,轉成台灣用字與詞彙)轉換。
  • 台灣用語替換:OpenCC 之後再補一張小字典,例如「前列腺」換成「攝護腺」、「外周血」換成「周邊血」、「迴圈」換成「循環」,把大陸慣用的醫學詞彙改成台灣的用法。

第二道:規則式剔除。以下情形整份丟掉:

  • 思考文字外洩、出現沒定義的佔位符。
  • 缺少 {NAME} 或 {DATE},或佔位符少於 3 個。
  • 佔位符以外的地方出現「像電話、身分證字號、日期、七位以上數字、醫院名稱」的字串(模型把「假號碼」直接寫進內文了)。
  • markdown 或範本殘留(例如 [Your Name])、出現泰文、俄文、日文、韓文等外來文字、佔位符堆疊在一起、內容太短。

被剔除的骨架會另存成 *.rejected.jsonl,附上剔除原因。看這份檔案,是理解模型常犯什麼錯最快的方法。

實測:三個生成模型的品質(E11)

以下是作者在 M5 32GB 的實測,生成骨架的品質比較。這是小規模的觀察,不是嚴謹評測:

生成模型通過規則檢查的比例速度品質觀察
Qwen3-4B-Instruct-2507(4-bit)68%快混入簡體字;醫學內容偶有不合理;常把假號碼直接寫進內文,被檢查器擋下
Gemma 4 E2B(4-bit)53–80%關掉思考後約 5 秒/份預設會輸出英文 thinking(需 system prompt、enable_thinking=False 與過濾);內容偏英文、科別與疾病不搭
Gemma 4 26B-A4B QAT(4-bit)89%約 18 秒/份(batch 4)結構完整、像台灣病歷;仍有少量醫學錯誤,例如急性骨髓性白血病(AML)出院帶藥出現 sildenafil

幾個教訓:

  1. 小模型跑得動,但寫不好。 2–4B 等級可以在 Mac 上很快地生成,但簡體字、科別與疾病不搭、醫學內容不合理的情況比較多。
  2. 26B 品質明顯較好,但仍需要品管。 通過率較高並不代表內容正確,醫學錯誤規則式檢查抓不到,需要人或另一個流程抽查。
  3. 規則式檢查只管得了「長相」。 它擋得住外洩的電話與缺漏的佔位符,卻擋不住「AML 病人帶 sildenafil 出院」這種語意錯誤。
  4. 記憶體要留意。 作者在 32 GB Mac 上跑 26B(約 15 GB)時,batch 8 加上另一個程序,曾發生 Metal 記憶體不足;單獨跑、batch 4 則正常。
  5. 作者的取捨:公開資料集用 26B 生成。

通過率的樣本數與重複次數有限,請把它當作方向,不要當成各模型的正式排名。

為什麼不能用 Claude、ChatGPT 這類雲端模型來生成訓練資料

這一節很重要,因為「叫 ChatGPT 幫我寫一千份病歷」是最直覺的做法。

先說結論:作者建議用開源、可商用授權的模型(例如 Apache-2.0 的 Gemma 4)來生成訓練資料。理由是條款,不是品質。

以下依據作者在 2026-10 對 Anthropic 條款的整理(本章引用的是作者的風險評估,不是法律意見):

  • Anthropic 使用政策(AUP,2025-09-15 版):在「不得濫用平台」一節,列有禁止「未經 Anthropic 事先授權,使用輸入與輸出來訓練 AI 模型」(文中舉例為 model scraping、model distillation)。這一項沒有「競爭」之類的限定詞。
  • 消費者條款(2025-10-08 生效):禁止開發與其服務競爭的產品,並在文字中把「訓練 AI 或機器學習模型」列為例子,同時要求遵守 AUP。「競爭」一詞沒有被定義。
  • 官方說明頁(更新於 2026-03-16):一方面說可以用輸出訓練「不與 Anthropic 模型競爭」的模型(例如資訊抽取工具),另一方面把「把輸出當成訓練目標」列為不允許的例子,也提到不可協助第三方這麼做。
  • 「資訊抽取」的用途看起來接近允許範圍,但上述的訓練目標描述讓它落在灰色地帶;作者的評估是「灰色偏紅」,若再公開資料集或模型,風險更高,因為公開等於邀請別人拿去訓練。

這個風險不是只有 Anthropic 才有。OpenAI、Google 等其他廠商的服務條款,也可能有類似限制,而且會改版,請你自行查證當下的版本。 本站不替你下結論。

用開源、可商用授權的模型生成,為什麼最乾淨?

  • Gemma 4 以 Apache-2.0 釋出,授權本身沒有限制你用它的輸出來訓練其他模型,也沒有限制你公開資料集。
  • 流程可以完全在本機重現,與本站「開源、可重現」的定位一致。
  • 資料集可以用開放授權釋出,學習者拿去用,不必擔心連帶違反哪份條款。

那 Claude 完全不能碰嗎? 本站的做法是:Claude 或其他 AI 不寫入任何訓練內容,只做品質判定(見下一節)。這種做法的風險較低,但作者無法保證完全沒有風險;若你想把它用在公開釋出或商業情境,建議先詢問機構的法務或直接向廠商取得書面同意。

品管:另一個 AI 只做「保留/剔除」

規則式檢查抓不到語意錯誤,所以本站再加一道:由另一個 AI 逐份讀骨架,只回答「保留」或「剔除」(附理由),不改寫、不補寫、不輸出任何要放進資料集的文字。

設計理由:

  • 內容永遠只來自開源模型與程式,AI 判定者的輸出不會成為訓練資料,條款與資料來源的問題單純得多。
  • 保留與剔除是一個窄任務,結果容易抽查;被剔除的骨架留有紀錄。
  • 判定者也會出錯,所以仍建議由人隨機抽查,尤其是用藥、診斷與左右側。

後續資料集組裝時,只使用被保留的骨架,並且(LV11 會詳細說明)以骨架為單位切分訓練集與測試集,避免同一份骨架的不同填值同時出現在兩邊而造成資料洩漏。

合成資料的限制

  • 分布單純:模板化的病歷比真實病歷整齊。用它訓練、再用它測試,分數容易偏高。作者的冒煙測試就出現過訓練與測試共用骨架導致分數接近滿分的情況,那是資料洩漏,不能引用。
  • 仍可能算個資:完全虛構的資料通常不對應任何自然人,個資法較難適用;但若合成資料是由真實病歷衍生、還能還原回特定個人,就宜視同衍生資料處理。本站的提示詞中不放任何真實病歷。
  • 巧合風險:假姓名、假電話有可能巧合對上真人,所以身分證字號刻意檢查碼不通過,資料卡也要聲明「虛構」。
  • 只供教學:本站的資料與模型不用於臨床決策。

實作

🍎 Mac:本機生成 50 份,再填值

作者的腳本以 mlx-lm 載入模型,批次生成骨架(需要已安裝 mlx-lm、opencc、faker):

python synth/gen_skeletons.py --model <本機模型資料夾> --n 50 --out data/skeletons.jsonl
python synth/fill_phi.py --in data/skeletons.jsonl --out data/notes.jsonl

逐行解釋:

  • 第一行:--model 指向本機的 MLX 模型(建議 16 GB Mac 用 Gemma 4 E2B 練習流程;32 GB 可以試 26B,品質較好);--n 50 是目標份數,通過檢查的才會算;--out 是輸出檔。程式可以中斷後繼續,已通過的會保留。
  • 被剔除的骨架會自動存到同名的 .rejected.jsonl,請打開看看。
  • 第二行:讀入骨架,填入假資料,輸出 notes.jsonl。每行是一份病歷,含 text(病歷全文)、phi(每個 PHI 的類型、內容、起訖位置)、deid_text(去識別化版本)。

記得用 Python 快速檢查輸出:

import json
notes = [json.loads(l) for l in open("data/notes.jsonl", encoding="utf-8")]
n = notes[0]
for p in n["phi"]:
    assert n["text"][p["start"]:p["end"]] == p["text"]
print(len(notes), "notes OK")
  • 逐筆檢查:用記下的位置切出文字,應該與記錄的內容完全相同;全部通過就印出份數。

☁️ Colab:批次生成更多份

填值程式(fill_phi.py)完全相同,因為它只依賴 Faker,不依賴 GPU。生成骨架的部分,可以改用 Unsloth 或 transformers 在 T4 上推論,一次生成較多份(練習可先做 500 份;本站公開的資料集是 1,000 份,由 Gemma 4 26B 在 Mac 上生成並經品管)。速度與通過率:【實測數據待補】。完成後把 notes.jsonl 下載回本機。

提醒:Colab 是雲端環境,只能放合成資料,不要上傳任何真實病歷。

看看公開版資料集

作者整理的公開版資料集放在 GitHub(或本站下載)。你可以打開比較自己生成的版本與公開版有什麼差異,特別是剔除前後的內容。

本關重點

  • 兩段式生成:LLM 寫帶佔位符的骨架,Python 填假值並記下位置,標註因此是精確、自動取得的。
  • 假資料的設計:Faker zh_TW、同一份病歷內人物一致、日期遞增、身分證刻意檢查碼不通過。
  • 生成後要做 OpenCC 簡轉繁、台灣用語替換與規則式剔除;規則式抓不到語意錯誤,需要再加一道品管。
  • E11(作者實測、小規模):2–4B 小模型跑得動但寫不好;26B 較好但仍有醫學錯誤,需要品管。
  • 不建議用 Claude、ChatGPT 這類雲端模型生成訓練資料:Anthropic AUP 字面上禁止未經授權用輸出訓練 AI 模型;其他廠商條款請自行查證;用 Apache-2.0 的開源模型生成最乾淨。另一個 AI 只做保留/剔除判定,不寫內容。

BOSS 戰:偽病人製造機失控

HP0/5
  1. 「兩段式生成法」把流程拆成骨架生成與佔位符填值兩段。這樣做最主要的好處是什麼?

  2. 為什麼假的身分證字號要刻意設計成「檢查碼不通過」?

  3. 同一份病歷裡的 {DATE} 為什麼要設計成日期遞增,而不是每個都隨機?

  4. 為什麼不建議用 Claude、ChatGPT 這類雲端模型生成本站的訓練資料?

  5. 本站的品質管控中,另一個 AI 被用來做什麼?