序列标注基础:从任务定义到数据格式

好,咱们正式开始讲序列标注。这是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