跳轉到內容

用預標籤和重新訓練實現迴圈中的模型標註

把訓練好的模型的答案作為預標籤展示出來,先稽核置信度最低的預標籤,並在標註者更正答案時重新訓練模型。

v2.14.0 新增

本教程讓一項情感研究走完一遍迴圈中的模型流程。模型在最初的標籤上訓練,模型的預測以預標籤的形式出現在標註者面前,稽核者更正置信度最低的預標籤,這些更正再送入下一次執行。教程寫給已經訓練過第一個模型(如訓練你的第一個模型所示),現在希望標註者與模型協同工作的研究負責人。除一步之外,每一步都能在筆記本上用 sklearn-text 執行。採集一步會訓練一個小型 transformer,在 CPU 上需要幾分鐘。

情感項目

這個項目是 Potato 的模型訓練示例,包含 160 條合成的商品評論,以及一個名為 sentiment 的 radio 方案。預標籤是模型在任何人標註某條目之前為該條目提出的一個標籤。回寫是把訓練執行的預測變成標註頁面上的預標籤的那一步。到教程結束時,項目會在前 120 個標籤上訓練一個分類器,在其餘評論上釋出預標籤,優先提供置信度最低的評論,隨著新標籤到來重新訓練,並把一個留出劃分儲存為資料集版本。

預標籤讓標註更快,同時也把標註者拉向模型的答案。用你的標籤訓練的模型輔助標註討論了何時值得做這種取捨,如何在帶預標籤的資料中區分認真稽核與不加思考的接受則討論不讀就接受預標籤的標註者。下面的配置從一開始就讓一部分標註者看不到任何預標籤。這些留出的答案讓研究能夠衡量這種牽引。

搭建項目

安裝 Potato。sklearn-text 除基礎包外不需要任何東西:

bash
pip install --upgrade potato-annotation

把 Potato 倉庫中的 examples/ai-assisted/model-training/ 複製到一個新資料夾。該資料夾中有 data/reviews.json(160 條評論)和 data/gold.json(模擬器據以作答的標籤)。用下面的配置替換示例的 config.yaml:

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 塊。啟動伺服器之前先檢查配置:

bash
potato validate config.yaml
potato start config.yaml -p 8000

標註種子集

用你的標籤訓練的模型,首先需要有標籤。當訓練劃分中的條目少於 10 個時,sklearn-text 拒絕擬合。可以在 http://localhost:8000 標註一些評論,也可以讓三位模擬標註者根據金標準檔案各標註 40 條評論:

bash
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。同樣的執行也可以從命令列啟動:

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

這次執行需要幾秒鐘。Potato 把已標註的評論劃分為 train、val 和 test(預設分別為 70%、15% 和 15%),在 train 上擬合分類器,並在 val 和 test 上為分類器評分。隨後狀態路由會報告得分和回寫報告:

bash
curl -H "X-API-Key: demo-admin-key" http://localhost:8000/admin/training/api/status

本教程的一次執行返回瞭如下內容,已裁剪為此處相關的欄位:

json
{
  "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 個成為預標籤。你的得分和數量在每次執行之間都會不同,因為模擬標註者會隨機犯錯,標註的評論也不同。每個跳過計數器對應四項回寫檢查之一,配置按順序列出了這些檢查。

回寫從不改變人工標籤。預測進入條目資料,而標註留在每位標註者自己的狀態中。用一個新使用者名稱登入即可看到結果。下一條評論到來時,已選中模型的答案:

Potato 標註頁面上的一條商品評論,模型已選中 Positive,標註者尚未作答新標註者會看到已選中的模型答案,並確認或更改該答案

標註者只是檢視預標籤時,預標籤不會被儲存。標註者編輯預標籤或轉到下一條評論時,預標籤才成為標註者的答案。因此未作答的評論不算完成,模型的答案也不會作為人工標籤回到訓練中。

在向標註者展示預標籤之前,請先看留出得分。一次執行在 20 條評論上的 val 得分為 0.73 macro-F1,仍留有很大空間讓錯誤的預標籤出現。上游回寫參考列出了其餘的跳過計數器,以及每種標註類型如何顯示預標籤。

稽核置信度最低的預標籤

在 llm_confidence 下,下一位標註者拿到的是 40 條未標註評論,按已釋出預標籤的置信度從低到高排列。第二種策略提供預標籤是為了品質控制,而不是為了標註。assignment_strategy: model_review 只提供帶有預標籤的條目,置信度最低的最先,並略去沒有預測的條目。要對模型輸出做一輪稽核,就切換到 model_review,而當每個條目仍需要標籤時,則保持 llm_confidence。上游模型稽核參考說明了這兩種佇列。

模型稽核報告會顯示佇列,以及模型漏掉了什麼:

bash
curl -H "X-API-Key: demo-admin-key" http://localhost:8000/admin/model-review

上面那次執行的報告,列表已截短:

json
{
  "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 金鑰的管理員:

bash
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/verdict

accept 表示模型是對的,correct 表示有人修改了答案,reject 表示模型是錯的。reject 會立即把預標籤從條目上撤下,之後的執行不會再為該條目和方案發布預測。這次 reject 仍計入模型的精確率,算作被拒絕的預標籤,而不是模型漏掉的標籤,再次傳送同一判定不會改變任何計數。correct 判定會把該條目移入之後每次執行的 train 劃分,詳見下面關於更正的一節。對同一條目記錄第二個判定會替換第一個,未知的判定會以 HTTP 400 被拒絕。

按採集得分排列佇列

按置信度排序,先提供的是模型最拿不準的條目。主動學習中的採集函式,按每個條目上的一個標籤對下一個模型有多大幫助,來為未標註條目排序。hf-text-classification 和 hf-image-classification 接受 acquisition 參數,會在每個預測上寫入這樣的得分,隨後 llm_confidence 先提供得分最高的條目。該訓練器需要文本訓練的 extra:

bash
pip install "potato-annotation[train-text]"

帶上該參數啟動一次執行,可以通過訓練頁面的 Parameters 欄位,也可以通過 API:

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": "hf-text-classification", "schemas": ["sentiment"],
       "params": {"acquisition": "margin"}}'

該參數接受五個值:

acquisition寫在每個預測上的得分
least_confident預設值。不寫入得分,條目保持置信度順序。
margin1 減去前兩大機率之間的差距。
entropy預測分佈的熵,縮放到 0–1。
bald標籤與模型權重之間的互資訊,由 mc_samples(預設 10)次開啟 dropout 的前向傳播得出。該方法來自 Houlsby et al. (2011)。在模型缺少資料的地方得分高,在無論資料多少條目本身都含糊不清的地方得分低。
coreset在模型的嵌入上,每個條目到最近的訓練條目或已選條目的距離。該方法來自 Sener and Savarese (2018)。該得分讓佇列分散到整個資料上,因此同一個難條目的十個近似副本不會佔據前十名。

margin 執行得到的預測會在置信度旁帶上得分:

json
{"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 個預標籤。要撤回一次執行的預標籤,請刪除這次執行的預測:

bash
curl -X DELETE -H "X-API-Key: demo-admin-key" \
  http://localhost:8000/admin/training/api/runs/<run_id>/predictions

撤回會移除該次執行已儲存的預測,並把這次執行的預標籤從條目上撤下。更早一次執行的預標籤不會恢復,所以請再訓練一次以重新發布。

自動重新訓練

auto_retrain 會隨著標註到來自行啟動執行。當某次執行的留出得分足以支援展示預標籤時,把這個塊加入 model_training:

yaml
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: 600

Potato 在每次儲存標註後檢查。自上次自動執行以來收到 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 判定不會移動條目。第二類是被固定的條目,由管理員手工送去訓練:

bash
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 判定之後,本教程的下一次執行記錄瞭如下內容:

text
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。按鈕背後的路由在命令列中接受同樣的請求:

bash
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 以上。用你的標籤訓練的模型輔助標註說明了如何解讀這種比較。

延伸閱讀

參考文獻

  • 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