はじめに
今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #21 ~機械学習入門編07~
次の記事:Python初心者の備忘録 #23 ~機械学習入門編09~
今回はアンサンブル学習(バギング、ランダムフォレスト、ブースティング、勾配ブースティング)、スタッキングについてまとめております。
本記事はDockerで環境構築を行っているので、環境構築がまだという方は下記記事を参考にして、環境構築をしてください。
Python初心者の備忘録 #06 ~DSに使われるライブラリ編01~
上記に加えて、下記コマンドを実行してpullしてきたimage名にdocker-compose.ymlを修正してください。
docker pull datascientistus/ds-python-env4
※また2025/05/17以前に環境構築された方は、Dockerfileの内容を更新しているので自身の環境の設定ファイルの更新もお願いいたします。
Dockerfile
FROM ubuntu:latest
# update
RUN apt-get -y update && apt-get install -y \
graphviz \
libsm6 \
libxext6 \
libxrender-dev \
libglib2.0-0 \
sudo \
wget \
vim
#install anaconda3
WORKDIR /opt
# download anaconda package and install anaconda
# archive -> https://repo.anaconda.com/archive/
RUN wget https://repo.anaconda.com/archive/Anaconda3-2022.10-Linux-x86_64.sh && \
sh /opt/Anaconda3-2022.10-Linux-x86_64.sh -b -p /opt/anaconda3 && \
rm -f Anaconda3-2022.10-Linux-x86_64.sh
# set path
ENV PATH /opt/anaconda3/bin:$PATH
# update pip and install pxackages
RUN pip install --upgrade pip && \
pip install --upgrade scikit-learn && \
pip install opencv-python && \
pip install nibabel && \
pip install --upgrade plotly && \
pip install chart_studio && \
pip install jupyter-dash && \
pip install --upgrade "ipywidgets>=7.6" && \
pip install lightgbm && \
pip install xgboost && \
pip install graphviz && \
pip install catboost && \
pip install category_encoders && \
pip install hyperopt && \
pip install hpsklearn
WORKDIR /
RUN mkdir /work
# execute jupyterlab as a default command
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"]
■学習に使用している資料
Udemy:【本番編】米国データサイエンティストがやさしく教える機械学習超入門【Pythonで実践】
■アンサンブル学習(emsemble)
▶アンサンブルとは
- 複数の機械学習モデルを組み合わせる手法で一般的には単一のモデルよりも精度が高い(※実務ではアンサンブルが基本)
下記図はアンサンブルの一種でスタッキングと呼ばれるもの
- 組み合わせのモデルはそれぞれの相関が低い方が良い
-> 多種多様なモデルを使用する - 「弱学習器」を使う
-> ランダムよりも少し精度のいい学習器(high bias low varianceのモデルであることが多い)
モデルを組み合わせることで精度が向上するイメージ
- ランダムに60%の確率で正解するモデル(Accuracy=0.6)を3つ用意する。
※この時それぞれのモデルに相関がない方が有効的 - それぞれに予測をしてもらい、多数決を取る形で正誤の判断を行うことで、1つが間違えたとしても他2つが正解していれば、正解することが可能。
▶バギング(bagging:bootstrap aggregationg)
- ブートストラップサンプリングをアンサンブルしたもの
ブートストラップサンプリング(bootstrap sampling)
- 母集団から重複を許してランダムに標本抽出(sampling)すること。
- これによりvarianceを下げ過学習を避けることができる。そのため高varianceである決定木を使うことが多い
Pythonでバギング
-
sklearn.ensemble.BaggingClassifier
クラスを使用する
(※回帰を行いたい場合は.BaggingRegresser
クラスを使用する)- インスタンス生成
引数-
estimator
:sklearnのモデルインスタンス(※デフォルトでは決定木) -
n_estimators
:モデルの数(※デフォルトでは10)
-
-
.fit(X, y)
で学習 -
.predict(X)
で予測
estimator
に.predict_proba()
が実装されていればsoft votion(確率の平均)、そうでなければhard voting(多数決)
- インスタンス生成
import seaborn as sns
import pandas as pd
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import BaggingClassifier, RandomForestClassifier
from sklearn.metrics import roc_auc_score
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt
# データ準備
df = sns.load_dataset('titanic')
df.dropna(inplace=True)
# X, yを作成
# X = df.loc[:, (df.columns!='survived') & (df.columns!='alive')]
# 後ほどXに対して更新をおこうなう際に,Viewに対して代入処理をするとSettingWithCopyWarningが発生するため,.dropを使用する
X = df.drop(['survived', 'alive'], axis=1)
y = df['survived']
# ラベルエンコーディング
oe = OrdinalEncoder()
# 出力結果をDataFrameにする.NumPyArrayだと後続の処理でエラーになることが多い
oe.set_output(transform='pandas') # sklearnが古い場合は!pip install --upgrade scikit-learnで更新
# デフォルトではうまくカテゴリカル変数のみをターゲットにしてくれないので,カテゴリカル変数に対してのみ処理をするようにする
cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
X[cat_cols] = oe.fit_transform(X[cat_cols])
# hold-out
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
# 学習と予測
clf = BaggingClassifier(random_state=0)
clf.fit(X_train, y_train)
y_pred = clf.predict_proba(X_test)
# 評価
print(f"bagging AUC: {roc_auc_score(y_test, y_pred[:, 1])}") # -> bagging AUC: 0.8846153846153846
# 単一の決定木の精度
single_tree = DecisionTreeClassifier(random_state=0).fit(X_train, y_train)
y_pred_tree = single_tree.predict_proba(X_test)
print(f"single tree AUC: {roc_auc_score(y_test, y_pred_tree[:, 1])}") # -> single tree AUC: 0.7596153846153846
# バギングよりもAUCが低いので、バギングにより正答率が上がっていることがわかる!
# 実際に使用した弱学習器一覧
clf.estimators_
"""出力結果
[DecisionTreeClassifier(random_state=2087557356),
DecisionTreeClassifier(random_state=132990059),
DecisionTreeClassifier(random_state=1109697837),
DecisionTreeClassifier(random_state=123230084),
DecisionTreeClassifier(random_state=633163265),
DecisionTreeClassifier(random_state=998640145),
DecisionTreeClassifier(random_state=1452413565),
DecisionTreeClassifier(random_state=2006313316),
DecisionTreeClassifier(random_state=45050103),
DecisionTreeClassifier(random_state=395371042)]
"""
▶ランダムフォレスト(random forest)
- バギングに一手間加えたアンサンブル学習で、簡単に言うと「バギング+決定木+特徴量のランダム選択」
-> 一部のデータと特徴量を使わないことでそれぞれ少し異なる決定木を作る
もし決定木について詳しく知りたい場合は、過去記事を参照してください
Python初心者の備忘録 #21 ~機械学習入門編07~
Pythonでランダムフォレスト
-
sklearn.ensemble.RandomForestClassifier
クラスを使用する
(※回帰を行いたい場合は.BaggingRegresser
クラスを使用する)- インスタンス生成
引数-
n_estimators
:決定木の数(※デフォルトでは100) -
max_features
:使用する特徴量数(※デフォルト$\sqrt{n}$) - 他にも
DecisionTreeClassifier
クラスに使うmax_depth
やmin_samples_split
が使用可能
-
-
.fit(X, y)
で学習 -
.predict(X)
で予測
- インスタンス生成
# Baggingでのデータや変数をそのまま使用しています
# ランダムフォレスト
rf = RandomForestClassifier(n_estimators=100, max_depth=1, random_state=0).fit(X_train, y_train)
y_pred_rf = rf.predict_proba(X_test)
print(f"random forest AUC: {roc_auc_score(y_test, y_pred_rf[:, 1])}") # -> random forest AUC: 0.9447115384615384
# 特徴量の重要度
print(rf.feature_importances_) # -> [0.01 0.17 0.12 0.06 0.01 0.06 0.01 0.01 0.22 0.24 0.06 0. 0.03]
# 描画
plt.barh(X.columns, rf.feature_importances_)
# 弱学習器の木構造を描画
plot_tree(rf.estimators_[1])
▶ブースティング(Boosting)
- バギングやランダムフォレストとは違い、1つずつ順番に弱学習器を作る手法(並列ではなく直列)
- うまく識別できないデータに重みをつけて再学習することで、改善を目指す
(※予測時にはモデル毎に重みをつける場合がある) - うまくいけばbiasとvarianceの両方を下げることができる
-> バギングよりも精度が高くなることが期待できる
▶AdaBoost(Adaptive Boosting)
- ブースティングの中でも最も有名なアルゴリズムの一つ
- うまく識別できなかったデータに重みをつけて次の学習で重点的に学習できるようにする
PythonでAdaBoost
-
sklearn.ensemble.AdaBoostClassifier
クラスを使用する
(※回帰を行いたい場合は.BaggingRegresser
クラスを使用する)- インスタンス生成
引数-
estimator
:使用する弱学習器のモデルクラス
通常はDecisionTreeClassifier
を使用(デフォルトはmax_depth=1
) -
n_estimators
:ブースティングの最大イテレーション数 -
learning_rate
:学習率(高いと過学習、低すぎると学習が終わらない)
-
-
.fit(X, y)
で学習 -
.predict(X)
で予測
- インスタンス生成
import seaborn as sns
import pandas as pd
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import AdaBoostClassifier, GradientBoostingRegressor, GradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.tree import DecisionTreeRegressor, plot_tree
import matplotlib.pyplot as plt
# データ準備
df = sns.load_dataset('titanic')
df.dropna(inplace=True)
# X, yを作成
X = df.drop(['survived', 'alive'], axis=1)
y = df['survived']
# ラベルエンコーディング
oe = OrdinalEncoder()
oe.set_output(transform='pandas')
cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
X[cat_cols] = oe.fit_transform(X[cat_cols])
# hold-out
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
# AdaBoost
ada = AdaBoostClassifier(n_estimators=100, learning_rate=0.01, random_state=0).fit(X_train, y_train)
y_pred_ada = ada.predict_proba(X_test)
print(f"adaboost AUC: {roc_auc_score(y_test, y_pred_ada[:, 1])}") # -> adaboost AUC: 0.9375
★細かい設定値が異なるので単純な比較はできないが、バギングよりも正答率が高くなっている。
# 特徴量重要度
print(ada.feature_importances_) # -> [0. 0. 0.02 0. 0. 0.07 0. 0. 0.29 0.62 0. 0. 0. ]
# 描画
plt.barh(X.columns, ada.feature_importances_)
■勾配ブースティング(gradient boosting)
▶勾配ブースティングとは
- 勾配情報を使用して徐々に残差を減らしていく手法で、通常は弱学習器に決定木を使う(GBDT:Gradient Boosted Decision Tree)
- 勾配ブースティング木は最もよく使われる機械学習アルゴリズムの一つでニューラルネットワークと双璧を成す高い精度を誇る
▶勾配ブースティング(回帰)
前提
$F_b$:b回目の全体モデル(実際は$F_b(x)$だが、$(x)$は省略)
$f_b$:b回目に作成した決定木
$r_b$:b回目の残差
$\eta$:shrinkage係数(学習率)
- 各決定木は損失の勾配を学習しており、それが残差で表されている
(※勾配降下法に似ている)
Pythonで勾配ブースティング
※スクラッチで勾配ブースティングを実装した場合
- クラス作成
- コンストラクタ(__init__)作成
- 引数:学習率、イテレーション数、木の深さ、ランダムシード
- 学習メソッド(.fit)の作成
- 引数:X, y
- 初期モデル$F_0$
- 各決定木($f_b(x)$)を学習
- 予測メソッド(.predict)作成
- 学習済みの決定木$f_{1~b}(x)$および$F_0$から最終的な予測値$F_b$を計算
class MyGradientBoostingRegressor:
# 引数はsklearnに合わせる
def __init__(self, learning_rate=0.03, n_estimators=100, max_depth=1, random_state=0):
self.learning_rate = learning_rate
self.n_estimators = n_estimators
self.max_depth = max_depth
self.random_state = random_state
self.estimators = [] # 弱学習器を格納する
def fit(self, X, y):
# 平均が最も損失を小さくする
self.F0 = y.mean()
Fb = self.F0
for _ in range(self.n_estimators):
# 残差を計算
r = y - Fb
# 弱学習器学習
estimator = DecisionTreeRegressor(max_depth=self.max_depth, random_state=self.random_state)
estimator.fit(X, r) # 正解データは残差rであることに注意
weight = estimator.predict(X)
# shrinkage(学習率)をかけて足し合わせていく
# この時weightはNumPyArrayなので,Fbもイテレーション後はNumPyArrayになることに注意
Fb += self.learning_rate * weight
self.estimators.append(estimator)
def predict(self, X):
Fb = self.F0
for estimator in self.estimators:
Fb += self.learning_rate * estimator.predict(X)
return Fb
# データ準備
df = sns.load_dataset('mpg')
df.dropna(inplace=True)
X = df['horsepower'].values.reshape(-1, 1)
y = df['mpg'].values
# データの散布図描画
sns.scatterplot(x=df['horsepower'], y=df['mpg'])
my_gbr = MyGradientBoostingRegressor()
my_gbr.fit(X, y)
my_gbr.predict(X)[:10]
"""出力結果
array([17.95434839, 15.74535562, 15.74535562, 15.74535562, 16.96376992,
15.74535562, 15.74535562, 15.74535562, 15.74535562, 15.74535562])
"""
-
sklearn.ensemble.GradientBoosthingRegressor
クラスを使用する- インスタンス生成
引数-
n_estimators
:モデルの数 -
learning_rate
:学習率(高いと過学習、低すぎると学習が終わらない)
-
-
.fit(X, y)
で学習 -
.predict(X)
で予測
- インスタンス生成
# データ準備
df = sns.load_dataset('mpg')
df.dropna(inplace=True)
X = df['horsepower'].values.reshape(-1, 1)
y = df['mpg'].values
# データの散布図描画
sns.scatterplot(x=df['horsepower'], y=df['mpg'])
# 勾配ブースティング
from sklearn.ensemble impoert GradientBoosthingRegressor
lr = 0.03
gbr = GradientBoostingRegressor(max_depth=1, learning_rate=lr, random_state=0).fit(X, y)
gbr.predict(X)[:10]
"""出力結果
array([17.95434839, 15.74535562, 15.74535562, 15.74535562, 16.96376992,
15.74535562, 15.74535562, 15.74535562, 15.74535562, 15.74535562])
"""
from sklearn.tree import plot_tree
# 弱学習器の木構描画
model = gbr.estimators_[0][0]
_ = plot_tree(model)
# 予測結果が弱学習器の組み合わせになっていることを確認する
results = []
# 一つのデータを選択し,そのデータを使って弱学習器の予測値を得て,最終的な予測値を計算し,.predictの結果と一致するか確認する
idx = 0
for t in gbr.estimators_:
result = t[0].predict([X[idx]])
results.append(result*lr)
F0 = np.mean(y)
Fb = F0 + np.cumsum(results)[-1] # -> 17.95434839
# 0~bまで足し合わせた最終的な結果が上記のgbr.predict(X)[0]の出力結果と一致している!
# 全てを出力すると、徐々に最終結果に近づいていることがわかる
Fb
"""出力結果
array([23.27816327, 23.11544082, 22.94214051, 22.83197263, 22.66717637,
22.56240126, 22.39804993, 22.29831511, 22.16208788, 22.06741995,
21.91791807, 21.82781582, 21.68550205, 21.48669171, 21.40205747,
21.21175047, 21.13072954, 20.93845051, 20.8608364 , 20.73833629,
20.55782906, 20.50937386, 20.40900234, 20.36266681, 20.19342967,
20.12570603, 19.9635777 , 19.92025031, 19.77227178, 19.72881191,
19.57859041, 19.53845808, 19.44223474, 19.38388218, 19.24400857,
19.20663613, 19.07207991, 19.03612626, 18.96133881, 18.8341415 ,
18.80004406, 18.8386471 , 18.80441851, 18.75583214, 18.79101068,
18.76000512, 18.65335356, 18.58370409, 18.55427539, 18.58646165,
18.55812939, 18.58911825, 18.54763487, 18.57735466, 18.55081739,
18.48743756, 18.46201512, 18.48989963, 18.43780306, 18.41367401,
18.43872786, 18.41448352, 18.43860796, 18.4045305 , 18.32739886,
18.30562399, 18.32804948, 18.30707665, 18.25674972, 18.27797958,
18.25802989, 18.21663751, 18.23678063, 18.21783775, 18.18985027,
18.21012963, 18.1922554 , 18.2107737 , 18.19281859, 18.13342068,
18.10827392, 18.12548502, 18.10935421, 18.07082487, 18.0860757 ,
18.07072377, 18.08644758, 18.07440253, 18.04354516, 18.0575529 ,
18.04606597, 18.0322826 , 18.04570816, 18.02544619, 17.97945888,
17.96889857, 17.98142911, 17.97124056, 17.94244361, 17.95434839])
"""
# 描画
plt.plot(Fb)
▶勾配ブースティング(分類)
- 最終的な予測値(確率)$p$はシグモイド関数を使用して確率の形にする
$p=\frac{1}{1+e^{-Fb}}$
Pytonで勾配ブースティング分類
※スクラッチで勾配ブースティング分類を実装した場合
- クラス作成
- コンストラクタ(__init__)作成
- 引数:学習率、イテレーション数、木の深さ、ランダムシード
- 学習メソッド(.fit)の作成
- 引数:X, y
- 初期モデル$F_0$
- 各決定木($f_b(x)$)を学習
- 予測メソッド(.predict_proba)作成
- 学習済みの決定木$f_{1~b}(x)$および$F_0$から最終的な予測値$p$を計算
class MyGradientBoostingClassifier:
def __init__(self, learning_rate=0.03, n_estimators=100, max_depth=1, random_state=0):
self.learning_rate = learning_rate
self.n_estimators = n_estimators
self.max_depth = max_depth
self.random_state = random_state
self.estimators = []
def fit(self, X, y):
self.F0 = np.log(y.mean()/(1-y.mean()))
# 特定の葉に対して処理をする必要があるためNumPyArrayにする必要がある
F0 = np.full(len(y), self.F0)
Fb = F0
for _ in range(self.n_estimators):
# シグモイド関数を使って確率の形にする
p = 1 / (1 + np.exp(-Fb))
# 残差計算
r = y - p
# 弱学習器学習
estimator = DecisionTreeRegressor(max_depth=self.max_depth, random_state=self.random_state)
estimator.fit(X, r)
# それぞれのデータXがどの葉にいくかを取得
X_leafs = estimator.apply(X)
# 葉のIDのリスト取得
leaf_ids = np.unique(X_leafs)
# それぞれの葉に処理
for leaf_id in leaf_ids:
# 弱学習器の出力の値を計算
fltr = X_leafs == leaf_id
num = r[fltr].sum() # 分子
den = (p[fltr]*(1-p[fltr])).sum() # 分母
estimator_pred_proba = num / den
# 弱学習器の出力を上書き
estimator.tree_.value[leaf_id, 0, 0] = estimator_pred_proba
# 当該データのFbを更新
Fb[fltr] += self.learning_rate * estimator_pred_proba
self.estimators.append(estimator)
def predict_proba(self, X):
Fb = np.full(X.shape[0], self.F0)
for estimator in self.estimators:
Fb += self.learning_rate * estimator.predict(X)
# シグモイド関数を返す
return 1 / (1 + np.exp(-Fb))
# データは回帰の時のものを使用
my_gbc = MyGradientBoostingClassifier()
my_gbc.fit(X_train, y_train) # titanicデータセット
my_gbc.predict_proba(X_test)
"""出力結果
array([0.5514342 , 0.4329777 , 0.80125347, 0.80125347, 0.5514342 ,
0.25797762, 0.5514342 , 0.83715418, 0.88099514, 0.88099514,
0.5514342 , 0.82137752, 0.82137752, 0.88099514, 0.5514342 ,
0.5514342 , 0.5514342 , 0.82137752, 0.88099514, 0.40100637,
0.88099514, 0.78760146, 0.88099514, 0.88099514, 0.80125347,
0.5514342 , 0.40100637, 0.80125347, 0.4329777 , 0.4329777 ,
0.4329777 , 0.88099514, 0.88099514, 0.88099514, 0.82137752,
0.88099514, 0.82137752, 0.88099514, 0.40100637, 0.5514342 ,
0.4329777 , 0.82137752, 0.5514342 , 0.78760146, 0.40100637,
0.88099514, 0.88099514, 0.5514342 , 0.5514342 , 0.5514342 ,
0.5514342 , 0.82137752, 0.5514342 , 0.88099514, 0.88099514])
"""
-
sklearn.ensemble.GradientBoosthingClassifier
クラスを使用する- インスタンス生成
引数-
n_estimators
:モデルの数 -
learning_rate
:学習率(高いと過学習、低すぎると学習が終わらない)
-
-
.fit(X, y)
で学習 -
.predict_proba(X)
で予測
- インスタンス生成
from sklearn.ensemble import GradientBoosthingClassifier
# 勾配ブースティング分類
# データは回帰の時のものを使用
gbc = GradientBoostingClassifier(n_estimators=100, learning_rate=0.03, max_depth=1, random_state=0).fit(X_train, y_train)
gbc.predict_proba(X_test)[:, 1]
"""出力結果
array([0.5514342 , 0.4329777 , 0.80125347, 0.80125347, 0.5514342 ,
0.25797762, 0.5514342 , 0.83715418, 0.88099514, 0.88099514,
0.5514342 , 0.82137752, 0.82137752, 0.88099514, 0.5514342 ,
0.5514342 , 0.5514342 , 0.82137752, 0.88099514, 0.40100637,
0.88099514, 0.78760146, 0.88099514, 0.88099514, 0.80125347,
0.5514342 , 0.40100637, 0.80125347, 0.4329777 , 0.4329777 ,
0.4329777 , 0.88099514, 0.88099514, 0.88099514, 0.82137752,
0.88099514, 0.82137752, 0.88099514, 0.40100637, 0.5514342 ,
0.4329777 , 0.82137752, 0.5514342 , 0.78760146, 0.40100637,
0.88099514, 0.88099514, 0.5514342 , 0.5514342 , 0.5514342 ,
0.5514342 , 0.82137752, 0.5514342 , 0.88099514, 0.88099514])
"""
# 評価
print(f"grad boost AUC: {roc_auc_score(y_test, gbc.predict_proba(X_test)[:, 1])}") # -> grad boost AUC: 0.9407051282051282
# 弱学習器の予測値からモデル全体の予測値が算出されることを確認する
results = []
for t in gbc.estimators_:
result = 0.03 * t[0].predict([X_test.iloc[0]]) # idx=0のデータで確認
results.append(result)
# 描画
plt.plot(results)
# 弱学習器を全て足し合わせる
F0 = np.log(y_train.mean()/(1-y_train.mean()))
Fb = F0 + np.cumsum(results)[-1]
# 確率pに変換
predict = 1 / (1 + np.exp(-Fb))
print(predict) # -> 0.551434197251246
# 0~bまで足し合わせた最終的な結果が上記のgbc.predict_proba(X_test)[:, 1][0]の出力結果と一致している!
# 弱学習器の数に対しての予測値の推移を描画
plt.plot(np.cumsum(results)+F0)
■有名な勾配ブースティング決定木アルゴリズム
▶XGBoost(eXtreme Gradient Boosting)
- 非常に精度が高いアルゴリズムで、GBDTに正則化項を加えることで過学習を防ぐ
- 木が大きくなりすぎないようにする
- 1つのモデルで残差を大きく減らさないようにする
★ $\frac{1}{2}\lambda||w||^2$はL2ノルム、$\alpha||w||_1$はL1ノルムを表している。
※$\gamma、 \lambda、 \alpha$はハイパーパラメーターなので、ここでの説明ではあまり重要ではない
過学習を防ぐ仕組み
- 正則化項(一番大事)
- shrinkage(0<$\eta$<1)
- Clolumn Subsampling
-> ランダムフォレスト同様、決定木の分割に使用する特徴量をランダムに選択する
PythonでXGBoost
事前にxgboostのパッケージとgraphvizをインストールしておく
apt-get install graphviz
pip install xgboost
pip install graphviz
-
xgboost.XGBClassifier
クラスを使用(回帰の場合はXGBRegressor
)
引数-
n_estimators
:イテレーションの回数(弱学習器の数) -
learning_rate
:shrinlageのパラメータ$\eta$ -
eval_metric
:early stoppingに使用する評価指標(sklern.metrics
のメソッドを指定) -
early_stopping_rounds
:early stoppingする際の最低限のイテレーション回数 -
impotance_type
:total_gain
を指定する(デフォルトではweight
(頻度))
-
-
.fit(X, y)
で学習
引数-
eval_set
:early stoppingを設定している場合に指定する。各イテレーション毎に評価したいデータ(例:[(X_val, y_val)]) -
verbose
:Trueを指定すると各イテレーションの評価結果をprintする
-
-
.predict(X)
で予測 -
xgboost.plot_tree(model, num_trees)
で任意の木構造を描画
import seaborn as sns
import pandas as pd
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
# !pip install xgboost
import xgboost
from xgboost import XGBClassifier
# !pip install lightgbm
import lightgbm as lgb
# !pip install catboost
from catboost import CatBoostClassifier, Pool
# データ準備
df = sns.load_dataset('titanic')
df.dropna(inplace=True)
# X, yを作成
X = df.drop(['survived', 'alive'], axis=1)
y = df['survived']
# ラベルエンコーディング
oe = OrdinalEncoder()
oe.set_output(transform='pandas')
cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
X[cat_cols] = oe.fit_transform(X[cat_cols])
# hold-out
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
xgb = XGBClassifier(learning_rate=0.01,
eval_metric='auc',
early_stopping_rounds=10,
importance_type='total_gain',
random_state=0)
# 学習
xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=True)
# 予測と評価
y_pred_xgb = xgb.predict_proba(X_test)
print(f"xgboost AUC: {roc_auc_score(y_test, y_pred_xgb[:, 1])}") # -> xgboost AUC: 0.8990384615384616
# 特徴量の重要度
xgb.feature_importances_
"""出力結果
array([0. , 0. , 0.10952041, 0. , 0. ,
0.22573484, 0. , 0. , 0. , 0.59854376,
0.05928717, 0. , 0.00691384], dtype=float32)
"""
# 弱学習器の木構造を描画
xgboost.plot_tree(xgb, num_trees=0)
▶Light GBM(Light Gradient Boosted Machine)
- XGBoost同様に勾配ブースティング決定木のアルゴリズムで、XGBoostよりも高速で精度が高くなるケースが多い
- leaf wiseで決定木を作ることで高速化
- histgram basedで決定木のノードを分割することで高速化
- Gradient-based One-Side Sampling(GOSS) で学習データを減らし高速化
- Exclusive Feature Bundling(EFB) で特徴小数を減らし高速化
level wise vs leaf wise
-
level wise(depth first)
左のノードから順に分割し、その階層の分割が全て終了したら次の階層に行くようにして決定木を構築していくやり方で、一般的な方法
-
leaf wise(best first)
最も損失が小さくなるようなノードから分割をしていくやり方で、とても速い
histgram based algorithm
- データをヒストグラム化し、bin単位で分割することで高速化する
計算量が減るので、その分早くなる
Gradient-based One-Side Sampling(GOSS)
‐ 残差が小さいデータ(うまく学習できているデータ)の一部を学習に使わないことで高速化する
Exclusive Feature Bundling(EFB)
- 互いに排他的な複数の特徴量を1つにまとめる
※排他的:同時に0ではない値を取らない -> [0, 1, 0, 0] ($\therefore$one-hot-vectorは互いに排他的)- 特徴量が多い場合は欠損地が多いスカスカ(疎:sparse)なデータであることが多い
-> bundleにしてまとめることで高速化 - 衝突が少ない特徴量同士をbundle化する
- それぞれの特徴量の値の範囲が被らないように値をずらす
- 特徴量が多い場合は欠損地が多いスカスカ(疎:sparse)なデータであることが多い
下図だと、aは1~6の値を取っているので、bのすべてに6を足してずらすことで、それぞれの特徴量の値が被らない形でbundleできる。
※一部排他的でない特徴量もあるが、閾値を決めてある程度の数は許容する(今回は単純にbで上書きしている)
PythonでLightGBM
必要に応じてパッケージをインストールする。
pip install lightgbm
-
lightgbm.LGBMClassifier
クラスを使用する
引数-
n_estimators
:イテレーションの回数(弱学習器の数) -
learning_rate
:shrinlageのパラメータ$\eta$ -
boosting_type
:ブースティングアルゴリズムを指定
'gbdt', 'dart', 'goss', 'rf'
から選択する -
max_depht
:決定木の最大の深さ
-
-
.fit(X, y)
で学習
引数-
eval_set
:各イテレーション毎に評価したいデータ(例:[(X_val, y_val)]) -
callbacks
:各イテレーション時に実行する関数のリスト(early stoppingに使用)
-
-
.predict(X)
で予測 -
lgb.plot_metric(model)
で学習曲線を描画 -
lgb.plot_tree(model, tree_index)
で任意の木構造を描画
import lightgbm as lgb
# LightGBMインスタンス生成
lgbmc = lgb.LGBMClassifier(boosting_type='goss', max_depth=5, random_state=0)
# early stoppingに使用する検証データ
eval_set = [(X_test, y_test)]
# イテレーション時に実施(callback)する関数
callbacks = []
callbacks.append(lgb.early_stopping(stopping_rounds=10))
callbacks.append(lgb.log_evaluation())
# 学習
lgbmc.fit(X_train, y_train, eval_set=eval_set, callbacks=callbacks)
# 予測と評価
y_pred_lgbmc = lgbmc.predict_proba(X_test)
print(f"light gbm AUC: {roc_auc_score(y_test, y_pred_lgbmc[:, 1])}") # -> light gbm AUC: 0.9383012820512822
# 学習曲線
lgb.plot_metric(lgbmc)
# 弱学習器の木構造を描画
lgb.plot_tree(lgbmc, tree_index=0)
▶CatBoost(Category Boosting)
- XGBoostやLightGBMより後発で、使い勝手がよく高精度が期待できる
- 工夫している点
- カテゴリかる変数の扱い -> 前処理が不要
- 決定木の葉の値の計算 -> 過学習を防ぐ、推論の高速化
カテゴリカル変数のencodeing
- one-hot eoncoding
-> 計算量が増加し、決定木が複雑になるので、決定木では悪手
- target encoding(TS:target statistics)
各カテゴリ毎のtarget(目的変数)の統計量(TS)で代替(下図では単純に平均を取っている)
-> 過学習しやすかったり、本来正解ラベルとなるtargetを使用するのでtarget leakageが起こる
★CatBoostでは次の方法を取っている
Ordered TS(Ordered Target Statistics)
- 加工のデータからTSを算出する
- 実際には寺家列ではないのでランダムに順序を決める
- 過学習を防ぐことが可能
- one-hot encodingに比べ、データが圧縮される
- 一度のOrdered TSだけではhigh varianceになるので、サンプル数を変えて複数回行う
-> Random Permutation
★他にもOrdered BoostingやSymmetric Treesを使用している
Ordered Boosting
-
従来の勾配ブースティングは勾配の推定で過学習気味になる
※勾配の推定で、そのデータを使って学習した学習器を使うので実際の分布とずれる - そのため、残差の計算に使うモデルを、推論に使うモデル群と分ける
-> 一つ前までのデータを使って学習することで過学習を避ける
Symmetric Trees
- 同じ階層ではすべてのノードで同じ特徴量の同じ条件で分割をする
- 全てのデータに対して共通の処理を行えるので効率が良い
- 特に推論の高速化が期待できる(並列化との相性◎)
上記だと弱い学習器になってしまうが、アンサンブル学習では弱学習器でいいので問題ない
PythonでCatBoost
必要に応じてパッケージをインストールする
pip install catboost
-
catboost.CatBoostClassifier
クラスを使用する
引数-
iterations
:イテレーションの回数(弱学習器の数) -
learning_rate
:学習率(shrinlageのパラメータ$\eta$) -
cat_features
:カテゴリカル変数のカラム名(or index)のリスト
-
-
.fit(X, y)
で学習
引数-
eval_set
:各イテレーション毎に評価したいデータ(例:[(X_val, y_val)]) -
early_stopping_rounds
:early stoppingする際の最低限のイテレーション数
-
-
.predict(X)
で予測 -
.plot_metric(tree_idx)
で任意の木構造を描画
from catboost import CatBoostClassifier Pool
# データ準備 (ラベルエンコーディングをしないバージョン)
df = sns.load_dataset('titanic')
df.dropna(inplace=True)
# X, yを作成
X = df.loc[:, (df.columns!='survived') & (df.columns!='alive')]
y = df['survived']
# カテゴリカル変数のカラム名のリスト
cat_cols = X.select_dtypes(exclude=np.number).columns.to_list()
# CatBoostはカテゴリカル編数の事前エンコーディングは不要
# oe = OrdinalEncoder()
# oe.set_output(transform='pandas')
# X = oe.fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
cbc = CatBoostClassifier(iterations=1000, learning_rate=0.01, cat_features=cat_cols)
eval_set = [(X_test, y_test)]
# 学習
cbc.fit(X_train, y_train, eval_set=eval_set, early_stopping_rounds=10, verbose=True)
# 予測と評価
y_pred_cbc = cbc.predict_proba(X_test)
print(f"catboost AUC: {roc_auc_score(y_test, y_pred_cbc[:, 1])}") # -> catboost AUC: 0.8942307692307693
# 弱学習器の描画
# symmetric treeになっていることが確認できる
pool = Pool(X_train, y_train, cat_features=cat_cols)
cbc.plot_tree(tree_idx=1, pool=pool)
■スタッキング(stacking)
▶スタッキングとは
- 複数段階に分けて学習と予測をする手法で、それぞれのモデルにアンサンブルのアルゴリズムを使うことも多い
- それぞれのデータの予測に使用するモデルは、そのデータを使って学習してはいけない
つまり、データ[A,B,C,D,E]があるとして、Aを予測する場合はそれ以外の[B,C,D,E]を使用して作成したモデルAを使用して予測する - CVで学習をし、それぞれのfoldの予測をそれ以外のfoldで学習したモデルを使って行う
- モデルAで全てのデータ(全てのfold)の予測値を作成したら、モデルB, モデルC...と繰り返していく
- テストデータの各モデルによる予測には2種類の方法がある
- 全データで再度学習して予測する
- 各foldのモデルの予測値を平均する
- 2層目のモデルでは、1層目の予測値を特徴量にしてモデルを学習する
(※学習時に元の特徴量を入れることも可能)
Pythonでスタッキング
sklearnにスタッキング用のsklearn.emsemble.StakingClissifier
クラスがあるが、こちらはCV(クロスバリデーション)に対応しておらず、若干過学習気味になってしまう。
そのため、以下でCVに対応したスタッキングのコード例を示す。
import seaborn as sns
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold
from sklearn.metrics import roc_auc_score
# データ準備
df = sns.load_dataset('titanic')
df.dropna(inplace=True)
# X, yを作成
X = df.loc[:, (df.columns!='survived') & (df.columns!='alive')]
y = df['survived']
# ラベルエンコーディング
oe = OrdinalEncoder()
oe.set_output(transform='pandas')
X = oe.fit_transform(X)
# hold-out
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
# CV対応スタッキングクラス(2値分類のみ対応)
class StackingClassifierCV():
def __init__(self, estimators, final_estimator, cv):
self.estimators = estimators # [('rf', RandomForestClassifier()), ('knn', KNeighborsCalssifier()), (,), ..]
self.final_estimator = final_estimator
self.cv = cv
def fit(self, X, y):
pred_features = {}
# 1層目のモデル学習
for model_name, model in self.estimators:
preds = []
new_y = []
for train_idx, val_idx in self.cv.split(X):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
model.fit(X_train, y_train)
pred = model.predict_proba(X_val)[:, 1].tolist()
preds += pred
# cv.splitによりXの順番が変わっているので,それに合わせて新しくyを作成する
new_y += y_val.tolist()
model.fit(X, y)
pred_features[model_name] = preds
# 2層目のモデル学習
new_X = pd.DataFrame(pred_features)
self.final_estimator.fit(new_X, new_y)
def predict_proba(self, X):
# 1層目のモデルで特徴量(予測値)生成
pred_features = {}
for model_name, model in self.estimators:
pred = model.predict_proba(X)[:, 1]
pred_features[model_name] = pred
new_X = pd.DataFrame(pred_features)
final_pred = self.final_estimator.predict_proba(new_X)
return final_pred
# 一層目のモデル
estimators=[('rf', RandomForestClassifier()), ('knn', KNeighborsClassifier()), ('logistic', LogisticRegression())]
# 二層目のモデル
final_estimator = LogisticRegression()
cv = KFold(n_splits=5, shuffle=True, random_state=0)
stacking_cv = StackingClassifierCV(estimators=estimators,
final_estimator=final_estimator,
cv=cv)
stacking_cv.fit(X_train, y_train)
y_pred_stacking_cv = stacking_cv.predict_proba(X_test)
# 評価
print(f"stackingCV AUC: {roc_auc_score(y_test, y_pred_stacking_cv[:, 1])}") # -> stackingCV AUC: 0.8397435897435899