序列标注基础:从任务定义到标注体系

做实体识别,说白了就是给文本里的每个字或词打标签。我刚开始接触这个领域时,觉得这不就是个分类问题嘛——把“张三”标成人名不就完了?后来才发现,事情远没那么简单。

你想想看,一个词在句子里的角色是动态的。“苹果”可以是水果,也可以是公司名。怎么让模型知道该标哪个?这就引出了我们今天要聊的核心——序列标注。

什么是序列标注?

序列标注,就是把输入序列(比如一句话)里的每个元素,都分配一个预定义的标签。拿实体识别来说,输入是“李明在北京上班”,输出就是“李/人、明/人、在/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 等原生支持

本章知识体系

下面这张图总结了序列标注的核心脉络,从任务定义到标注体系再到数据格式,每一步都有对应的选择策略。

序列标注知识体系 序列标注任务定义 BIO 标注体系 BIOSO 标注体系 CoNLL 格式 JSON 格式 根据场景选择标注体系与数据格式 小数据量、快速实验 → BIO + CoNLL 生产系统、复杂标注 → BIOSO + JSON