4.2 命名实体识别 命名实体识别(Named Entity Recognition,NER)是信息抽取领域的核心任务,目标是从非结构化文本中识别并分类预定义类别的命名实体,如人名(PER)、地名(LOC)、机构名(ORG)等。NER 是知识图谱构建、问答系统、搜索引擎、金融风控等众多应用的基础组件。 4.2.1 NER 任务概述 什么是命名实体 命名实体指文本中具有特定意义的实体,通常分为三大类和若干扩展类别: 实体类(ENAMEX):人名(张三)、地名(北京)、机构名(清华大学)、产品名(iPhone) 时间类(TIMEX):日期(2024年3月15日)、时间(下午三点)、时间段(去年全年) 数字类(NUMEX):金额(500万元)、百分比(3.
命名实体识别(Named Entity Recognition,NER)是信息抽取领域的核心任务,目标是从非结构化文本中识别并分类预定义类别的命名实体,如人名(PER)、地名(LOC)、机构名(ORG)等。NER 是知识图谱构建、问答系统、搜索引擎、金融风控等众多应用的基础组件。
命名实体指文本中具有特定意义的实体,通常分为三大类和若干扩展类别:
中文 NER 的独特挑战在于:
无显式边界:中文没有空格分隔词,"南京市长江大桥"可以切分为"南京市/长江大桥"或"南京/市长/江大桥",边界歧义极大。
嵌套实体:"北京大学计算机系"中包含嵌套关系——"北京大学"是机构名,整体也是机构名;"计算机系"是部门名。
新实体涌现:网络流行语、新公司名、新品牌不断出现,训练集外的实体识别率受限。
复合实体:"中国工商银行北京分行"是一个完整的机构名,但包含"中国工商银行"和"北京"两个子实体。
NER 任务本质上是 Token 级别的序列标注问题。最常用的标注方案是 BIO 体系:
举例说明:
文本: 北京 大学 在 北京 海淀区 标注: B-ORG I-ORG O B-LOC B-LOC I-LOC
对于 BERT 这类中文字符级模型,每个中文字符对应一个标签:
字符: 北 京 大 学 在 北 京 海 淀 区 标注: B-ORG I-ORG I-ORG I-ORG O B-LOC I-LOC I-LOC I-LOC I-LOC
from transformers import AutoModelForTokenClassification # 定义标签映射 label_list = ["O", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC", "B-MISC", "I-MISC"] label2id = {label: i for i, label in enumerate(label_list)} id2label = {i: label for i, label in enumerate(label_list)} # 加载预训练模型,自动添加 Token 分类头 model = AutoModelForTokenClassification.from_pretrained( "bert-base-chinese", num_labels=len(label_list), id2label=id2label, label2id=label2id, ) # 分类头结构 # (dropout): Dropout(p=0.1) # (classifier): Linear(in_features=768, out_features=9)
模型对每个 token 输出一个 9 维向量,通过 Softmax 得到各类别的概率分布。
NER 数据处理的关键在于对齐——原始文本中的字符标签与 Tokenizer 输出的 Token 标签必须严格对齐:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") def tokenize_and_align_labels(examples): tokenized_inputs = tokenizer( examples["tokens"], # 已分词的字符列表 truncation=True, max_length=128, is_split_into_words=True, # 关键:告诉 tokenizer 输入已分词 ) labels = [] for i, label in enumerate(examples["ner_tags"]): word_ids = tokenized_inputs.word_ids(batch_index=i) previous_word_idx = None label_ids = [] for word_idx in word_ids: if word_idx is None: # 特殊 token([CLS]、[SEP])标记为 -100,计算 loss 时忽略 label_ids.append(-100) elif word_idx != previous_word_idx: # 单词的首个 token,保留原始标签 label_ids.append(label[word_idx]) else: # 单词的后续 subword token,标记为 -100 或 I- 标签 label_ids.append(-100) previous_word_idx = word_idx labels.append(label_ids) tokenized_inputs["labels"] = labels return tokenized_inputs
标准的 BERT Token 分类器对每个 token 独立预测标签,但 NER 标签之间有严格的转移约束:
条件随机场(Conditional Random Field,CRF)通过学习标签之间的转移概率,自动捕捉这些约束,显著减少非法标注序列。
import torch import torch.nn as nn from transformers import BertPreTrainedModel, BertModel class BERT_CRF(nn.Module): def __init__(self, bert_model_name, num_labels): super().__init__() self.bert = BertModel.from_pretrained(bert_model_name) self.dropout = nn.Dropout(0.1) self.classifier = nn.Linear(768, num_labels) self.num_labels = num_labels # CRF 转移矩阵:transitions[i][j] 表示从标签 j 转移到标签 i 的分数 self.transitions = nn.Parameter( torch.randn(num_labels, num_labels) ) # 起始和结束转移分数 self.start_transitions = nn.Parameter(torch.randn(num_labels)) self.end_transitions = nn.Parameter(torch.randn(num_labels)) def _viterbi_decode(self, emissions, mask): """Viterbi 算法解码最优标签序列""" batch_size, seq_len, num_labels = emissions.shape # 初始化 score = self.start_transitions + emissions[:, 0] # [batch, num_labels] history = [] for i in range(1, seq_len): # 计算所有可能的转移分数 score_t = ( score.unsqueeze(2) # [batch, prev_labels, 1] + self.transitions.unsqueeze(0) # [1, prev_labels, curr_labels] + emissions[:, i].unsqueeze(1) # [batch, 1, curr_labels] ) score_t, indices = score_t.max(dim=1) history.append(indices) score = score_t # 加入结束转移 score = score + self.end_transitions # 回溯最优路径 _, best_last = score.max(dim=1) best_tags = [best_last] for hist in reversed(history): best_last = hist[0, best_last] best_tags.append(best_last) best_tags.reverse() return torch.stack(best_tags, dim=1) def forward(self, input_ids, attention_mask, labels=None): outputs = self.bert(input_ids, attention_mask=attention_mask) sequence_output = outputs.last_hidden_state sequence_output = self.dropout(sequence_output) emissions = self.classifier(sequence_output) if labels is not None: # 计算 CRF 负对数似然损失 # ... (省略 CRF 前向算法实现) pass # 解码最优标签序列 decoded_tags = self._viterbi_decode(emissions, attention_mask.bool()) return {"logits": emissions, "predicted_tags": decoded_tags}
在 CoNLL-2003 英文 NER 评测上,BERT-CRF 相比 BERT-Softmax 通常提升 0.5-1.5 个 F1 点:
| 方法 | Precision | Recall | F1 |
|---|---|---|---|
| BERT + Softmax | 91.2 | 89.8 | 90.5 |
| BERT + CRF | 92.1 | 90.5 | 91.3 |
| RoBERTa + CRF | 93.0 | 91.2 | 92.1 |
CRF 的优势在中文 NER 上更为明显,因为中文实体边界歧义问题更严重,CRF 的全局约束能显著减少边界错误。
NER 的评估不是按 token 计算,而是按完整实体计算——只有当实体的所有 token 都被正确识别,且类型正确时,才算命中:
def compute_ner_metrics(predictions, labels, id2label): """计算实体级别的 Precision、Recall、F1""" def extract_entities(tag_seq, id2label): """从 BIO 标注序列中提取实体""" entities = [] current_entity = None for i, tag_id in enumerate(tag_seq): if tag_id == -100: continue tag = id2label[tag_id] if tag.startswith("B-"): if current_entity: entities.append(current_entity) current_entity = {"type": tag[2:], "start": i, "end": i + 1} elif tag.startswith("I-") and current_entity and tag[2:] == current_entity["type"]: current_entity["end"] = i + 1 else: if current_entity: entities.append(current_entity) current_entity = None if current_entity: entities.append(current_entity) return set((e["type"], e["start"], e["end"]) for e in entities) total_pred, total_true, total_correct = 0, 0, 0 for pred, gold in zip(predictions, labels): pred_entities = extract_entities(pred, id2label) gold_entities = extract_entities(gold, id2label) total_pred += len(pred_entities) total_true += len(gold_entities) total_correct += len(pred_entities & gold_entities) precision = total_correct / total_pred if total_pred else 0 recall = total_correct / total_true if total_true else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0 return {"precision": precision, "recall": recall, "f1": f1}
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir="./ner_model", num_train_epochs=10, # NER 通常需要更多 epoch per_device_train_batch_size=16, learning_rate=3e-5, weight_decay=0.01, warmup_ratio=0.1, eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="f1", fp16=True, gradient_accumulation_steps=2, # 等效 batch_size=32 )
在医疗领域,NER 用于从电子病历中抽取关键信息:
# 使用专门领域的预训练模型 model = AutoModelForTokenClassification.from_pretrained( "hfl/chinese-ner-medical", # 医疗领域中文 NER 模型 )
医疗 NER 的特殊挑战:术语复杂("慢性粒细胞白血病"是一个实体),同义词多("高血压"= "血压升高"),新药名不断出现。
金融场景中,NER 用于信息抽取和风控:
# 金融 NER 典型 pipeline text = "腾讯控股2024年第一季度营收1595亿元,同比增长6%" # 抽取结果: # ORG: 腾讯控股 # DATE: 2024年第一季度 # MONEY: 1595亿元 # PERCENT: 6%
from transformers import pipeline nlp = pipeline( "ner", model="./ner_model/best", tokenizer="./ner_model/best", aggregation_strategy="simple", # 自动合并 BIO 片段为完整实体 device=0, ) results = nlp("马云在杭州创办了阿里巴巴集团") for entity in results: print(f"{entity['entity_group']}: {entity['word']} " f"(置信度: {entity['score']:.4f})") # PER: 马云 (0.9876) # LOC: 杭州 (0.9623) # ORG: 阿里巴巴集团 (0.9512)
通过 HuggingFace Transformers,你可以快速构建一个工业级的 NER 系统。BERT-CRF 的组合在大多数场景下表现优异,结合领域预训练和 LoRA 微调,即使数据量有限也能取得不错的效果。