80% 以上的醫療數據都是文字形式—臨床記錄、實驗室報告、出院總結。本文是開發該寶藏的關鍵。
1. 為什麼臨床NLP與常規NLP不同?
臨床文件:
"Bn nam 58t, tiền sử THA, ĐTĐ typ 2, vào viện vì ĐAU NGỰC trái lan lên vai,
kèm khó thở, không sốt. SpO2 94%, M 110 bpm, ECG bình thường.
TnI 0.8 ng/mL (ref < 0.04)."
問題:
- 密集縮寫:Hypertension、ECG、ECG、TnI
- 陰性:「不發燒」-需檢測陰性
- 時間性:“過去”與“當前”
- 追蹤上下文:ECG 中的「正常」≠ 正常 SpO2
2. 使用 BioBERT 實現 NER Health
2.1。領域模型
| 型號 | 訓練資料 | 最適合 |
|---|---|---|
| BioBERT | 考試研究+PMC | 研究 NER |
| PubMedBERT | PubMed(草稿) | 最佳 BioNLP |
| 臨床BERT | MIMIC-III | 臨床筆記 |
| Bio_ClinicalBERT | PubMed + MIMIC | 普通醫學NLP |
2.2。代幣分類(NER)
from transformers import (
AutoTokenizer, AutoModelForTokenClassification,
TrainingArguments, Trainer, DataCollatorForTokenClassification
)
import torch
LABEL_LIST = [
"O",
"B-DISEASE", "I-DISEASE",
"B-DRUG", "I-DRUG",
"B-SYMPTOM", "I-SYMPTOM",
"B-ANATOMICAL", "I-ANATOMICAL",
"B-LAB_VALUE", "I-LAB_VALUE",
]
LABEL2ID = {l: i for i, l in enumerate(LABEL_LIST)}
ID2LABEL = {i: l for l, i in LABEL2ID.items()}
MODEL_NAME = "dmis-lab/biobert-base-cased-v1.2"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForTokenClassification.from_pretrained(
MODEL_NAME,
num_labels=len(LABEL_LIST),
id2label=ID2LABEL,
label2id=LABEL2ID
)
def tokenize_and_align_labels(examples):
"""Align NER labels với BERT wordpieces."""
tokenized = tokenizer(
examples["tokens"],
truncation=True,
is_split_into_words=True,
max_length=512
)
all_labels = []
for i, label_ids in enumerate(examples["ner_tags"]):
word_ids = tokenized.word_ids(batch_index=i)
previous_word_idx = None
label_ids_aligned = []
for word_idx in word_ids:
if word_idx is None:
label_ids_aligned.append(-100)
elif word_idx != previous_word_idx:
label_ids_aligned.append(label_ids[word_idx])
else:
# Wordpiece continuation: dùng I- label
current_label = LABEL_LIST[label_ids[word_idx]]
if current_label.startswith("B-"):
inside_label = "I-" + current_label[2:]
label_ids_aligned.append(LABEL2ID.get(inside_label, -100))
else:
label_ids_aligned.append(-100)
previous_word_idx = word_idx
all_labels.append(label_ids_aligned)
tokenized["labels"] = all_labels
return tokenized
2.3。處理否定和暫時性
NEGATION_TERMS = [
"không", "chưa", "phủ nhận", "không ghi nhận", "âm tính",
"no", "not", "without", "denies", "absent", "negative", "ruled out"
]
TEMPORAL_PATTERNS = {
"past": ["tiền sử", "trước đây", "cũ", "đã", "history of", "prior"],
"family": ["gia đình", "bố mẹ", "family history"],
"current": ["hiện tại", "hiện", "currently", "presenting with"],
}
def detect_negation_scope(text: str, entity_start: int, window: int = 5) -> bool:
"""Check 5 words trước entity xem có negation term không."""
tokens = text[:entity_start].split()
window_tokens = [t.lower() for t in tokens[-window:]]
return any(neg in window_tokens for neg in NEGATION_TERMS)
def classify_temporality(text: str, entity_start: int) -> str:
before = text[:entity_start].lower()[-80:]
for category, patterns in TEMPORAL_PATTERNS.items():
if any(p in before for p in patterns):
return category
return "current"
def extract_entities_with_context(text: str, ner_pipeline) -> list[dict]:
"""NER + negation + temporality."""
raw_entities = ner_pipeline(text, aggregation_strategy="max")
enriched = []
for e in raw_entities:
enriched.append({
"text": e["word"],
"label": e["entity_group"],
"confidence": round(e["score"], 3),
"is_negated": detect_negation_scope(text, e["start"]),
"temporality": classify_temporality(text, e["start"]),
})
return enriched
3. ICD-10 自動編碼
from transformers import AutoModelForSequenceClassification, pipeline
class ICD10AutoCoder:
"""
Multi-label classification: discharge summary → ICD-10 codes.
Dataset: MIMIC-III (top 50 codes)
Metric: micro-AUC, P@8, R@8
"""
def __init__(self, model_path: str, icd_list: list[str]):
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
self.model = AutoModelForSequenceClassification.from_pretrained(model_path)
self.icd_list = icd_list
self.model.eval()
def predict(self, discharge_summary: str, top_k: int = 10) -> list[dict]:
inputs = self.tokenizer(
discharge_summary,
return_tensors="pt",
max_length=4096, # Dùng Longformer/BigBird cho long docs
truncation=True,
)
with torch.no_grad():
logits = self.model(**inputs).logits
probs = torch.sigmoid(logits).squeeze()
top_k_indices = torch.topk(probs, k=min(top_k, len(self.icd_list))).indices
return [
{
"icd_code": self.icd_list[i],
"confidence": round(float(probs[i]), 3)
}
for i in top_k_indices.tolist()
]
4.NER評估
from seqeval.metrics import classification_report, f1_score
def evaluate_ner(all_true_labels: list, all_pred_labels: list):
"""
seqeval dùng entity-level F1 (strict span match).
Khác với token-level accuracy — entity-level khắt khe hơn nhiều.
"""
print(classification_report(all_true_labels, all_pred_labels))
micro_f1 = f1_score(all_true_labels, all_pred_labels, average="micro")
macro_f1 = f1_score(all_true_labels, all_pred_labels, average="macro")
print(f"Micro F1: {micro_f1:.4f} | Macro F1: {macro_f1:.4f}")
5. 練習
1.下載I2B2 2010 NER資料集。微調 ClinicalBERT 和 PubMedBERT。比較每種實體類型(疾病/藥物/治療)的 F1 分數。 2. 實作否定評估:註 100 個句子,統計 False Positive(預測為否定但沒有)和 False Negative。 3. 在 MIMIC-III 上建構 ICD-10 top-50 分類器。目標精度@5 > 0.70。
第 8 課:使用 RAG + LLM 建造 醫療問答聊天機器人。