スプレッドシートでのラベル付け作業を Potato に移す
ラベル付き行の CSV を Potato に読み込み、二人のアノテーターに古いラベルを見せずに行へラベルを付け直してもらい、アノテーターごとに一列ずつ戻します。
Excel や Google スプレッドシートで行にラベルを付けているチームは、このチュートリアルに従って、その作業を Potato 2.13.1 に移し、二人のアノテーターに、互いのラベルにも、シートにすでにあるラベルにも触れさせずに 40 文へのラベル付けをやり直してもらうことができます。最終的には、二人のアノテーターの一致度の数値、シートのラベルとの比較、そして同じ表計算プログラムで開ける、文ごとに一行、アノテーターごとに一列の CSV が手に入ります。数百行を一人でラベル付けするなら、スプレッドシートで十分うまくいきます。二人目が加わるとスプレッドシートがどこで限界を迎えるかは、アノテーションツールを作るか買うかで説明しています。アノテーションツールの事前の経験は想定していません。
シート
文は Financial PhraseBank(参考文献 1)のもので、経済ニュースから取った 4,840 の文の集合です。金融の背景を持つ 16 人が、各文を投資家にとって肯定的、否定的、中立のいずれかにラベル付けし、一文あたり 5 から 8 人のアノテーターが担当しました。この公開データは CC BY-NC-SA 3.0 でライセンスされています。これらの文から作ったシートは、帰属表示を行い同じライセンスの下で、非商用の用途に限って共有できます。
このチュートリアルのシートには、無作為に抽出した 40 文があります。20 文は、元のアノテーター全員が一致した文のファイルから、20 文は 50% から 66% しか一致しなかった文から取りました。label 列には公開データの多数決のラベルがあり、同僚がすでにシートに入力したラベルの代わりになります。agreement 列は、各文がどちらのグループから来たかを記録します。シートの三行を下に示します。
id,sentence,label,agreement
s08,"Finnish financial group Aktia reports operating profit of EUR 44.4 mn in January-September 2009 , up from EUR 37.3 mn in the corresponding period in 2008 .",positive,100%
s16,"It generated an operating loss of EUR 96.3 mn , down from a profit of EUR 43.9 mn .",negative,100%
s35,It is the most awkward thing to use if you 're holding the phone with one hand .,negative,50-65%ご自身のシートには、すべての行で値が異なる id 列が必要です。シートに ID 列がない場合は、行番号や s01 のようなコードの列を追加してください。
シートの CSV としての保存
Potato は CSV、TSV、JSON、JSON Lines、Parquet ファイルを読み込み(データ形式)、Excel のブックは読み込みません。Google スプレッドシートでは、「ファイル」、「ダウンロード」、「カンマ区切りの値(.csv)」の順に選びます。Excel では、「名前を付けて保存」で種類に「CSV UTF-8」を選びます。設定でブックを指定すると、サーバーは Unsupported input file format xlsx で停止します。代わりに Python でブックを変換するには、Potato がインストールする pandas と、openpyxl(pip install openpyxl)を使います。
import pandas as pd
pd.read_excel("sheet.xlsx", dtype=str).to_csv("sheet.csv", index=False)dtype=str は、すべてのセルをテキストとして読み込みます。このオプションがないと、空のセルが一つある列では、pandas は ID 007 を数値 7.0 として読み込みます。Potato はサーバー起動時にファイルの問題を報告し、テスト実行では下のすべての問題に遭遇しました。
- アクセント付きの文字があると、サーバーが
UnicodeDecodeErrorで停止します。 Excel の通常の「CSV」形式は、古い Windows のエンコーディングで書き出します。「CSV UTF-8」で保存するか、設定のdata_filesの項目を- {path: data/items.csv, encoding: cp1252}と書いて、エンコーディングを指定してください。 - 下部の空白行は
Empty ID in column 'id'になります。 保存する前に、シートの空行を削除してください。 - セルの間のセミコロンは、セル数のエラーになります。 一部のヨーロッパのロケールの Excel は、セルをセミコロンで区切ります。区切り文字を変えるか、Google スプレッドシートからエクスポートしてください。
- 大文字の
.CSV拡張子は、2.12.1 では拒否されました。 Potato 2.13.1 はそのファイルを受け付け、2.12.1 ではファイル名を.csvで終わるように変更してください。
古いラベルをアノテーターから隠す
Potato は、行のすべての列をアノテーターのブラウザに送り、行全体がアノテーションページのページソースに入ります。テスト実行では、ページには文しか表示されていないのに、すべての項目のページソースにシートの label の値が見つかりました。古いラベルをアノテーターから隠すには、id と sentence の列だけを持つファイルを Potato に渡し、後の比較のために sheet.csv は保管しておきます。
import pandas as pd
pd.read_csv("sheet.csv", dtype=str)[["id", "sentence"]].to_csv("data/items.csv", index=False)代わりにスプレッドシート内でファイルを作るには、タブをコピーし、コピーからラベルの列を削除して、コピーを CSV としてダウンロードします。
Potato 2.13.1 からは、item_properties.private_keys: [label] が、その列をアノテーターから隠すもう一つの方法です。Potato は label 列を、ブラウザに送るすべてのものから除外し、その列はエクスポートには残ります。
設定
Potato をインストールし、sheet.csv、data/items.csv、下の設定を入れたプロジェクトフォルダーを作ります。
pip install potato-annotation==2.13.1この設定は完全なもので、2.12.1 と 2.13.1 の両方で python -m potato.validate_cli config.yaml --strict を通ります。
annotation_task_name: Financial news sentiment
task_dir: .
output_annotation_dir: annotation_output/
data_files:
- data/items.csv
item_properties:
id_key: id
text_key: sentence
user_config:
allow_all_users: true
users: []
num_annotators_per_item: 2
annotation_schemes:
- annotation_type: radio
name: sentiment
description: Would this news move the company's share price up, down, or not at all?
labels: [positive, neutral, negative]id_key と text_key は、data/items.csv の二つの列を指定します。num_annotators_per_item: 2 は、すべての文を二人のアノテーターに送ります。radio スキームは、各文に三つのラベルから一つの回答を与えます。ラベル名はシートの label 列の綴りに合わせてあり、二組のラベルは、コードを付け替えずに比較できます。アノテーションスキームにはその他の質問タイプが一覧されており、設定の基本は、その他のキーを説明しています。
ラベル付け
プロジェクトフォルダーからサーバーを起動します。
potato start config.yaml -p 8000各アノテーターは http://localhost:8000 を開き、ユーザー名とパスワードで登録して、文の下に三つのラベルが付いた状態で、一度に一文を見ます。キー 1、2、3 でラベルを選び、「Next」で回答を保存します。別のコンピューターにいる同僚には、そのコンピューターから届くアドレスでサーバーが必要で、これは VPS への Potato のデプロイで説明されています。
このページの数値は、Potato 2.12.1 で、ヘッドレスブラウザのアノテーションページを通じて 40 文すべてにラベルを付けた、スクリプト化された二人のアノテーター ana と ben による実行から得たものです。最初のアノテーターのラベルは、各文を読んで作られました。二人目のアノテーターのラベルは別々に書かれ、不確かな文で最初のアノテーターと異なります。したがって、この数値は二人のシミュレートされたアノテーターを表すものであり、実際のアノテーターでのスキームの信頼性については何も述べていません。
二人のアノテーター間の一致度
管理ページは、ラベル付けが進行中でも一致度を報告します。http://localhost:8000/admin/iaa を開き、誰かが初めて管理ページを要求したときに Potato がプロジェクトフォルダーの admin_api_key.txt に書き込む管理者キーを入力します。二人のスクリプト化されたアノテーターでは、ページは、両方のアノテーターがラベル付けした 40 項目、Krippendorff のアルファ 0.580、Cohen の kappa 0.575、文の 75% での一致を報告しました。Cohen の kappa と Krippendorff のアルファは、どちらも、二人のアノテーターが偶然に達する一致を差し引きます。二つの係数の読み方は、アノテーター間一致度で説明されています。
レポートのためには、ラベル付けが終わったら信頼性エクスポートを一度実行します。
python -m potato.export --config config.yaml --format reliability --output export/コマンドは export/reliability.csv と export/reliability.md を書き出します。二人のスクリプト化されたアノテーターの表です。
| Variable | Level | Krippendorff's α | 95% CI | % agreement | Units | Coders |
|---|---|---|---|---|---|---|
| sentiment | nominal | 0.58 | [0.303, 0.8] | 75.0 | 40 | 2 |40 文は小さな標本なので、区間は 0.303 から 0.8 に及びます。標本が大きくなれば、区間は狭くなります。
シートのレイアウトでラベルを戻す
Potato の CSV エクスポートは、アノテーターと文ごとに一行、ラベルごとに一列を持ちます。
python -m potato.export --config config.yaml --format csv --output export/コマンドは、40 文に対する 80 行の export/annotations.csv を書き出します。
instance_id,user_id,sentiment.positive,sentiment.negative,sentiment.neutral
s27,ana,positive,,表計算ソフトのユーザーは通常、元のシートのレイアウトを望みます。下のスクリプトは、三つのラベル列を一つの sentiment 列にまとめ、各アノテーターを列にし、その列を元のシートの隣に置きます。
"""One row per sentence, one column per annotator, next to the original sheet."""
import pandas as pd
sheet = pd.read_csv("sheet.csv", dtype=str)
potato = pd.read_csv("export/annotations.csv", dtype=str)
# Potato writes one column per option (sentiment.positive, ...); keep whichever is filled
potato["sentiment"] = potato.filter(like="sentiment.").bfill(axis=1).iloc[:, 0]
wide = potato.pivot(index="instance_id", columns="user_id", values="sentiment")
out = sheet.merge(wide, left_on="id", right_index=True, how="left")
out.to_csv("labels_by_annotator.csv", index=False)プロジェクトフォルダーから python back_to_sheet.py でスクリプトを実行します。labels_by_annotator.csv には、シートの列に続いて ana と ben があります。
id label agreement ana ben
s01 neutral 50-65% neutral positive
s02 neutral 50-65% negative neutral
s03 neutral 100% neutral neutral
s04 neutral 50-65% positive positive--format tsv エクスポートは、タブを期待するスクリプトのために、同じ行を export/annotations.tsv として書き出します。
シートとの一致度
このサイトの一致度計算ツールは、最初の列に項目、その後ろにアノテーターごとの列を持つ表を読み込みます。labels_by_annotator.csv の id、label、ana の列を計算ツールに貼り付け、次に id、label、ben の列を貼り付けます。テスト実行では、次の値が得られました。
| シートとの比較 | Krippendorff のアルファ | Cohen の κ | 同じラベル |
|---|---|---|---|
| ana | 0.745 | 0.743 | 40 件中 34 件 |
| ben | 0.563 | 0.558 | 40 件中 30 件 |
ana、ben、シートのいずれかに不一致があった 13 行のすべては、元のアノテーターの意見が割れていた 20 文から来ていました。元のアノテーター全員が一致した 20 文では、ana、ben、シートは毎回同じラベルを付けていました。三行では、ana と ben が互いに一致し、シートとは一致しませんでした。その三行についてのシートのラベルは、最初に確認すべきものです。より大きなシートでそのような行を見つけるには、labels_by_annotator.csv を表計算ソフトで開き、ana または ben 列が label と異なる行を絞り込んでください。
シートのラベルを Potato に取り込む
上の手順では、シートのラベルは Potato に入らず、二人のアノテーターはシートとは独立にラベルを付けます。シートのラベルから作業を続けたいチームには、他に二つの経路があります。
シートのラベルを出発点となる回答として使う。 目的がシートとの一致度の測定ではなく、シートの確認と修正である場合、Potato はすべての文でシートのラベルを事前選択できます。下のスクリプトは、行 ID をキーにして、ラベルを data/prelabels.json に書き出します。
import csv
import json
with open("sheet.csv", encoding="utf-8") as f:
prelabels = {row["id"]: {"sentiment": row["label"]} for row in csv.DictReader(f) if row["label"]}
with open("data/prelabels.json", "w", encoding="utf-8") as f:
json.dump(prelabels, f, indent=1)上の設定に、このブロックを追加します。
pre_annotation:
enabled: true
predictions_file: data/prelabels.jsonテスト実行では、各文が開いたとき、40 文すべてでシートのラベルがすでに選択されていました。回答を変えずに「Next」をクリックしたアノテーターは、シートのラベルをそのアノテーター自身のものとして保存します。したがって、事前ラベル付きの文でのシートとの一致度は、独立した判断の間の一致度ではありません。Potato 2.12.1 は、pre_annotation が Potato 3.0 で削除される予定であるという注意を表示していました。pre_annotation は Potato のインポーターが既存の回答を取り込む手段であるため、Potato 2.13.1 はこの注意を取り下げました。ラベリングエンジンには、契約で証明されなければならないラベル用の static ソースがあります。
シートの列をアノテーターとして使う。 一人につき一列のシートは、それらの人々の保存済みの作業として Potato に入れることができます。そうすると、管理ページと信頼性エクスポートには、シートでラベル付けをした人々が含まれます。この経路は、Potato の Label Studio インポーターを通ります(プロジェクトのインポート)。下のスクリプトは、指定した各列を、Label Studio のエクスポート形式での一人分の回答として書き出します。
"""Write each annotator column of sheet.csv as that person's Label Studio annotations."""
import csv
import json
import sys
columns = sys.argv[1:] # the sheet's annotator columns
tasks = []
with open("sheet.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
tasks.append({
"id": row["id"],
"data": {"sentence": row["sentence"]},
"annotations": [
{"completed_by": {"email": col},
"result": [{"from_name": "sentiment", "to_name": "sentence",
"type": "choices", "value": {"choices": [row[col]]}}]}
for col in columns if row[col]
],
})
with open("sheet_tasks.json", "w", encoding="utf-8") as f:
json.dump(tasks, f, ensure_ascii=False)ラベリング設定がない場合、インポーターは使用されているラベルから質問を作り、誰も選ばなかったラベルは除外します。質問の三つのラベルを sheet.xml として保存します。
<View>
<Text name="sentence" value="$sentence"/>
<Choices name="sentiment" toName="sentence">
<Choice value="positive"/><Choice value="neutral"/><Choice value="negative"/>
</Choices>
</View>アノテーター列の名前を指定してスクリプトを実行し、次にインポートを実行します。
python sheet_to_tasks.py label
potato import -i sheet_tasks.json -f label_studio --ls-config sheet.xml --keep-annotators -o importedインポートは、imported/ に、設定、data/sheet_tasks.jsonl としての文、label という名前のユーザーの作業として保存された列の 40 個のラベルを持つ新しいプロジェクトを書き出します。新しい設定は、インポートされた名前だけにパスワードなしでのログインを許可します。ana と ben を追加するには、imported/config.yaml の user_config ブロックに二つの名前を追加し、ブロックの後に num_annotators_per_item の行を追加します。
user_config:
allow_all_users: false
users:
- label
- ana
- ben
num_annotators_per_item: 3num_annotators_per_item: 3 では、各文がシートのユーザーと二人のアノテーターに送られ、管理ページは、三人全員が回答した後に文を採点します。テスト実行では、管理ページと信頼性エクスポートは、三者にわたる Krippendorff のアルファ 0.628、95% 区間 0.414 から 0.8 を報告しました。サーバーをネットワークに置く前に、require_password: true を設定し、インポートの README が述べているとおり、imported/annotation_output/user_config.json の各ユーザーにパスワードを設定してください。
Google スプレッドシートから直接読み込む
Potato は、ダウンロードの手順なしで、google_sheets データソースを通じて Google スプレッドシートから直接行を読み込めます。このソースには Google の認証情報が必要なため、テスト実行では実際のシートには到達しませんでした。下の設定はバリデーターを通り、設定手順は Potato のソースコードと Google のドキュメントによるものです。
Potato はサービスアカウントで Google にサインインし、スプレッドシートへの読み取りアクセスだけを求めます。Google Cloud コンソールでプロジェクトを作成し、Google Sheets API を有効にし、サービスアカウントを作成して、そのアカウントの JSON キーをプロジェクトフォルダーの credentials/ にダウンロードします。次に、同僚と共有するのと同じように、キーの client_email フィールドにあるアドレスとシートを共有します。pip install google-api-python-client google-auth でクライアントライブラリをインストールし、上の設定の data_files ブロックを次で置き換えます。
data_sources:
- type: google_sheets
spreadsheet_id: "YOUR_SPREADSHEET_ID"
sheet_name: Sheet1
range: "A:B"
credentials_file: credentials/service_account.jsonスプレッドシート ID は、シートの URL の /d/ と /edit の間にある長い文字列です。range: "A:B" は最初の二つの列だけを読み込み、id と sentence が A 列と B 列であれば、ラベルの列は Potato に入りません。Potato は credentials_file のパスを、プロジェクトフォルダーからの相対パスとして読みます。
次に読むもの
- アノテーションツールを作るか買うか - スプレッドシートで十分な場合と、ツールが見合う場合について
- データ形式 - Potato が読み込むファイル形式と、各行に必要なフィールドについて
- リモートデータソース - URL、クラウドストレージ、データベース、Google スプレッドシートからの読み込みについて
- エクスポート形式 - すべてのエクスポート形式とそのオプションについて
- アノテーター間一致度 - 一致度の係数の選び方と読み方について
- 品質管理 - 事前アノテーションの設定、注意力チェック、ゴールド項目について
- Potato でのテキストアノテーション - Potato が実行するその他のテキストタスクについて
参考文献
- Malo, P., Sinha, A., Korhonen, P., Wallenius, J., & Takala, P. (2014). Good debt or bad debt: Detecting semantic orientations in economic texts. Journal of the Association for Information Science and Technology, 65(4), 782-796. https://doi.org/10.1002/asi.23062