概要
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()

このデータセットを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()

結果を見ると、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())

余分な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()

Validationデータに対するAUCはおよそ0.93あたりで、TrainデータとTestデータの分布が同じ場合に比べモデルのパフォーマンスが良いことがわかります。これは今回用いたTrainデータとTestデータの分布が異なるため、モデルが容易に分類できるからです。
このようにして得られた結果から、TrainデータのうちTestデータである確率が高いと予測されたデータを取り出すことでTestデータに似たValidationデータを作成することが可能になります。またTrainデータとTestデータの分布が異なるかどうか見分ける指標としてAdversarial Validationを使うこともできます。
特徴量選択への応用
最後にAdversarial Validationを応用して特徴量選択を行う方法を紹介します。
まず先程のTrainデータとTestデータの分布が異なる場合に、予測に寄与した特徴量の重要度を見てみましょう。

これを見ると 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()

たしかにAUCスコアが低くなりTrainデータとTestデータの分類がしづらくなっていることがわかります。
このようにしてAdversarial Validationを応用することによって、TrainデータとTestデータで乖離している特徴量を見つけることができます。
まとめ
今回はKaggleなどの機械学習コンペで注目されているAdversarial Validationを紹介しました。Kaggleにおいて手元のバリデーションスコアとLeader Boardのスコアに乖離がある際に、適切なValidation setの作成や特徴量選択に役立ちます。実際に私もコンペで利用しており、今後ますます注目を浴びるのではないかと思われます。
Twitter・Facebookで定期的に情報発信しています!