正在幫醫院的臨床決策支援系統,加裝一個「查詢舊病歷與臨床指引」的語意搜尋功能,方便值班護理師快速找到處置建議
你平常用的搜尋引擎、公司內部知識庫,背後大多是一種叫 dense embedding model把一整段文字壓縮成一個向量的做法,你可以想成是把一整本書濃縮成一句話的摘要 的技術。它把一句話、一篇文件通通壓成一串固定長度的數字(向量),要比對兩份文件像不像,只要算這兩串數字的內積就好,又快又省空間。
但「壓縮」是有代價的,細節會被平均掉。這篇 Hugging Face 文章要教的是另一種做法——多向量模型(multi-vector model,又稱 late-interaction 或 ColBERT 風格模型)不是把整段話壓成一句摘要,而是幫句子裡每一個字都保留一個屬於自己的小向量,查詢的時候逐字去比對。查詢和文件比對時,用的是 MaxSim讓查詢裡的每一個字,去文件裡找出跟它最像的那個字,把所有「最佳配對分數」加總起來,當作這份文件的總分 這個計分方式,而不是一次性的內積。
這篇文章的重點不是介紹這個技術「是什麼」(那是姊妹文的工作),而是教你怎麼 微調(finetune)拿一個已經訓練好的模型,用你自己領域的少量資料再訓練一次,讓它更懂你這行的專業用語與「什麼算相關」的判斷標準 一個屬於自己領域的多向量模型。不管是法律、醫療還是程式碼搜尋,「什麼詞彙常出現」「使用者怎麼下查詢」「什麼叫相關」,每個領域的答案都不一樣,通用模型很難通吃。
Sentence Transformers 這個套件把整套微調流程拆成六個可以各自替換的積木:模型本身、資料集、loss 函數訓練時用來告訴模型「這次猜得有多離譜」的計分方式,模型會朝著讓這個分數變小的方向,一步步調整自己內部的參數、訓練參數、evaluator(評估器)在訓練過程中,定期把模型抓出來做小考,量化它是進步了還是退步了,還有負責把這一切兜起來執行的訓練器(trainer)。搞懂這六塊積木怎麼組合,你就能自己動手微調一個檢索模型。
| 面向 | 一般 dense 嵌入模型 | 多向量(ColBERT 風格)模型 |
|---|---|---|
| 儲存方式 | 整段文字 → 1 個向量 | 每個字 → 1 個小向量 |
| 比對方式 | 算 1 次內積 | MaxSim,逐字找最佳配對後加總 |
| 索引大小 | 小 | 大(每個字都要存) |
| 細節保留程度 | 容易把細節平均掉 | 保留逐字的細微差異 |
| 適合情境 | 大範圍粗篩、資源有限 | 要求高精準度的專業檢索(法律、醫療、程式碼) |
這段是文章示範的完整微調流程精簡版,把模型、loss、訓練參數、訓練器四個角色兜在一起。
from sentence_transformers import MultiVectorEncoderfrom sentence_transformers.multi_vector_encoder.losses import CachedMultiVectorMultipleNegativesRankingLossfrom sentence_transformers import MultiVectorEncoderTrainingArguments, MultiVectorEncoderTrainermodel = MultiVectorEncoder("answerdotai/ModernBERT-base", model_kwargs={"torch_dtype": "float32"})loss = CachedMultiVectorMultipleNegativesRankingLoss(model=model, mini_batch_size=16)args = MultiVectorEncoderTrainingArguments(output_dir="models/my-colbert", num_train_epochs=1, per_device_train_batch_size=128, learning_rate=1e-4, bf16=True, eval_strategy="steps", eval_steps=0.1)trainer = MultiVectorEncoderTrainer(model=model, args=args, train_dataset=train_dataset, loss=loss, evaluator=dev_evaluator)trainer.train()model.save_pretrained("models/my-colbert/final")這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。
"""迷你版 ColBERT 風格多向量模型微調示範
只用 8 筆玩具資料、CPU 可跑,目的是體驗六大訓練元件如何兜起來,
不是拿來做正式的醫療檢索系統。"""
from datasets import Dataset
from sentence_transformers import (
MultiVectorEncoder,
MultiVectorEncoderTrainingArguments,
MultiVectorEncoderTrainer,
)
from sentence_transformers.multi_vector_encoder.losses import (
CachedMultiVectorMultipleNegativesRankingLoss,
)
# 1. 玩具資料集:8 組「臨床問題 -> 正確處置說明」配對
train_rows = {
"query": [
"急性心肌梗塞合併冒冷汗要立刻做什麼處置",
"某降血壓藥物與葡萄柚汁併用有什麼禁忌",
"第一型糖尿病病人出現果香味呼吸要懷疑什麼",
"打完蜂螫後全身起疹合併喘不過氣怎麼辦",
"疑似中風時要用哪個口訣快速評估",
"懷疑敗血症時有哪三項指標要優先確認",
"氣喘急性發作合併說話只能講單字怎麼處理",
"血鉀過高在心電圖上會看到什麼變化",
],
"passage_text": [
"病人主訴胸痛合併冒冷汗、噁心,應立即建立靜脈輸液通路、給予嚼服阿斯匹靈、12 導程心電圖確認是否為 ST 段上升,並啟動心導管室待命。",
"鈣離子通道阻斷劑(如 amlodipine)與葡萄柚汁併服會抑制 CYP3A4 酵素代謝,導致藥物血中濃度異常升高,增加低血壓與心悸風險,應間隔至少兩小時或避免併用。",
"呼吸帶有果香味、意識改變合併血糖過高,應懷疑糖尿病酮酸血症,須立即檢驗血酮與動脈血氣分析並給予胰島素與輸液治療。",
"蜂螫後出現全身蕁麻疹合併呼吸喘鳴,屬於過敏性休克表現,應立即肌肉注射腎上腺素並準備氣道處置與輸液擴容。",
"使用 FAST 口訣評估疑似中風:Face 臉歪、Arm 手無力、Speech 說話不清、Time 記錄發作時間並立即送醫。",
"懷疑敗血症時優先確認呼吸速率、意識狀態與收縮壓,也就是 qSOFA 三項指標,任兩項異常即需提高警覺並啟動敗血症照護組合。",
"氣喘急性發作只能講單字、端坐呼吸屬於重度發作,應立即給予高流量氧氣與吸入型支氣管擴張劑,並評估是否需要全身性類固醇。",
"血鉀過高在心電圖上常見 T 波高聳窄尖,嚴重時會出現 P 波消失與 QRS 波變寬,需立即給予鈣劑穩定心肌並降鉀處置。",
],
}
train_dataset = Dataset.from_dict(train_rows)
# 2. 模型:用一顆極小的 BERT(bert-tiny)當骨架包成多向量模型,CPU 幾分鐘內可訓練完
model = MultiVectorEncoder(
"prajjwal1/bert-tiny",
model_kwargs={"torch_dtype": "float32"},
)
# 3. Loss:對比學習,讓「正確配對」的 MaxSim 分數比「批次內其他配對」高
loss = CachedMultiVectorMultipleNegativesRankingLoss(model=model, mini_batch_size=2)
# 4. 訓練參數:刻意調小、關閉混合精度,讓沒有獨立顯卡的電腦也能跑
args = MultiVectorEncoderTrainingArguments(
output_dir="models/toy-colbert-medical",
num_train_epochs=3,
per_device_train_batch_size=2,
learning_rate=2e-5,
fp16=False,
bf16=False,
eval_strategy="no",
save_strategy="no",
logging_steps=1,
run_name="toy-colbert-medical",
)
# 5. 交給 Trainer 開跑(這個玩具規模在一般筆電 CPU 大約 1-3 分鐘內跑完;
# 為了讓示範簡單好跑,這裡先不掛評估器,正式專案請照 code_walk 掛上 evaluator)
trainer = MultiVectorEncoderTrainer(
model=model,
args=args,
train_dataset=train_dataset,
loss=loss,
)
trainer.train()
model.save_pretrained("models/toy-colbert-medical/final")
print("訓練完成,模型已存到 models/toy-colbert-medical/final")
"""載入剛剛微調好的玩具多向量模型,
用一個新問題去跟四篇候選文件比對,
驗證「逐字比對(MaxSim)」機制真的會依語意排序,而不是亂猜。"""
from sentence_transformers import MultiVectorEncoder
model = MultiVectorEncoder("models/toy-colbert-medical/final")
query = ["病人胸口悶痛還一直冒汗,該先做什麼?"]
candidates = [
"病人主訴胸痛合併冒冷汗、噁心,應立即建立靜脈輸液通路、給予嚼服阿斯匹靈、12 導程心電圖確認是否為 ST 段上升,並啟動心導管室待命。",
"鈣離子通道阻斷劑與葡萄柚汁併服會抑制 CYP3A4 酵素代謝,導致藥物血中濃度異常升高。",
"使用 FAST 口訣評估疑似中風:Face 臉歪、Arm 手無力、Speech 說話不清、Time 記錄發作時間。",
"氣喘急性發作只能講單字、端坐呼吸屬於重度發作,應立即給予高流量氧氣與吸入型支氣管擴張劑。",
]
query_embeddings = model.encode_query(query)
document_embeddings = model.encode_document(candidates)
scores = model.similarity(query_embeddings, document_embeddings)
print("各候選文件的 MaxSim 分數:")
for text, score in sorted(zip(candidates, scores[0].tolist()), key=lambda x: -x[1]):
print(f"{score:.4f} {text[:24]}...")