Adversarial Validation のやり方

Posted on 2020/11/25 in 機械学習 , Updated on: 2020/11/25

はじめに

機械学習コンペでよく使用される Adversarial Validation について、内容・実装方法を紹介する。

コンペなどでは、基本的に学習用データ(目的変数を含む)とテスト用データ(目的変数を含まない)を提供され、学習用データで、モデルの作成を行い、そのモデルを使ってテスト用データの目的変数を推論する。
この際、学習用・テスト用データは共通の特徴量を持つことが前提となっているが、同じ特徴量でも、学習用・テスト用でデータの分布や傾向が異なっていることがある。この場合、モデルは学習用データの中身を前提に推論を行うので、テスト用データでの結果(スコア)が悪化する可能性がある。
特にコンペでは、テストデータの推論結果の一部を使って Public Leader Board などでスコアのランクを確認できるが、コンペ終了後、すべての推論結果を使ったスコア (Private Leader Board) の結果が大きく悪化する事が生じる。

Adversarial Validation は、学習用データとテスト用データの分布が、異なっているか否かを判定する方法である。Adversarial Validation の結果、分布の異なる特徴量が判明すれば、この特徴量を省いたり、両データ内で特徴量の性質をそろえる処理をしたりすることで、より汎用力のある機械学習モデルの作成が可能になる。

方法

実装方法は、単純で、学習用データ・テスト用データに対して新たな目的変数を追加して、それらを2値分類させて精度を測る。この分類に大きく寄与する特徴量は、学習・テスト用を分類する傾向を持っているということになるので、これが分布の異なる特徴量ということになる。

本記事では、新たな目的変数の値を、学習用データ(train) は 0、テスト用データ (test) は 1 とする。また、分類には、lightGBM を使用する。

train/test の分布が似ている場合

まず、例として train/test の分布が似ている場合を検証する。

データセットの作成

データは、sklearnmake_classification を使って分類データを作成する。2値分類問題で、視覚化しやすいよう特徴量は 2つのみとする。
サンプル数は 2000 として、train/test にそれぞれ 1000個ずつランダムに分割する。
分割後、train/test でそれぞれ目的変数別に色分けして可視化する。

In [79]:
import numpy as np
import pandas as pd
import lightgbm as lgb

# グラフ描画ライブラリ
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('darkgrid')
%matplotlib inline
cmap = plt.get_cmap("tab20")

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_validate

# データセット作成
X, y = make_classification(n_samples=2000,
                           n_features=2,
                           n_redundant=0,
                           n_informative=2,
                           n_clusters_per_class=1,
                           n_classes=2,
                           class_sep=1.5,
                           random_state=58)

# train/test に分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.5, shuffle=True, random_state=42)

# train/test の DataFrame を作成
train_df = pd.DataFrame(X_train, columns=['Feature 1', 'Feature 2'])
train_df['target'] = y_train
test_df = pd.DataFrame(X_test, columns=['Feature 1', 'Feature 2'])
test_df['target'] = y_test


# 可視化
plt.figure(figsize=(6,6))

plt.scatter(train_df[train_df['target'] == 1]['Feature 1'],
            train_df[train_df['target'] == 1]['Feature 2'],
            s=15, alpha=0.3, label='Train (target=1)')
plt.scatter(test_df[test_df['target'] == 1]['Feature 1'],
            test_df[test_df['target'] == 1]['Feature 2'],
            s=15, alpha=0.3, label='Test (target=1)')
plt.scatter(train_df[train_df['target'] == 0]['Feature 1'],
            train_df[train_df['target'] == 0]['Feature 2'],
            s=15, alpha=0.3, label='Train (target=0)')
plt.scatter(test_df[test_df['target'] == 0]['Feature 1'],
            test_df[test_df['target'] == 0]['Feature 2'],
            s=15, alpha=0.3, label='Test (target=0)')


plt.xlabel('Feature 1', fontsize=15)
plt.ylabel('Feature 2', fontsize=15)
plt.legend(fontsize=13, facecolor='white')
plt.show()

上記の図を見ると、目的変数 (target) が 1, 0 は、train/test できれいに分布が重なっていることがわかる。これは同じ分布からランダムに抜き出して train/test と分割しただけなので、当然の結果である。

Adversarial Validation

これらのデータを使って、Adversarial Validation を実行する。手順としては、下記の流れになる。

  • 新たな目的変数を作成し、train/test のデータを結合する
  • 結合したデータで、交差検証を実行
  • validation データで分類精度を検証する
In [89]:
# 目的変数の作成
train_df['new_target'] = 0
test_df['new_target'] = 1

# train/test の結合 (もとの target は除去)
df = pd.concat([train_df, test_df], axis=0)
df = df.drop(columns=['target'])
features = ['Feature 1', 'Feature 2']

# 交差検証には、StratifiedKFold を使用
# 分類には LightGBM を使用
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
clf = lgb.LGBMClassifier(n_estimators=100,
                         random_state=1)
score = cross_validate(clf, df[features], df['new_target'], cv=skf)

# Cross Validation 結果を表示
print('Accuracy : {:.1f}%'.format(score['test_score'].mean()*100))
Accuracy : 51.8%

交差検証結果から、51.8% の精度でしか分類できないことがわかる。結合前の元のデータは、train/test ともに同サンプル数なので、ランダムに抜き出した一つのサンプルが train か test かは、50%の確立であるため、ほぼ分類できていないことがわかる。

Adversarial Validation で、分類精度が悪いということは、train/test で各特徴量の分布が異なっていないことを意味する。

train/test で分布が異なる場合

上記の、train/test で分布が似ている(同じ)場合は、同じデータセットを半分に分けて作成したが、今回は分布を異なるようにするため、random_state を変化させて train/test 別の分布を持ったデータを作成する。(※ あまりにも分布が異なるデータは、Adversarial Validation を行うまでもないので、あえて少し似た分布になるデータを作成している)

In [90]:
# train データセット作成
X, y = make_classification(n_samples=1000,
                           n_features=2,
                           n_redundant=0,
                           n_informative=2,
                           n_clusters_per_class=1,
                           n_classes=2,
                           class_sep=1.5,
                           random_state=58)
train_df = pd.DataFrame(X, columns=['Feature 1', 'Feature 2'])
train_df['target'] = y

# test データセット作成
X, y = make_classification(n_samples=1000,
                           n_features=2,
                           n_redundant=0,
                           n_informative=2,
                           n_clusters_per_class=1,
                           n_classes=2,
                           class_sep=1.5,
                           random_state=8)
test_df = pd.DataFrame(X, columns=['Feature 1', 'Feature 2'])
test_df['target'] = y


# 可視化
plt.figure(figsize=(6,6))

plt.scatter(train_df[train_df['target'] == 1]['Feature 1'],
            train_df[train_df['target'] == 1]['Feature 2'],
            s=15, alpha=0.3, label='Train (target=1)')
plt.scatter(test_df[test_df['target'] == 1]['Feature 1'],
            test_df[test_df['target'] == 1]['Feature 2'],
            s=15, alpha=0.3, label='Test (target=1)')
plt.scatter(train_df[train_df['target'] == 0]['Feature 1'],
            train_df[train_df['target'] == 0]['Feature 2'],
            s=15, alpha=0.3, label='Train (target=0)')
plt.scatter(test_df[test_df['target'] == 0]['Feature 1'],
            test_df[test_df['target'] == 0]['Feature 2'],
            s=15, alpha=0.3, label='Test (target=0)')


plt.xlabel('Feature 1', fontsize=15)
plt.ylabel('Feature 2', fontsize=15)
plt.legend(fontsize=13, facecolor='white')
plt.show()

グラフから、目的変数 (target) が同じ場合でも、train/test で分布が異なっているデータが作成できた。

Adversarial Validation

本データと使って、上記と同様に train/test を結合して、分類精度を検証する。

In [91]:
# 目的変数の作成
train_df['new_target'] = 0
test_df['new_target'] = 1

# train/test の結合 (もとの target は除去)
df = pd.concat([train_df, test_df], axis=0)
df = df.drop(columns=['target'])
features = ['Feature 1', 'Feature 2']

# 交差検証には、StratifiedKFold を使用
# 分類には LightGBM を使用
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
clf = lgb.LGBMClassifier(n_estimators=100,
                         random_state=1)
score = cross_validate(clf, df[features], df['new_target'], cv=skf)

# Cross Validation 結果を表示
print('Accuracy : {:.1f}%'.format(score['test_score'].mean()*100))
Accuracy : 75.5%

結果、ランダムに選んだ場合 (50%) よりも分類精度が高くなった。つまり、train/test 間で特徴量の分布が異なっていることがわかる。

まとめ

今回は、可視化しやすいように 2つの特徴量のみをもつデータセットで Adversarial Validation を実施した。コンペ等においては、Adversarial Validation 時の特徴量重要度が高いものは除去することや、学習時の重みづけなどで、特徴量選択することで、Local CV と Public Leader Board のスコアの乖離を抑制できる可能性がある。参考 : kaggle_notebook