序列标注基础:从任务定义到数据格式
好,咱们正式开始讲序列标注。这是NLP里最基础、也最实用的技术之一。说白了,序列标注就是给文本里的每个字或词打上一个标签。你想想看,我们做命名实体识别,本质上就是在做序列标注——把“张三”标成“人名”,把“北京”标成“地点”。
我个人习惯把序列标注比作“给句子里的每个词发身份证”。每个词拿到的身份证上写着它的角色——是实体的一部分,还是普通词。这个比喻虽然简单,但很直观。
序列标注任务定义
先给个正式定义:给定一个输入序列 X = (x₁, x₂, ..., xₙ),我们要为每个位置预测一个标签 yᵢ。这个标签来自一个预定义的标签集合。
举个例子:
输入:我 在 北京 天安门 广场 玩
标签:O O B-LOC I-LOC I-LOC O
这里的 B-LOC 表示“地点实体的开始”,I-LOC 表示“地点实体的内部”,O 表示“非实体”。
核心要点:序列标注的输入和输出长度必须一致。每个输入位置都有一个对应的输出标签。这是它和文本分类、序列到序列任务最大的区别。
我在项目中遇到过不少新手,上来就问:“为什么我的模型输出比输入短?”嗯,这就是没理解序列标注的基本约束——长度对齐。
BIO 标注体系
BIO 是最常用的标注体系。B 代表 Begin(开始),I 代表 Inside(内部),O 代表 Outside(外部)。
具体规则:
- B-XXX:实体开始的第一个词
- I-XXX:实体内部的后续词
- O:非实体词
看个例子:
句子: 小明 在 北京 大学 读书
标签: B-PER O B-ORG I-ORG O
这里“小明”是人物实体,“北京大学”是组织实体。注意“北京”是 B-ORG,“大学”是 I-ORG,表示它们属于同一个实体。
小技巧:我建议新手先用 BIO 体系上手。它简单直观,90%的场景都够用。我曾经在一个电商项目里用 BIO 做商品属性抽取,效果就很不错。
BIESO 标注体系
BIESO 是 BIO 的增强版。它在 BIO 的基础上增加了两个标签:
- E-XXX:实体结束(End)
- S-XXX:单字实体(Single)
为什么要加这两个?你想想看,BIO 体系里,一个实体结束时没有明确标记。模型需要自己推断“哦,I 后面跟 O 了,说明实体结束了”。但有时候模型会搞混,尤其是实体边界不清晰的时候。
BIESO 就解决了这个问题:
句子: 我 爱 北京 天安门
BIO: O O B-LOC I-LOC
BIESO:O O B-LOC E-LOC
句子: 去 上海 玩
BIO: O B-LOC O
BIESO:O S-LOC O
看到区别了吗?“上海”是单字实体,BIO 用 B-LOC 表示,BIESO 用 S-LOC 明确标记。这样模型就知道“这是个完整的实体,就一个字”。
避坑指南:我曾经在一个医疗文本抽取项目里只用 BIO,结果模型总是把“北京医院”和“北京”搞混。后来换成 BIESO,边界识别准确率提升了 3 个点。如果你的实体长度差异很大(比如既有“京”这样的单字实体,又有“中国人民解放军总医院”这样的长实体),我强烈建议用 BIESO。
标注数据格式
数据格式决定了你怎么喂数据给模型。最常用的有两种:CoNLL 格式和 JSON 格式。
CoNLL 格式
CoNLL 格式来自 CoNLL 2003 共享任务,是序列标注的“标准普通话”。每行一个词,空行分隔句子。
我 O
在 O
北京 B-LOC
天安门 I-LOC
广场 I-LOC
玩 O
他 O
叫 B-PER
张三 I-PER
。 O
格式规则:
- 每行包含词和标签,用空格或制表符分隔
- 空行表示句子结束
- 文件通常包含多列(词、词性、组块标签、实体标签等)
实际项目中,CoNLL 格式经常扩展成多列:
我 O O
在 O O
北京 NS B-LOC
天安门 NS I-LOC
广场 NS I-LOC
玩 V O
第一列是词,第二列是词性标签,第三列是实体标签。这样一份数据可以同时训练词性标注和实体识别模型。
我的习惯:做数据预处理时,我一般用 Python 的 pandas 读取 CoNLL 文件。但要注意,空行分隔符在 pandas 里不太好处理。我通常自己写个解析器,按空行切分句子,再逐行处理。代码大概长这样:
def read_conll(filepath):
sentences = []
with open(filepath, 'r', encoding='utf-8') as f:
tokens, labels = [], []
for line in f:
line = line.strip()
if not line:
if tokens:
sentences.append((tokens, labels))
tokens, labels = [], []
else:
parts = line.split()
tokens.append(parts[0])
labels.append(parts[-1])
if tokens:
sentences.append((tokens, labels))
return sentences
JSON 格式
JSON 格式更灵活,适合现代深度学习框架。每个样本是一个 JSON 对象:
{
"text": "我在北京天安门广场玩",
"labels": ["O", "O", "B-LOC", "I-LOC", "I-LOC", "O"],
"entities": [
{"type": "LOC", "start": 2, "end": 5, "text": "北京天安门广场"}
]
}
JSON 格式的好处:
- 可以包含额外信息(如实体列表、文本原文)
- 易于序列化和反序列化
- 支持嵌套结构
我在实际项目中,通常用 JSON 格式做训练数据,用 CoNLL 格式做评测数据。为什么?因为 JSON 方便调试——你可以直接看实体列表对不对。CoNLL 格式则方便和传统工具(如 CRF++、Stanford NER)对接。
格式选择建议:
- 用 PyTorch/TensorFlow 做深度学习 → JSON 格式
- 用 CRF、HMM 等传统方法 → CoNLL 格式
- 团队协作、数据标注 → CoNLL 格式(标注工具大多支持)
- 模型部署、推理服务 → JSON 格式
标注体系对比总结
| 特性 | BIO | BIESO |
|---|---|---|
| 标签数量 | 2N+1(N为实体类型数) | 4N+1 |
| 边界标记 | 隐式(I→O表示结束) | 显式(E标记结束) |
| 单字实体 | 用B表示 | 用S明确标记 |
| 适用场景 | 实体长度均匀、边界清晰 | 实体长度差异大、边界模糊 |
| 实现复杂度 | 低 | 中 |
嗯,到这里序列标注的基础就讲完了。说白了,就是三件事:理解任务定义、选对标注体系、用对数据格式。下一章我们会讲怎么用这些标注数据训练一个真正的 NER 模型。到时候我会分享一些我在实际项目中踩过的坑——比如标签不平衡怎么处理、OOV 词怎么应对,这些都是实战中绕不开的问题。
公众号:蓝海资料掘金营,微信deep3321