序列标注基础:从任务定义到标注体系
做实体识别,说白了就是给文本里的每个字或词打标签。我刚开始接触这个领域时,觉得这不就是个分类问题嘛——把“张三”标成人名不就完了?后来才发现,事情远没那么简单。
你想想看,一个词在句子里的角色是动态的。“苹果”可以是水果,也可以是公司名。怎么让模型知道该标哪个?这就引出了我们今天要聊的核心——序列标注。
什么是序列标注?
序列标注,就是把输入序列(比如一句话)里的每个元素,都分配一个预定义的标签。拿实体识别来说,输入是“李明在北京上班”,输出就是“李/人、明/人、在/O、北/地、京/地、上/O、班/O”。
我习惯把它理解成“给每个字发身份证”。模型的任务,就是根据上下文判断每个字的身份。
核心定义:给定一个长度为 n 的序列 X = (x₁, x₂, ..., xₙ),为每个位置 i 预测一个标签 yᵢ ∈ Y,其中 Y 是预定义的标签集合。
这里有个关键点:标签之间不是独立的。比如“李”被标成人名开头(B-PER),那“明”大概率是人名内部(I-PER),而不是地名。这种依赖关系,正是序列标注区别于普通分类的地方。
BIO 标注体系:最经典的方案
BIO 是实体标注的“Hello World”。它把标签分成三类:
- B-xxx(Begin):实体的开始
- I-xxx(Inside):实体的内部
- O(Outside):非实体
举个例子,句子“张三去北京开会”用 BIO 标出来就是:
张 B-PER
三 I-PER
去 O
北 B-LOC
京 I-LOC
开 O
会 O
我在项目里第一次用 BIO 时,踩过一个坑。当时有个实体叫“张三丰”,我标成了 B-PER、I-PER、I-PER。结果模型训练完,遇到“张三”也标成 B-PER、I-PER,但“三”后面跟的不是“丰”而是“去”,模型就懵了。后来我才意识到,BIO 对实体边界的约束其实很弱——它只保证 I 前面必须有同类型的 B 或 I,但不保证实体长度合理。
我的经验:BIO 适合实体长度相对固定的场景。如果实体长度变化很大(比如从2个字到20个字),建议考虑更严格的标注体系。
BIOSO 标注体系:解决相邻实体问题
BIO 有个明显的缺陷——它无法区分两个相邻的同类型实体。比如“北京上海”,如果标成:
北 B-LOC
京 I-LOC
上 I-LOC
海 I-LOC
模型会认为这是一个实体“北京上海”,而不是两个。怎么办?BIOSO 就是来救场的。
BIOSO 在 BIO 的基础上加了两个标签:
- S-xxx(Single):单字实体
- O(Outside):非实体(含义不变)
用 BIOSO 标“北京上海”就清晰了:
北 B-LOC
京 I-LOC
上 B-LOC
海 I-LOC
等等,这不还是 BIO 吗?别急,看这个例子——“我去京沪出差”。“京”和“沪”都是单字地名,用 BIO 标会变成:
我 O
去 O
京 B-LOC
沪 B-LOC
出 O
差 O
两个 B-LOC 连在一起,模型会困惑:第二个 B 是新实体开始,还是标注错误?用 BIOSO 就明确多了:
我 O
去 O
京 S-LOC
沪 S-LOC
出 O
差 O
S-LOC 明确表示这是一个独立的单字实体,不会和前后混淆。
注意:BIOSO 的标签数量比 BIO 多,意味着模型需要学习更多类别。如果你的数据量不大(比如少于1万条),BIO 可能更稳妥。我曾经在一个小数据集上硬上 BIOSO,结果模型过拟合得一塌糊涂。
标注体系对比
| 特性 | BIO | BIOSO |
|---|---|---|
| 标签数量 | 2N+1(N为实体类型数) | 3N+1 |
| 相邻实体区分 | 不支持 | 支持 |
| 单字实体处理 | 用B-xxx,语义模糊 | 用S-xxx,语义明确 |
| 数据需求 | 较低 | 较高 |
| 适用场景 | 实体长度稳定、无相邻实体 | 实体长度多变、有相邻实体 |
标注数据格式:CoNLL 与 JSON
格式这东西,看着简单,但选错了能让你多写几百行解析代码。我见过太多人在这上面浪费时间了。
CoNLL 格式
CoNLL 格式来自计算自然语言学习会议(CoNLL)的评测任务。它的核心思想是“一行一个 ,空行分隔句子”。
李 B-PER
明 I-PER
去 O
北 B-LOC
京 I-LOC
上 O
班 O
王 B-PER
五 I-PER
在 O
深 B-LOC
圳 I-LOC
这种格式的优点:
- 人类可读性强,用文本编辑器就能直接看
- 按行对齐,方便做序列级别的处理
- 几乎所有序列标注框架都原生支持
缺点也很明显:
- 不支持嵌套实体(比如“北京大学”里的“北京”是地名,“北京大学”是机构名)
- 扩展信息(如词性、依存关系)需要额外列,格式容易乱
我的习惯:做原型验证时用 CoNLL,因为可以直接用 cat 和 grep 查看数据。但一旦涉及复杂标注(比如嵌套实体、实体属性),我会立刻转 JSON。
JSON 格式
JSON 格式更灵活,可以表达复杂的标注结构。一个典型的实体标注 JSON 长这样:
{
"text": "李明去北京上班",
"entities": [
{"type": "PER", "start": 0, "end": 2, "text": "李明"},
{"type": "LOC", "start": 3, "end": 5, "text": "北京"}
]
}
start 和 end 是字符级别的偏移量。注意,end 是开区间,也就是“北京”占位置 3 和 4,end=5 表示结束位置的下一个。
JSON 的优势:
- 支持嵌套实体(可以在 entities 里再套 entities)
- 可以附加任意元信息(如实体ID、置信度、来源)
- 与 Web 系统、数据库交互方便
但 JSON 也有坑。我记得有一次,标注员在 JSON 里把 start 和 end 写反了,模型训练了三天才发现。从那以后,我每次拿到 JSON 数据都会先跑一个校验脚本,检查偏移量是否合法、是否越界。
格式选择建议
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 快速原型、小规模实验 | CoNLL | 简单直接,无需解析库 |
| 生产系统、大规模标注 | JSON | 灵活、可扩展、易校验 |
| 嵌套实体标注 | JSON | CoNLL 无法表达层级关系 |
| 与深度学习框架集成 | CoNLL | Hugging Face、AllenNLP 等原生支持 |
本章知识体系
下面这张图总结了序列标注的核心脉络,从任务定义到标注体系再到数据格式,每一步都有对应的选择策略。