跳到主要內容

NLP Day 6 命名實體辨識與序列標註

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 本身只是把實體標出來,真正的價值是接到下游應用。常見的下游應用有四種:

  1. 知識圖譜構建:把 NER 結果當作節點,再搭配關係抽取(RE)連邊,就能建構領域知識圖譜。Day 30 的 GraphRAG 會用這條路徑。
  2. 結構化搜尋:把 NER 標籤當作搜尋過濾條件,例如「搜尋 2024 年提到台積電的合約」。這種「實體感知搜尋」比純關鍵字搜尋精準很多。
  3. 工單自動分類:把 NER 抽出的實體(產品型號、訂單編號)當作工單 metadata,自動分派給對應客服專員,減少人工標籤時間。
  4. 個資偵測與遮罩:金融、醫療產業的法規要求個資遮罩。NER 可以自動識別姓名、地址、身分證字號,再用規則把它們遮罩成「姓名 #1」、「地址 #2」之類的代號。

NER 在企業落地時最大的瓶頸不是模型品質,而是「標籤定義」。很多團隊低估了「怎麼定義一個實體」的難度。例如「台積電」算一個組織嗎?「台積電創辦人張忠謀」中的「張忠謀」是單獨的 PER,還是附屬於台積電?這些問題沒有標準答案,必須由領域專家開會決定,再讓模型學會。Day 11 的標註篇章會專門討論這個議題。

與資訊抽取的關係

NER 只是資訊抽取(Information Extraction,IE)的第一步。完整的 IE 流程通常包含:

  1. NER:找出實體(人、事、時、地)。
  2. 關係抽取(Relation Extraction,RE):找出實體之間的關係,例如「張忠謀 → 創辦 → 台積電」。
  3. 事件抽取(Event Extraction):找出事件及其參與者,例如「簽約事件:張忠謀、台積電、2024-03-22」。
  4. 指代消解(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 評估有三個常見陷阱:

  1. 只看 token-level accuracy:83% 的 token 是 O,模型全部預測為 O 也有 83% 正確率。一定要用 entity-level F1。
  2. 嚴格 vs 寬鬆匹配:嚴格匹配要求 entity 的邊界與類別都對;寬鬆匹配只看類別不看邊界。CoNLL-2003 預設是嚴格匹配,企業實務常會加寬鬆匹配當輔助指標。
  3. 類別不平衡: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

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...