第一次作业 · BiLSTM-CRF 中文命名实体识别

任务说明

在 ResumeNER 简历领域中文数据集上实现并训练一个 BiLSTM + CRF 序列标注模型,识别 8 类命名实体。数据以字符为单位,每行是“字符 + BIO 标签”,空行分隔句子:

高 B-NAME
勇 I-NAME
: O

中 B-CONT
国 I-CONT

BIO 中,B-* 表示实体起点,I-* 表示同一实体内部,O 表示不属于实体。8 类实体加上 O 后共有 17 个标签。

标签含义标签含义
NAME人名TITLE职称、头衔
CONT国籍EDU学历
RACE民族PRO专业
LOC籍贯ORG机构、组织
查看实现与实验结果

数据集

划分句子数字符数
训练集3,821124,099
开发集46313,890
测试集47715,100

模型怎样工作

整个模型可以压缩成四步:

字符 ID
  → 128 维字符向量
  → 双向 LSTM 编码上下文
  → 线性层产生每个位置的 17 个发射分数
  → CRF 对整条标签路径打分
  → Viterbi 解码出最高分路径

对位置 tt,双向 LSTM 把左到右和右到左的状态拼接:

ht=[ht→;ht←].h_t=[\overrightarrow{h_t};\overleftarrow{h_t}].

这里的总隐藏维度是 256,即两个方向各 128 维。线性层再把 hth_t 映射为 17 个标签的发射分数。这样,BiLSTM 负责回答“结合上下文,这个字像什么标签”。

CRF 则进一步回答“整条标签序列是否协调”。设 et(yt)e_t(y_t) 是位置 tt 选择标签 yty_t 的发射分数,Ai,jA_{i,j} 是从标签 ii 转到标签 jj 的转移分数,则一条路径的分数可写为

s(x,y)=Astart,y1+∑t=1Tet(yt)+∑t=2TAyt−1,yt+AyT,end.s(x,y)=A_{\mathrm{start},y_1} +\sum_{t=1}^{T}e_t(y_t) +\sum_{t=2}^{T}A_{y_{t-1},y_t} +A_{y_T,\mathrm{end}}.

训练时最大化金标签路径的条件对数似然:

log⁡P(y∣x)=s(x,y)−log⁡∑y′exp⁡s(x,y′).\log P(y\mid x)=s(x,y)-\log\sum_{y'}\exp s(x,y').

分母要汇总所有可能标签路径,代码用前向算法动态规划计算;推理时不需要枚举所有路径,而是用 Viterbi 算法保留每一步的最大分数和回溯指针。CRF 层是手写实现,没有依赖第三方 CRF 库。

一个必须更正的边界:CRF 没有硬编码 BIO 规则

原作业说明曾把 CRF 描述成会“保证输出序列合法”,但实际代码并没有硬编码 BIO 合法转移。句首、句尾和标签间转移参数都从 [−0.1,0.1][-0.1,0.1] 均匀初始化,然后随训练数据一起学习;代码中没有把 O → I-ORG、B-NAME → I-ORG 等非法转移屏蔽为负无穷。

因此,更准确的说法是:

  • CRF 能从数据中学习标签转移偏好,通常会压低不合理路径的分数;
  • 它仍可能输出违反 BIO 约定的序列,不能给出形式上的合法性保证;
  • 推理脚本只在 I-* 类型与当前实体一致时继续拼接,否则就结束当前实体。这个后处理能容忍异常序列,但也不等于约束了解码过程。

如果要严格保证合法性,需要显式建立允许转移矩阵,在训练与 Viterbi 解码时把非法路径屏蔽掉。

训练设置

模型共 498,516 个可训练参数,完全使用字符级输入,没有加载预训练词向量。

设置取值
字表大小1,794
字向量维度128
BiLSTM 总隐藏维度256
LSTM 层数1
Dropout0.5
Batch size64
优化器Adam
学习率10−310^{-3}
Weight decay10−510^{-5}
最大训练轮数40
早停 patience8
梯度裁剪5.0
最低字符频次1
随机种子42

依赖版本为 PyTorch 2.12.0、NumPy 2.4.6 和 seqeval 1.2.2。默认使用 CPU,也可以通过参数选择 MPS 或 CUDA。

怎样复现

进入第一次作业目录后运行:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python train.py
python predict.py --text "高勇:男,中国国籍,汉族,本科学历,工程师。"

训练脚本每轮在开发集上计算实体级 F1,按开发集 F1 选择最佳轮次,最后在测试集上统一评估。评价采用 seqeval 的严格实体级匹配:实体类型和完整边界都正确,才算一个真正例;它比逐字准确率更能反映抽取质量。

ResumeNER 实验结果

CPU 训练耗时 600.9 秒。第 39 轮取得最佳开发集 F1 0.9294,测试集结果如下:

指标数值
Precision0.9276
Recall0.9356
F10.9316
测试实体数1,630
实体类型PrecisionRecallF1Support
CONT1.00001.00001.000028
EDU0.96430.96430.9643112
LOC1.00000.83330.90916
NAME0.99100.98210.9865112
ORG0.90390.91860.9112553
PRO0.85290.87880.865733
RACE1.00001.00001.000014
TITLE0.92930.93650.9329772
Micro average0.92760.93560.93161,630

ORG 名称往往较长、形式变化大,PRO 的测试样本又只有 33 个,因此两类更难;LOC 的召回率看似明显偏低,但测试支持数只有 6,不能据此做很强的泛化结论。后续可以尝试预训练字向量或 BERT 编码器、字形与词典特征,以及针对低频类别的数据增强。

评论