アンサンブル分類モデルは,優れた性能を達成し,新しい未知のデータセットにうまく汎化できる強力な機械学習ツールになり得る.
アンサンブル分類器の価値は,複数の分類器の予測を組み合わせることで,個々の分類器による誤差を補正し,全体としてより高い精度を実現できることにあります.それでは、さまざまなアンサンブル分類法と、これらの分類器がScikit-Learnでどのように実装できるかを見ていきましょう。
機械学習におけるアンサンブルモデルとは?
出典:Pixabay
アンサンブルモデルとは、異なるアルゴリズムを組み合わせて行うアンサンブル学習手法のことです。その意味では、アルゴリズムそのものというより、メタアルゴリズムといえる。アンサンブル学習法は、予測モデルの性能を向上させることができるため、価値があります。
アンサンブル学習法は、複数の分類器の予測を結びつけることで、予測精度を向上させたり、偏りや分散などの側面を低減させたりして、より良い性能を導き出すという考えに基づいている。
一般に、アンサンブルモデルは、逐次アプローチと並列アプローチの2つのカテゴリに分類される。
逐次型アンサンブルモデルは、ベースとなる学習者/モデルを順番に生成することで動作します。逐次的なアンサンブルモデルは、以前に誤分類された例を再度重み付けすることにより、不正確な予測を補正することができるため、一般的に全体のパフォーマンスを向上させるために使用されます。この顕著な例としてAdaBoostがあります。
並列モデルは推測できるかもしれませんが、基本学習器を並列に作成し学習させる手法です。並列手法は多くのモデルを並列に学習させ、その結果を平均化することで誤差を減らすことを目的としています。並列手法の顕著な例として,Random Forest Classifierがあります.
別の考え方として、同種の学習者と異種の学習者を区別することもできる。アンサンブル学習法の多くは同種のベース学習器(同じ種類の学習器が多数)を用いるが、アンサンブル法の中には異種の学習器(異なる学習アルゴリズムが一緒になったもの)を用いるものがある。
おさらいすると
- 逐次モデルは例題を再重み付けして性能を向上させようとするもので、モデルは順番に生成される。
- 並列モデルは、多くのモデルを同時に学習させ、その結果を平均化することで動作する。
これらのモデルを用いて、機械学習の分類問題を解く方法を説明する。
異なるアンサンブル分類法
バギング
出典:ウィキメディア・コモンズ
バギング(Bagging)はブートストラップ集計としても知られ、複数の推定値を平均化することで推定値の分散を減らすことを目的とした分類法である。バギングでは学習者が学習するメインデータセットからサブセットを作成します。
異なる分類器の予測値を集約するために、回帰では平均化、分類では投票アプローチ(多数決に基づく)のいずれかが使用されます。
バギング分類法の一例として、ランダムフォレスト分類器があります。ランダムフォレスト分類器の場合,個々の木はすべてデータセットの異なるサンプルで学習されます.
また,木はランダムに選択された特徴量を用いて学習されます.その結果が平均化されると、全体の分散が減少し、結果としてモデルの性能が向上します。
ブースト
ブースティング(Boosting)アルゴリズムとは、弱くて性能の低いモデルを強いモデルに変換することができるアルゴリズムです。ブースティング・アルゴリズムの背後にある考え方は、データセットに多くの弱い学習モデルを割り当て、その後の学習ラウンドで誤分類された例の重みを調整することである。
分類器の予測値は集約され、加重和(回帰の場合)または加重多数決(分類の場合)によって最終的な予測がなされる。
ブースティング分類法の一例としてAdaBoostがあり、前述のアルゴリズムから派生したグラディエント・ブースティングも同様である。
Gradient Boostingの詳細やその理論については、すでに以前の記事で紹介しています。
スタッキング
出典:ウィキメディア・コモンズ
スタッキングアルゴリズムは、異なる回帰または分類アルゴリズムの決定を結合するアンサンブル学習法である。構成モデルは学習データセット全体に対して学習される。これらのコンポーネントモデルが学習された後、異なるモデルからメタモデルが組み立てられ、次にコンポーネントモデルの出力に対して学習される。通常、コンポーネントモデルは異なるアルゴリズムであるため、この手法では異種アンサンブルを作成することになります。
実装例
アンサンブルモデルを作成するために使用できるさまざまな方法を検討したので、次に、さまざまな方法を使用して分類器を実装する方法を見てみましょう。
しかし、アンサンブル分類器を実装する様々な方法を見る前に、使用するデータセットを選択し、そのデータセットの前処理を行う必要があります。
ここではTitanicデータセットを使用します。データの前処理として、欠損値を取り除き、データを均一な範囲にスケーリングしましょう。そして、アンサンブル分類器の設定を行います。
データ前処理
まず始めに、それぞれのライブラリから必要な関数をすべてインポートすることから始めます。データの読み込みと変換にはPandasとNumpyを使い、LabelEncoder
とStandardScaler
のツールも使用します。
また、機械学習メトリクスと train_test_split
関数も必要です。最後に、使用する分類器が必要です。
import pandas as pd
import numpy as np
import warnings
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import accuracy_score, f1_score, log_loss
from sklearn.model_selection import train_test_split, KFold, cross_val_score
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier, ExtraTreesClassifier
まずは学習データとテストデータをロードして、NULL 値があるかどうかをチェックする関数を作成します。
training_data = pd.read_csv("train.csv")
testing_data = pd.read_csv("test.csv")
def get_nulls(training, testing):
print("Training Data:")
print(pd.isnull(training).sum())
print("Testing Data:")
print(pd.isnull(testing).sum())
get_nulls(training_data, testing_data)
たまたまですが、Age
とCabin
のカテゴリに多くの欠損値があります。
Training Data:
PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 177
SibSp 0
Parch 0
Ticket 0
Fare 0
Cabin 687
Embarked 2
dtype: int64
Testing Data:
PassengerId 0
Pclass 0
Name 0
Sex 0
Age 86
SibSp 0
Parch 0
Ticket 0
Fare 1
Cabin 327
Embarked 0
dtype: int64
まず、無駄になりそうなカラム、Cabin
カラムと Ticket
カラムを削除することから始めましょう。Cabinカラムは欠損値が多すぎるし、
Ticket` カラムは単純にカテゴリが多すぎて使い物にならないからです。
その後、いくつかの欠損値をインプットする必要があります。その際、データセットが若干右傾化していることを考慮する必要があります(若い年齢が高い年齢よりも若干目立つ)。大きな外れ値のために平均値をとると、データセットの中心から遠く離れた値をインポートしてしまうので、データをインポートするときは中央値を使うことにします。
# Drop the cabin column, as there are too many missing values
# Drop the ticket numbers too, as there are too many categories
# Drop names as they won't really help predict survivors
training_data.drop(labels=['Cabin', 'Ticket', 'Name'], axis=1, inplace=True)
testing_data.drop(labels=['Cabin', 'Ticket', 'Name'], axis=1, inplace=True)
# Taking the mean/average value would be impacted by the skew
# so we should use the median value to impute missing values
training_data["Age"].fillna(training_data["Age"].median(), inplace=True)
testing_data["Age"].fillna(testing_data["Age"].median(), inplace=True)
training_data["Embarked"].fillna("S", inplace=True)
testing_data["Fare"].fillna(testing_data["Fare"].median(), inplace=True)
get_nulls(training_data, testing_data)
これで、欠損値がなくなったことがわかります。
Training Data:
PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 0
SibSp 0
Parch 0
Fare 0
Embarked 0
dtype: int64
Testing Data:
PassengerId 0
Pclass 0
Name 0
Sex 0
Age 0
SibSp 0
Parch 0
Fare 0
Embarked 0
dtype: int64
次に、数値以外のデータをエンコードする必要があります。LabelEncoderをセットアップして
Sex特徴にフィットさせ、エンコーダーでデータを変換しましょう。次に、
Sex特徴の値をエンコードされた値に置き換え、
Embarked` 特徴に対しても同じことをします。
最後に、StandardScaler
を使ってデータをスケーリングし、値に大きな変動がないようにします。
encoder_1 = LabelEncoder()
# Fit the encoder on the data
encoder_1.fit(training_data["Sex"])
# Transform and replace training data
training_sex_encoded = encoder_1.transform(training_data["Sex"])
training_data["Sex"] = training_sex_encoded
test_sex_encoded = encoder_1.transform(testing_data["Sex"])
testing_data["Sex"] = test_sex_encoded
encoder_2 = LabelEncoder()
encoder_2.fit(training_data["Embarked"])
training_embarked_encoded = encoder_2.transform(training_data["Embarked"])
training_data["Embarked"] = training_embarked_encoded
testing_embarked_encoded = encoder_2.transform(testing_data["Embarked"])
testing_data["Embarked"] = testing_embarked_encoded
# Any value we want to reshape needs be turned into array first
ages_train = np.array(training_data["Age"]).reshape(-1, 1)
fares_train = np.array(training_data["Fare"]).reshape(-1, 1)
ages_test = np.array(testing_data["Age"]).reshape(-1, 1)
fares_test = np.array(testing_data["Fare"]).reshape(-1, 1)
# Scaler takes arrays
scaler = StandardScaler()
training_data["Age"] = scaler.fit_transform(ages_train)
training_data["Fare"] = scaler.fit_transform(fares_train)
testing_data["Age"] = scaler.fit_transform(ages_test)
testing_data["Fare"] = scaler.fit_transform(fares_test)
データの前処理が終わったので、特徴とラベルを選択し、train_test_split
関数で学習データ全体を学習セットとテストセットに分割します。
# Now to select our training/testing data
X_features = training_data.drop(labels=['PassengerId', 'Survived'], axis=1)
y_labels = training_data['Survived']
print(X_features.head(5))
# Make the train/test data from validation
X_train, X_val, y_train, y_val = train_test_split(X_features, y_labels, test_size=0.1, random_state=27)
これで、アンサンブル分類法の実装を開始する準備が整いました。
シンプルな平均化手法
先に説明した3つのアンサンブル手法に入る前に、アンサンブル手法の非常に簡単な方法である予測値の平均化について説明します。選択した分類器の異なる予測値を単純に足し合わせ、全体の値を得るために床分割を使用して分類器の総数で割ります。
このテストケースでは、ロジスティック回帰、決定木分類器、サポートベクター分類器を使用します。データに対して分類器をあてはめ、その予測値を変数として保存します。そして、予測値を単純に足し合わせて割り算します。
LogReg_clf = LogisticRegression()
DTree_clf = DecisionTreeClassifier()
SVC_clf = SVC()
LogReg_clf.fit(X_train, y_train)
DTree_clf.fit(X_train, y_train)
SVC_clf.fit(X_train, y_train)
LogReg_pred = LogReg_clf.predict(X_val)
DTree_pred = DTree_clf.predict(X_val)
SVC_pred = SVC_clf.predict(X_val)
averaged_preds = (LogReg_pred + DTree_pred + SVC_pred)//3
acc = accuracy_score(y_val, averaged_preds)
print(acc)
この方法で得られた精度は以下の通りです。
0.8444444444444444
VotingStackingの分類例
積み上げ分類器や投票分類器を作成する場合、Scikit-Learnはそのために使える便利な関数を提供してくれます。
VotingClassifierは、異なる推定値のリストと投票方法を引数として受け取ります。ハードな投票方法は予測されたラベルと多数決システムを利用し、ソフトな投票方法は予測された確率の合計のargmax/最大予測値に基づいてラベルを予測する。
目的の分類器を提供した後は、得られたアンサンブル分類器オブジェクトを適合させる必要があります。その後、予測値を取得し、精度メトリクスを使用することができます。
voting_clf = VotingClassifier(estimators=[('SVC', SVC_clf), ('DTree', DTree_clf), ('LogReg', LogReg_clf)], voting='hard')
voting_clf.fit(X_train, y_train)
preds = voting_clf.predict(X_val)
acc = accuracy_score(y_val, preds)
l_loss = log_loss(y_val, preds)
f1 = f1_score(y_val, preds)
print("Accuracy is: " + str(acc))
print("Log Loss is: " + str(l_loss))
print("F1 Score is: " + str(f1))
以下は、VotingClassifier
の性能に関するメトリクスの内容です。
Accuracy is: 0.8888888888888888
Log Loss is: 3.8376684749044165
F1 Score is: 0.8484848484848486
バギング分類の例
Scikit-LearnでBagging分類を実装する方法を紹介します。SklearnのBaggingClassifierは、選択した分類モデルと、使用したい推定量の数を受け取ります – ロジスティック回帰や決定木のようなモデルを使うことができます。
Sklearnはまた、決定木分類を修正した RandomForestClassifier
と ExtraTreesClassifier
にもアクセスできます。これらの分類器は K-folds クロスバリデーションツールと一緒に使用することもできます。
ここでは、いくつかの異なるバギング分類アプローチを比較し、K-foldクロスバリデーション・スコアの平均結果をプリントアウトします。
logreg_bagging_model = BaggingClassifier(base_estimator=LogReg_clf, n_estimators=50, random_state=12)
dtree_bagging_model = BaggingClassifier(base_estimator=DTree_clf, n_estimators=50, random_state=12)
random_forest = RandomForestClassifier(n_estimators=100, random_state=12)
extra_trees = ExtraTreesClassifier(n_estimators=100, random_state=12)
def bagging_ensemble(model):
k_folds = KFold(n_splits=20, random_state=12)
results = cross_val_score(model, X_train, y_train, cv=k_folds)
print(results.mean())
bagging_ensemble(logreg_bagging_model)
bagging_ensemble(dtree_bagging_model)
bagging_ensemble(random_forest)
bagging_ensemble(extra_trees)
以下は、分類器から得られた結果です。
0.7865853658536585
0.8102439024390244
0.8002439024390245
0.7902439024390244
ブースティング分類の例
最後に、ブースティング分類法の使い方を紹介します。前述の通り、Gradient Boostingについては別記事で紹介していますので、こちらをご覧ください。
Scikit-LearnにはAdaBoost分類器が組み込まれており、第一引数として指定した数の推定値を取り込みます。forループを使って値を変えて分類性能がどう変わるか試してみたり、K-Foldsクロスバリデーションというツールと組み合わせたりすることもできます。
k_folds = KFold(n_splits=20, random_state=12)
num_estimators = [20, 40, 60, 80, 100]
for i in num_estimators:
ada_boost = AdaBoostClassifier(n_estimators=i, random_state=12)
results = cross_val_score(ada_boost, X_train, y_train, cv=k_folds)
print("Results for {} estimators:".format(i))
print(results.mean())
以下は、得られた結果です。
Results for 20 estimators:
0.8015243902439024
Results for 40 estimators:
0.8052743902439025
Results for 60 estimators:
0.8053048780487805
Results for 80 estimators:
0.8040243902439024
Results for 100 estimators:
0.8027743902439024
まとめ
これまで、3種類のアンサンブル分類法(votingstacking、bagging、boosting)の背後にある考え方を説明しました。
Scikit-Learnでは、様々なアンサンブル分類器のインスタンスを簡単に作成することができます。これらのアンサンブルオブジェクトは、K-Foldsクロスバリデーションなどの他のScikit-Learnツールと組み合わせることができます。
アンサンブル分類器の適切な使い方やその背後にある理論についてもっと知りたい場合は、こちらかこちらのリンクをチェックすることをお勧めします。