第一次作业 · 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,821 | 124,099 |
| 开发集 | 463 | 13,890 |
| 测试集 | 477 | 15,100 |
模型怎样工作
整个模型可以压缩成四步:
字符 ID
→ 128 维字符向量
→ 双向 LSTM 编码上下文
→ 线性层产生每个位置的 17 个发射分数
→ CRF 对整条标签路径打分
→ Viterbi 解码出最高分路径
对位置 ,双向 LSTM 把左到右和右到左的状态拼接:
这里的总隐藏维度是 256,即两个方向各 128 维。线性层再把 映射为 17 个标签的发射分数。这样,BiLSTM 负责回答“结合上下文,这个字像什么标签”。
CRF 则进一步回答“整条标签序列是否协调”。设 是位置 选择标签 的发射分数, 是从标签 转到标签 的转移分数,则一条路径的分数可写为
训练时最大化金标签路径的条件对数似然:
分母要汇总所有可能标签路径,代码用前向算法动态规划计算;推理时不需要枚举所有路径,而是用 Viterbi 算法保留每一步的最大分数和回溯指针。CRF 层是手写实现,没有依赖第三方 CRF 库。
一个必须更正的边界:CRF 没有硬编码 BIO 规则
原作业说明曾把 CRF 描述成会“保证输出序列合法”,但实际代码并没有硬编码 BIO 合法转移。句首、句尾和标签间转移参数都从 均匀初始化,然后随训练数据一起学习;代码中没有把 O → I-ORG、B-NAME → I-ORG 等非法转移屏蔽为负无穷。
因此,更准确的说法是:
- CRF 能从数据中学习标签转移偏好,通常会压低不合理路径的分数;
- 它仍可能输出违反 BIO 约定的序列,不能给出形式上的合法性保证;
- 推理脚本只在
I-*类型与当前实体一致时继续拼接,否则就结束当前实体。这个后处理能容忍异常序列,但也不等于约束了解码过程。
如果要严格保证合法性,需要显式建立允许转移矩阵,在训练与 Viterbi 解码时把非法路径屏蔽掉。
训练设置
模型共 498,516 个可训练参数,完全使用字符级输入,没有加载预训练词向量。
| 设置 | 取值 |
|---|---|
| 字表大小 | 1,794 |
| 字向量维度 | 128 |
| BiLSTM 总隐藏维度 | 256 |
| LSTM 层数 | 1 |
| Dropout | 0.5 |
| Batch size | 64 |
| 优化器 | Adam |
| 学习率 | |
| Weight decay | |
| 最大训练轮数 | 40 |
| 早停 patience | 8 |
| 梯度裁剪 | 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,测试集结果如下:
| 指标 | 数值 |
|---|---|
| Precision | 0.9276 |
| Recall | 0.9356 |
| F1 | 0.9316 |
| 测试实体数 | 1,630 |
| 实体类型 | Precision | Recall | F1 | Support |
|---|---|---|---|---|
CONT | 1.0000 | 1.0000 | 1.0000 | 28 |
EDU | 0.9643 | 0.9643 | 0.9643 | 112 |
LOC | 1.0000 | 0.8333 | 0.9091 | 6 |
NAME | 0.9910 | 0.9821 | 0.9865 | 112 |
ORG | 0.9039 | 0.9186 | 0.9112 | 553 |
PRO | 0.8529 | 0.8788 | 0.8657 | 33 |
RACE | 1.0000 | 1.0000 | 1.0000 | 14 |
TITLE | 0.9293 | 0.9365 | 0.9329 | 772 |
| Micro average | 0.9276 | 0.9356 | 0.9316 | 1,630 |
ORG 名称往往较长、形式变化大,PRO 的测试样本又只有 33 个,因此两类更难;LOC 的召回率看似明显偏低,但测试支持数只有 6,不能据此做很强的泛化结论。后续可以尝试预训练字向量或 BERT 编码器、字形与词典特征,以及针对低频类别的数据增强。