アノテーション結果の検証

データから規則を学習する機械学習では、データの内容に従って学習を行うためデータが最も重要になります。

あるタスクについて機械学習したい場合の多くは、公開されているデータセットをそのまま使えることは少なく、 自分でアノテーションを設計・実行し、その結果を検証する必要があります。 ここでは、アノテーション結果の検証と改善方法についてみていきましょう。

今回は例として、公開されているJRTEコーパスを利用します。 タスクとしては、投稿される宿のレビューを分類し、効率的にネガティブな意見を確認したいとします。

そのためにはまずは宿のレビューに対してポジネガのラベルを付与します。 ラベル付与のためにアノテーションを設計し、マニュアルを作成しアノテータにラベル付けを依頼します。

ここでは、JRTEコーパスに従って、次のように設計したとします。

  • 一つのレビューに対して3名でアノテーションを行う

  • positiveであれば1, neutralであれば0, negativeであれば-1のラベルを付与

今回はすでにこの方針でアノテーションが完了したとして、その結果の確認・改善方法についてみていきたいと思います。

Note

以下でJRTEコーパスのアノテーション結果についてみていきますが、アノテーション結果とはタスクに依存するものです。 今回設定したタスクはJRTEコーパスでアノテーションしている感情極性ラベルのタスクと一致していない可能性があります。

ですので、ここでのアノテーション結果の検証内容は、あくまで今回設定したタスクに対してアノテーションが行われたと仮定したときの話であり、 JRTEコーパスのアノテーション結果そのものに対する意見ではないことに注意してください。

まずはアノテーション済みの結果ファイルをロードします。

import pandas as pd

data = pd.read_csv("input/pn.csv")
data.head()
label text judges
0 neutral 出張でお世話になりました。 {"0": 3}
1 neutral 朝食は普通でした。 {"0": 3}
2 positive また是非行きたいです。 {"1": 3}
3 positive また利用したいと思えるホテルでした。 {"1": 3}
4 positive 駅から近くて便利でした。 {"0": 1, "1": 2}

Excelファイルで確認する

まずはアノテーション結果を目視で確認しましょう。 結果を確認するときは、全体をざっとみた後に、ラベルでフィルタリングして内容を丁寧に見ていくのがおすすめです。

Pandasで結果を表示するだけでは、全体像を把握することが難しい場合も多いため、 慣れているツールのフィルターが使えるように結果を出力しましょう。

例えばエクセルに出力するには to_excel を使えます。

Note

エクセルに出力する場合には openpyxl のインストールが必要です。

$ pip install openpyxl==3.0.9
data.to_excel("input/pn.xlsx")

一致率を確認する

アノテーションでは、アノテーションマニュアルがどの程度信頼できる質かを確認するために ひとつのサンプルに対して複数人でアノテーションを行い、アノテーションされたラベルにどの程度の揺れがあるかの 一致率を確認することが一般的です。

一致率を見るためにjudgesカラムのキーの数をカウントしてみてみましょう。 キーがひとつの時は一致していることを、2以上の場合は一致していないことを表しています。

import json

# サンプルに対してキーが一つであれば全員のアノテーション結果が一致していることを表す
annot_consistency = data["judges"].apply(lambda x: len(json.loads(x))).value_counts()
annot_consistency
1    4186
2    1330
3      37
Name: judges, dtype: int64

一致率を計算してみましょう。

annot_consistency.loc[1] / annot_consistency.sum()
0.7538267603097425

この結果を見ると、75%の一致率、つまり25%程度の結果でアノテーションの結果が揺れていることがわかります。 これは、4つのアノテーション結果をみるとだいたい一つはアノテーションラベルが揺れていることになります。

この一致率をどう判断するかはタスクに依存しますが、今回は25%の揺れは大きいと判断して、揺れを小さくする方針を検討することにしましょう。

揺れへの対策を検討

アノテーション結果の揺れが大きいと言うことは、 判断基準がはっきりしていないため人間がサンプルを確認してもどのようにラベル付けしていいのかわからない状態 であると言えます。 このような状態では、機械でも自動で判定するのは難しい状況です。

ここでの対策は、アノテーション結果が一致していないサンプルを確認し、アノテーションマニュアルを改訂することです。

アノテーション結果が一致していないサンプルを、例えばExcelのフィルターの機能で確認し、主要なケースを書き出してみましょう。 例えば次のようなケースが考えられます。

data.query('text in ["と感じてしまいました。", "初めて利用です。", "天気は雨。"]')
label text judges
14 negative と感じてしまいました。 {"-1": 2, "0": 1}
92 neutral 初めて利用です。 {"0": 2, "1": 1}
263 negative 天気は雨。 {"-1": 2, "0": 1}

1番目のケースでは入力が文として成り立っていません。 このようなケースはサンプルとして不適切なので学習データから除く方針が一つ考えられます。 その上で、実際の入力に不完全な文が多い場合には別途学習データ追加していくことが考えられるでしょう。

2, 3番目のケースでは、感情に関わらない事実を述べています。 このようなケースではニュートラルにつけてらもうようにアノテーションマニュアルに具体例を追加する方針が考えられます。

一方で、本質的にアノテーションが難しいケースもあります。

data.query('text in ["豪華さはありません。", "風呂も普通です。", "部屋は必要十分。", "値段を考えれば妥当だと思います。"]')
label text judges
2285 positive 部屋は必要十分。 {"0": 1, "1": 2}
2684 neutral 豪華さはありません。 {"-1": 1, "0": 2}
2888 neutral 風呂も普通です。 {"-1": 1, "0": 1, "1": 1}
3089 neutral 値段を考えれば妥当だと思います。 {"-1": 1, "0": 2}

これらのアノテーションをどうするかはタスクの方針を考えて事前に判断しておく必要があるでしょう。 例えばなるべくネガティブの内容を抽出したいのであれば、2番目の例はネガティブに判断するのがよいでしょう。

このように、タスクに合わせてアノテーションの方針を設計するのが重要です。

最終的なアノテーションラベル

アノテーションマニュアルを改訂してアノテーション結果の揺れを改善し、許容できる内容になったとしましょう。 その上で最終的なラベルの決定方法について考えていきます。

アノテーション結果は程度の差はあれど、複数人でジャッジをした場合はラベルに揺れが発生します。 揺れがあるサンプルに対して、最終的なラベルを決定する方法として次の二つが大きく挙げられます。

  • 揺れがあるサンプルは除外する

  • 揺れがある場合、多数決で最終的なラベルを決める

「揺れがあるサンプルは除外する」方法で学習データを作成した場合、 データがアノテーションマニュアルに従って整合性が取れているため学習に適していると考えられます。 一方で、揺れがあるサンプルは除外してしまうため、学習データが実際の入力を十分にカバーしない可能性が上がります。

「多数決で最終的なラベルを決める」方法は「揺れがあるサンプルは除外する」場合の逆のことが言えます。

どちらの方針を使えばいいかはデータを見て最終的な判断を下すべきですが、 マニュアルが十分整備されて一致率が大きくなっている状況であれば、マニュアルが信頼にたる質に達していると考えて、 揺れがある場合でも多くの人が判断した結果にはマニュアル上から読み取れる理由が入っているとして 多数決を取る方針がよいでしょう。

一方で、一致率が小さい場合には、マニュアルの質が疑わしく、結果アノテーション結果に信頼が持てないため 揺れがないサンプルのみを利用する方針がよいでしょう。

# 揺れがあるサンプルは除外する場合
data[data["judges"].apply(lambda x: len(json.loads(x)) == 1)].reset_index(drop=True).head()
label text judges
0 neutral 出張でお世話になりました。 {"0": 3}
1 neutral 朝食は普通でした。 {"0": 3}
2 positive また是非行きたいです。 {"1": 3}
3 positive また利用したいと思えるホテルでした。 {"1": 3}
4 neutral 新婚旅行で利用しました。 {"0": 3}
# 多数決で最終的なラベルを決める場合
def judge_label(judges):
    # ジャッジが多い順にソート
    # ジャッジが同じ場合はnegative, neutral, positiveの順にソート
    label, num = list(sorted(judges.items(), key=lambda x: (-x[1], int(x[0]))))[0]
    mapper = {"-1": "negative", "0": "positive", "1": "neutral"}
    return mapper[label]

label = data["judges"].apply(lambda x: judge_label(json.loads(x)))
data["label"] = label
data.head()
label text judges
0 positive 出張でお世話になりました。 {"0": 3}
1 positive 朝食は普通でした。 {"0": 3}
2 neutral また是非行きたいです。 {"1": 3}
3 neutral また利用したいと思えるホテルでした。 {"1": 3}
4 neutral 駅から近くて便利でした。 {"0": 1, "1": 2}