ログイン新規登録

Qiitaにログインして、便利な機能を使ってみませんか?

あなたにマッチした記事をお届けします

便利な情報をあとから読み返せます

この記事は限定共有記事です。投稿者本人とURLを知っているユーザーのみがアクセスできます。
記事を全体公開するには、公開設定で公開範囲を「全体に公開」にしてください。

この記事は最終更新日から3年以上が経過しています。

Numeraiトーナメント:直交化に関するテクニック

投稿日

表題の件、Numeraiトーナメントにおける直交化に関するテクニックを紹介します。

直交化とは

Numeraiでは、しばしば直交化(neutralization、orthogonalization)という所作が用いられます。直交化とは、ある対象の数値ベクトルについて可能な限り元の情報を維持しつつ、別の数値ベクトルによる寄与分を控除する手法です。
もう少し簡単に言うと、直交化前の数値ベクトルをa、直交化に用いる数値ベクトルをb、直交化後のベクトルをa'とすると、

  • aa'の相関は1.0に近い値を保持する(bの選び方によっては低下する場合もある)
  • a'bの相関はほぼ0となる

ここで、Numeraiのフォーラムでは、以下のような直交化するための関数が紹介されています。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def neutralize_series(series, by, proportion=1.0):
   scores = series.values.reshape(-1, 1)
   exposures = by.values.reshape(-1, 1)
   exposures = np.hstack((exposures, np.array([np.mean(series)] * len(exposures)).reshape(-1, 1)))
   correction = proportion * (exposures.dot(np.linalg.lstsq(exposures, scores)[0]))
   corrected_scores = scores - correction
   neutralized = pd.Series(corrected_scores.ravel(), index=series.index)
   return neutralized

例えば、Numeraiトーナメントの特徴量dexterity7をtargetに対して直交化してみましょう。

data   = pd.read_csv("numerai_training_data.csv").set_index("id")
f1     = data["feature_dexterity7"]
target = data["target"]
f1_nt  = neutralize_series(f1, target)

print(np.corrcoef(f1, target)[0, 1])     # dexterity7とtargetの相関
print(np.corrcoef(f1_nt, target)[0, 1])  # 直交化したdexterity7とターゲットの相関
print(np.corrcoef(f1, f1_nt)[0, 1])      # 直交化前後の相関

<実行画面>

-0.012174692404028822
-7.319948614012781e-13
0.9999258856859752

上記の通り、直交化前は-0.0121であった相関係数が、直交化後はほぼ0となり且つ元々のデータとの相関はほぼ1を維持していることが分かります。

線形回帰による直交化

さて、上記の直交化関数は非常に複雑な計算をしているようですが、実のところは線形回帰の残差を取っているだけなのです(proportion=1.0の場合)。以下のようにdexterity7をtargetで回帰して残差を取ると、直交化関数の結果と一致することが分かります。

from sklearn.linear_model import LinearRegression

lm = LinearRegression()
lm.fit(data[["target"]], data["feature_dexterity7"])
f1_nt2 = data["feature_dexterity7"] - lm.predict(data[["target"]])

pd.DataFrame({"NT1":f1_nt, "NT2":f1_nt2})

<実行画面>

                       NT1       NT2
id                                  
n000315175b67977  0.500019  0.500019
n0014af834a96cdd -0.254801 -0.254801
n001c93979ac41d4  0.495199  0.495199
n0034e4143f22a13 -0.254801 -0.254801
n00679d1a636062f  0.254839  0.254839
...                    ...       ...
nff6a8a8feaeeb52  0.000019  0.000019
nff6af62a0996372 -0.495161 -0.495161
nff9288983b8c040 -0.004801 -0.004801
nffaab4e1cacc4b1  0.500019  0.500019
nffba5460b572cfa  0.250019  0.250019

[501808 rows x 2 columns]

重回帰による直交化

前章で「直交化とは線形回帰の残差である」ということが分かりました。もう少し考えてみると、重回帰を行うことで複数の数値ベクトルに対してまとめて直交化を行うことができることに気付きます。
ここでは例として特徴量同士の直交化を行ってみましょう。

まず、クラスターマップを使って特徴量同士の相関を確認します。

import seaborn as sns

intelligence = [f for f in data.columns if "intelligence" in f]
sns.clustermap(data[intelligence].corr())

<実行画面>
01.png

特徴量同士の相関はそれなりに大きくなっています。まず最初にintelligence1をその他の特徴量を用いて直交化してみます。

import copy

data2 = data[intelligence].copy()

f = "feature_intelligence1"
x_feature = copy.deepcopy(intelligence)
x_feature.remove(f)

tmp_model = LinearRegression()
tmp_model.fit(data2[x_feature], data2[f])
data2[f] -= tmp_model.predict(data2[x_feature])

sns.clustermap(data2[intelligence].corr())

<実行画面>
04.png

御覧の通り、他の特徴量との相関がほぼ0となりました。このような所作を繰り返すと、全ての特徴量間において相関を0にすることができます。

data2 = data[intelligence].copy()

for f in intelligence:
    x_feature = copy.deepcopy(intelligence)
    x_feature.remove(f)
    tmp_model = LinearRegression()
    tmp_model.fit(data2[x_feature], data2[f])
    data2[f] -= tmp_model.predict(data2[x_feature])

sns.clustermap(data2[intelligence].corr())

<実行画面>
02.png

上記の通り、特徴量同士の相関は0となります。このとき、元のデータに対してどれくらい情報を保持しているでしょうか。直交化前後の数値ベクトルの相関を観察してみましょう。

corr = []
name = ["int"+str(i+1) for i in range(12)]

for f in intelligence:
    corr.append(np.corrcoef(data[f], data2[f])[0, 1])

plt.bar(name, corr)

<実行画面>
03.png

特徴量の中には大きく相関が低下しているものもあります。これは直交化の順序にも依りますが、元の情報から大きく相関が低下する特徴量は、そもそも保有している独自の情報が少ないことを示唆します。

少しだけインテリジェントな直交化

最後に少しだけ工夫した使い方を紹介します。それはLASSOやRIDGEによる直交化です。
以下、事例です。

from sklearn.linear_model import Lasso

feature_names = [f for f in tournament_data.columns if "feature" in f]

lasso = Lasso(alpha=0.0005)
lasso.fit(tournament_data[feature_names], tournament_data["prediction"])
tournament_data["prediction"] -= lasso.predict(tournament_data[feature_names])

このようにすると、自身の予測結果に対して関連の強い特徴量のみを選んで直交化することができ、効率的にfeature exposureを下げることができます。シャープの改善に寄与できる場合があります。

以上。

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
  3. ダークテーマを利用できます
ログインすると使える機能について

コメント

この記事にコメントはありません。

いいね以上の気持ちはコメントで

新規登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる

ソーシャルアカウントでログイン・新規登録

メールアドレスでログイン・新規登録