見出し画像

Kaggle Courses: Intermediate Machine Learning ノート(前編)

 はじめまして。私は Kaggle でデータ分析技術の基本を習得するために勉強しています。今回は Kaggle が提供している無料教材の「Intermediate Machine Learning」に取り組んだのでその内容をまとめました。半ば自分用のノートになっている所がありますが、大まかにこのコースで学んだ内容や思うことなどを日本語でまとめました。


まえがき

 想定する読者は、(私同様)Kaggle やデータ分析の超初心者で、これからどんな講座を受講しようか考えていらっしゃる方の判断材料になってくれたら幸いです。本記事を通して、このコースでやっている内容を大まかにつかめたのであればとても嬉しいです。

 私自身データ分析を学び始めて間もないため、内容の正確さを追求するとひやりとする表現があるかもしれませんが、その際はコメントでご指摘いただけると大変勉強になります。

 また、この記事自体、実は1年ほど前に書き溜めていたものを公開しているので途中で出てくるたとえ話の内容が若干古く感じると思います。ご容赦ください。

1. Introduction

 初級MLの最終課題と同様、Housing Prices Competition (HPC) の予測結果を提出するだけでほとんど内容は変わりませんでした。

2. Missing Values

 ここでは、分析対象のデータの一部が欠損しているような場合でも分析する方法を紹介しています。

 引き続き、Housing Prices Competition (HPC) のデータを使いますが、ここではガレージの築年数`GarageYrBlt`や間口の広さ`LotFrontage`などの一部に欠損があります。こうしたデータを分析するためには、あらかじめ前処理というものが必要になります。

 欠損のあるデータの前処理方法としてまず考えられるのが、「欠損データのあるデータ列を消去する」です。この手法はとてもシンプルな操作で処理ができますが、もし予測対象(被説明変数)である家の価格を決定するうえでその列が重要な情報をもつ場合、モデル予測の精度に関わる深刻な問題になります。

 一方、何か適当な値を「補完(imputation)」することも考えられるでしょう。しかし、欠損データにはどんな値を代入したらモデル予測を損なわないものになるでしょうか。

 データ補完に関しては一概にこれが良いというものがありませんが、平均値や中央値といった代表値を用いることが多いようです。

from sklearn.impute import SimpleImputer
imputer = SimpleImputer() # デフォルトでは平均値を代入します。
imputer_med = SimpleImputer(strategy='median') # 中央値を代入します。
imputer_most = SimpleImputer(strategy='most_frequent') # 最頻値を代入します。

(最も予測曲線が線形で描けるようなものであれば、「欠損データのための」予測は容易かもしれませんが、実データはそこまできれいに乗っているものではありません。一般には欠損データの正確な補完(もはや予測問題かもしれませんが)は容易ではないと思います。)

余談:ずいぶん前に『ファクトフルネス』を読んだことを思い出しました。この本は、普段我々が想像以上に様々なバイアスや情報のある側面だけで世の中を観察していることを気付かせてくれる面白い本です。その中に、直線本能というものがありました。我々が何らかの数量を目の当たりにした時、時系列に関して直線的に(一次関数的に)変化すると思い込む性質があります。例えば、昨年から毎日新型コロナウイルスの新規感染者数が報道されていますが、これにもどこか私たちは「緊急事態宣言下では、直線的に日に日に感染者数が減少するだろう」みたいに直線本能で考えていたかもしれませんね。しかし、実際のところ、週初めは感染者が前日比で下がる傾向にあり、一週間で山の型を描きながら、縮小していきます。このように、必ずしも緊急事態宣言が直ちに感染者数を直線的に減らしているわけではないことが分かります。

3. Categorical Variables

 この節では、連続的な数量でなく、定性的な値をもつカテゴリー変数の取り扱い方について紹介しています。

 現実で扱うデータはどれも連続的な値をとったものとは限りません。例えば、「好き」と「嫌い」といった数値で表されていない情報を含むデータを分析することがしばしばあります。こうした変数をカテゴリー変数といいます。

 ここでも、2節同様、HPCのデータセットを用いて進めています。

 まず、カテゴリー変数を含むデータに遭遇したとき、今まで習った内容で分析しようと思ったら、まずカテゴリー変数の列を消去することが考えられます。つまり、連続的な数値をとるものだけを説明変数として選べばよいのです。しかし、これも2節で説明したことと同様に、家の価格を決める重要な要素であれば消してしまうのはもったいないので、なんとか定量化して進められないかと思うわけです。

1. 順序変数(ordinal variables)

 はじめに、定量化する方法の一つとしてそれぞれの名目に対して番号付けを行い、順序変数(ordinal variables)に符号化することを紹介します。

 これは、順序付けが可能なカテゴリー変数にとても有効です。例えば、頻度に関して「まったくない」「たまにある」「普通」「よくある」「毎日ある」といったものはそれぞれ、0点から4点に対応付けることでうまくいきそうです。

注意:現実では、順序変数付けをする場合、学習データには存在せず検証(validation)データにのみ存在するような名目を持つデータセットを分析することがあります。例えば、学習データではカテゴリー変数の名目は「A」「B」「C」「D」の4種類しかないが、検証データには同じ列の名目に「E」が存在した場合、学習用データを順序変数付けしても、検証データとの整合性がとれずscikit-learnはエラーを出力します。これを回避するためには、(現状このコースでは)そのようなイレギュラーな列は分析対象から外すようにしています。

2. One-Hot 符号化(One-Hot Encoding)

 順序付けができないようなカテゴリー、例えば色は場合にもよるかもしれませんが、一般には順位付けできません。このようなデータはそれぞれのカテゴリーごとに新たな列を作ってしまい、0 と 1 で表現するという方法があります(下図を参照)。

 このようなデータ前処理はブランドや名目を数値化するのに便利な手法です。ただし、一般に15以上のカテゴリーが存在するときはテーブルサイズが爆発的に増加するため、計算量の都合上あまり推奨されません。

 例えば、ある列のカテゴリーが20種類あるとし、行数(データのサンプル数)が 10,000 個あったとします。このとき One-Hot 符号化を行うと、テーブルのセル数は 19*10,000 = 190,000 個増えることになります。

 そのため、カテゴリーがあまりに多い場合は決定木やランダムフォレストといった予測モデルの計算パフォーマンスを大きく落とす要因になることから使用しません。

余談:教材の資料では触れられていませんでしたが、One-Hot 符号化を行うと上図のようにほとんどの成分は 0 となります。このようなデータはスパースな(疎な)データといえます。スパースなデータ(疎行列)は表現方法を工夫することで時間計算量と空間計算量ともに削減できることが期待できます!そのため、scikit learn の One-Hot エンコーダには、スパース行列表現を行うか否かを選択できるオプションがあります。しかし、こうした話はもう少し全体を学んでから深めていきたいと思います。

3*. 演習課題(任意提出課題)

 これは数値計算上の観点からすると、Exerciseの「Generate test predictions and submit your results(任意課題)」では除去してしまった`high_cardinality_cols` を順序変数に変換して取り扱いました。

# (Optional) Your code here
# 課題のシートから一部抜粋しています
OH_encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
OH_cols_train = pd.DataFrame(OH_encoder.fit_transform(X_train[low_cardinality_cols])) # Your code here
OH_cols_valid = pd.DataFrame(OH_encoder.transform(X_valid[low_cardinality_cols])) # Your code here

OH_cols_train.index = X_train.index
OH_cols_valid.index = X_valid.index

num_X_train = X_train.drop(low_cardinality_cols, axis=1)
num_X_valid = X_valid.drop(low_cardinality_cols, axis=1)

OHO_X_train = pd.concat([num_X_train, OH_cols_train], axis=1)
OHO_X_valid = pd.concat([num_X_valid, OH_cols_valid], axis=1)

ordinal_encoder = OrdinalEncoder()
OHO_X_train[high_cardinality_cols] = ordinal_encoder.fit_transform(OHO_X_train[high_cardinality_cols])
OHO_X_valid[high_cardinality_cols] = ordinal_encoder.transform(OHO_X_valid[high_cardinality_cols])

print("MAE from My New Approach (Ordinal Encoding & One-Hot):") 
print(score_dataset(OHO_X_train, OHO_X_valid, y_train, y_valid))

# 実行結果
# MAE from My New Approach (Ordinal Encoding & One-Hot):
# 17101.395228310503

 すべての'object'を順序変数に変換した場合とあまり変わりませんでした。どうも、このデータセットにおいては一部だけ One-Hot 符号化を用いてもモデルの予測精度の向上に寄与していないように思われます。

 他にも、One-Hot 符号化をするうえで、`low_cardinality_cols`の閾値10から15に上げてみることも一つアイデアとして考えられますが、この辺りで終わりにしました(しらみつぶしというより、ある程度根拠をもってあたりを付けて方策を立てないと、コンペで勝てないんだろうなとコンペ未参加ですが感じました)。

 教材では「One-Hot が一番うまくいくよ!」と書かれていましたが、教材で紹介されていた検証データがたまたま悪かったのでしょうか、学習者としては期待通りの結果ではなく、列削除がうまくいっているように見えました。もう少し予測に強い影響を与えるカテゴリー変数を持つデータを使って自身で試してみるのが復習にもなりますし良いかと思いました。そういいつつも、思い切った舵取りとして最初に紹介した列削除も選択肢に入れておく必要はありそうです(バリバリ Kaggler の方はどう考えていらっしゃるのでしょうか?気になります!)。

4. Pipelines

 このレッスンではこれまでに学んだ欠損値の補完(imputation)とカテゴリー変数の変換を一挙に行うパイプラインを紹介しています。

 まず、標題の『パイプライン』とは以下のような意味を持ちます。

プロセッサが命令を実行する工程を複数の段階に分割し、各段階をオーバーラップさせて処理できるようにする仕組みのこと。ある命令の実行中に、次の命令の処理にとりかかる、といった具合に、連続する命令が効率よく実行できるようになる。プロセッサの高速化技法の1つ。

@IT「パイプライン」

 なかなか素人には読解に苦労しますが、データ分析において、我々ユーザーの利点は主に実装がすっきりすることです。以下が具体例です。

・コードがすっきりするため、可読性が高まる
・すでに組み込まれたモジュールを使用するため、データ前処理でバグに遭遇しにくくなる
・プロトタイプデータから本格的な大規模データへの移行と展開が楽になるため、生産性が向上する
・検証データのデータ前処理も学習用と同じように行ってくれるため、2度同じような処理を書かなくてよい
・クロスバリデーションなど多様な予測モデルの検証を行ってくれるオプションが実装されている

 Scikit-learn のパイプラインは以下のように使います。

from sklearn.ensemble import RandomForestRegressor
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer

# 数値データに対する前処理(準備)
numerical_transformer = SimpleImputer(strategy='median')
# カテゴリーデータに対する前処理(準備)
categorical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='most_frequent'),
    ('onehot', OneHotEncoder(handle_unknown='ignore')),
])
# 前処理(上のtransformerを組み合わせた処理)
preprocessor = ColumnTransformer(
    transformers=[
        # 数値データに対する前処理(つまり中央値の補完のみ行う)
        ('num', numerical_transformer, numerical_cols),
        # カテゴリーデータに対する前処理(最頻の名目で補完 ⇒ One-Hot 符号化)
        ('cat', categorical_transformer, categorical_cols)
    ]
)
model = RandomForestRegressor(n_estimators=100, random_state=0)
# データ前処理とモデルを組み合わせたパイプラインを作成
clf = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('model', model),
])

 `steps=リスト`という構造で各要素は「処理の名称」と「処理内容」で構成されるタプルとなっています。

5. Cross-Validation

 クロスバリデーション(交差検証)は、すべてのデータセットをいくつかのブロックに分割し、検証データを変えながら繰り返し実験を行うことをいい、モデル精度を向上させることを目標としています。ここで、モデルの精度とは汎用性を意味し、テストデータに対する頑健性を高めることに主眼を置いています。

 クロスバリデーションのイメージは下図のように表せます(すべてのデータセットをいくつかに分割したブロックのことを英語で "fold" と呼びます)。

 これまでやってきた予測モデルは一部の検証データだけを用いてモデルの性能(平均絶対誤差:MAE)を測っていましたが、クロスバリデーションは検証データを(ダブらないように)選択し、最終的にすべての検証データの fold を網羅するように繰り返し実験を行います。

 クロスバリデーションを行うことで、例えば「実験1」ではMAEが14,000だったのに、それ以外の実験ではMAEが17,000以上になったという場合、このモデル(またはパラメータ設定)では未知のテストデータに対する予測値の頑健性(信頼性)は低い、と判断できるわけです。

 検証データを変えながら実験することはもちろん時間のかかることなので、クロスバリデーションを用いるかどうかの判断はデータサイズの規模によります。小さいデータセットであれば、クロスバリデーションを行うべきだと思いますが、明確なしきいはありません。

 演習問題では、実際にハイパーパラメータチューニング(ランダムフォレストモデルで用いるパラメータ `n_estimators` の最適値探索)のためにクロスバリデーションを行います。

おわりに

 今回は第5節の交差検証までのノートを作成しました。これからも学習が進み次第、後編を執筆します!

この記事が気に入ったらサポートをしてみませんか?