4. 命名实体识别(NER):从规则到深度学习的进化之路

命名实体识别,简称 NER,是信息抽取里最基础也最关键的环节。说白了,就是让计算机从一段文本里找出人名、地名、作物名、病害名这些有特定意义的实体。我刚开始接触农业领域时,发现通用 NER 模型在农业文本上表现很差——"稻瘟病"会被拆成"稻"和"瘟病","红富士"被识别成颜色加富士山。嗯,这就是领域适配的重要性。

核心定义:命名实体识别(NER)是从非结构化文本中识别出具有特定意义的实体,并将其归类到预定义的类别中。在农业领域,常见的实体类型包括作物名称、病害名称、农药名称、地理位置、时间等。

下面这张图展示了 NER 技术从规则到深度学习的演进脉络,也是我这节课要讲的核心内容。

命名实体识别技术演进路线 基于规则的方法 词典 + 正则表达式 基于统计的方法 HMM / CRF 深度学习方法 BiLSTM-CRF / BERT 特征工程强度:高 → 中 → 低 | 模型复杂度:低 → 中 → 高 | 标注数据需求:低 → 中 → 高 代表技术 词典匹配 正则表达式 规则模板 代表技术 最大熵模型 条件随机场(CRF) 特征模板 代表技术 BiLSTM-CRF BERT + CRF 预训练 + 微调 适用场景:规则法适合小规模固定格式 | CRF适合中等规模标注数据 | 深度学习适合大规模语料

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 的流程:

  1. 加载预训练的 BERT 模型(比如 bert-base-chinese)
  2. 在农业标注数据上微调
  3. 在输出层加一个线性分类器做标签预测
  4. 可以再加 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% 以上的准确率。技术迭代的速度,确实让人感慨。