NLP Day 6 命名實體辨識與序列標註
執行需求:Colab T4 可跑。今天進入 NLP 第二大經典任務:命名實體辨識(Named Entity Recognition,NER)。會用 CoNLL-2003 英文資料集做微調,並且用 transformers 的 token classification pipeline 把句子中的「人名、組織、地點、時間」自動標出來。也會介紹 BIO 標註格式、子詞(subword)對齊的常見踩雷、以及中文 NER 的特殊處理(會在 Day 7 深入)。
引言
什麼是命名實體辨識?給一段「Apple was founded by Steve Jobs in California in 1976.」,NER 系統要自動標出:Apple(組織)、Steve Jobs(人名)、California(地點)、1976(時間)。這個任務比情緒分類難很多,因為它要對句子中「每個 token」都做分類,而不是整句話一個標籤。這種「每個 token 一個標籤」的任務,NLP 領域稱為序列標註(sequence labeling)。
NER 在企業應用上非常常見:合約審查自動抓出當事人與金額、新聞彙整自動標出人物與組織、客服對話自動偵測商品名稱與時間。BERT 出現後,NER 的正確率從 2017 年的 F1 90% 跳到 92-95%,成為 NLP 商業化最快的任務之一。
今天的範例用 CoNLL-2003 英文 NER 資料集(Hugging Face 上有 14,041 訓練句、3,250 測試句,標籤採用 BIO 格式)。會做四件事:第一,介紹 BIO 標註與 token-level 分類的差異;第二,用 transformers 載入預訓練 NER 模型直接推論;第三,用 BERT 微調一個自定義 NER 模型;第四,處理中文 NER(會在 Day 7 完整展開)。
BIO 標註格式與 token 分類
NER 最常用的標註格式是 BIO:
- B-X(Begin):實體的開始 token。例如「Apple」是 B-ORG(組織的開始)。
- I-X(Inside):實體的內部 token。例如「Jobs」如果是「Steve Jobs」的一部分,標 I-PER。
- O(Outside):不是任何實體的 token。例如「was」、「founded」標 O。
舉例來說,「Apple was founded by Steve Jobs in California in 1976.」標註後:
- Apple = B-ORG
- was = O
- founded = O
- by = O
- Steve = B-PER
- Jobs = I-PER
- in = O
- California = B-LOC
- in = O
- 1976 = B-MISC(時間或數字)
- . = O
注意 CoNLL-2003 把時間視為 MISC(miscellaneous)。不同資料集對類別的定義會略有差異,OntoNotes 用 18 個類別(含 DATE、TIME、MONEY、PERCENT 等),更細緻也更難。
另一個重要觀念是「子詞對齊」(subword alignment)。BERT 用 WordPiece 把每個詞切成 subword(例如「Jobs」可能被切成「Job」+「##s」)。標註時,子詞的標籤要對齊:通常第一個 subword 保留原始標籤,後續 subword 用「X」標記(表示它是前一個 token 的延續)。transformers 的 tokenizer 提供 word_ids() 函式來幫忙處理這個對齊。
用預訓練 NER 模型直接推論
transformers 內建多個預訓練 NER 模型,最常用的是 dslim/bert-base-NER(基於 bert-base-cased,在 CoNLL-2003 上微調,F1 約 91%):
from transformers import pipeline
# 載入預訓練 NER 模型
ner = pipeline(
task="token-classification",
model="dslim/bert-base-NER",
aggregation_strategy="simple",
)
text = "Apple was founded by Steve Jobs in California in 1976."
results = ner(text)
print(f"輸入:{text}")
print()
print("實體".ljust(20) + "類別".ljust(10) + "起點".ljust(8) + "終點".ljust(8) + "信心")
print("-" * 60)
for r in results:
entity = r["word"]
label = r["entity_group"]
start = r["start"]
end = r["end"]
score = r["score"]
print(entity.ljust(20) + label.ljust(10) + str(start).ljust(8) + str(end).ljust(8) + f"{score:.4f}")
# 輸出範例:
# 輸入:Apple was founded by Steve Jobs in California in 1976.
#
# 實體 類別 起點 終點 信心
# ------------------------------------------------------------
# Apple ORG 0 5 0.9988
# Steve Jobs PER 21 31 0.9945
# California LOC 35 45 0.9978
# 1976 MISC 49 53 0.9456
這段程式用 transformers 的 pipeline 介面做 NER,幾行就能拿到完整結果。aggregation_strategy="simple" 會把同一個實體的多個 subword 合併,例如「Steve Jobs」會被合併成一個 PER 實體。如果用 aggregation_strategy="none",則會保留每個 subword 的獨立標籤。後者適合需要微調標籤對齊的進階應用。
用 BERT 微調 NER 模型
預訓練 NER 模型對通用英文表現不錯,但對領域專有名詞(醫療、財經、法律)效果有限。要在特定領域做 NER,必須自己微調。以下示範用 CoNLL-2003 資料集微調 BERT:
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForTokenClassification,
TrainingArguments,
Trainer,
)
# 載入 CoNLL-2003 資料集
dataset = load_dataset("eriktks/conll2003", trust_remote_code=True)
print(f"資料集結構:{dataset}")
# 輸出:資料集結構:DatasetDict({
# train: Dataset({features: ['id', 'tokens', 'pos_tags', 'chunk_tags', 'ner_tags'], num_rows: 14041})
# validation: Dataset({...}),
# test: Dataset({...}),
# })
# 標籤對應
label_list = dataset["train"].features["ner_tags"].feature.names
print(f"標籤:{label_list}")
# 輸出:標籤:['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
# 載入 tokenizer 與模型
model_name = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForTokenClassification.from_pretrained(
model_name,
num_labels=len(label_list),
)
# 把 token 與標籤對齊
def tokenize_and_align_labels(examples):
tokenized = tokenizer(
examples["tokens"],
truncation=True,
is_split_into_words=True,
)
labels = []
for i, label in enumerate(examples["ner_tags"]):
word_ids = tokenized.word_ids(batch_index=i)
previous_word_idx = None
label_ids = []
for word_idx in word_ids:
if word_idx is None:
label_ids.append(-100)
elif word_idx != previous_word_idx:
label_ids.append(label[word_idx])
else:
label_ids.append(-100)
previous_word_idx = word_idx
labels.append(label_ids)
tokenized["labels"] = labels
return tokenized
tokenized = dataset.map(tokenize_and_align_labels, batched=True)
tokenized.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
# 取子集做快速示範
train_ds = tokenized["train"].shuffle(seed=42).select(range(2000))
eval_ds = tokenized["validation"].shuffle(seed=42).select(range(500))
args = TrainingArguments(
output_dir="./ner_model",
num_train_epochs=2,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
eval_strategy="epoch",
save_strategy="no",
learning_rate=2e-5,
report_to="none",
)
trainer = Trainer(
model=model,
args=args,
train_dataset=train_ds,
eval_dataset=eval_ds,
)
print("開始微調 NER 模型...")
trainer.train()
# 評估
metrics = trainer.evaluate()
print(f"\n評估結果:{metrics}")
# 輸出範例:
# 開始微調 NER 模型...
# {'eval_loss': 0.1234, 'eval_runtime': 6.2, ...}
# 實際數字會依訓練略有不同
這段程式用 BERT 微調 NER,最關鍵的步驟是 tokenize_and_align_labels:因為 BERT 會把每個 token 切成多個 subword,標籤要對齊到「第一個 subword 保留原標籤、後續 subword 用 -100 表示忽略」。Trainer 會自動把 -100 排除在 loss 計算之外,這是 transformers 的慣用做法。實際上 seqeval 函式庫提供更完整的 F1、precision、recall 評估,這裡簡化只用 Trainer 的內建評估。
資料集檢視:CoNLL-2003 結構
在進入微調之前,先理解 CoNLL-2003 的資料結構:
from datasets import load_dataset
dataset = load_dataset("eriktks/conll2003", trust_remote_code=True)
# 看一筆訓練資料
sample = dataset["train"][0]
print(f"句子:{sample['tokens']}")
print(f"POS 標籤:{sample['pos_tags']}")
print(f"Chunk 標籤:{sample['chunk_tags']}")
print(f"NER 標籤:{sample['ner_tags']}")
# 輸出範例:
# 句子:['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
# POS 標籤:[22, 42, 16, 21, 35, 37, 16, 21, 7]
# Chunk 標籤:[11, 21, 11, 12, 21, 22, 11, 12, 0]
# NER 標籤:[3, 0, 7, 0, 0, 0, 7, 0, 0]
# 對應:B-ORG, O, B-MISC, O, O, O, B-MISC, O, O
# 統計類別分布
from collections import Counter
all_labels = []
for sample in dataset["train"]["ner_tags"]:
all_labels.extend(sample)
counter = Counter(all_labels)
label_names = dataset["train"].features["ner_tags"].feature.names
print()
print("NER 標籤分布:")
for idx, count in sorted(counter.items(), key=lambda x: -x[1]):
name = label_names[idx]
print(f" {name}:{count} ({100 * count / len(all_labels):.2f}%)")
# 輸出範例:
# NER 標籤分布:
# O:169578 (83.05%)
# B-PER:11404 (5.59%)
# I-PER:8550 (4.19%)
# B-LOC:9214 (4.51%)
# I-LOC:1872 (0.92%)
# B-ORG:7394 (3.62%)
# I-ORG:3395 (1.66%)
# B-MISC:4508 (2.21%)
# I-MISC:2184 (1.07%)
# 實際數字會依 CoNLL-2003 版本略有不同
從類別分布可以看到 CoNLL-2003 是高度不平衡的:83% 的 token 是「O」(非實體)。這種不平衡在評估時要用 macro F1 而不是 accuracy;否則一個把全部 token 都標成 O 的 baseline 模型也能拿到 83% 正確率,但完全沒用。實務上評估 NER 一定要用 entity-level F1(seqeval 函式庫提供)。
完整實作:可重複使用的 NER 預測函式
把微調後的 NER 模型包裝成實用的函式,能處理長文件並支援中文:
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch
# 載入微調後的模型
model_path = "./ner_model/checkpoint-250"
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
model = AutoModelForTokenClassification.from_pretrained(model_path)
model.eval()
label_list = ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
def extract_entities(text, max_length=256):
"""從一段文字中抽取所有命名實體"""
inputs = tokenizer(
text,
return_tensors="pt",
truncation=True,
max_length=max_length,
return_offsets_mapping=True,
)
offset_mapping = inputs.pop("offset_mapping")[0]
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.argmax(outputs.logits, dim=-1)[0].tolist()
entities = []
current_entity = None
for pred, offset in zip(predictions, offset_mapping):
label = label_list[pred]
start, end = offset.tolist()
if start == end:
continue
if label.startswith("B-"):
if current_entity:
entities.append(current_entity)
current_entity = {
"text": text[start:end],
"label": label[2:],
"start": start,
"end": end,
}
elif label.startswith("I-") and current_entity:
current_entity["text"] += text[start:end]
current_entity["end"] = end
else:
if current_entity:
entities.append(current_entity)
current_entity = None
if current_entity:
entities.append(current_entity)
return entities
# 測試
text = "Microsoft was founded by Bill Gates and Paul Allen in Albuquerque in 1975."
entities = extract_entities(text)
print(f"輸入:{text}")
print()
print("實體".ljust(25) + "類別".ljust(10) + "位置")
print("-" * 50)
for e in entities:
print(e["text"].ljust(25) + e["label"].ljust(10) + f"{e['start']}-{e['end']}")
# 輸出範例:
# 輸入:Microsoft was founded by Bill Gates and Paul Allen in Albuquerque in 1975.
#
# 實體 類別 位置
# --------------------------------------------------
# Microsoft ORG 0-9
# Bill Gates PER 24-34
# Paul Allen PER 39-48
# Albuquerque LOC 52-63
# 實際位置會略有不同
這段 extract_entities 函式是 NER 預測的標準實作:先 tokenize、跑模型、把 token-level 預測合併成 entity-level 結果。B/I 標籤的合併邏輯是關鍵:B 開新實體、I 延續前一個實體、O 結束當前實體。實務上會再處理「不連續 B」、「空實體」等邊界情況,seqeval 函式庫提供了標準實作可以參考。
NER 評估:seqeval 函式庫
NER 評估必須用 entity-level F1,而不是 token-level accuracy。下面用 seqeval 示範標準評估流程:
try:
from seqeval.metrics import classification_report as seq_report, f1_score
seqeval_available = True
except ImportError:
seqeval_available = False
print("未安裝 seqeval,可執行 pip install seqeval 安裝")
# 假設我們有一批預測結果
y_true = [
["B-PER", "I-PER", "O", "B-ORG", "O"],
["O", "B-LOC", "I-LOC", "O", "O"],
]
y_pred = [
["B-PER", "I-PER", "O", "B-ORG", "O"], # 全對
["O", "B-LOC", "O", "O", "O"], # I-LOC 漏掉
]
if seqeval_available:
print("Entity-level 分類報告:")
print(seq_report(y_true, y_pred))
f1 = f1_score(y_true, y_pred)
print(f"Macro F1:{f1:.4f}")
# 輸出範例:
# Entity-level 分類報告:
# precision recall f1-score support
#
# LOC 0.00 0.00 0.00 1
# ORG 1.00 1.00 1.00 1
# PER 1.00 1.00 1.00 1
#
# micro avg 0.67 0.67 0.67 3
# macro avg 0.67 0.67 0.67 3
# Macro F1:0.6667
seqeval 會把整個 entity(包含所有 B + I)視為一個單位:錯一個 token 整個 entity 就算錯。這個標準在 CoNLL-2003 的官方評估中被廣泛採用,也是 Hugging Face 線上排行榜的預設指標。如果用 token-level accuracy 看起來很高(例如 95%),但 entity-level F1 可能只有 70%,差異很大。
中文 NER 的特殊議題
中文 NER 比英文難,主要有三個原因:第一,中文沒有空格,每個字要單獨 tokenize;第二,中文實體邊界模糊(例如「台灣大學」是否為一個組織?);第三,中文有繁簡、語碼混用等問題。Day 7 會深入展開,這裡先展示一個中文 NER 的 baseline:
# 用 spaCy 的中文 NER 模型做快速示範
import spacy
# 需要先下載中文模型:python -m spacy download zh_core_web_trf
nlp_zh = spacy.load("zh_core_web_sm") # 用 sm 版本省時間
texts_zh = [
"台積電的張忠謀在台北發表演說。",
"阿里巴巴在杭州成立,由馬雲創辦。",
"中央銀行宣布利率維持不變。",
]
for t in texts_zh:
doc = nlp_zh(t)
print(f"\n文字:{t}")
print("實體".ljust(10) + "類別".ljust(10))
print("-" * 25)
for ent in doc.ents:
print(ent.text.ljust(10) + ent.label_.ljust(10))
# 輸出範例(依模型品質而定):
# 文字:台積電的張忠謀在台北發表演說。
# 實體 類別
# -------------------------
# 台積電 ORG
# 張忠謀 PERSON
# 台北 GPE
#
# 文字:阿里巴巴在杭州成立,由馬雲創辦。
# 實體 類別
# -------------------------
# 阿里巴巴 ORG
# 杭州 GPE
# 馬雲 PERSON
#
# 文字:中央銀行宣布利率維持不變。
# 實體 類別
# -------------------------
# 中央銀行 ORG
spaCy 的中文模型 zh_core_web_sm 雖然輕量(約 20 MB),但能識別常見的組織、人名、地名。對企業應用,建議改用 zh_core_web_trf(基於 Transformer、約 500 MB),品質會好很多,但 CPU 上會慢很多。Day 7 會示範用 BERT 微調中文 NER。
常見錯誤與踩雷
第一個雷是「subword 標籤對齊錯誤」。最常見的錯誤是「所有 subword 都給原始標籤」或「所有 subword 都給 -100」。正確做法:第一個 subword 給原標籤,後續 subword 給 -100。Trainer 會自動忽略 -100,這樣只對「第一個 subword」計算 loss。
第二個雷是「評估時用 token-level 指標」。token-level F1 會被「O」標籤淹沒。應該用 entity-level F1(seqeval 函式庫提供),才算對實體的完整正確率。
第三個雷是「忘了對齊 word_ids() 對應」。當 batch_size 大於 1 時,要用 word_ids(batch_index=i) 取得每個樣本的對應。多個樣本批次處理時容易出錯。
第四個雷是「CoNLL-2003 標籤混淆」。CoNLL 把時間標為 MISC,但 OntoNotes 細分成 DATE、TIME、MONEY 等。預訓練模型在不同資料集訓練會用不同類別對應,部署前要先驗證標籤定義。
效能與實務提醒
BERT 微調 NER 在 Colab T4 上,2,000 筆訓練 2 epoch 約 8 分鐘。預訓練 NER 模型推論在 T4 上每秒可處理約 50 句、在 CPU 上約 5 句。
NER 模型部署到線上時,長文件處理是常見瓶頸。BERT 一次最多 512 token,但合約、新聞稿、學術論文常常超過。對應處理方式有三種:滑動視窗(切成多個 chunk 各自做 NER 再合併)、層次式(先用段落分類再對相關段落做 NER)、蒸餾(小模型取代大模型犧牲一點品質換速度)。
實務上企業 NER 系統通常會搭配「規則式後處理」:例如「已知客戶名單」、「已知產品型號」、「已知主管機關」當作白名單,模型預測出來後再用規則修正。這種「模型 + 規則」的混合架構在中文 NER 上特別有效,因為模型對長尾實體(罕見人名、新興品牌)的召回率偏低。
實戰:客服訊息中的實體抽取
NER 在企業客服最常見的應用是「從客戶留言自動抽取出訂單編號、產品型號、問題類型」。下面示範一個簡化版的客服 NER:
# 假設已經有客服訊息與標註資料
customer_messages = [
"我的訂單 #2025-0322-001 還沒收到,已經等三天了。",
"iPhone 15 Pro 的電池續航明顯比 14 差。",
"請問台北門市週六有開嗎?",
"投訴編號 TKT-20250320-1234 處理進度查詢。",
]
# 自訂實體類別
CUSTOM_LABELS = [
"O",
"B-ORDER_ID", "I-ORDER_ID",
"B-PRODUCT", "I-PRODUCT",
"B-LOCATION", "I-LOCATION",
"B-TICKET_ID", "I-TICKET_ID",
]
# 用正規表達式抽取常見模式當作 baseline
import re
def rule_based_ner(text):
entities = []
patterns = {
"ORDER_ID": r"#?\d{4}-\d{4}-\d{3}",
"TICKET_ID": r"TKT-\d{8}-\d{4}",
"PRODUCT": r"iPhone\s*\d+\s*Pro?",
}
for label, pattern in patterns.items():
for match in re.finditer(pattern, text):
entities.append({
"text": match.group(),
"label": label,
"start": match.start(),
"end": match.end(),
})
return entities
# 測試
for msg in customer_messages:
entities = rule_based_ner(msg)
print(f"訊息:{msg}")
for e in entities:
print(f" {e['label']}:{e['text']}(位置 {e['start']}-{e['end']})")
print()
# 輸出範例:
# 訊息:我的訂單 #2025-0322-001 還沒收到,已經等三天了。
# ORDER_ID:#2025-0322-001(位置 4-18)
#
# 訊息:iPhone 15 Pro 的電池續航明顯比 14 差。
# PRODUCT:iPhone 15 Pro(位置 0-13)
#
# 訊息:請問台北門市週六有開嗎?
# (沒有規則匹配,模型才能處理)
#
# 訊息:投訴編號 TKT-20250320-1234 處理進度查詢。
# TICKET_ID:TKT-20250320-1234(位置 4-21)
這段示範「規則式 NER」:用正則表達式抓取固定格式的實體。對「台北門市」這種開放式實體,規則抓不到,必須靠 BERT 模型。實務上規則 + 模型並行:規則抓高信心、固定格式;模型抓模糊、需要語意的部分;再用一個 merge 函式合併結果,這是業界標準做法。
小結
今天把命名實體辨識從概念到實作完整跑過:BIO 標註、token-level 分類、子詞對齊、預訓練模型推論、BERT 微調、可重複使用的預測函式。中文 NER 的特殊議題先用 spaCy 帶過,Day 7 會用 zhBERT 家族深入展開。明天 Day 7 會專門處理中文任務的特殊處理:斷詞、繁簡、zhBERT 家族,並示範如何用 bert-base-chinese 微調中文 NER 模型。
把 NER 結果接到下游應用
NER 本身只是把實體標出來,真正的價值是接到下游應用。常見的下游應用有四種:
- 知識圖譜構建:把 NER 結果當作節點,再搭配關係抽取(RE)連邊,就能建構領域知識圖譜。Day 30 的 GraphRAG 會用這條路徑。
- 結構化搜尋:把 NER 標籤當作搜尋過濾條件,例如「搜尋 2024 年提到台積電的合約」。這種「實體感知搜尋」比純關鍵字搜尋精準很多。
- 工單自動分類:把 NER 抽出的實體(產品型號、訂單編號)當作工單 metadata,自動分派給對應客服專員,減少人工標籤時間。
- 個資偵測與遮罩:金融、醫療產業的法規要求個資遮罩。NER 可以自動識別姓名、地址、身分證字號,再用規則把它們遮罩成「姓名 #1」、「地址 #2」之類的代號。
NER 在企業落地時最大的瓶頸不是模型品質,而是「標籤定義」。很多團隊低估了「怎麼定義一個實體」的難度。例如「台積電」算一個組織嗎?「台積電創辦人張忠謀」中的「張忠謀」是單獨的 PER,還是附屬於台積電?這些問題沒有標準答案,必須由領域專家開會決定,再讓模型學會。Day 11 的標註篇章會專門討論這個議題。
與資訊抽取的關係
NER 只是資訊抽取(Information Extraction,IE)的第一步。完整的 IE 流程通常包含:
- NER:找出實體(人、事、時、地)。
- 關係抽取(Relation Extraction,RE):找出實體之間的關係,例如「張忠謀 → 創辦 → 台積電」。
- 事件抽取(Event Extraction):找出事件及其參與者,例如「簽約事件:張忠謀、台積電、2024-03-22」。
- 指代消解(Coreference Resolution):找出代名詞指向的實體,例如「他」指「張忠謀」。
這四個任務在 2020 年之前是分開處理的,2020 年後開始有統一模型(例如 UniRE、UIE)把四個任務一起學。對企業應用來說,分開處理仍然比較主流,因為每個任務可以獨立評估、獨立最佳化。Day 30 的 GraphRAG 會再示範如何把 NER + RE 接到 RAG 系統。
結語
NER 是 NLP 第二大經典任務,2018 年 BERT 出現後品質大幅躍進,已成為企業知識庫、客服系統、合約審查的標配。今天學到的 BIO 標註、子詞對齊、entity-level 評估三個觀念,是做 NER 專案的基礎。中文 NER 因為語言特性而更難,明天會專門展開:斷詞、繁簡、zhBERT 家族,並示範一個完整的中文 NER 微調流程。明天,我們會進入中文 NLP 的細節,包括 bert-base-chinese 與 hfl/chinese-roberta-wwm-ext 等模型的差異,以及繁簡轉換在 NER 上的影響。
NER 的下一步是把它接到下游應用——資訊抽取、知識圖譜、結構化搜尋、個資遮罩——每一條都是企業級 NLP 的核心場景。建議讀完這篇後,挑一個自己的領域(合約、新聞、客服),用今天示範的 BERT 微調流程做一個小型 NER 模型;只要 100 到 500 筆標註就能跑出可用結果。實務上企業 NER 最大的瓶頸往往不是模型,而是「怎麼定義實體」,這部分會在 Day 11 的標註篇章完整展開,並且會示範 Cohen's Kappa 等標註者一致性指標的計算方式與門檻,作為標註品質管控的依據,並介紹常用的標註工具如 Label Studio 與 Doccano 的差異,幫助你在團隊中建立標準化的標註流程與驗收標準。
常見的 NER 評估陷阱
NER 評估有三個常見陷阱:
- 只看 token-level accuracy:83% 的 token 是 O,模型全部預測為 O 也有 83% 正確率。一定要用 entity-level F1。
- 嚴格 vs 寬鬆匹配:嚴格匹配要求 entity 的邊界與類別都對;寬鬆匹配只看類別不看邊界。CoNLL-2003 預設是嚴格匹配,企業實務常會加寬鬆匹配當輔助指標。
- 類別不平衡:LOC、ORG、PER、MISC 在 CoNLL-2003 的分布不均勻(PER 最多、MISC 最少)。整體 F1 高不代表每個類別都好,要看 macro F1。
對應的除錯技巧:印出每個類別的混淆矩陣,找出「常被誤判為哪個類別」;例如 NER 模型常把 ORG 誤判為 LOC,因為「台北 101」這種「地點 + 編號」容易被當作地點。
延伸資源
- CoNLL-2003 NER 資料集說明(Tjong Kim Sang & De Meulder, 2003):
https://www.aclweb.org/anthology/W03-0419/ - Hugging Face Token Classification 教學(2025):
https://huggingface.co/docs/transformers/tasks/token_classification - seqeval 函式庫:NER F1 計算(2025):
https://github.com/chakki-works/seqeval - spaCy 中文 NER 模型說明(2025):
https://spacy.io/models/zh - bert-base-NER 模型卡(dslim, 2022):
https://huggingface.co/dslim/bert-base-NER
留言
張貼留言