4. 命名实体识别(NER):从规则到深度学习的进化之路
命名实体识别,简称 NER,是信息抽取里最基础也最关键的环节。说白了,就是让计算机从一段文本里找出人名、地名、作物名、病害名这些有特定意义的实体。我刚开始接触农业领域时,发现通用 NER 模型在农业文本上表现很差——"稻瘟病"会被拆成"稻"和"瘟病","红富士"被识别成颜色加富士山。嗯,这就是领域适配的重要性。
核心定义:命名实体识别(NER)是从非结构化文本中识别出具有特定意义的实体,并将其归类到预定义的类别中。在农业领域,常见的实体类型包括作物名称、病害名称、农药名称、地理位置、时间等。
下面这张图展示了 NER 技术从规则到深度学习的演进脉络,也是我这节课要讲的核心内容。
4.1 NER 任务定义
NER 本质上是一个序列标注问题。给定一个句子,模型需要为每个字或词打上一个标签,标明它属于哪个实体类型。我习惯用 BIO 标注体系——B 表示实体开始,I 表示实体内部,O 表示非实体。
举个例子,句子"水稻感染了稻瘟病"的标注结果应该是:
水/B-CROP 稻/I-CROP 感/O 染/O 了/O 稻/B-DISEASE 瘟/I-DISEASE 病/I-DISEASE
在农业知识图谱里,我一般把实体分为这几类:
| 实体类型 | 标签 | 示例 |
|---|---|---|
| 作物 | CROP | 水稻、小麦、玉米、红富士苹果 |
| 病害 | DISEASE | 稻瘟病、白粉病、锈病 |
| 虫害 | PEST | 蚜虫、红蜘蛛、稻飞虱 |
| 农药 | PESTICIDE | 吡虫啉、百菌清、草甘膦 |
| 地理位置 | LOC | 黑龙江、寿光、新疆 |
| 时间 | TIME | 2024年6月、抽穗期、苗期 |
我的经验:在农业领域,实体边界往往比通用领域更模糊。比如"小麦条锈病"是一个完整的病害名,但新手容易拆成"小麦"和"条锈病"两个实体。我建议在标注规范里明确:复合病害名作为一个整体实体处理。
4.2 基于规则的方法
规则法是最早的 NER 实现方式。说白了,就是靠词典加正则表达式硬匹配。我在早期做农业知识图谱时,第一版用的就是规则法——因为当时标注数据太少,只能先搭个能用的。
规则法的核心组件:
- 领域词典:收集所有已知的作物名、病害名、农药名等
- 正则表达式:匹配数字、日期、百分比等模式
- 上下文规则:比如"感染了"后面大概率跟病害名
一个简单的规则匹配示例:
import re
# 农业领域词典
crop_dict = {"水稻", "小麦", "玉米", "大豆", "棉花"}
disease_dict = {"稻瘟病", "白粉病", "锈病", "枯萎病"}
def rule_based_ner(text):
entities = []
# 词典匹配
for crop in crop_dict:
if crop in text:
entities.append((crop, "CROP"))
for disease in disease_dict:
if disease in text:
entities.append((disease, "DISEASE"))
# 正则匹配时间
time_pattern = r'\d{4}年\d{1,2}月'
for match in re.finditer(time_pattern, text):
entities.append((match.group(), "TIME"))
return entities
text = "2024年6月,水稻感染了稻瘟病"
print(rule_based_ner(text))
# 输出:[('2024年6月', 'TIME'), ('水稻', 'CROP'), ('稻瘟病', 'DISEASE')]
避坑指南:我曾经在做一个农业政策文档抽取项目时,完全依赖规则法。结果发现同一个"稻瘟病",在文档里出现了"水稻稻瘟病""稻瘟病""穗颈瘟"三种写法。规则法根本覆盖不全。所以规则法只适合做快速原型,千万别指望它搞定所有情况。
规则法的优缺点很明显:
| 优点 | 缺点 |
|---|---|
| 不需要标注数据 | 维护成本高,规则越写越多 |
| 在小规模数据上精度高 | 泛化能力差,遇到新词就失效 |
| 可解释性强 | 实体边界模糊时容易出错 |
| 部署简单,速度快 | 无法处理歧义和上下文依赖 |
4.3 基于统计的方法(CRF)
条件随机场,简称 CRF,是统计 NER 的扛把子。它不像规则法那样死板,而是通过学习标注数据中的特征模式来做预测。我当年从规则法切换到 CRF 时,感觉就像从手动挡换成了自动挡——省心多了。
CRF 的核心思想:给定输入序列 X,计算输出标签序列 Y 的条件概率 P(Y|X)。它最大的优势是能考虑标签之间的转移关系——比如 B-DISEASE 后面不能直接跟 O,必须先经过 I-DISEASE。
特征模板是 CRF 的关键。在农业领域,我常用的特征包括:
- 当前词本身(比如"稻瘟病")
- 前后各两个词(上下文窗口)
- 词性标注(名词、动词等)
- 是否在领域词典中
- 词的前后缀(比如"病"结尾的词很可能是病害)
一个简单的 CRF 训练流程:
# 使用 sklearn-crfsuite 库
import sklearn_crfsuite
from sklearn_crfsuite import metrics
# 特征提取函数
def word2features(sent, i):
word = sent[i][0]
features = {
'word': word,
'word.isdigit': word.isdigit(),
'word.suffix_1': word[-1:],
'word.suffix_2': word[-2:] if len(word) >= 2 else '',
'word.prefix_1': word[:1],
'word.prefix_2': word[:2] if len(word) >= 2 else '',
'word.in_crop_dict': word in crop_dict,
'word.in_disease_dict': word in disease_dict,
}
# 上下文特征
if i > 0:
features['prev_word'] = sent[i-1][0]
else:
features['BOS'] = True
if i < len(sent)-1:
features['next_word'] = sent[i+1][0]
else:
features['EOS'] = True
return features
# 训练 CRF 模型
crf = sklearn_crfsuite.CRF(
algorithm='lbfgs',
c1=0.1, # L1 正则化
c2=0.1, # L2 正则化
max_iterations=100,
)
crf.fit(X_train, y_train)
# 预测
y_pred = crf.predict(X_test)
print(metrics.flat_classification_report(y_test, y_pred))
我的经验:CRF 的特征工程很考验功底。我在做农业 NER 时发现,加入"词是否以'病'结尾"这个特征后,病害识别的 F1 值直接提升了 5 个百分点。你想想看,这就是领域知识的价值。
4.4 基于深度学习的方法
深度学习把 NER 带到了新高度。它不再需要手工设计特征,而是让模型自动学习文本的深层表示。我目前在实际项目中主要用两种架构:BiLSTM-CRF 和 BERT。
4.4.1 BiLSTM-CRF
BiLSTM-CRF 是深度学习 NER 的经典组合。BiLSTM 负责捕捉上下文信息,CRF 层负责约束标签之间的转移关系。说白了,BiLSTM 是"眼睛",CRF 是"大脑"——一个负责看,一个负责想。
架构示意图:
输入: "水稻感染了稻瘟病"
↓
词嵌入层: 每个词转成向量
↓
BiLSTM层: 前向+后向LSTM捕捉上下文
↓
CRF层: 约束标签转移,输出最优序列
↓
输出: B-CROP I-CROP O O O B-DISEASE I-DISEASE I-DISEASE
PyTorch 实现的核心代码:
import torch
import torch.nn as nn
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_to_id, embedding_dim, hidden_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,
bidirectional=True, batch_first=True)
self.hidden2tag = nn.Linear(hidden_dim, len(tag_to_id))
self.transitions = nn.Parameter(
torch.randn(len(tag_to_id), len(tag_to_id))
)
def forward(self, x):
embeds = self.embedding(x)
lstm_out, _ = self.lstm(embeds)
emissions = self.hidden2tag(lstm_out)
return emissions
避坑指南:我曾经在训练 BiLSTM-CRF 时,发现模型总是把"玉米"识别成"玉"和"米"两个词。后来检查发现是分词器的问题——农业领域的专有名词经常被错误切分。我建议在农业 NER 中使用字级别的标注,而不是词级别,能避免很多分词带来的错误。
4.4.2 BERT
BERT 的出现让 NER 又上了一个台阶。它通过预训练加微调的方式,能更好地理解上下文语义。我最近的项目里,BERT 在农业 NER 上的 F1 值比 BiLSTM-CRF 高了 8-10 个百分点。
用 BERT 做 NER 的流程:
- 加载预训练的 BERT 模型(比如 bert-base-chinese)
- 在农业标注数据上微调
- 在输出层加一个线性分类器做标签预测
- 可以再加 CRF 层做标签约束(BERT+CRF)
HuggingFace 实现示例:
from transformers import BertTokenizer, BertForTokenClassification
from transformers import Trainer, TrainingArguments
# 加载预训练模型
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForTokenClassification.from_pretrained(
'bert-base-chinese',
num_labels=len(tag_to_id)
)
# 数据预处理
def tokenize_and_align_labels(examples):
tokenized_inputs = tokenizer(
examples['text'],
truncation=True,
padding='max_length',
max_length=128
)
# 对齐标签
labels = align_labels_with_tokens(
tokenized_inputs, examples['ner_tags']
)
tokenized_inputs['labels'] = labels
return tokenized_inputs
# 训练参数
training_args = TrainingArguments(
output_dir='./agriculture_ner_model',
num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=2e-5,
save_steps=500,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()
三种方法的对比:
| 方法 | 数据需求 | 特征工程 | 泛化能力 | 农业场景适用性 |
|---|---|---|---|---|
| 规则法 | 无 | 高(手工写规则) | 差 | 小规模、固定格式文本 |
| CRF | 中等(千级标注) | 中(设计特征模板) | 中 | 中等规模、领域特征明显 |
| BiLSTM-CRF | 较大(万级标注) | 低 | 较好 | 大规模语料、通用场景 |
| BERT | 较大(万级标注) | 极低 | 强 | 大规模语料、复杂语义场景 |
我的建议:在实际项目中,我通常采用"规则法兜底 + BERT 主力"的混合策略。规则法处理那些格式固定的文本(比如农药登记号、日期),BERT 处理需要语义理解的复杂文本。这样既保证了精度,又控制了成本。
嗯,到这里 NER 的核心方法就讲完了。从规则到统计再到深度学习,每一步都是在前人基础上往前走。你想想看,十年前做农业 NER 还得靠人工写几百条规则,现在用 BERT 微调一下就能达到 95% 以上的准确率。技术迭代的速度,确实让人感慨。