DEVELOPER’s BLOG

技術ブログ

Kaggleで役立つAdversarial Validationとは

2020.01.06 岡本 和斗
kaggle 分類 機械学習
Kaggleで役立つAdversarial Validationとは

概要

Adversarial ValidationはTrainデータとTestデータの分布が異なる際に、Testデータに似たValidationデータを作成するのに使われる手法です。
Kaggleなどのデータ分析コンペではTrainデータとTestデータの一部が与えられ、コンペ終了まではこの一部のTestデータに対するスコアのみ知ることができます。一部のTestデータだけを見てモデルを評価していると、全体のテストデータに対しては良いスコアが出ずに最終的に低い順位に終わることがあります。ですのでCross Validationなどを用いて求めたValidationデータに対するのスコアと一部のTestデータに対するスコアに相関があることが望ましい状態です。そのために重要なことはTestデータに似た分布を持つValidationデータを作成することです。


手法

最終的な目標はTrainデータとTestデータを分類するモデルを作成することです。

まずどちらのデータセットからのデータかをラベル付けするために、TrainデータとTestデータにそれぞれ新しい目的変数を与えます。例えばTrainデータには "1"を、Testデータには"0"を与えます。そしてTrainデータとTestデータを結合して、データがどちら由来か予測するモデルを作成します。
もしTrainデータとTestデータが同じ分布を持つとするとどちら由来かを予測できないはずです。逆にTrainデータとTestデータの分布が異なる場合は予測は容易になります。
予測結果からTrainデータとTestデータの分布が異なるようであれば、Testデータの分布に似たValidationデータを作成する必要があります。これはTrainデータのうち、モデルがTestデータに由来する確率が高いと予測したデータを選ぶことで作成できます。


それでは必要なライブラリをインポートして実装に進みましょう。

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
warnings.filterwarnings('ignore')
from tqdm import tqdm_notebook as tqdm

from sklearn.preprocessing import LabelEncoder
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
import lightgbm as lgb


TrainデータとTestデータの分布が同じ場合

まずデータセットを作成し、プロットしてみます。

X, y = make_classification(
    n_samples=10000,
    n_features=2,
    n_informative=1,
    n_redundant=0, 
    n_classes=2,
    n_clusters_per_class=1,
    class_sep=2.0,
    random_state=42
)

plt.scatter(X[y == 0, 0], X[y == 0, 1], s=10, alpha=0.5, label='class A')
plt.scatter(X[y == 1, 0], X[y == 1, 1], s=10, alpha=0.5, label='class B')
plt.legend(loc='upper right')
plt.show()

plot of dataset


このデータセットをTrainデータとTestデータに分け、Trainデータには "1"を、Testデータには"0"をそれぞれ新しく目的変数として与えます。その後、TrainデータとTestデータを結合して1つのデータセットに戻します。

X_train, X_test = train_test_split(X, test_size=0.33, random_state=42)

y_train = np.zeros(len(X_train))
y_test = np.ones(len(X_test))

X_all = np.concatenate([X_train, X_test], axis=0)
y_all = np.concatenate([y_train, y_test], axis=0)


これでTrainデータとTestデータが混ざったデータセットが出来上がりました。ここから新たに与えた目的変数を元にTrainデータとTestデータを分類するモデルを作成していきます。

X_train_adv, X_valid_adv, y_train_adv, y_valid_adv = \
    train_test_split(X_all, y_all, test_size=0.33, random_state=42, shuffle=True)

model = lgb.LGBMClassifier(
    n_estimators=1000,
    random_state=42)

model.fit(
    X_train_adv,
    y_train_adv,
    eval_set=[(X_train_adv, y_train_adv), (X_valid_adv, y_valid_adv)],
    eval_names=['train', 'valid'],
    eval_metric='auc',
    verbose=100)


このときのValidationデータを作成しモデルの性能を評価します。 ではモデルのパフォーマンスを見てみましょう。

ax = lgb.plot_metric(model.evals_result_, metric='auc')
plt.show()

plot of learing curve


結果を見ると、Validationデータに対してAUCはほぼ0.5あたりを示しています。AUCが0.5を示すということはモデルのパフォーマンスがランダムに分類するのと変わらないことを意味しています。これはモデルがTrainデータとTestデータをうまく分類できてないということです。つまりTrainデータとTestデータの分布が同じであることがわかります。


TrainデータとTestデータにの分布が異なる場合

次にTrainデータとTestデータの分布が異なる場合を見ていきましょう。

データはKaggleの2019 Data Science Bowlのデータを引っ張ってきました。データの中身や特徴量作成に関して、ここではコードだけにして割愛させていただきます。

test = pd.read_csv("../input/data-science-bowl-2019/test.csv")
train = pd.read_csv("../input/data-science-bowl-2019/train.csv")


def get_data(user_sample, test_set=False):
    all_assessments = []
    type_count = {'Clip':0, 'Activity': 0, 'Assessment': 0, 'Game':0}

    for i, session in user_sample.groupby('game_session', sort=False):
        session_title = session['title'].iloc[0]
        session_type = session['type'].iloc[0]

        if (session_type == 'Assessment') and (test_set or len(session) > 1):
            event_code = 4110 if session_title == 'Bird Measurer (Assessment)' else 4100
            all_attempts = session.query(f'event_code == {event_code}')
            num_correct = all_attempts['event_data'].str.contains('true').sum()
            num_incorrect = all_attempts['event_data'].str.contains('false').sum()

            features = {}
            features['installation_id'] = session['installation_id'].iloc[-1]
            features['title'] = session_title
            features['num_correct_attempts'] = num_correct

            features.update(type_count.copy())

            if test_set:
                all_assessments.append(features)
            elif num_correct + num_incorrect > 0:
                all_assessments.append(features)

        type_count[session_type] += 1

    if test_set:
        return all_assessments[-1]

    return all_assessments


compiled_train = []
for ins_id, user_sample in tqdm(train.groupby('installation_id', sort=False), total=17000):
    compiled_train += get_data(user_sample)

compiled_test = []
for ins_id, user_sample in tqdm(test.groupby('installation_id', sort=False), total=1000):
    test_data = get_data(user_sample, test_set=True)
    compiled_test.append(test_data)

X_train = pd.DataFrame(compiled_train)
X_test = pd.DataFrame(compiled_test)


特徴量作成の後にできたTrainデータとTestデータは次のようになっています。

display(X_train.head())
display(X_test.head())

heading of training and test dataframes


余分なinstallatio_idカラムは削除して、titleカラムのエンコーディングを行っておきましょう。

X_train = X_train.drop(['installation_id'], axis=1)
X_test = X_test.drop(['installation_id'], axis=1)

le = LabelEncoder()
le.fit(list(X_train['title'].values) + list(X_test['title'].values))
X_train['title'] = le.transform(list(X_train['title'].values))
X_test['title'] = le.transform(list(X_test['title'].values))


ここから先は上記のプロセスと同じくTrainデータとTestデータに新たな目的変数を与えて結合した後、TrainデータとTestデータが混ざったデータからそれぞれを分類していきます。

X_train['adv_target'] = 0
X_test['adv_target'] = 1
train_test_adv = pd.concat([X_train, X_test], axis=0).reset_index(drop=True)

train_adv, valid_adv = train_test_split(train_test_adv, test_size=0.33, random_state=42)
X_train_adv = train_adv.drop(['adv_target'], axis=1)
y_train_adv = train_adv['adv_target']
X_valid_adv = valid_adv.drop(['adv_target'], axis=1)
y_valid_adv = valid_adv['adv_target']

model = lgb.LGBMClassifier(
    n_estimators=1000,
    random_state=42)

model.fit(
    X_train_adv,
    y_train_adv,
    eval_set=[(X_train_adv, y_train_adv), (X_valid_adv, y_valid_adv)],
    eval_names=['train', 'valid'],
    eval_metric='auc',
    verbose=100)


ではモデルのパフォーマンスを見てみましょう。

ax = lgb.plot_metric(model.evals_result_, metric='auc')
plt.show()

plot of learing curve


Validationデータに対するAUCはおよそ0.93あたりで、TrainデータとTestデータの分布が同じ場合に比べモデルのパフォーマンスが良いことがわかります。これは今回用いたTrainデータとTestデータの分布が異なるため、モデルが容易に分類できるからです。

このようにして得られた結果から、TrainデータのうちTestデータである確率が高いと予測されたデータを取り出すことでTestデータに似たValidationデータを作成することが可能になります。またTrainデータとTestデータの分布が異なるかどうか見分ける指標としてAdversarial Validationを使うこともできます。


特徴量選択への応用

最後にAdversarial Validationを応用して特徴量選択を行う方法を紹介します。

まず先程のTrainデータとTestデータの分布が異なる場合に、予測に寄与した特徴量の重要度を見てみましょう。

plot of feature importance


これを見ると num correct attempts の重要度が高くなっています。これは num correct attempts がTrainデータかTestデータかの分類に大きく寄与していることを示しています。つまりTrainデータとTestデータの間でこの特徴量に乖離があることを意味しています。

これを受けて num correct attempts のカラムを削除してTrainデータとTestデータの分類を行ってみると、

new_X_train_adv = train_adv.drop(['num_correct_attempts'], axis=1)
new_X_valid_adv = valid_adv.drop(['num_correct_attempts'], axis=1)

model = lgb.LGBMClassifier(
    n_estimators=2000,
    random_state=42)

model.fit(
    X_train_adv,
    y_train_adv,
    eval_set=[(new_X_train_adv, y_train_adv), (new_X_valid_adv, y_valid_adv)],
    eval_names=['train', 'valid'],
    eval_metric='auc',
    verbose=100)

ax = lgb.plot_metric(model.evals_result_, metric='auc')
plt.show()

plot of learing curve


たしかにAUCスコアが低くなりTrainデータとTestデータの分類がしづらくなっていることがわかります。

このようにしてAdversarial Validationを応用することによって、TrainデータとTestデータで乖離している特徴量を見つけることができます。


まとめ

今回はKaggleなどの機械学習コンペで注目されているAdversarial Validationを紹介しました。Kaggleにおいて手元のバリデーションスコアとLeader Boardのスコアに乖離がある際に、適切なValidation setの作成や特徴量選択に役立ちます。実際に私もコンペで利用しており、今後ますます注目を浴びるのではないかと思われます。


Twitter・Facebookで定期的に情報発信しています!

関連記事

Kaggleバスケコンペ(NCAA)解法の紹介

NCAAコンペ概要  全米大学体育協会バスケットボールトーナメントの試合の勝敗を予測するコンペでした。男女別にコンペが開かれました。リーグ戦の試合結果の詳細とトーナメントの試合結果のデータが年ごとに与えられ、今年のトーナメントの試合結果を予測します。評価指標はLoglossでした。 結果  新型コロナウイルスの影響で、大会自体がキャンセルになってしまいました。リークなしのLBの最も良いスコアは0.52586です。 取り組み内容 コンペの内容を理解してから

記事詳細
Kaggleバスケコンペ(NCAA)解法の紹介
kaggle 機械学習
Kaggleコンペ 2019 Data Science Bowlの振り返り

はじめに 昨日まで開催されていたKaggleの2019 Data Science Bowlに参加しました。結果から言いますと、public scoreでは銅メダル圏内に位置していたにも関わらず、大きなshake downを起こし3947チーム中1193位でのフィニッシュとなりました。今回メダルを獲得できればCompetition Expertになれたので悔しい結果となりましたが、このshake downの経験を通して学ぶことは多くあったので反省点も踏まえて

記事詳細
Kaggleコンペ 2019 Data Science Bowlの振り返り
kaggle 回帰 機械学習
Kaggle解説: 銅メダル獲得 NFLコンペについて

はじめに KaggleのNFLコンペで2038チーム中118位となり、銅メダルをとることができました。以下に、参加してからの取り組みや、反省点を書いていきたいと思います。 コンペ参加前の状況 10ヶ月ほど前にTitanicコンペに参加してから、「Predicting Molecular Properties」と「IEEE-CIS Fraud Detection」というコンペに参加してみましたが、公開されているカーネルを少しいじってみた程度でメダルには到底届

記事詳細
Kaggle解説: 銅メダル獲得 NFLコンペについて
kaggle 機械学習
KaggleコンペGreat Energy Predictor III で銅メダル獲得!

アクセルユニバース株式会社(以下当社)では、人が対応している業務を画像認識、音声認識、文字認識等を活用して効率化する機械学習ソリューション開発をおこなっています。 インターン生は業務の一環としてKaggleに取り組んでおり、先日のASHRAE - Great Energy Predictor IIIにて銅メダルを獲得しました。 メダルを獲得した田村くんのコメントです。 今回は、他の方が提出したもののブレンド(混ぜる)の仕方を工夫しました。 まずはなるべ

記事詳細
KaggleコンペGreat Energy Predictor III で銅メダル獲得!
kaggle 機械学習

お問い合わせはこちらから