TargetEncodingのスムーシング
はじめてのFeature Engineering で紹介したTargetEncoding, LikelihoodEncodingはカテゴリ変数の前処理としては非常に強力な手法だが、あるカテゴリに所属するデータの数が少ないと過学習の原因になってしまう可能性がある。データ数が少ないカテゴリを持つデータセットに対して効果的にTragetEncodingを使うために、この論文ではTargetEncodingのスムージングという手法を提案している。
まず通常のTargetEncoding、LikelihoodEncodingは以下のような式で表現できる。
ここで はクラスタi に所属しているデータの数、 はクラスタiに所属していて目的変数が1の数を表している。、の数が十分に大きければ過学習を気にする必要は無いかもしれないが、これらの値が小さいと間接的に目的変数の値を予想できるようになってしまうので過学習の要因になってしまう。この問題を解決するために以下のように改良された式が提案されている。
上で説明した式に加えて、、 という変数が追加されている。、 はデータの総数、はデータセット全体の中で目的変数が1の数を表している。また は 対して単調増加で、0〜1の範囲を取る関数を表している。
この式は の数が十分に大きければ第一項の影響が大きくなり普通のTargetEncodingと同じような値を示す。一方で の数が小さいと第二項の影響が大きくなりデータセット全体の目的変数が1の割合に近づく。このようにデータ数が少ないクラスタに対してはデータセット全体の値を用いることで、機械学習モデルが過学習することを防いでいる。
上の式で出てきたには次のような関数を用いることが多いようだ。
この関数はシグモイド関数的な特徴(k = 0、f = 1とすれば標準シグモイド関数)を示し、k、fの値を調整することでチューニングすることができる。
参考
- A Preprocessing Scheme for High-Cardinality Categorical Attributes in Classification and Prediction Problems https://kaggle2.blob.core.windows.net/forum-message-attachments/225952/7441/high%20cardinality%20categoricals.pdf
- Python target encoding for categorical features https://www.kaggle.com/ogrellier/python-target-encoding-for-categorical-features
はじめてのFeature Engineering
先日KaggleのAdTracking Competition に参加したが惨敗したため、特訓することにした。 Ad Tracking Competition で4位の人がFeature Engineeringについて非常にまとまった資料を公開していたので、その要約をまとめた。 勉強がてらPythonでサンプルコードを書いたので一緒においておく。
h2o-meetups/Feature Engineering.pdf at master · h2oai/h2o-meetups · GitHub
何故Feature Engineeringが必要なのか
価値のある特徴量を生成し、ノイズの原因となる特徴量を除去することでシンプルで精度のいいモデルを実現する。
たとえば下の図で、左の状態だと円形にプロットが分布していて、ニューラルネットワークやSVMなどの非線形モデルでないと分類することができない。 しかし各点を極座標で表して右のように変換すれば、非線形の手法を使わなくても決定木や線形分類器などのシンプルな手法で識別できるようになる。
具体的な手法(カテゴリ変数の場合)
多くの機械学習手法(ニューラルネットワークsvm)はカテゴリ変数を直接学習させることが出来ない。これらのモデルにカテゴリ変数を学習させたい場合には、カテゴリ変数を数値に変換する必要がある。よりモデルがカテゴリ変数の意味を理解しやすいように様々な手法が考案されている。カテゴリ変数の特徴(値に大小関係があるか、疎な入力でも問題がないか、特定の値だけが重要なのか)などを考えてどのような前処理を施すか決定することになる。
Label Encoding
単純にラベルを数字に置き換えるだけの処理。 多くの人が使ったことがあるのではないかと思う。下の例だとA→0、B→1、C→2という様に置き換えてる。pythonだとsklearnのLabelEncoderを使うのが楽。
idx | category | encoded_feature |
---|---|---|
0 | A | 0 |
1 | A | 0 |
2 | A | 0 |
3 | B | 1 |
4 | B | 1 |
5 | C | 2 |
import pandas as pd from sklearn.preprocessing import LabelEncoder df = pd.DataFrame({ "category" : ["A","A","A","B","B","C"], }) le = LabelEncoder() df["encoded_feature"] = le.fit_transform(df["category"]) df.head(10)
One Hot Encoding
カテゴリ変数の各値ごとにカラムを分けて0、1で表現する方法。下の例だとcategory_A, category_B, category_Cという列を新しく作って、Aの場合には(1, 0, 0)、Bの場合には(0, 1 ,0)、Cの場合には(0, 0, 1)と表現する。ニューラルネットワークでカテゴリ変数を表すときはOne Hot Encodingが第一選択肢になる気がする。
LabelEncodingのときと同様、One Hot Encoderがsklearnに収録されているので、それを使うのが楽。pandasにはget_dummiesというメソッドが収録されているので、それを使っても同じことが出来る。
category | category_A | category_B | category_C | |
---|---|---|---|---|
0 | A | 1 | 0 | 0 |
1 | A | 1 | 0 | 0 |
2 | A | 1 | 0 | 0 |
3 | B | 0 | 1 | 0 |
4 | B | 0 | 1 | 0 |
5 | C | 0 | 0 | 1 |
#one hot encoding sample import pandas as pd from sklearn.preprocessing import OneHotEncoder df = pd.DataFrame({ "category" : ["A","A","A","B","B","C"], }) #文字をLabel Encodingして数値に変換 le = LabelEncoder() df["encoded_feature"] = le.fit_transform(df["category"]) #One Hot Encoding ohe = OneHotEncoder() ohe_array = ohe.fit_transform(df[["encoded_feature"]]) df.drop("encoded_feature", axis = 1, inplace = True) #DataFrameに変換 ohe_df = pd.DataFrame(ohe_array.todense()).astype("uint8") ohe_df.columns = ["category_A","category_B","category_C"] #元のDataFrameと結合 df = pd.concat([df,ohe_df], axis = 1) df.head(10)
Frequency Encoding
各値の出現頻度で表現する方法。例えば以下の表におけるAの値は、3(Aの出現回数)/ 6(全体の行数)で0.5となる。pandasを使えばgroupbyとmergeを駆使して比較的に簡単に書くことが出来る。
category | category_counts | frequency_encoding | |
---|---|---|---|
0 | A | 3 | 0.500000 |
1 | A | 3 | 0.500000 |
2 | A | 3 | 0.500000 |
3 | B | 2 | 0.333333 |
4 | B | 2 | 0.333333 |
5 | C | 1 | 0.166667 |
#Frequency Encoding Sample import pandas as pd from sklearn.preprocessing import OneHotEncoder df = pd.DataFrame({ "category" : ["A","A","A","B","B","C"], }) #各カテゴリーの出現回数を計算 grouped = df.groupby("category").size().reset_index(name='category_counts') #元のデータセットにカテゴリーをキーとして結合 df = df.merge(grouped, how = "left", on = "category") df["frequency"] = df["category_counts"]/df["category_counts"].count() df.head(10)
Target Mean Encoding
Likelihood Encodingとも呼ばれる。Google検索でのヒット数的にはTarget Mean Encodingの方がメジャー。あるカテゴリ変数の値を持ち目的変数が1の個数を、その値の出現回数で割ったもの。 下の例だと、Aの出現回数は3回、カテゴリーがAで目的変数が1を持つものの出現回数は2回なのでTarget Mean Encodingの値は2/3 = 0.666 となる。
category | outcome | category_counts | outcome_counts | target_mean_encoding | |
---|---|---|---|---|---|
0 | A | 1 | 3 | 2 | 0.666667 |
1 | A | 1 | 3 | 2 | 0.666667 |
2 | A | 0 | 3 | 2 | 0.666667 |
3 | B | 1 | 2 | 1 | 0.500000 |
4 | B | 0 | 2 | 1 | 0.500000 |
5 | C | 1 | 1 | 1 | 1.000000 |
#Target Mean Sample import pandas as pd from sklearn.preprocessing import OneHotEncoder df = pd.DataFrame({ "category" : ["A","A","A","B","B","C"], "outcome" : [1,1,0,1,0,1], }) 単純に計算すれば次のようなコードになる。 #各カテゴリーともう一つの特徴量の出現回数を計算 grouped_category = df.groupby("category")["category"].count().reset_index(name='category_counts') grouped_outcome = df.groupby("category")["outcome"].sum().reset_index(name='outcome_counts') #元のデータセットにカテゴリーをキーとして結合 df = df.merge(grouped_category, how = "left", on = "category") df = df.merge(grouped_outcome, how = "left", on = "category") df["target_mean_encoding"] = df["outcome_counts"]/df["category_counts"] df.head(10)
Target Mean Encodingには過学習を防ぐための leave one out schemaという手法も提案されている。これは上の表で1列目のAに対してTarget Mean Encodingを計算するときに、自分自身の値を除いて計算する方法だ。この方法を用いて1列目のAに対してTarget Mean Encoding を計算すると(自分を除いたカテゴリーがAで目的変数が1の総数) / (自分を除いたAの出現回数)= 0.5 となる。
category | outcome | category_counts | outcome_counts | target_mean_encoding | |
---|---|---|---|---|---|
0 | A | 1 | 3 | 2 | 0.5 |
1 | A | 1 | 3 | 2 | 0.5 |
2 | A | 0 | 3 | 2 | 1.0 |
3 | B | 1 | 2 | 1 | 0.0 |
4 | B | 0 | 2 | 1 | 1.0 |
5 | C | 1 | 2 | 2 | 1.0 |
6 | C | 1 | 2 | 2 | 1.0 |
#Target Mean Sample with leave one out scheme import pandas as pd from sklearn.preprocessing import OneHotEncoder df = pd.DataFrame({ "category" : ["A","A","A","B","B","C","C"], "outcome" : [1,1,0,1,0,1,1], }) #各カテゴリーの出現回数を計算 grouped_category = df.groupby("category")["category"].count().reset_index(name='category_counts') grouped_outcome = df.groupby("category")["outcome"].sum().reset_index(name='outcome_counts') #元のデータセットにカテゴリーをキーとして結合 df = df.merge(grouped_category, how = "left", on = "category") df = df.merge(grouped_outcome, how = "left", on = "category") #計算する際に自分自身の値は除く df["target_mean_encoding"] = (df["outcome_counts"] - df["outcome"])/(df["category_counts"] - 1) df.head(10)
おわりに
4種類のカテゴリ変数の数値化に関する方法を紹介した。「カテゴリー変数に対する手法」という目次をつけておいて、時間が無くて数値変数に対する手法の紹介まで出来なかったのが申し訳ない。 もとのスライドには、Weight of Evidecneと呼ばれる手法や、具体的な特徴量抽出の手順などが記載されているので時間があったら追記するかもしれない。
feather - pandasの読み書きを高速化
kaggleに参加して巨大なCSVファイルを読み書きしていると、それだけで数分近くかかってしまうことがある。 そこで紹介されていたfeatherというDataFrame保存用ライブラリを使ってみた。 featherはpandasのDataFrameを高速に保存、ロードするために開発されたApache Arrowのpythonラッパーだ。
Apache Arrow特有のカラム型メモリ管理をすることで、データの読み書きを高速化しているそうだ。 ベースになっているApache Arrowが他言語や他オープンソース(SparkやHadoop)との互換性を重視しているために、feather形式で保存すればRなどでも読み込める。
featherのインストールはpipから簡単にできる。
pip install feather-format
使い方も簡単でwrite_dataframe、read_dataframeを呼ぶだけ。
import feather import pandas as pd #データフレームの読み込み df = feather.read_dataframe(path_feather) #データフレームの書き込み feather.write_dataframe(df, path_feather)
またpandasのバージョン0.20.0からpandas.DataFrame.to_featherやpandas.read_featherというメソッドが追加されているので、以下のような方法でも使える。
import pandas as pd #データフレームの読み込み df = pd.read_feather(path_feather) #データフレームの書き込み df.to_feather(path_feather)
約18列、150000行のデータのデータセットで、csvとfeatherそれぞれの読み書きの速度を計測すると、
読み込み速度
# csvからの読み込み %time df = pd.read_csv(path_csv) # feather formatからの読み込み %time df = pd.read_feather(path_feather) %time df = feather.read_dataframe(path_feather)
CPU times: user 29.1 s, sys: 27.7 s, total: 56.8 s Wall time: 1min 38s CPU times: user 5.53 s, sys: 10.1 s, total: 15.6 s Wall time: 37.7 s CPU times: user 5.46 s, sys: 11.1 s, total: 16.5 s Wall time: 35.7 s
書き込み速度
# csvへの書き込み %time df.to_csv(path_csv) # featherへの書き込み %time df.to_feather(path_feather) %time feather.write_dataframe(df, path_feather)
CPU times: user 53 s, sys: 11.4 s, total: 1min 4s Wall time: 1min 30s CPU times: user 18.3 s, sys: 10.1 s, total: 28.4 s Wall time: 39 s CPU times: user 18.8 s, sys: 8.76 s, total: 27.5 s Wall time: 35.5 s
実時間で読み書きともに約2.5倍程度と非常に高速に動作することが確認できた。データセットの保存や読み込みに使えば大幅な時間の節約になる。 長期のデータには向いていない(破損とかに弱い?)そうなので、あくまで前処理したデータのキャッシュなどに使うのがいいと思う。
Talking Data AdTracking Fraud Detection Challenge
先日開催されたTalking Data AdTracking Fraud Detection Challengeに参加した。 Talking Data AdTracking Fraud Detection Challengeは中国の広告企業TalkingData主催のコンペで、広告クリックのログから不正なクリックを検出するというコンペティションだ。
TalkingData AdTracking Fraud Detection Challenge | Kaggle
初めてのkaggleで最終的な結果は1012位/3967位、上位26%だった。最初のコンペティションとしては悪くない結果だとは思うが、個人的にはもう少し上を狙いたかった。
コンペティション概要
以下の表に示す特徴を持つ、約180,000,000のレコードを持つ巨大なデータセットが与えられた。これらの特徴は全てLabel Encodeされていて全て整数で与えられている。
特徴量 | 説明 |
---|---|
ip | クリック元のipアドレス |
os | クリック元のOSやバージョンに関する情報 |
app | クリック元のアプリ |
device | クリック元の携帯電話の種類 |
channel | クリックされた広告のid的なもの |
click_time | クリックされた時刻 |
is_attributed | 目的変数 0は通常のクリック、1は不正なクリック |
ノートパソコンで今回のコンペに参加したが到底データセット全てはメモリに乗らず、一部を利用して学習させる方針で挑んだ。以下実際に参加したログと感想。
4/17
click_timeをパースして、day、hour、minuteなどの特徴量に分解した。 また一時間ごとのipの出現回数や、一日ごとのip-osの組み合わせの出現回数を特徴に加えxgboostでデータの一部を学習した。 与えられた学習データを学習用とテスト用に分けて評価したところ、テストデータでは約96%を達成。 しかしテストデータを送信したところ94%程度しか達成できなかった。2900人中2700位とか。
4/18
データセットの前半と後半でデータの傾向が少し違うらしい。 データが膨大でメモリに乗らないので(mac book airでは厳しいものがある)後半の一部を使用。 また単位時間あたりのipアドレス数を初めとした特徴量を導入した。 これらの結果リーダーボードで96.3%を達成。3000人中1800位とか。まだまだ。
4/21
当初はxgboostを使っていたが、Kernelを見ていると多くの人がlightgbmを使っている人が多かったのでlightgbmを試してみた。 lightgbmはカテゴリ変数をいい感じに取り扱ってくれるらしくて、そのおかげか96.5%を達成できた。あとパラメータによるのかもしれないが、少し計算速度が速くなったように感じた。
4/25
Kernelで公開されていた、次のクリックまでの経過時間を表すnext_clickという特徴を導入した。 この特徴量を導入しただけで飛躍的に精度が向上し、精度97.5を達成し3100人中上位700位まで一気に順位があがった。
4/23
Test Dataには4, 5, 9, 10, 13, 14時のデータしか含まれていないという情報がKernelで流れていた。 そこで学習に使うデータも4, 5, 9, 10, 13, 14時に絞ってみたところ、精度97.82を達成して3300人中500位近くまで順位を上げることが出来た。
4/25日以降
他に何個か特徴量を試したが精度の向上に繋がることはなかった。途中で最高精度を達成したファイルを直接書き換えてしまい、ロールバックすることも出来ず精度97.5あたりをうろついていた。怒涛の勢いで追い上げをくらい、順位を3900人中1000位近くまで落としてしまった。
最終日に誰かがディスカッションボードで精度98.11(上位5%程度の精度)のKernelを公開して物議を醸していて笑った。
感想
初めてKaggleのコンペティションに参加してみての感想としては、Kernelで初心者向けのコードが公開されていたりするので、英語さえ読めれば参加のハードルは低いと思った。正直、機械学習用のsklearn、xgboost、lightgbm、kerasなどのライブラリはpythonが使えれば中身を理解していなくても動かせてしまう(それで勝てるかは別問題として)。参加してみるとDiscussionとかで色々な知識を身につけることが出来るので、機械学習とかに興味がある人はとりあえず参加してみれば力をつけることが出来るのではないかと思う。ただ常時kaggleのことが気になってしまって他の作業ができなくなるののにだけは注意方がいい。
やはり精度を上げるための肝は、いかに優れた特徴量を設計するかにかかっているように感じた。下手な特徴量では逆に精度をさげてしまうため、効率的に有効な特徴量を見つけていくノウハウを学ぶ必要がある。
今回の反省点としては、
- 試してみた特徴量やプログラムは、リーダーボードのスコアと一緒に管理しておく。
- 積極的にKernel、Discussionを見て人の意見を取り入れていくこと。
- 一度作成したモデルもしっかり保管しておく。きちんと管理しておけば、最後の最後でBaggingやStackingして少し精度を挙げられる可能性がある。
あたりであろうか。
またCERNのコンペティションやロシアの広告会社Avitoのコンペティションが開催されるようなので参加してみようかと思う。Kaggleに参加してみたいけど難しそうと思っている人は、とりあえず適当なこんぺ