將標註匯出到 Hugging Face Datasets
用一條匯出命令把 Potato 標註推送到 Hugging Face Hub,再把匯出的各個 split 整理成分類、NER、音訊和影像訓練所需的形狀。
一條命令就能把 Potato 標註作為資料集推送到 Hugging Face Hub,其中包含一個 annotations split、可選的 spans 和 items split,以及一份自動生成的資料集卡片。
訓練程式碼讀取的是 Hugging Face 資料集,而研究進行期間 Potato 並不按這種形狀儲存標註。標註過程中它儲存的是每個使用者的狀態,所以磁碟上的檔案是圍繞標註者組織的,而不是圍繞訓練樣本。匯出這一步就是兩種形狀相遇的地方,而 Potato 自帶一個 Hub 匯出器,你不必自己寫這個轉換。
標註過程中 Potato 寫了什麼
Potato 把每位標註者的工作儲存為 output_annotation_dir 下的一個 user_state.json 檔案,每個使用者一個目錄:
annotation_output/
├── user1/
│ └── user_state.json
├── user2/
│ └── user_state.json
└── ...
這些檔案是即時記錄,不是匯出格式。下游的一切都通過匯出 CLI 來讀取它們,而這也正是把 schema 名稱、span 偏移量和原始條目欄位統一對齊起來的地方。
用內建匯出器推送到 Hub
Hub 匯出器需要可選依賴,通過 huggingface extra 安裝:
pip install potato-annotation[huggingface]一條命令即可構建資料集並推送。--output 的值是 Hub 上的倉庫 id,token 需要寫許可權:
python -m potato.export \
--config config.yaml \
--format huggingface \
--output your-org/my-annotations \
--option token=hf_xxx如果你不想在命令列裡傳 token,可以在環境變數中設定 HF_TOKEN,或者執行 huggingface-cli login。另有四個選項控制上傳內容:private 建立私有資料集,commit_message 替換預設的提交資訊,而 include_items 和 include_spans(預設都是 true)決定源條目和 span 標註是否隨標籤一起上傳。
最終落在 Hub 上的資料集最多包含三個 split。annotations split 中每個 (instance_id, user_id) 組合一行,每個標註 schema 一列。spans split 中每條 span 標註一行,帶有 instance_id、user_id、schema_name、start、end、label 和 text。items split 中每個原始資料條目一行,以 item_id 為鍵,你的其餘源欄位與之並列。Potato 還會生成一份資料集卡片,說明各個 schema 及其標籤,統計標註記錄數,並附上用法片段和指回項目的連結。
把它讀回來就是普通的 load_dataset 呼叫:
from datasets import load_dataset
ds = load_dataset("your-org/my-annotations")
for row in ds["annotations"]:
print(row["instance_id"], row["sentiment"])如果你想在自己動手之前先看看資料的形狀,有兩個公開的匯出示例:Blablablab/potato-demo-single-choice-annotations 和 Blablablab/potato-demo-agent-trace-annotations。
為訓練任務重整匯出結果
Hub 匯出保留的是每個標註者對每個條目各一行,這對研究分歧是合適的形狀,對 Trainer 則是錯誤的形狀。當你需要訓練樣本時,改為匯出 JSONL 再在本地重整。JSONL 匯出器會在輸出目錄裡寫入 annotations.jsonl,每個 (instance_id, user_id) 組合一條記錄:
python -m potato.export --config config.yaml --format jsonl --output ./export/每條記錄帶有 instance_id、user_id、labels、spans 和 links。span 在匯出時已經把它所覆蓋的文本切好了,所以你不必再拿偏移量去對原始檔重新求解。
import json
records = []
with open("export/annotations.jsonl") as f:
for line in f:
records.append(json.loads(line))
print(records[0]["instance_id"], records[0]["labels"])每個條目一個訓練樣本
如果每個條目有三位標註者,分類器需要的是每個條目一個標籤,所以得先把按標註者分開的行摺疊起來。多數投票是常用做法,而把標註者數量一併留著,可以讓你丟掉投票人數太少的條目:
from collections import defaultdict
from statistics import mode
from datasets import Dataset
by_item = defaultdict(list)
for rec in records:
by_item[rec["instance_id"]].append(rec["labels"]["sentiment"])
rows = [
{"id": item_id, "label": mode(labels), "num_annotators": len(labels)}
for item_id, labels in by_item.items()
]
dataset = Dataset.from_list([r for r in rows if r["num_annotators"] >= 2])按標註者分開的行也要留著。標註者產生分歧的條目才是值得細讀的,而當任務確實帶有主觀性時,用軟標籤訓練出的模型往往勝過用多數投票訓練的模型。
附上條目文本
標籤行是按 id 引用條目的,所以文本必須來自 items split 或你自己的資料檔案:
items = {row["item_id"]: row for row in load_dataset("your-org/my-annotations")["items"]}
dataset = Dataset.from_list([
{"text": items[r["id"]]["text"], "label": r["label"]}
for r in rows
if r["id"] in items
])修正標籤類型
字串標籤列也能訓練,但它丟掉了類別順序,也丟掉了模型配置裡的類別名。轉成 ClassLabel 可以把兩者都補回來,而這些名稱必須和你的 Potato schema 輸出的標籤一致:
from datasets import ClassLabel
dataset = dataset.cast_column(
"label",
ClassLabel(names=["Positive", "Negative", "Neutral"])
)訓練、驗證和測試劃分
按條目劃分,而不是按標註行劃分。按行劃分會把兩位標註者對同一條目的判斷分到不同的劃分裡,等於洩露了答案:
from datasets import DatasetDict
split = dataset.train_test_split(test_size=0.2, seed=42)
held_out = split["test"].train_test_split(test_size=0.5, seed=42)
dataset = DatasetDict({
"train": split["train"],
"validation": held_out["train"],
"test": held_out["test"],
})把 span 標註變成 token 標籤
序列標註模型讀的是每個 token 一個標籤,而 spans split 存的是字元偏移量。無論 span 來自 span schema 還是來自匯入的語料,轉換方式都一樣,只是需要一個能把偏移量映射回字元的分詞器:
def to_iob(text, spans, tokenizer):
encoding = tokenizer(text, return_offsets_mapping=True)
tags = ["O"] * len(encoding["input_ids"])
for span in spans:
for i, (start, end) in enumerate(encoding["offset_mapping"]):
if start == end:
continue
if start >= span["start"] and end <= span["end"]:
prefix = "B" if start == span["start"] else "I"
tags[i] = f"{prefix}-{span['label']}"
return encoding["input_ids"], tags先按 instance_id 把 span 分好組,因為匯出結果是每條 span 一行,而不是每個條目一行。
音訊列和影像列
音訊和影像任務在條目欄位裡存的是一個路徑或一個 URL,當這一列被轉成對應的特徵類型時,datasets 就會載入相應的媒體檔案:
from datasets import Audio, Image
audio_ds = audio_ds.cast_column("audio", Audio(sampling_rate=16000))
image_ds = image_ds.cast_column("image", Image())取樣率必須和模型的預期一致,所以選定之前先查一下模型卡片。轉換是在讀取時重取樣,而不是重寫檔案。
資料集卡片裡該記錄什麼
自動生成的卡片涵蓋了各個 schema、標籤集合和記錄數。其餘部分是複用這份資料集的人會問到的內容,而趁研究還新鮮時寫下來要容易得多:
---
license: cc-by-4.0
task_categories:
- text-classification
language:
- en
---
## Annotation process
Who annotated, how many annotators per item, how they were recruited and paid,
and what the guidelines said. Link the guidelines if they are public.
## Agreement
Which coefficient you computed, over which items, and what it came out at.
## Label definitions
One sentence per label, in the words the annotators saw.許可證要寫在 frontmatter 裡,而不是留給別人去猜,因為沒有許可證的資料集別人無法放心使用。標籤發生變化時給倉庫打上版本,這樣論文才能引用它所用資料的那個狀態。
把結果載入訓練程式碼
現在這個資料集已經是 Trainer 期望的形狀,從匯出到訓練迴圈之間沒有任何環節需要再動它:
from transformers import Trainer
trainer = Trainer(
model=model,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
)匯出器的完整選項列表,以及從 Hub 回到一個新 Potato 任務的往返流程,見 Hugging Face 匯出文件。Potato 寫出的其他格式見資料格式。