cha_kabuのNotebooks

個人的な機械学習関連勉強のアウトプット置き場です。素人の勉強録なので、こちらに辿り着いた稀有な方、情報はあまり信じない方が身のためです。

RankGaussについて勉強する

はじめに

現在参加中の某コンペで、「RankGaussが効く!」とされているのですが、慣れない手法なのでちゃんと調べてみました。kaggle本でもちゃんと紹介されているのですが、普段ニューラルネットから逃げているので全然頭に入っていませんでした…

素人が調べて書いているので、間違いもあるかと思いますので情報の信じすぎにはご注意下さい。

概要

kaggle本の説明が分かりやすかったので引用します。

数値変数を順位に変換したあと、順序を保ったまま半ば無理矢理正規分布となる様に変換する手法です。

数値の大小関係は変わらないのでGBDTなど木関係のアルゴリズムでは効果が無いようですが、特にニューラルネットでモデルを作成する際の変換として通常の標準化よりも良い性能を示すとのこと。

何で良い性能になるのかも調べてみたのですが理解できる情報に巡り合えず…むしろ、「RankGaussからMinMaxScalerに変えたらうまくいった」*1なんて記事もあったので、色々やってみる選択肢の一つとして捉えた方が良いのかも知れません。

低い理解力でメリットとデメリットを考えると、以下のあたりでしょうか。

メリット:値→順序に直すので、極端に頻度の高い値をある程度分散させられる?外れ値の影響が軽減される?

デメリット:順序尺度になってしまうので、差や比を捉えられなくなる?

実装

titanicデータセットを使って実装してみたいと思います。まずはデータの用意と簡単な前処理から。

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()

# データのロード
df = sns.load_dataset("titanic")

# 欠損データの削除
df = df.dropna(subset=["age"]).reset_index(drop=True)

# ヒストグラムの確認
df["age"].hist()
plt.show()

f:id:cha_kabu:20201026123257p:plain

ほぼほぼ正規分布に近いですが、若干崩れています。

ライブラリを使った実装

scikit-learnのQuantileTransformerで簡単に実装できます。DataFrameに適用する場合は不要ですが、Seriesに適用する場合はreshapeしてやる必要があります。

from sklearn.preprocessing import QuantileTransformer

# RankGaussによる変換を定義
transformer = QuantileTransformer(n_quantiles=100, random_state=0, output_distribution='normal')
transformer.fit(df["age"].values.reshape(-1,1))

# 変換後のデータを新規列に格納
df["age_quantiled"] = transformer.transform(df["age"].values.reshape(-1,1))

# ヒストグラムの確認
df["age_quantiled"].hist()
plt.show()

f:id:cha_kabu:20201026123301p:plain

正規分布っぽくなりました。

自力で実装してみる

ライブラリを使うと簡単ですが、何が起こっているのか良くわからないので、できるだけ自力で実装してみます。

なお、先に言っておくとそれっぽい結果にはなるので全然違うことをやっているわけではなさそうですが、全く同じ結果にはならなかった&他の自力実装している人のGitHubを覗くともっと難しいことをやっていたので、あくまで雰囲気を掴むためだけにチャレンジしています。実際のアルゴリズムとは異なるのでご注意ください。

まずはageをrankに変換して、各列を昇順にして結果を確認します。

# ageをrank変換
df["age_rank"] = df["age"].rank()

# 変換結果を確認
df[["age","age_rank"]].sort_values("age")
age age_rank
0.42 1.0
0.67 2.0
0.75 3.5
0.75 3.5
0.83 5.5
... ...
70.50 710.0
71.00 711.5
71.00 711.5
74.00 713.0
80.00 714.0

714 rows × 2 columns

ちゃんと順番通りにrankがついています(同じ値があると平均が取られxx.5などの順位になります)。

続いて、作成したage_rankを-1~1の範囲にスケーリングします。この際、後程処理時に-1と1に変換された値はinfになってしまうので、すごく小さな値を足して(引いて)おきます。

from sklearn.preprocessing import MinMaxScaler

# MinMaxScalerを定義
# 範囲は-1+極小値~1-極小値
scaler = MinMaxScaler(feature_range=(-1+(1e-7),1-(1e-7)))
scaler.fit(df["age_rank"].values.reshape(-1,1))

# 変換後のデータを新規列に格納
df["age_rank_scaled"] = scaler.transform(df["age_rank"].values.reshape(-1,1))

# 変換結果を確認
df[["age","age_rank","age_rank_scaled"]].sort_values("age")
age age_rank age_rank_scaled
0.42 1.0 -1.000000
0.67 2.0 -0.997195
0.75 3.5 -0.992987
0.75 3.5 -0.992987
0.83 5.5 -0.987377
... ... ...
70.50 710.0 0.988780
71.00 711.5 0.992987
71.00 711.5 0.992987
74.00 713.0 0.997195
80.00 714.0 1.000000

714 rows × 3 columns

順位が-1(+極小値)~+1(-極小値)の値に変換されました。

最後に、age_rank_scaledを逆誤差関数(誤差関数の逆関数

 {erf}^{-1}(z)=\sum_{k=0}^{\infty}\frac{c_k}{2k+1}\left(\frac{\sqrt{\pi}}{2}z\right)^{2k+1}

に通して正規分布になる様に変換します。なにやら難しい関数ですが、scipyに関数が用意されていますので、計算は機械に任せてしまいます。

from scipy.special import erfinv

# 逆誤差関数の適用
df["age_rank_scaled_norm"] = erfinv(df["age_rank_scaled"])

# ヒストグラムの確認
df["age_rank_scaled_norm"].hist()
plt.show()

f:id:cha_kabu:20201026123323p:plain

QuantileTransformerの結果と一致はしていませんが、なにやらそれっぽくなったので大体あっているはず…

さいごに

RankGaussについてまとめました。大小関係は変わらないので木のアルゴリズムには無意味というのは分かりやすいですが、何故ニューラルネットだと他のスケーリングに比べて効きやすいのかまではまだよく分かっていません…その辺は色んなところで使いながらどんなところで効く/効かないを掴んでいきたいと思います。