跳转到内容

用你的标注训练第一个模型

在没有 GPU 的笔记本上运行 Potato 的示例评论研究,用模拟器标注 120 条评论,训练 sklearn-text,并查看模型给出的预标签。

v2.14.0 新增

本教程在一个示例研究自己的标签上训练情感分类器,并把分类器的答案作为预标签展示给下一位标注者。教程写给第一次尝试 Potato 模型训练的研究负责人,每一步都能在没有 GPU 的笔记本上运行,除 Potato 本身外无需任何安装。你要启动 Potato 的示例评论项目,让用户模拟器标注项目 160 条评论中的 120 条,在管理员训练页面训练 sklearn-text,查看这次运行的留出得分,再以新标注者的身份登录,看到模型的答案。关于循环中每个部分的一般作用,请参阅模型训练概览。

安装 Potato 和示例项目

本教程使用 sklearn-text。这个训练器把每条评论转换成 TF-IDF 特征,并拟合一个逻辑回归分类器。sklearn-text 无需额外安装,因为 scikit-learn 是 Potato 的核心依赖之一。示例项目在 Potato 的 GitHub 仓库里,而不在 pip 包中,所以要安装 Potato 并克隆仓库:

bash
pip install --upgrade potato-annotation
git clone https://github.com/davidjurgens/potato.git
cd potato

下面的每条命令都在仓库根目录运行。项目文件夹 examples/ai-assisted/model-training/ 中有三个文件:

  • config.yaml 是研究配置,注释里写着本教程用到的命令。
  • data/reviews.json 包含 160 条合成的商品评论,每行一个 JSON 对象,带有 id 和 text。
  • data/gold.json 给出每条评论的正确标签。模拟器按这个文件作答。

示例配置

配置定义了一个名为 sentiment 的 radio 方案,标签为 positive 和 negative,并开启了模型训练。下面的代码块是去掉注释的原始文件,该文件无需修改即可通过 python -m potato.validate_cli --strict:

yaml
annotation_task_name: "Review Sentiment (model training demo)"
 
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
 
annotation_schemes:
  - annotation_type: "radio"
    name: "sentiment"
    description: "Is this review positive or negative?"
    labels:
      - "positive"
      - "negative"
    sequential_key_binding: true
 
model_training:
  enabled: true
  writeback:
    schemas: ["sentiment"]
    include_unlabeled: true
    min_confidence: 0.6

四项设置决定了教程的走向:

  • num_annotators_per_item: 1 让每条评论得到一个标签。三位模拟标注者各标 40 条,就能覆盖 120 条不同的评论,剩下 40 条未标注的留给模型。
  • require_password: false 让新标注者只凭用户名即可登录。最后一步就用这种方式登录。
  • admin_api_key: "demo-admin-key" 用来打开管理员页面。在其他人能访问的服务器上,请更换这个密钥。
  • model_training 注册 /admin/training 路由。writeback 块会把 sentiment 方案的预测展示给标注者,下文回写一节会说明其中每个键。

在 8000 端口启动服务器

用示例配置启动 Potato:

bash
potato start examples/ai-assisted/model-training/config.yaml -p 8000

服务器会加载这 160 条评论,并在 http://localhost:8000 提供标注页面。如果你的机器上 8000 端口已被占用,请给 -p 指定另一个端口,并在下面的每个 URL 里使用该端口。

用模拟器标注 120 条评论

模型需要人工标签才能学习。用户模拟器代替三位标注者。每位模拟标注者按金标准文件作答,大多数答案是正确的。在第二个终端里,从仓库根目录运行模拟器:

bash
python -m potato.simulator --server http://localhost:8000 --users 3 \
  --strategy gold_standard --competence good=1.0 --max-annotations 40 \
  --gold-file examples/ai-assisted/model-training/data/gold.json \
  --sequential --fast-mode --no-export

--strategy gold_standard 让每位模拟标注者按 gold.json 作答,--competence good=1.0 让三位标注者都使用模拟器的 good 档案。good 标注者会抽取一个 0.80 到 0.90 之间的准确率,并以该概率答对每条评论。因此这些标签含有错误,和真实团队的标签一样。--sequential 让三位标注者依次运行,--fast-mode 去掉作答之间的停顿,--no-export 则跳过模拟器自身结果文件的写入。模拟器大约两秒完成,并以一份摘要收尾。继续之前,请确认摘要中报告了 Total annotations: 120。你也可以在 http://localhost:8000 手工标注评论。当 train 划分中已标注的条目少于 10 个时,sklearn-text 拒绝拟合。

用管理员密钥打开训练页面

训练页面需要管理员会话。打开 http://localhost:8000/admin,在页面询问 API 密钥时输入 demo-admin-key,然后打开 http://localhost:8000/admin/training。

Potato 训练页面,已选择 sklearn-text、sentiment 方案和 local 后端,上方是 Start a run 面板,下方的 Runs 表列出两次已完成的运行,留出得分分别为 0.847 和 0.840在示例项目上完成两次 sklearn-text 运行后的训练页面

顶部的 Start a run 面板包含全部设置。Schema 列出项目唯一的方案 sentiment (radio)。Trainer 列出全部 38 个训练器,项目无法运行的训练器会在名称旁标注原因,例如 "not installed"。页面选中 sklearn-text,只要 sklearn-text 适用于该方案。Runs on 显示项目唯一的后端 local (this machine)。

训练 sklearn-text

保持 Parameters 为空以使用训练器的默认值,然后点击 Train now。这次运行先把标注合并成每条评论一个标签,再把 120 条已标注的评论划分为 train、val 和 test 三个集合,并在单独的进程中拟合模型。我们的运行用了 1.5 秒,运行结束时面板显示 "Run finished."。

sklearn-text 默认用 scikit-learn 的 TfidfVectorizer 提取特征,用 LogisticRegression 作分类器。当 train 划分中有 50 个或更多条目时,sklearn-text 还会对分类器的置信度做校准。训练条目少于 1,000 时用 Platt 缩放,达到 1,000 条起用保序回归。只有当校准使训练准确率下降不超过 0.05 时,才保留校准后的模型。校准在这里很重要,因为回写步骤会把每个置信度与 min_confidence 比较。

要用脚本启动同样的运行,请在 X-API-Key 请求头中发送管理员密钥:

bash
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"]}'

响应中带有新的 run_id 和 "started": true。

运行页面和留出得分

完成的运行会出现在 Runs 表中,附有 train 和 val 的大小以及它的 Held-out score。点击运行 ID,即可打开 /admin/training/runs/<run_id> 处的运行页面。运行页面列出该次运行的状态、训练器、后端、划分大小、划分种子、留出得分和参数,然后是一张包含每项指标的表以及运行日志。

默认情况下,一次运行会把已标注条目的 15% 留出作 val,15% 留出作 test,由 split_seed 决定哪些条目落入哪个集合。只要 split_seed 的值不变,每次运行的划分就保持一致,所以对同一批标签的两次运行是在相同的条目上评分的。sklearn-text 运行的留出得分是 val 上的 macro-F1。Macro-F1 对每个标签(这里是 positive 和 negative)的 F1 得分取平均。运行还会记录 test 上的准确率和 macro-F1。train 划分上的准确率记为 train_accuracy,从不用作得分。在我们的运行中,日志如下:

text
[info] Read 76 labelled rows from the train split
[info] Applied sigmoid calibration over 5 folds
[info] Held-out val macro_f1: 0.725

Potato 自己的文档报告了同一项目上的一次运行,下表把那次运行与我们的运行并排列出:

Potato 的文档我们的运行
已标注评论120120
train、val 和 test 条目数未说明76、19 和 25
留出得分(val macro-F1)0.840.725
test macro-F1未说明0.826
已发布的预标签8073
低于 min_confidence 的预测411

你的数字会与两列都不同,因为每位模拟标注者随机抽取一个准确率,并犯下不同的错误。在我们的运行中,模拟标签在 120 条评论中有 97 条与 gold.json 一致,在 19 条 val 评论中有 15 条一致。留出得分衡量的是与你的标注者所给标签的一致程度,而不是与任何标准答案的一致程度,所以 val 中的标注错误会拉低得分。我们的运行发布的 73 个预标签全部与 gold.json 相符。在没有金标准文件的真实研究里,看到较低的留出得分时,应先检查标注者之间的一致性,再去怪模型。

为方案开启回写

一次运行的预测只通过回写送到标注者面前。示例配置已经为 sentiment 方案开启了回写。对于你自己的项目,请在配置的 model_training 下加入这个块,并重启服务器:

yaml
model_training:
  enabled: true
  writeback:
    schemas: ["sentiment"]
    include_unlabeled: true
    min_confidence: 0.6

writeback.schemas 指定标注者能看到其预测的方案。该列表默认为空,所以开启训练后,在你指定方案之前,标注页面不会有任何变化。include_unlabeled: true 让每次运行除了对 val 和 test 评论做预测,也对无人标注的评论做预测。没有 include_unlabeled 时,一次运行只对 val 和 test 评论做预测,而这些评论已经有人工标签。min_confidence: 0.6 会在模型置信度低于 0.6 时,不把该预测放到标注页面上。Potato 仍会保存这个预测。

回写在每次成功运行之后执行,并跳过该次运行 train 划分中的所有条目。在我们的运行中,模型对 84 条评论做了预测(19 条 val、25 条 test 和 40 条未标注),其中 11 个预测低于 0.6,运行发布了其余 73 个。GET /admin/training/api/status 的 writeback 字段给出相同的数量,摘要为 "Published 73 prediction(s) as prelabels."。已保存的预测会在服务器启动时再次回写,所以预标签在重启后依然存在。若要从条目上撤下某次运行的预标签,请发送 DELETE /admin/training/api/runs/<run_id>/predictions。

新标注者看到的预标签

在隐私浏览窗口中打开 http://localhost:8000,用一个没人用过的用户名登录,例如 newannotator。每条已标注的评论都已有它的那一个标签,所以新标注者被分配到 40 条未标注的评论,我们的运行中进度计数器显示 0/40。我们的第一条评论是 "The toaster: Disappointed, it stopped working within a week. Much smaller than the photos suggest.",其中 Negative 已被选中,置信度为 0.68。

Potato 标注页面,显示一条烤面包机评论,模型已选中 Positive 选项,该条目仍标记为 Not labeled一条打开时已选中模型答案的评论,此时标注者还没有保存任何内容

标注者只是查看评论时,预标签不会被保存,状态标签会一直显示 "Not labeled",直到标注者保存答案。标注者更改选择或点击 Next 时,模型的答案就成为标注者的答案。在我们的运行中,预测低于 min_confidence 的 5 条未标注评论打开时没有任何选中项。通过 ai_support.holdout 被排除在机器协助之外的标注者,完全看不到预标签。预标签会把标注者拉向模型的答案,所以请等留出得分高到值得信任时再开启回写,并用检测被不加思考接受的预标签中的检查,留意那些照单全收所有预标签的标注者。

后续步骤

在换成你自己的数据之前,同一个项目还支持几个实验:

  • 更改一个参数。 在 Parameters 中输入 {"vectorizer_kwargs": {"ngram_range": [1, 2]}} 并再次训练,把词对加入特征。我们加入词对的第二次运行在同一划分上得到 0.774 的 val macro-F1,并发布了 74 个预标签。sklearn-text 不接受的参数名会被拒绝,并附上该训练器接受的名称列表。
  • 试试 transformer。 hf-text-classification(pip install "potato-annotation[train-text]")和 setfit(pip install "potato-annotation[embeddings]")可以学习同一个方案,训练器页面给出了每个训练器的参数。
  • 闭合循环。 循环中的模型加入了审核队列、用 auto_retrain 随标注到来而重新训练,以及把一次运行的划分保存为数据集版本。
  • 把训练移到 GPU 上。 在 GPU 服务器、SLURM 集群或 NAT 后的机器上训练会把同样的运行发送到另一台机器。
  • 训练其他类型的模型。 关于微调 LLM、视觉模型、音频模型和科学模型的教程,用其他训练器和方案遵循相同的步骤。

这里用到的每个键都列在模型训练配置参考中。要开始你自己项目的第一次运行,请把上面的 model_training 块复制到配置里,并在 writeback.schemas 下写上你的方案。

延伸阅读