コンテンツへスキップ
チュートリアル16分で読めます

スプレッドシートでのラベル付け作業を Potato に移す

ラベル付き行の CSV を Potato に読み込み、二人のアノテーターに古いラベルを見せずに行へラベルを付け直してもらい、アノテーターごとに一列ずつ戻します。

Potato Team

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 列は、各文がどちらのグループから来たかを記録します。シートの三行を下に示します。

csv
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)を使います。

python
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 は保管しておきます。

python
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、下の設定を入れたプロジェクトフォルダーを作ります。

bash
pip install potato-annotation==2.13.1

この設定は完全なもので、2.12.1 と 2.13.1 の両方で python -m potato.validate_cli config.yaml --strict を通ります。

yaml
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 列の綴りに合わせてあり、二組のラベルは、コードを付け替えずに比較できます。アノテーションスキームにはその他の質問タイプが一覧されており、設定の基本は、その他のキーを説明しています。

ラベル付け

プロジェクトフォルダーからサーバーを起動します。

bash
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 のアルファは、どちらも、二人のアノテーターが偶然に達する一致を差し引きます。二つの係数の読み方は、アノテーター間一致度で説明されています。

レポートのためには、ラベル付けが終わったら信頼性エクスポートを一度実行します。

bash
python -m potato.export --config config.yaml --format reliability --output export/

コマンドは export/reliability.csv と export/reliability.md を書き出します。二人のスクリプト化されたアノテーターの表です。

text
| 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 エクスポートは、アノテーターと文ごとに一行、ラベルごとに一列を持ちます。

bash
python -m potato.export --config config.yaml --format csv --output export/

コマンドは、40 文に対する 80 行の export/annotations.csv を書き出します。

csv
instance_id,user_id,sentiment.positive,sentiment.negative,sentiment.neutral
s27,ana,positive,,

表計算ソフトのユーザーは通常、元のシートのレイアウトを望みます。下のスクリプトは、三つのラベル列を一つの sentiment 列にまとめ、各アノテーターを列にし、その列を元のシートの隣に置きます。

python
"""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 があります。

text
 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 の κ同じラベル
ana0.7450.74340 件中 34 件
ben0.5630.55840 件中 30 件

ana、ben、シートのいずれかに不一致があった 13 行のすべては、元のアノテーターの意見が割れていた 20 文から来ていました。元のアノテーター全員が一致した 20 文では、ana、ben、シートは毎回同じラベルを付けていました。三行では、ana と ben が互いに一致し、シートとは一致しませんでした。その三行についてのシートのラベルは、最初に確認すべきものです。より大きなシートでそのような行を見つけるには、labels_by_annotator.csv を表計算ソフトで開き、ana または ben 列が label と異なる行を絞り込んでください。

シートのラベルを Potato に取り込む

上の手順では、シートのラベルは Potato に入らず、二人のアノテーターはシートとは独立にラベルを付けます。シートのラベルから作業を続けたいチームには、他に二つの経路があります。

シートのラベルを出発点となる回答として使う。 目的がシートとの一致度の測定ではなく、シートの確認と修正である場合、Potato はすべての文でシートのラベルを事前選択できます。下のスクリプトは、行 ID をキーにして、ラベルを data/prelabels.json に書き出します。

python
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)

上の設定に、このブロックを追加します。

yaml
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 のエクスポート形式での一人分の回答として書き出します。

python
"""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 として保存します。

xml
<View>
  <Text name="sentence" value="$sentence"/>
  <Choices name="sentiment" toName="sentence">
    <Choice value="positive"/><Choice value="neutral"/><Choice value="negative"/>
  </Choices>
</View>

アノテーター列の名前を指定してスクリプトを実行し、次にインポートを実行します。

bash
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 の行を追加します。

yaml
user_config:
  allow_all_users: false
  users:
  - label
  - ana
  - ben
num_annotators_per_item: 3

num_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 ブロックを次で置き換えます。

yaml
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 のパスを、プロジェクトフォルダーからの相対パスとして読みます。

次に読むもの

参考文献

  1. 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