教師ラベルの間違えを効率的に修正する魔法の方法Confident Learningとその実装cleanlab

注目すべき理由

教師ラベルには間違えがよく含まれる

教師ラベルが間違っていると、どんな賢いアルゴリズムを用いても精度を挙げることが困難となります。
間違った教師ラベルが振られてしまうことは以下の理由からよくあります。

  1. 知識不足、単純な繰り返し作業によるヒューマンエラー
  2. 昔は検知の対象ではなかったが、新規クラスとして対応しなければいけない

原理上、人の手が加わる限りは1.から逃れることはかなり困難です。
裏を返せば教師ラベルの品質が保証しきれる範囲では学習データが足りていないことがほとんどです。
この場合は、そもそもルールベースで十分なタスクか、精度が上がりきっていないケースに当てはまります。

2.についても、ユーザの要望による機能追加等でサービスのユーザが増えるほどに顕在化してくる問題です。

50万円〜300万円ほどのプロジェクト費用が浮くかも!?

今までであれば数千〜数十万件のすべてのデータを再確認しながらラベルを修正する必要がありますが、こちらのConfident Learningでは優先度をつけながら簡単に修正すべき候補が見つけられます。
一般的に、過去データに対するクラスの修正だけで2週間〜数ヶ月かかるものが数日まで削減できるため、プロジェクト予算を先述した価格まで低減することができます。
大きくなっているプロジェクトほど恩恵が大きくなりますが、AIの使用感は多数のユーザに使われてから段々と明確になるため、この手法にはAI開発のプロジェクトに対して大きな影響力があると言えます

デモンストレーション

公式で配布されているデモに手を加えたものを以下に掲載していきます( https://github.com/cgnorthcutt/cleanlab/blob/master/examples/iris_simple_example.ipynb )。
最初に必要なライブラリを読み込みます。

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn import datasets
from sklearn.metrics import accuracy_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_predict
from cleanlab.classification import LearningWithNoisyLabels
from cleanlab.noise_generation import generate_noisy_labels
from cleanlab.util import value_counts
from cleanlab.latent_algebra import compute_inv_noise_matrix
from cleanlab.pruning import get_noise_indices

模擬データの準備

皆さん大好きIrisデータセットをベースに準備していきます。

seed = 2
clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
rp = LearningWithNoisyLabels(clf=clf, seed=seed)
np.random.seed(seed=seed)

iris = datasets.load_iris()
X = iris.data
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

try:
    get_ipython().run_line_magic('matplotlib', 'inline')
    from matplotlib import pyplot as plt

    _ = plt.figure(figsize=(12, 8))
    color_list = plt.cm.tab10(np.linspace(0, 1, 6))
    _ = plt.scatter(X_train[:, 1], X_train[:, 3],
                    color=[color_list[z] for z in y_train], s=50)
    ax = plt.gca()
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    _ = ax.get_xaxis().set_ticks([])
    _ = ax.get_yaxis().set_ticks([])
    _ = plt.title("Iris dataset (feature 3 vs feature 1)", fontsize=30)
except Exception as e:
    print(e)
    print("Plotting is only supported in an iPython interface.")

上記を実行すると、以下のような図が表示されます。
今回はIrisデータセットの1,3番目の特徴量を使用します。

次にラベル情報にノイズを加えます。
こちらはあくまでも模擬データの準備なので、本番データでは必要ありません。

noise_matrix = np.array([
    [0.5, 0.0, 0.0],
    [0.5, 1.0, 0.5],
    [0.0, 0.0, 0.5],
])

py = value_counts(y_train)
s = generate_noisy_labels(y_train, noise_matrix)

try:
    get_ipython().run_line_magic('matplotlib', 'inline')
    from matplotlib import pyplot as plt

    _ = plt.figure(figsize=(15, 8))
    color_list = plt.cm.tab10(np.linspace(0, 1, 6))
    for k in range(len(np.unique(y_train))):
        X_k = X_train[y_train == k]  # data for class k
        _ = plt.scatter(
            X_k[:, 1],
            X_k[:, 3],
            color=[color_list[noisy_label] for noisy_label in s[y_train == k]],
            s=200,
            marker=r"${a}$".format(a=str(k)),
            linewidth=1,
        )
    _ = plt.scatter(
        x=X_train[s != y_train][:, 1],
        y=X_train[s != y_train][:, 3],
        color=[color_list[z] for z in s],
        s=400,
        facecolors='none',
        edgecolors='black',
        linewidth=2,
        alpha=0.5,
    )
    ax = plt.gca()
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    _ = ax.get_xaxis().set_ticks([])
    _ = ax.get_yaxis().set_ticks([])
    _ = plt.title("Iris dataset (features 3 and 1). Label errors circled.",
                  fontsize=30)
except Exception as e:
    print(e)
    print("Plotting is only supported in an iPython interface.")

上記を実行すると以下の図が表示されます。

先に作成した図と比較すると一目瞭然ですが、○でくくられたデータポイントはノイズ情報によってクラスが変わりました。
ここまでで、ラベル情報が間違っているかもしれないデータセットの準備ができました。

怪しいデータの検知

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
psx = cross_val_predict(clf,X_train,y_train,method='predict_proba')
noise_targets = get_noise_indices(s=s, psx=psx,sorted_index_method='normalized_margin')
changed = np.where(s != y_train)[0]

今回はロジスティック回帰を用いましたが、モデル自体はなんでもokです。
ポイントは cross_val_predict で交差検定することで、各クラスの予測確率を計算することです。

def diff(changes, noise_targets):
    match_acc, failed_acc = [], []
    for gt in changed:
        if gt in set(noise_targets):
            match_acc.append(gt)
        else:
            failed_acc.append(gt)
    return match_acc, failed_acc

match_acc, failed_acc = diff(changed, noise_targets)
print("検知できたケース")
print(match_acc)
print("実際には誤ったラベルだが、検知できなかったケース")
print(failed_acc

怪しいデータから順番に出力されるため、本番データではこの情報を元に教師ラベルの修正を行うと効率的になります。

ここまでで、検知できたケースとできなかったケースのそれぞれのデータを特定することができました。
上記のデータポイントを改めて図で表示します。

mask_match = np.zeros(len(X_train), dtype=bool)
mask_failed = np.zeros(len(X_train), dtype=bool)
mask_match[match_acc] = 1
mask_failed[failed_acc] = 1

try:
    get_ipython().run_line_magic('matplotlib', 'inline')
    from matplotlib import pyplot as plt

    _ = plt.figure(figsize=(15, 8))
    color_list = plt.cm.tab10(np.linspace(0, 1, 6))
    for k in range(len(np.unique(y_train))):
        X_k = X_train[y_train == k]  # data for class k
        _ = plt.scatter(
            X_k[:, 1],
            X_k[:, 3],
            color=[color_list[noisy_label] for noisy_label in s[y_train == k]],
            s=200,
            marker=r"${a}$".format(a=str(k)),
            linewidth=1,
        )
    _ = plt.scatter(
        x=X_train[s != y_train][:, 1],
        y=X_train[s != y_train][:, 3],
        color=[color_list[z] for z in s],
        s=400,
        facecolors='none',
        edgecolors='black',
        linewidth=2,
        alpha=0.5,
    )
    # Success
    _ = plt.scatter(
        x=X_train[mask_match][:, 1],
        y=X_train[mask_match][:, 3],
        color=[color_list[z] for z in s],
        s=400,
        facecolors='#1ABC9C',
        #edgecolors='black',
        linewidth=2,
        alpha=0.1,
    )
    # Failed
    _ = plt.scatter(
        x=X_train[mask_failed][:, 1],
        y=X_train[mask_failed][:, 3],
        color=[color_list[z] for z in s],
        s=400,
        facecolors='#E67E22',
        #edgecolors='black',
        linewidth=2,
        alpha=0.1,
    )
    ax = plt.gca()
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    _ = ax.get_xaxis().set_ticks([])
    _ = ax.get_yaxis().set_ticks([])
except Exception as e:
    print(e)
    print("Plotting is only supported in an iPython interface.")

ちょっと見にくいですが、○の中が青い色のものがラベル間違えの検出に成功したもの、赤いものは検出に失敗したものになります。
最後に数値的にどれだけ精度が上がるのか見ていきます。

ラベル間違いの検知によって、どれだけ精度が上がるのか?

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
rp = LearningWithNoisyLabels(clf=clf, seed=seed)

print('WITHOUT confident learning,', end=" ")
clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
_ = clf.fit(X_train, s)
pred = clf.predict(X_test)
print("Iris dataset test accuracy:", round(accuracy_score(pred, y_test), 2))

print("\nNow we show improvement using cleanlab to characterize the noise")
print("and learn on the data that is (with high confidence) labeled correctly.")
print()
print('WITH confident learning (noise matrix given),', end=" ")
_ = rp.fit(X_train, s, noise_matrix=noise_matrix)
pred = rp.predict(X_test)
print("Iris dataset test accuracy:", round(accuracy_score(pred, y_test), 2))

print('WITH confident learning (noise / inverse noise matrix given),', end=" ")
inv = compute_inv_noise_matrix(py, noise_matrix)
_ = rp.fit(X_train, s, noise_matrix=noise_matrix, inverse_noise_matrix=inv)
pred = rp.predict(X_test)
print("Iris dataset test accuracy:", round(accuracy_score(pred, y_test), 2))

print('WITH confident learning noise not given,', end=" ")
clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
rp = LearningWithNoisyLabels(clf=clf, seed=seed)
_ = rp.fit(X_train, s)
pred = rp.predict(X_test)
print("Iris dataset test accuracy:", round(accuracy_score(pred, y_test), 2))

LearningWithNoisyLabelsを使用すると、モデルフィッティングの段階で自動でラベルの間違え検知を行った上で学習を行います。

正解ラベルにノイズの乗ったデータセットでは、普通にモデルフィッティングした場合は精度が0.60となりました。
いくつかハンパーパラメータを使用しましたが、Confident Learningによる手法を活用すると、それぞれ0.83, 0.83, 0.77とベースラインに比較すると十分に高い精度で正解ラベルを予測することができるようになりました。

まとめ

最後にConfident Learning(cleanlab)を使用するにあたって要点を掲載します。
・教師ラベルが間違っているデータセットで大幅に精度が向上
・交差検定が必要

交差検定が必要なため、ディープなモデルとは実のところ相性があまりよくありません。
大規模モデルの場合、モデルを蒸留する等のいくつかのトリックが必要になるかもしれません。
ちなみに、弊社で取り扱った案件では交差検定で予測確率を算出するまでに4日ほどかかりました。
しかしながら、弊社の取り扱っているタスクで精度が0.91ほどで頭打ちになっていたものが0.95まで上昇させることができたため、新規に機械学習モデルを提案するよりも優先度が高い手法であることは間違いないと言えます。

SAI-SHIKIでは、最新の手法をキャッチアップすることで円滑にAIプロジェクトを進める体制が整っています。
データ分析やAI開発のご相談がございましたら、以下からお問い合わせください。
https://www.sai-shiki.com/contact/

【参考】
[cleanlab](https://github.com/cgnorthcutt/cleanlab)
[Confident Learning: Estimating Uncertainty in Dataset Labels](https://arxiv.org/abs/1911.00068)