ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用主动学习重塑NLP数据标注:Prodigy工作流实战解析

用主动学习重塑NLP数据标注:Prodigy工作流实战解析

做NLP项目的人,大概都经历过这种时刻:辛辛苦苦拉了几万条语料,投了人力和时间标完之后,拿给模型一跑,发现效果还不如手工写规则。我最初对“数据标注工具”的认知,基本就是把人当标注机器,喂数据、导结果,跟模型训练完全脱节。直到后来把 Prodigy 这套主动学习驱动的工作流引入实际项目,整个重心才从“堆标注数量”真正转向“提升标注质量与迭代效率”。Prodigy 不是又一个标签页面工具,它把主动学习、模型反馈和人工判断揉进同一条链路,每次标注都在为下一轮模型改进提供燃料。这篇内容适合做 NLP 开发的算法工程师、数据团队负责人,也适合自己从零搭建文本模型的研究者,看完你至少能搞懂它为什么能改变常规的标注工作流,并且能直接上手跑通一个标注任务。

1. 为什么“模型参与标注”才算真正改变了工作流

1.1 传统标注流程的三个死穴

先说说我早期做数据集的标准流程:写标注规范、招人或者拉同事开会、批量分派语料、收回来检查质量、清洗以后训练模型。听上去没问题,但实际跑几轮就会发现三个致命问题。

第一个问题是数量驱动,而不是价值驱动。几万条语料里可能有八千条都是模型已经很容易判断的简单样本,真正让模型犯错的难例只占一小部分。传统人工标注不会区分这些,大家按顺序从头标到尾,时间全花在了简单样本上,难例可能还没轮到,项目周期已经结束了。

第二个问题是标注标准和模型训练结果互相脱节。标注员按照主观理解打标签,完全不关心模型能不能从这些标签里学到边界。比如一段文本同时涉及退款和投诉,标成哪一类完全取决于个人判断,这类模糊样本恰恰是模型最需要明确决策边界的,却被轻松略过。

第三个问题是无法追踪错误来源。模型上线后效果不好,你很难说是标注错误、样本分布问题还是特征不足。所有标注记录堆在Excel或JSON里,没有结构化保存,出现争议时连当时是谁标的、在哪一轮标的都查不到。

这三个死穴会让整个数据生产链陷入“标得多但模型不长进”的怪圈。Prodigy 的设计刚好瞄准了这些问题,它把模型拉到标注现场,让标注行为和模型反馈形成闭环。

1.2 主动学习与迭代标注:数据越标越少,模型越训越准

主动学习这个概念并不新鲜,核心思路是让模型自己选出“最能让它进步”的样本,再让标注者处理这些最有价值的样本。

我习惯把它类比成刷题。你做一套卷子时,如果从头到尾全部做完,大量时间浪费在已经掌握的知识点上。聪明的做法是先把不会的题挑出来重点练习,练完再考,再挑不会的,反复循环。主动学习做的事就是这个:模型把不确定的样本优先抛给你,你标完之后立刻训练,下一轮模型推荐的样本又会更精准,训练集规模可以大幅缩小,但模型效果往往持平甚至更好。

Prodigy 把这个过程做成可视化流程。在 ner.teach 或 textcat.teach 这类模式下,模型运用预训练权重对未标注样本打分,界面会区分高分、中分、低分三档。高分表示模型判断很自信,中分意味着模型拿不准,低分则是模型“瞎猜”。标注者重点处理中分和低分样本就行,高分样本偶尔抽查即可。这一下把人力集中到了刀刃上。

1.3 Prodigy 在工作流里的角色定位:发牌手、记录员与裁判

如果把标注工作比喻成一场牌局,Prodigy 同时扮演了三个角色:发牌手决定下一张给你看什么牌,记录员完整保存你对每张牌的处理结果,裁判根据模型反馈判断哪张牌优先要你处理。三者由同一条技术链路串联,这是普通标注工具做不到的。

在常规工具里,标注和训练是前后两个阶段。Prodigy 打破了这个边界:标注过程中随时可以借助模型做预标,也可以对模型已标注的内容做校正,还可以把上一轮训练的模型直接塞进新一轮标注会话。看似只是工具功能多了点,实际上改变了整个团队的工作节奏。以前是一鼓作气标完全部数据再训练,现在可以标两百条就训练一轮,看效果,再补两三百条,再训练。这种小步快跑的节奏更适合真实项目。

2. 核心机制拆解:Recipe、流、数据库与预训练模型

2.1 Recipe:把标注流程变成可编程组件

Prodigy 最核心的抽象叫 Recipe,可以理解为一个“标注任务模板”。一个 Recipe 定义了数据从哪来、用什么界面展示、以什么方式预处理、答案存到哪个数据集。你通过命令行启动 Recipe,每个 Recipe 实例就是一次独立的标注会话。

Recipe 的本质是 Python 函数,用装饰器注册。系统内置了大量配方,比如 ner.manual 用于纯手工标注实体,ner.teach 用模型辅助筛选需要标注的样本,textcat.manual 做文本分类标注,textcat.teach 则按模型预测置信度从高到低推荐难以判断的文本。

举个例子,启动一个文本分类的纯手工标注会话,命令行大致是这样:

prodigy textcat.manual 情感分析数据集 ./data/未标注文本.jsonl --label 积极,消极

这里的情感分析数据集是数据集名称,用来隔离不同项目的标注结果,文件路径指向待标注语料,--label 定义类别。启动后浏览器打开本地地址,界面会逐条展示文本,标注者点选类别即可。

Recipe 的可编程性非常关键。你可以自己写一个 Recipe,把“规则过滤+模型预标+人工确认”三步合成一条流水线。这种能力让标注工作承载了算法逻辑,而不只是机械打点。

2.2 Stream:按需产生的待标注样本流

与一次性读入所有文件不同,Prodigy 引入的概念是 Stream,可理解为按需产生的待标注数据流。Stream 可以是普通文件、数据库查询结果,也可以是模型实时生成的样本,甚至能通过管道对数据做变换。

举个例子,你可以先跑一个 Python 脚本从原始日志里按条件筛选文本,再通过规则筛掉长度不合适的记录,最后把符合条件的内容送进 Prodigy。这样进入标注页面的每一批数据都经过了一次预筛选,标注者不会看到明显没价值的样本。

更实用的是,通过 --exclude 参数可以排除已经标注或已经被抛弃的样本,避免同一条文本反复出现在标注界面。实际体验中,这个参数对保持标注速度非常重要。如果没有它,第二轮标注时你会看到很多眼熟的内容,既浪费时间又影响信心。

2.3 存储层:用 SQLite 做注释记录,用导出做交接

Prodigy 默认把标注结果、会话信息、答案全部记录在本地 SQLite 数据库里。很多人一开始不理解为什么要用数据库存这么点数据,直接写 JSONL 不更简单吗?

后来我体会到了好处。SQLite 存储方式支持追溯,可以清楚看到哪一条样本在哪个 Session 里被标注过,被哪个 Recipe 产生,标注答案是什么。当出现标注纠纷时,能直接定位到当时场景。相比之下,普通 JSONL 文件一旦被覆盖,所有历史痕迹都没了。

最常用的数据导出命令是:

prodigy db-out 情感分析数据集 ./导出结果.jsonl

导出的 JSONL 包含原始文本、标注答案、标注者信息、时间戳和模型预测分数等信息。这份导出文件可以直接喂给训练脚本,也可以归档做版本管理。我通常会另存一份带标注时长的导出记录,用来评估团队标注效率。

2.4 预训练模型如何辅助打分与预标

Prodigy 本身并不内置某一个大模型,而是允许你把外部的预训练模型集成进来。如果你启动 ner.teach 这类依赖模型打分的 Recipe,需要传入一个模型参数,常见的是 spaCy 的中小型模型。

模型在主动学习里的作用就是做“Teacher”,给每个未标注样本打分。模型预测置信度区间可以用来区分硬样本和软样本。在界面中,每条样本的预测概率被换算成高、中、低三档,标注者可以刚开始只处理中低档样本,大幅减少标注量。

在实际项目里,我踩过的坑是:直接用通用预训练模型跑中文语料,打分结果往往不太可靠,因为通用模型和项目领域差异很大。解决思路是先用少量数据手工标注出一份种子集,训练一个弱模型,再把弱模型带入 teach 流程。这个过程在下一章展开讲。

2.5 界面交互:用好快捷键才能跑出效率

标注工具的界面设计直接影响效率。Prodigy 的浏览器界面默认展示一条文本和候选标签,支持实体划选、点击标签、确认、拒绝、跳过等操作。常见快捷键包括接受、拒绝和跳过,实体标注模式里还能快速调整实体边界。

很多人觉得点选鼠标就够了,但我实测发现,只要跨过快捷键适应期,标注速度至少提高两倍。我的习惯是左手放在快捷键上,右手只做文本划选,所有判断通过键盘按键确认,不要移动鼠标去点页面按钮。界面左侧是主工作区,右侧实时显示进度和当前样本的模型置信度,可以经常瞄一眼进度。

2.6 与常规标注工具的差异化对比

用表格对比更容易看出差异,我拿常见的人工标注工具和 Prodigy 做对比:

维度常规开源标注工具Prodigy
核心逻辑先标完全部数据再训练主动学习驱动的边标边练
数据流静态导入导出流式生成,可动态组合
模型参与通常不支持支持预训练模型预标与打分
标注追溯弱,通常只有标签文件完整会话记录,可回溯
自定义能力前端配置为主Recipe 编程化扩展
多人协作通常允许支持多人并行,需设计共享策略

常规工具适合一次性、大批量、目标明确的标注任务,Prodigy 更适合需要反复迭代模型底座的场景。如果项目预算充足、业务场景复杂,长期看 Prodigy 的主动学习减少的总标注量完全能覆盖成本。

3. 从零开始跑通一个实体标注任务:实操全记录

3.1 安装、授权与启动准备

Prodigy 不是完全开源的免费工具,需要先获取合法使用授权,再通过官方渠道安装 Python 包。安装完成之后,建议用一个独立目录存放项目数据、脚本与导出文件。

启动前要确认三件事:本地安装的 Python 版本兼容性、是否有可用的预训练模型、是否有待标注语料文件。如果本机网络不在理想状态,提前把 spaCy 模型下载到本地缓存,避免启动 Recipe 时临时下载导致超时。这里顺便说一句,当前网络环境下下载大模型文件经常又慢又不稳定,预先把模型资产准备齐全总没错。

3.2 准备待标注数据:JSONL 格式与字段约定

Prodigy 支持最常见的 JSONL 格式,每行一个 JSON 对象,至少包含 text 字段表示待标注文本。如果要预置实体标注结果,可以增加 spans 字段,每条 span 包含 start、end、label 三个属性。

以实体标注为例,原始文件大概长这样:

{"text": "某科技公司今天宣布完成新一轮融资,创始人表示将继续加大研发投入。", "spans": []} {"text": "市场分析师认为,这款新产品的定位十分清晰。", "spans": []}

如果是从数据库导出的文本,建议先做一遍去重、去空行、筛长文。文本长度太大会影响标注页面显示和模型推理效率。每条文本尽量控制在 200 字以内,实体密集的句子拆成多个短句会更利于一致性标注。

3.3 启动 ner.manual 纯手工标注过程

对实体识别任务,如果手头还没有像样的底座模型,第一轮别急着用 teach 模式,先用 ner.manual 手工标注一份干净的小数据集。

命令大致如下:

prodigy ner.manual 基础实体集 ./data/原始语料.jsonl --label PERSON,ORG,TIME

运行后本地会起一个 Web 服务,默认地址是本地端口,浏览器打开就能进入标注页面。在页面上选中文本片段,弹出标签选项,点击对应类别即可给实体打标。标注完每条文本之后,通过快捷键确认并进入下一条。

有一点要说明:ner.manual 模式下模型不参与打分,所以刚才说的主动学习优势在这里用不上。它存在的意义是,用最短时间建立一份高质量种子集,这份种子集会用于训练初始模型,再接上 teach 流程。

3.4 从 manual 到 teach:用小模型驱动主动学习

种子集标到三五百条左右,就可以进入第二阶段。导出这批数据,训练一个小的文本实体识别模型,哪怕效果一般也没关系。然后把训练好的模型带进 ner.teach 模式:

prodigy ner.teach 扩展实体集 zh_core_web_md ./data/未标注扩充.jsonl --label PERSON,ORG,TIME

运行后模型会对所有未标注样本打分,按置信度排序,标注者优先处理模型最不确定的样本。每处理完一批,模型权重还不会立刻更新,这需要先导出再训练再重新启动 Recipe。实际操作中我的节奏是:每标两百到三百条,就跑一次训练,更新模型,再继续标注。这个过程让标注量显著下降,我记得有一次只标了约四分之一的数据,模型 F1 值已经追平全量标注的效果。

3.5 导出数据并复查质量

标注过程中随时可以导出检查:

prodigy db-out 扩展实体集 ./export/hebing_round1.jsonl

导出后我会用脚本检查实体边界是否合规,比如 start 和 end 是否对应完整 token,是否出现重叠实体,标签分布是否极端失衡。这一步多数标注工具都不提供现成能力,但通过简单的 Python 脚本就能完成。一旦发现问题,把问题样本重新导回 Prodigy 做 ner.correct 校正即可。

4. 把 Prodigy 融入模型迭代闭环:训练、评估、再补标

4.1 小步快跑:训练与补标的反馈节奏

标注工作不能等全部完成再开始训练。我现在的标准节奏是:初始化一百条种子标注,训练一个不堪入目的快速模型,用它在未标注数据上跑预测,挑出错得离谱的样本回炉补标,再训练,再找错。

这个过程像查漏补缺。前期模型漏洞很多,每轮补标收获巨大;到后期模型边角案例都被磨得差不多,再补标收益就开始下降。此时也就能判断项目是否到了收尾阶段。这种节奏非常适合预算有限、希望快速上线 MVP 的团队。

Prodigy 在设计上刻意服务这种节奏,数据集的隔离能力和导出接口让每一轮补标都能高效衔接。你甚至可以在每次训练后用新模型重新启动 teach 模式,把新的困难样本递送给标注者。

4.2 与常用训练框架的集成方式

Prodigy 的数据字段和 spaCy 格式天然兼容,导出后的 JSON L 基本可以直接喂给 spaCy 训练。如果用的是 Transformers 类框架,通常只需要写一段转换脚本,把 spans 字段转成训练所需的 BIO 标签序列。

我自己用过一个小工具脚本,将导出的标注数据转换为 Hugging Face 数据集格式。大致逻辑是:按文本切分 token,将 spans 中的实体区间映射到 token 级标签,最后统一成 IOB2 格式。想做文本分类时,把答案字段转成分类标签即可。

要注意的是,主动学习模式下模型推荐的数据会天然偏向困难样本,直接用这些数据训练出来的模型,在简单样本上的表现可能有所波动。解决办法是每一轮补标都掺入一部分随机采样数据,让训练集分布不至于和真实分布偏离太远。

4.3 写一个最简单的自定义 Recipe:从文件到界面

内置 Recipe 功能很全,但真正的生产力提升来自自定义 Recipe。这里写一个简化版骨架,用来演示“读取文件→交给人工标注→保存答案”的完整链路:

import prodigy from prodigy.components.stream import get_stream from prodigy.util import split_string @prodigy.recipes.recipe("custom.textcat_flow") def custom_textcat_flow(dataset, source_path, label): # 从 JSONL 文件读取流数据 stream = get_stream(source_path) # 给每个样本注入候选标签 def add_label(item): item["options"] = [{"id": label, "text": label}] return item stream = (add_label(item) for item in stream) return { "dataset": dataset, "view_id": "textcat_manual", "stream": stream, "config": { "labels": split_string(label) } }

调用方式:

prodigy custom.textcat_flow 自定义项目 ./data/输入.jsonl --label 投诉 --label 咨询 -F recipe.py

这只是一个骨架,生产环境还会加入数据清洗、重复文本过滤、规则预标记等功能。Recipe 的真正价值在于,你可以把团队特有的业务逻辑直接塞进标注链路,让标注者只在最关键的位置做判断。

4.4 多人协作与数据版本控制的实践经验

Prodigy 面向本地使用,多人协作需要自己设计流程。我实践下来比较顺的方式是多人各自启动相同 Recipe、分别标注不同子集,通过数据文件拆分任务,隔段时间统一导出合并。

合并脚本要特别注意文本去重。如果两个人标了同一段文本,把标注置信度和时长作为权重,优先保留更可靠的结果。每轮标注数据都会导出一份存档,命名里带日期和版本号,这样出现效果回退时能定位是哪一轮数据引起的。

数据版本管理上,很多人只关注模型文件版本,却忽略数据版本。标注数据同样需要版本化管理,我的建议是每轮标注冻结一份导出文件,记录关联的模型版本和 Recipe 参数。这样整个数据生产链就完全可复现。

5. 常见问题排查与避坑速查表

5.1 进程与页面异常

用表格整理我遇到过的问题:

现象可能原因处理建议
启动后浏览器打不开页面端口被占用或本地网络异常检查端口占用,换端口重新启动
页面一直转圈没数据Stream 生成异常或文件为空检查源文件路径与 JSONL 格式,预览流前 5 条
启动 Recipe 报模型加载错误模型未下载或路径错误提前手动下载模型,确认模型名
标注结果导出后字段缺失Recipe 配置缺少回调确认 Recipe 返回的答案绑定了必要组件

浏览器页面打不开这个问题,十有八九是端口号记错或服务进程没起来。启动时留意命令行输出里的实际地址,不要背默认为 localhost。另外注意在代理环境下,本地地址访问有时会被代理干扰,建议临时对本地地址做直连处理。这个细节我踩过几次坑,排查了半天才发现是代理设置的问题。

5.2 中文语料标注的边界与一致性

中文没有天然空格分词,实体边界经常让标注者头痛。比如“北京市朝阳区”到底是一个完整地名还是按区划分,不同标注者会有不同判断。解决这个问题没有魔法,只能在标注规范里写清边界规则,并且在刚开始时让几个人标同样的样本算一致性。

我的经验是,在标注规范里给出“最小完整语义单元”原则,比如“北京市”作为行政区划整体标注,不拆市和区,除非业务需要单独提取区名。另一条建议是使用预标注,先用规则词典在界面上标出一部分候选实体,人工只需校对边界和删除误报。这样可以大幅减少手工划选工作量。

5.3 主动学习引入的分布偏差

主动学习会让训练集偏向模型难以判断的样本,这本来是好事,但过度依赖会导致模型在简单样本上的能力下降。因为训练集里全是难题,模型没见过太多普通样本,退化是必然的。

规避方法是一手主动学习,一手随机采样。每次导出一批困难样本后,按一定比例混合随机样本进入训练集。我在项目中通常保持困难和随机样本七三开,模型稳定性和针对性都有保障。定期测试模型在真实分布数据上的表现也是必须的,不能只盯着主动学习批次上的指标看。

5.4 标注质量的一致性控制

多人标注时标签漂移很容易发生,前半段大家都严格按标准走,后半段疲劳了就随意。控制质量靠两件事:一是定期双标校验,选百分之五的样本让两个标注人都标一遍,计算一致率,低于阈值就暂停并重新校准标准;二是把标注时长统计出来,单条耗时剧烈缩短的时段往往就是质量下行区。

更高级一点的做法是把一致性校验结果回灌进数据库。Prodigy 的会话记录支持把已经确认的样本再次送进校正 Recipe,我常用读 ner.correct 对分歧样本做二次纠偏,让最终数据质量可控。

5.5 数据安全与备份

本地 SQLite 数据库文件包含全部标注记录,这是重要资产。我一般在每次标注会话结束就备份一次数据库文件,同时导出 JSONL 存档。数据库文件很小,备份成本忽略不计。

容器化部署时注意持久化挂载。如果把 Prodigy 跑在容器里,数据库路径必须挂载到宿主机持久化目录,否则容器重建会丢数据。这个坑我给不少人排过,项目快结束才发现数据库在容器里没存下来,那种心情很难形容。

6. 我的一些个人体会

最早我把 Prodigy 当成一个“更好用的标注工具”来用,后来才意识到它更应该被当成“模型开发工作流的一部分”来设计。真正改变效率的不是界面多炫,而是主动学习帮你省掉了大量无效标注,Recipe 的可编程性让标注流程和业务逻辑真正咬合在一起。项目中最划算的投资,是花半天时间把一个标准 Recipe 调好,而不是让标注团队一口气标完整批数据再返工。

最后分享一个小技巧:每一轮主动学习训练出来的模型,我会保留一个副本,命名里带当时的数据集版本号。等到模型效果突然下滑时,可以快速回退到旧标注链路做 diff,找出是数据问题还是模型问题。这种习惯让我在多次项目里少走了很多弯路。你要做的,是尽快拿一小批真实业务语料跑通闭环,先用几十条数据走完全流程,再决定投入多少人力做大标注。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进