用预标签和重新训练实现循环中的模型标注
把训练好的模型的答案作为预标签展示出来,先审核置信度最低的预标签,并在标注者更正答案时重新训练模型。
v2.14.0 新增
本教程让一项情感研究走完一遍循环中的模型流程。模型在最初的标签上训练,模型的预测以预标签的形式出现在标注者面前,审核者更正置信度最低的预标签,这些更正再送入下一次运行。教程写给已经训练过第一个模型(如训练你的第一个模型所示),现在希望标注者与模型协同工作的研究负责人。除一步之外,每一步都能在笔记本上用 sklearn-text 运行。采集一步会训练一个小型 transformer,在 CPU 上需要几分钟。
情感项目
这个项目是 Potato 的模型训练示例,包含 160 条合成的商品评论,以及一个名为 sentiment 的 radio 方案。预标签是模型在任何人标注某条目之前为该条目提出的一个标签。回写是把训练运行的预测变成标注页面上的预标签的那一步。到教程结束时,项目会在前 120 个标签上训练一个分类器,在其余评论上发布预标签,优先提供置信度最低的评论,随着新标签到来重新训练,并把一个留出划分保存为数据集版本。
预标签让标注更快,同时也把标注者拉向模型的答案。用你的标签训练的模型辅助标注讨论了何时值得做这种取舍,如何在带预标签的数据中区分认真审核与不加思考的接受则讨论不读就接受预标签的标注者。下面的配置从一开始就让一部分标注者看不到任何预标签。这些留出的答案让研究能够衡量这种牵引。
搭建项目
安装 Potato。sklearn-text 除基础包外不需要任何东西:
pip install --upgrade potato-annotation把 Potato 仓库中的 examples/ai-assisted/model-training/ 复制到一个新文件夹。该文件夹中有 data/reviews.json(160 条评论)和 data/gold.json(模拟器据以作答的标签)。用下面的配置替换示例的 config.yaml:
annotation_task_name: "Review Sentiment (model in the loop)"
data_files:
- "data/reviews.json"
item_properties:
id_key: "id"
text_key: "text"
task_dir: "."
output_annotation_dir: "annotation_output"
require_password: false
admin_api_key: "demo-admin-key"
num_annotators_per_item: 1
assignment_strategy: llm_confidence
annotation_schemes:
- annotation_type: "radio"
name: "sentiment"
description: "Is this review positive or negative?"
labels:
- "positive"
- "negative"
sequential_key_binding: true
ai_support:
holdout:
fraction: 0.2
salt: "review-sentiment"
datasets:
enabled: true
model_training:
enabled: true
writeback:
schemas: ["sentiment"]
include_unlabeled: true
min_confidence: 0.6加到示例配置中的每个块各司其职:
assignment_strategy: llm_confidence依次提供每个条目,置信度最低的预标签最先。没有预测的条目排在最后,按队列顺序。ai_support.holdout对 20% 的标注者与条目配对不提供任何形式的 AI 协助,预标签也包括在内。请在标注开始之前设置留出,因为在研究中途更改fraction或salt会重新洗牌各组。datasets.enabled让一次运行的划分可以在最后一步保存为数据集版本。writeback.schemas指定标注者能看到其预测的方案。该列表默认为空,在列表中写入某个方案之前,开启训练不会让标注页面发生任何变化。include_unlabeled: true让每次运行对尚无人标注的评论做预测。没有这项设置时,一次运行只对该次运行留出的 val 和 test 条目做预测。这些条目已经带有人工标签。min_confidence: 0.6让置信度低于 0.6 的预测不出现在标注者眼前。Potato 仍会把该预测与这次运行一起保存。
writeback.field 保持默认值 predictions,即预标签写入的条目字段。使用默认值时,不需要 pre_annotation 块。启动服务器之前先检查配置:
potato validate config.yaml
potato start config.yaml -p 8000标注种子集
用你的标签训练的模型,首先需要有标签。当训练划分中的条目少于 10 个时,sklearn-text 拒绝拟合。可以在 http://localhost:8000 标注一些评论,也可以让三位模拟标注者根据金标准文件各标注 40 条评论:
python -m potato.simulator --server http://localhost:8000 --users 3 \
--strategy gold_standard --competence good=1.0 --max-annotations 40 \
--gold-file data/gold.json --sequential --fast-mode --no-export--competence good=1.0 让每位模拟标注者使用 good 档案,该档案能答对 80–90% 的评论。在 num_annotators_per_item: 1 下,三位标注者覆盖 120 条不同的评论,剩下 40 条未标注留给模型。
训练并阅读回写报告
用管理员密钥打开 http://localhost:8000/admin/training,选择 sklearn-text 和 sentiment 方案,然后点击 Train now。同样的运行也可以从命令行启动:
curl -X POST -H "X-API-Key: demo-admin-key" -H "Content-Type: application/json" \
http://localhost:8000/admin/training/api/runs \
-d '{"trainer": "sklearn-text", "schemas": ["sentiment"]}'这次运行需要几秒钟。Potato 把已标注的评论划分为 train、val 和 test(默认分别为 70%、15% 和 15%),在 train 上拟合分类器,并在 val 和 test 上为分类器评分。随后状态路由会报告得分和回写报告:
curl -H "X-API-Key: demo-admin-key" http://localhost:8000/admin/training/api/status本教程的一次运行返回了如下内容,已裁剪为此处相关的字段:
{
"metrics": {
"heldout_metric": "val macro_f1",
"heldout_score": 0.7333,
"n_train": 76.0,
"val_n": 20.0,
"test_n": 24.0,
"test_macro_f1": 0.8222
},
"writeback": {
"n_read": 84,
"n_written": 78,
"n_skipped_low_confidence": 6,
"n_skipped_train_split": 0,
"n_skipped_rejected": 0,
"n_skipped_schema_not_enabled": 0,
"summary": "Published 78 prediction(s) as prelabels."
}
}这次运行对 84 条评论做了预测,即 44 条 val 和 test 评论,加上无人标注的 40 条。有 6 个预测低于 0.6 的下限,其余 78 个成为预标签。你的得分和数量在每次运行之间都会不同,因为模拟标注者会随机犯错,标注的评论也不同。每个跳过计数器对应四项回写检查之一,配置按顺序列出了这些检查。
回写从不改变人工标签。预测进入条目数据,而标注留在每位标注者自己的状态中。用一个新用户名登录即可看到结果。下一条评论到来时,已选中模型的答案:
新标注者会看到已选中的模型答案,并确认或更改该答案
标注者只是查看预标签时,预标签不会被保存。标注者编辑预标签或转到下一条评论时,预标签才成为标注者的答案。因此未作答的评论不算完成,模型的答案也不会作为人工标签回到训练中。
在向标注者展示预标签之前,请先看留出得分。一次运行在 20 条评论上的 val 得分为 0.73 macro-F1,仍留有很大空间让错误的预标签出现。上游回写参考列出了其余的跳过计数器,以及每种标注类型如何显示预标签。
审核置信度最低的预标签
在 llm_confidence 下,下一位标注者拿到的是 40 条未标注评论,按已发布预标签的置信度从低到高排列。第二种策略提供预标签是为了质量控制,而不是为了标注。assignment_strategy: model_review 只提供带有预标签的条目,置信度最低的最先,并略去没有预测的条目。要对模型输出做一轮审核,就切换到 model_review,而当每个条目仍需要标签时,则保持 llm_confidence。上游模型审核参考说明了这两种队列。
模型审核报告会显示队列,以及模型漏掉了什么:
curl -H "X-API-Key: demo-admin-key" http://localhost:8000/admin/model-review上面那次运行的报告,列表已截短:
{
"n_items": 160,
"n_prelabelled": 78,
"queue": ["review_062", "review_024", "review_044", "review_122", "..."],
"worst_first": ["review_062", "review_024", "review_044", "..."],
"n_empty": 82,
"empty_prediction_ids": ["review_006", "review_007", "review_008", "..."],
"provenance": {
"review_062": {
"sentiment": {
"run_id": "20261104T043431-de5686",
"trainer": "sklearn-text",
"heldout_metric": "val macro_f1",
"heldout_score": 0.7333
}
}
}
}条目在 queue 中的排名取决于该条目在各方案中最低的预测置信度。worst_first 是队列中置信度最低的 20%,至少包含一个条目。empty_prediction_ids 列出没有预标签的条目。在这个项目中,82 个空条目是 76 条训练评论加上低于下限的 6 个预测。训练评论永远得不到预标签。
provenance 记录每个预标签的来源。对于队列中每个预标签由训练运行写入的条目,该记录会写明运行、训练器和这次运行的留出得分。随数据文件一起到来的预标签没有记录。当某个预标签看起来不对时,运行 id 会指向 /admin/training/runs/<run_id> 处的运行页面,该页面显示参数以及每个条目所在的划分。
记录判定
判定记录审核者对某一个预标签的判断,与审核者自己的标注相互独立。“模型是对的”和“有人独立选择了这个标签”是不同的事实,把两者分开,才能让模型的精确率保持可测量。Potato 通过一个路由接收判定,来自该条目所分配到的标注者,或来自持有 API 密钥的管理员:
curl -X POST -H "Content-Type: application/json" -H "X-API-Key: demo-admin-key" \
-d '{"instance_id": "review_062", "verdict": "correct", "schema_name": "sentiment"}' \
http://localhost:8000/api/model-review/verdictaccept 表示模型是对的,correct 表示有人修改了答案,reject 表示模型是错的。reject 会立即把预标签从条目上撤下,之后的运行不会再为该条目和方案发布预测。这次 reject 仍计入模型的精确率,算作被拒绝的预标签,而不是模型漏掉的标签,再次发送同一判定不会改变任何计数。correct 判定会把该条目移入之后每次运行的 train 划分,详见下面关于更正的一节。对同一条目记录第二个判定会替换第一个,未知的判定会以 HTTP 400 被拒绝。
按采集得分排列队列
按置信度排序,先提供的是模型最拿不准的条目。主动学习中的采集函数,按每个条目上的一个标签对下一个模型有多大帮助,来为未标注条目排序。hf-text-classification 和 hf-image-classification 接受 acquisition 参数,会在每个预测上写入这样的得分,随后 llm_confidence 先提供得分最高的条目。该训练器需要文本训练的 extra:
pip install "potato-annotation[train-text]"带上该参数启动一次运行,可以通过训练页面的 Parameters 字段,也可以通过 API:
curl -X POST -H "X-API-Key: demo-admin-key" -H "Content-Type: application/json" \
http://localhost:8000/admin/training/api/runs \
-d '{"trainer": "hf-text-classification", "schemas": ["sentiment"],
"params": {"acquisition": "margin"}}'该参数接受五个值:
acquisition | 写在每个预测上的得分 |
|---|---|
least_confident | 默认值。不写入得分,条目保持置信度顺序。 |
margin | 1 减去前两大概率之间的差距。 |
entropy | 预测分布的熵,缩放到 0–1。 |
bald | 标签与模型权重之间的互信息,由 mc_samples(默认 10)次开启 dropout 的前向传播得出。该方法来自 Houlsby et al. (2011)。在模型缺少数据的地方得分高,在无论数据多少条目本身都含糊不清的地方得分低。 |
coreset | 在模型的嵌入上,每个条目到最近的训练条目或已选条目的距离。该方法来自 Sener and Savarese (2018)。该得分让队列分散到整个数据上,因此同一个难条目的十个近似副本不会占据前十名。 |
margin 运行得到的预测会在置信度旁带上得分:
{"label": "positive", "confidence": 0.5207,
"probabilities": {"negative": 0.4793, "positive": 0.5207},
"acquisition": 0.9586}在 llm_confidence 下,有采集得分的条目排在最前,然后是只有置信度的条目,再然后是预测没有置信度的条目,最后是没有预测的条目。acquisition 适用于单标签方案(radio、select、likert)。排序读取的是已发布的预标签,所以该方案必须在 writeback.schemas 中,并且必须设置 include_unlabeled: true,未标注条目才会得到得分。model_review 忽略该得分并保持置信度顺序,因为审核环节要找的是模型的错误答案。上游关于选择下一步问什么的一节有详细说明。
在保留新运行的预标签之前,先把新运行的留出得分与 sklearn-text 运行比较。Potato 对每个条目和方案只保留一个预测,较新运行的预测会替换较旧的。当较新运行的预测已保存但不显示时,例如低于 min_confidence,较旧运行的预标签也会从条目上撤下,详见配置。在本教程于 CPU 上的一次运行中,默认模型训练三个 epoch(params 中的 epochs: 3),在 84 条训练评论上的 test macro-F1 为 0.52,低于使用相同标签的 sklearn-text 运行,该次运行发布了 37 个预标签。要撤回一次运行的预标签,请删除这次运行的预测:
curl -X DELETE -H "X-API-Key: demo-admin-key" \
http://localhost:8000/admin/training/api/runs/<run_id>/predictions撤回会移除该次运行已保存的预测,并把这次运行的预标签从条目上撤下。更早一次运行的预标签不会恢复,所以请再训练一次以重新发布。
自动重新训练
auto_retrain 会随着标注到来自行启动运行。当某次运行的留出得分足以支持展示预标签时,把这个块加入 model_training:
model_training:
enabled: true
writeback:
schemas: ["sentiment"]
include_unlabeled: true
min_confidence: 0.6
auto_retrain:
enabled: true
trainer: sklearn-text
schemas: ["sentiment"]
update_frequency: 25
min_interval_s: 600Potato 在每次保存标注后检查。自上次自动运行以来收到 25 条标注,并且距上次运行结束已过 600 秒时,就会启动一次运行。这两个值都是默认值,trainer 和 schemas 是必需的。params 传递训练器参数,与训练页面的 Parameters 字段相同。
请在第一次手动运行之后再开启 auto_retrain,而不要从第一条标注起就开。还没有任何运行的项目,一旦有了 update_frequency 条标注就会训练。从一开始就设置 update_frequency: 20 的话,这个项目的第一次自动运行只在 12 条评论上训练,在 3 条评论上的 val macro-F1 得分为 1.0,并发布了 72 个预标签。3 条评论上的留出得分,几乎说明不了接下来的 72 条。
当某次检查决定不启动运行时,/admin/training/api/status 中的 skip_reason 会给出原因,例如 "waiting for 3 more annotation(s)" 或 "waiting 299 more second(s) since the last run"。自动运行不会加入运行队列。队列清空之后保存的下一条标注会触发一次新的检查。计数器保存在内存中,重启之后,曾经运行过的项目会从重启起重新计数新的标注。许可证需要确认的训练器(ultralytics-detect、ultralytics-segment、layoutlmv3)还需要在该块中写 accept_licence: true,否则每次自动运行都会被跳过。含有这两个块的完整配置能通过 potato validate config.yaml --strict。
训练中被更正和被固定的条目
有两类条目会进入之后每次运行的 train 划分,无论它们在稳定划分中处于什么位置。第一类是带有 correct 判定的条目。更正来自模型表现最差的那些条目,如果这些更正留在 val 或 test 中,就等于用模型自己犯错的样本给模型评分。accept 和 reject 判定不会移动条目。第二类是被固定的条目,由管理员手工送去训练:
curl -X POST -H "X-API-Key: demo-admin-key" -H "Content-Type: application/json" \
http://localhost:8000/admin/training/api/pinned \
-d '{"instance_ids": ["review_007", "review_008"]}'被固定的条目在有人标注之后才会参与训练,在此之前,每次运行都把该条目算作等待中。设置 curation: {enabled: true} 后,目录页面上某个切片旁的 Train on these 按钮会固定该切片匹配的所有内容,例如内置的 model-found-nothing 切片。DELETE /admin/training/api/pinned 取消固定。
当一次运行把留出条目移入 train 时,会记录一条带数量的警告。在一次 correct 判定之后,本教程的下一次运行记录了如下内容:
1 held-out item(s) moved to train (corrected: 1). Held-out scores are computed without them, so they are not directly comparable with a run that had them.这些数量在运行数据包的 manifest.json 中,位于 stats.forced_train 之下。比较各次运行的留出得分时请记住这条警告,因为这两次运行评分的条目不同。
把一次运行的 test 划分保存为数据集
留出得分会随划分的变化而变化,而一组固定的条目才能让两个模型可以比较。运行页面上的 Keep this split as a dataset 一节,会把一次运行的 val、test 或 train 条目连同这些条目一致认可的人工标签,保存为数据集的一个新版本。选择划分,输入数据集名称,然后点击 Save as dataset version。按钮背后的路由在命令行中接受同样的请求:
curl -X POST -H "X-API-Key: demo-admin-key" -H "Content-Type: application/json" \
http://localhost:8000/admin/training/api/runs/<run_id>/pin-split \
-d '{"dataset": "sentiment-test", "split": "test"}'响应会写明新版本和条目数,这里是 23 条 test 评论保存为版本 v0001。版本在保存之后不再改变,所以之后的模型,以及数据集实验运行器中的 LLM 提示,都可以恰好在这些条目上评分。保存到已有版本的数据集时,会新建一个只包含该划分的版本,因此之后某次运行的 val 条目不会与那次运行训练过的条目混在一起。每个样本的输入都略去了预标签字段,所以评估无法读到模型的答案。当未设置 datasets.enabled 时,该路由返回 HTTP 400。
让标注者留出预标签
配置中的 ai_support.holdout 块,从第一条标注起就让 20% 的标注者与条目配对免于协助。对于留出的配对,标注页面不显示预标签,发送到浏览器的条目数据也略去了 writeback.field(这里是 predictions)和 pre_annotation.field。抽取依据的是对每位标注者和条目加盐的哈希,所以一个配对在每次访问以及重启之后都保持在同一组。每条评论有多位标注者时,大多数评论既有受协助的标注者,也有留出的标注者。管理员仪表盘的 AI influence 标签页会比较这两组,看答案与模型一致的频率、答案与金标准一致的频率,以及每组内部的一致性。在 num_annotators_per_item: 1 下,每条评论只落入一组,所以要研究这种影响,就把该值提高到 1 以上。用你的标签训练的模型辅助标注说明了如何解读这种比较。
延伸阅读
- 模型训练解释训练循环和训练页面。
- 模型训练配置列出每个
writeback和auto_retrain键及其默认值。 - 训练你的第一个模型搭建这个项目并完成第一次
sklearn-text运行。 - 在图像标注上训练检测与分割模型在图像上用框检测器运行同样的循环。
- 用你的标签训练的模型辅助标注讨论预标签何时节省时间,何时会锚定答案。
- 如何在带预标签的数据中区分认真审核与不加思考的接受找出不读就接受预标签的标注者。
- 主动学习讨论在不展示模型答案的情况下选择下一个条目。
- 上游模型训练参考给出每一项回写检查和每个路由。
参考文献
- Houlsby, N., Huszár, F., Ghahramani, Z., and Lengyel, M. (2011). Bayesian Active Learning for Classification and Preference Learning. arXiv:1112.5745. https://arxiv.org/abs/1112.5745
- Sener, O., and Savarese, S. (2018). Active Learning for Convolutional Neural Networks: A Core-Set Approach. ICLR 2018. https://arxiv.org/abs/1708.00489