简介:这份操作手册面向自然语言处理方向的研究人员、工程师及学生,以及希望搭建高质量语料加工流程的技术团队,系统讲解如何借助Label Studio完成文本标注并衔接UIE框架的下游训练。内容覆盖安装配置、项目创建、数据上传、标签构建、任务标注、数据导出与格式转换全流程,并针对命名实体识别、关系抽取、事件抽取、文本分类、句子级情感分类及实体/评价维度分类等任务给出具体实现方案,同时深入解析prompt构造原则对零样本效果的影响,如关系类型P需满足“{S}的{P}为{O}的语义合理性。资源包为1个PDF文件,约4.68MB,结构紧凑,便于按章节查阅与实操对照。目前已有810人学习,适合需要独立完成标注平台搭建、为模型训练准备高质量数据并优化标注方案的开发者参考。
1. 从一堆 txt 到 UIE 能吃的训练集:这套标注链路到底值不值得搭
手里攒了几百上千条业务文本,想微调一个抽取模型,结果卡在第一步——数据没有标签。找外包标?贵且慢。自己写脚本标?规则一多就崩。这时候多数人会想到 Label Studio,但真正落地时才发现:装完只是开始,怎么把标好的数据转成 PaddleNLP UIE 能直接训练的格式,才是分水岭。这份操作手册来自 PaddleNLP 仓库的applications/information_extraction/label_studio_text.md,它把「安装 Label Studio → 建项目 → 配标签 → 标注 → 导出 JSON → 用label_studio.py转成 UIE 数据格式」整条链路串了起来,覆盖命名实体识别、关系抽取、事件抽取、句子级分类、实体/评价维度分类五类任务。适合正在做 NLP 信息抽取、手头有标注需求又不想从零造轮子的工程师和研究者。下面我按自己复现的节奏,把每一步的参数、坑和边界拆开讲。
2. 环境搭建与项目初始化:版本锁死与任务类型选择
2.1 为什么必须锁 label-studio==1.6.0
这份手册明确写了环境配置:Python 3.8+、label-studio==1.6.0、paddleocr >= 2.6.0.1。很多人看到版本号会习惯性忽略,直接pip install label-studio拉最新版,然后导出的 JSON 结构和label_studio.py解析逻辑对不上,报一堆 KeyError。这不是玄学,是 Label Studio 在 1.6 之后调整过导出格式的字段命名。
安装命令本身不复杂:
# 建议在独立虚拟环境里操作,避免和已有 paddle 环境冲突 python -m venv lsenv source lsenv/bin/activate # Windows 用 lsenv\Scripts\activate # 锁死版本安装 pip install label-studio==1.6.0 pip install "paddleocr>=2.6.0.1" # 启动服务 label-studio start启动后浏览器打开http://localhost:8080/,首次使用需要注册一个本地账号。这个账号只存在本地 SQLite 里,不联网,所以随便填。登录进去之后,先别急着建项目,确认一下左下角显示的版本号是不是 1.6.0。如果不是,后面导出的 JSON 里annotations字段的嵌套层级可能不一样,转换脚本会直接挂掉。
提示:如果你之前装过其他版本的 label-studio,先
pip uninstall label-studio再装 1.6.0,残留的数据库文件在~/.label-studio/下,必要时清掉重新初始化。
2.2 项目创建时任务类型怎么选才不返工
手册里给了一张对应关系:命名实体识别、关系抽取、事件抽取、实体/评价维度分类选Relation Extraction;文本分类、句子级情感倾向分类也写的是Relation Extraction。这里原文有一处明显的笔误——句子级分类应该选Text Classification,但手册写成了Relation Extraction。我实际试过,如果句子级分类选了 Relation Extraction 模板,后面标签构建阶段根本配不出分类选项,只能删项目重来。
正确的对应关系我整理成表:
| 任务类型 | 项目模板选择 | 标签构建方式 |
|---|---|---|
| 命名实体识别 | Relation Extraction | Span 标签 |
| 关系抽取 | Relation Extraction | Span + Relation 标签 |
| 事件抽取 | Relation Extraction | Span + Relation 标签 |
| 句子级分类 | Text Classification | Choices 标签 |
| 实体/评价维度分类 | Relation Extraction | Span + Relation + Choices |
创建项目时填名称和描述,描述可以写清楚这批数据的来源和标注规范,方便多人协作时对齐。标签可以先跳过,进项目后在 Setting → Labeling Interface 里配,也可以创建时直接加。我一般选择先跳过,因为标签 XML 需要根据任务类型仔细写,创建向导里的简易编辑器容易漏字段。
2.3 数据上传的格式边界
手册说「先从本地上传 txt 格式文件,选择 List of tasks」。这里有个容易翻车的点:Label Studio 1.6.0 对 txt 的解析是按行切分的,一行就是一条 task。如果你的原始文本里本身有换行(比如一段多行的话术),上传后会被拆成多条独立任务,标注时上下文就断了。
常见做法是先把每条待标文本整理成单行,用\n替换掉内部换行,再上传。或者直接构造 JSON 格式的 task 列表:
import json # 把多行文本整理成 Label Studio 能正确解析的单行 task texts = [ "张三于2023年5月加入阿里巴巴,担任高级算法工程师。", "李四在2022年从清华大学毕业,主修计算机科学。", ] tasks = [{"data": {"text": t}} for t in texts] with open("tasks.json", "w", encoding="utf-8") as f: json.dump(tasks, f, ensure_ascii=False, indent=2)然后在 Label Studio 里选JSON导入方式,上传这个文件。这样每条 task 的data.text字段就是完整的一段文本,不会被意外切分。参数上注意ensure_ascii=False,否则中文会变成\uXXXX转义,虽然不影响解析,但人工检查时看着累。
3. 标签体系构建:Span、Relation 与 Choices 的 XML 写法
3.1 Span 类型标签:实体抽取的起点
实体抽取的标签配置在 Labeling Interface 里用 XML 写。手册里展示了实体类型标签的构建,核心是<Labels>标签下定义<Label>:
<View> <Text name="text" value="$text"/> <Labels name="label" toName="text"> <Label value="时间" background="#FFA39E"/> <Label value="选手" background="#D4380D"/> <Label value="赛事名称" background="#FFC069"/> <Label value="得分" background="#AD8B00"/> </Labels> </View>这段 XML 的逻辑是:<Text>声明了要标注的文本字段,value="$text"对应 task 里的data.text;<Labels>里的每个<Label>就是一个实体类别。background只是 UI 上的颜色,不影响导出数据,但建议不同类别用差异明显的颜色,标注时肉眼区分快很多。
对应的 schema 在转换阶段要写成列表:
schema = ['时间', '选手', '赛事名称', '得分']这个 schema 会传给label_studio.py,用来告诉转换脚本哪些标签是有效实体类型。如果 XML 里定义了但 schema 里没写,转换时会被忽略;反过来 schema 里有但 XML 里没标,转换脚本会报找不到对应标签。
3.2 Relation 类型标签:关系抽取的 P 值设计
关系抽取需要在 Span 基础上加<Relations>:
<View> <Text name="text" value="$text"/> <Labels name="label" toName="text"> <Label value="作品名" background="#FFA39E"/> <Label value="歌手" background="#D4380D"/> </Labels> <Relations> <Relation value="歌手"/> <Relation value="发行时间"/> <Relation value="所属专辑"/> </Relations> </View>手册里特别强调了一段关于 P 类型设置的原则:「{S}的{P}为{O}」需要能构成语义合理的短语。比如三元组 (S, 父子, O),关系类别叫「父子」没问题,但按 UIE 的 prompt 构造方式,「S的父子为O」读起来不通顺,改成「孩子」更好,即「S的孩子为O」。这个细节直接决定零样本效果——P 类型越自然,模型在 prompt 上的语义匹配越准。
我自己的经验是,配 Relation 标签时先把所有候选 P 值列出来,逐个套进「{S}的{P}为{O}」念一遍,拗口的就换词。比如「所属专辑」比「专辑」更顺,「发行时间」比「发布时间」更常见。这一步花十分钟,后面模型效果能差出好几个点。
3.3 Choices 类型标签:分类任务的配置
句子级分类和实体/评价维度分类需要 Choices 标签:
<View> <Text name="text" value="$text"/> <Choices name="sentiment" toName="text" choice="single"> <Choice value="正向"/> <Choice value="负向"/> </Choices> </View>choice="single"表示单选,如果是多标签分类改成multiple。对应的 schema 在转换时写成字符串:
schema = '情感倾向[正向,负向]'注意方括号和逗号都是中文全角,这是 UIE 的 prompt 格式要求。手册里句子级分类的 schema 写的就是'情感倾向[正向,负向]',照抄即可,别手改成英文标点。
实体/评价维度分类的 schema 是嵌套字典:
schema = { '评价维度': [ '观点词', '情感倾向[正向,负向]' ] }这种结构表示「评价维度」这个实体下面,既要抽「观点词」这个 Span,又要分类「情感倾向」。转换脚本会根据这个嵌套关系自动构造 prompt,比如「XXX的情感倾向[正向,负向]」。
4. 标注操作与数据导出:从人工点击到 JSON 落地
4.1 五类任务的标注界面差异
标注本身是在浏览器里点选,但不同任务的交互逻辑差别不小。实体抽取就是选中文本片段,弹出标签列表选一个;关系抽取需要先标出两个实体,然后从第一个实体拖一条线到第二个实体,再选关系类型;事件抽取本质上也是 Span + Relation,只是 schema 设计上触发词和论元要分开;句子级分类是选中整句后点分类按钮;实体/评价维度分类最复杂,既要标 Span 又要挂分类。
手册里给了几个标注示例的 schema,我挑事件抽取说一下:
schema = { '地震触发词': [ '时间', '震级' ] }标注时先把「地震」这个词标成「地震触发词」,再把时间、震级分别标成对应 Span,最后用 Relation 把触发词和论元连起来。导出后转换脚本会根据这个 schema 构造「地震触发词的时间为X」「地震触发词的震级为Y」这样的 prompt。
4.2 导出 JSON 的字段结构与重命名
标完之后勾选已标注的文本 ID,选择导出类型为 JSON。导出的文件默认名字可能是一串 UUID,手册要求重命名为label_studio.json并放入./data目录。这一步不是强迫症,是因为后面label_studio.py的--label_studio_file参数默认指向这个路径。
导出的 JSON 结构大致是:
[ { "id": 1, "data": {"text": "张三于2023年5月加入阿里巴巴。"}, "annotations": [ { "result": [ { "value": {"start": 0, "end": 2, "text": "张三", "labels": ["人物"]}, "type": "labels" } ] } ] } ]转换脚本会读annotations[0].result里的value字段,按type区分是 Span 还是 Relation 还是 Choices。如果导出时没勾选「包含标注结果」,annotations会是空数组,转换出来就是空数据集。这个坑我踩过一次,标了一下午导出发现没勾选项,血泪经验。
5. 数据转换脚本:label_studio.py 的参数拆解与避坑
5.1 抽取式任务的转换命令
抽取式任务(命名实体识别、关系抽取、事件抽取)的转换命令:
python label_studio.py \ --label_studio_file ./data/label_studio.json \ --save_dir ./data \ --splits 0.8 0.1 0.1 \ --task_type ext--label_studio_file指向导出的 JSON;--save_dir是转换后 train/dev/test 的保存目录;--splits按 8:1:1 划分;--task_type ext表示抽取式任务。执行完会在./data下生成train.json、dev.json、test.json,格式是 UIE 需要的{text, prompt, result_list}结构。
这里有个默认行为要注意:每次执行脚本会覆盖同名文件。如果你调了参数想对比效果,先把上一次的输出重命名或挪走,否则后悔药没得吃。
5.2 句子级分类任务的 prompt 构造
句子级分类的转换命令多了--prompt_prefix和--options:
python label_studio.py \ --label_studio_file ./data/label_studio.json \ --task_type cls \ --save_dir ./data \ --splits 0.8 0.1 0.1 \ --prompt_prefix "情感倾向" \ --options "正向" "负向"转换脚本会自动构造 prompt,比如「情感倾向[正向,负向]」。--prompt_prefix是 prompt 的前缀文本,--options是分类标签列表。这两个参数只对task_type cls有效,抽取式任务传了也会被忽略。
5.3 实体/评价维度分类的 separator 参数
实体/评价维度分类的转换命令:
python label_studio.py \ --label_studio_file ./data/label_studio.json \ --task_type ext \ --save_dir ./data \ --splits 0.8 0.1 0.1 \ --prompt_prefix "情感倾向" \ --options "正向" "负向" \ --separator "##"--separator是实体类别和分类标签之间的分隔符,默认##。比如评价维度是「屏幕」,分类是「正向」,构造出来的 prompt 可能是「屏幕##情感倾向[正向,负向]」。这个分隔符要和 schema 里的嵌套结构对应,改错了模型学到的 prompt 模式就乱了。
5.4 完整参数表与默认值
手册 2.7 节列了所有参数,我整理成表方便对照:
| 参数 | 作用 | 默认值 | 生效范围 |
|---|---|---|---|
| label_studio_file | 导出的标注 JSON 路径 | 无,必填 | 全部 |
| save_dir | 训练数据保存目录 | ./data | 全部 |
| negative_ratio | 最大负例比例 | 5 | 仅抽取任务 |
| splits | 训练/验证/测试比例 | [0.8, 0.1, 0.1] | 全部 |
| task_type | 任务类型 ext/cls | 无,必填 | 全部 |
| options | 分类类别标签 | ["正向", "负向"] | 仅分类任务 |
| prompt_prefix | 分类 prompt 前缀 | "情感倾向" | 仅分类任务 |
| is_shuffle | 是否随机打散 | True | 全部 |
| seed | 随机种子 | 1000 | 全部 |
| schema_lang | schema 语言 ch/en | ch | 全部 |
| separator | 实体与分类的分隔符 | "##" | 实体/评价维度分类 |
negative_ratio只对训练集有效,验证集和测试集默认构造全负例,这是为了保证评估指标准确。负例数量 = negative_ratio × 正例数量,适当构造负例能提升模型区分能力,但设太大也会让训练集正负失衡。
6. 避坑与排查:标注到训练链路上的五个真实翻车点
6.1 导出 JSON 后转换报 KeyError: 'annotations'
现象:运行label_studio.py直接抛KeyError: 'annotations'。 原因:导出时没勾选「包含标注结果」,或者导出格式选成了 CSV/TSV 而不是 JSON。 解决:重新导出,确认文件类型是 JSON,且每条 task 的annotations数组非空。可以先用python -c "import json; d=json.load(open('label_studio.json')); print(len(d[0]['annotations']))"快速检查。
6.2 转换后 train.json 为空文件
现象:脚本跑完没报错,但train.json只有几行或直接是空数组。 原因:schema 里的标签名和 Label Studio XML 里定义的value不一致,转换脚本匹配不到任何标注。 解决:把 XML 里的<Label value="...">和转换时用的 schema 列表逐字对照,注意中英文标点和空格。比如 XML 里写「赛事名称」,schema 里写成「赛事 名称」就匹配不上。
6.3 关系抽取的 P 值拗口导致零样本效果差
现象:模型在没见过的关系类型上几乎抽不出正确三元组。 原因:P 类型设置不符合「{S}的{P}为{O}」的自然语义,prompt 构造出来模型理解不了。 解决:按手册原则逐个念一遍,把「父子」改成「孩子」、「所属」改成「所属专辑」这类更自然的表达。P 值越接近日常语言,零样本迁移越好。
6.4 每次执行转换脚本覆盖已有数据
现象:调完参数重新跑,发现之前的 train.json 被覆盖,没法对比。 原因:脚本默认按save_dir和固定文件名输出,同名直接覆盖。 解决:每次转换前把save_dir改成不同目录,比如./data_v1、./data_v2,或者转换后立刻重命名。我一般会在命令后面加&& mv ./data/train.json ./data/train_$(date +%s).json做备份。
6.5 句子级分类选了 Relation Extraction 模板
现象:建项目时选了 Relation Extraction,进标注界面发现没有分类按钮。 原因:手册原文在句子级分类处写的是 Relation Extraction,这是笔误,实际应选 Text Classification。 解决:删掉项目重建,模板选 Text Classification,标签用 Choices。已经标了的数据可以在导出后手动改 JSON 结构,但不如重建省事。
7. 进阶技巧:用 negative_ratio 和 schema_lang 把数据质量再提一档
数据转换跑通只是及格线,真正拉开差距的是负例构造和 schema 语言选择。negative_ratio默认 5,意思是训练集里自动构造的负例数量是正例的 5 倍。这个值不是越大越好——我试过设到 20,模型在验证集上的 F1 反而掉了 3 个点,因为负例太多导致正例信号被淹没。比较稳的做法是从 5 开始,按 5、8、10 三档做消融,看验证集指标拐点。
负例的构造逻辑是:对于每个正例 prompt,随机替换实体或分类标签生成不匹配的样本。比如正例是「张三的出生地为北京」,负例会构造成「张三的出生地为上海」或「张三的出生地为歌手」。验证集和测试集默认全负例,所以评估指标反映的是模型在「正例极少、负例极多」场景下的判别能力,这和真实抽取场景更接近。
schema_lang控制 prompt 的构造语言,默认ch。如果你的训练数据以英文为主,或者下游模型是在英文语料上预训练的,改成en会让 prompt 变成英文模板,比如「sentiment[positive,negative]」。这个参数影响的是 prompt 文本本身,不改变标注数据。我一般中文数据保持ch,中英混合数据先试ch,如果英文样本的抽取效果明显差一截,再切en对比。
还有一个容易被忽略的点:is_shuffle和seed。默认is_shuffle=True、seed=1000,意味着每次转换的数据顺序是固定的。如果你改了splits比例想重新划分,但没改seed,划分结果可能和上次高度重叠,导致验证集泄漏。我的习惯是每次调整划分比例时,把seed也换一个值,比如--seed 2024,确保划分真正独立。
最后说一个验证转换结果是否正确的笨办法:打开生成的train.json,随便抽三条,把prompt和result_list字段读一遍,看 prompt 是不是符合「{S}的{P}为{O}」或「XXX的情感倾向[正向,负向]」的格式,result_list 里的实体位置能不能和原文对上。这个动作花两分钟,能挡住后面训练时大半的数据格式问题。从那以后我每次转换完都强制走一遍这个抽查,再也没出现过训练到一半发现标签错位的情况。希望帮到你。
本文还有配套的精品资源,点击获取