0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python初心者の備忘録 #22 ~機械学習入門編08~

Last updated at Posted at 2025-06-11

はじめに

今回私は最近はやりの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)

▶アンサンブルとは

  • 複数の機械学習モデルを組み合わせる手法で一般的には単一のモデルよりも精度が高い(※実務ではアンサンブルが基本)
    下記図はアンサンブルの一種でスタッキングと呼ばれるもの

image.png

  • 組み合わせのモデルはそれぞれの相関が低い方が良い
    -> 多種多様なモデルを使用する
  • 「弱学習器」を使う
    -> ランダムよりも少し精度のいい学習器(high bias low varianceのモデルであることが多い)

モデルを組み合わせることで精度が向上するイメージ

  1. ランダムに60%の確率で正解するモデル(Accuracy=0.6)を3つ用意する。
    ※この時それぞれのモデルに相関がない方が有効的
  2. それぞれに予測をしてもらい、多数決を取る形で正誤の判断を行うことで、1つが間違えたとしても他2つが正解していれば、正解することが可能。

image.png

▶バギング(bagging:bootstrap aggregationg)

  • ブートストラップサンプリングをアンサンブルしたもの

ブートストラップサンプリング(bootstrap sampling)

  • 母集団から重複を許してランダムに標本抽出(sampling)すること。
  • これによりvarianceを下げ過学習を避けることができる。そのため高varianceである決定木を使うことが多い

★バギングの様子
image.png

Pythonでバギング

  • sklearn.ensemble.BaggingClassifierクラスを使用する
    (※回帰を行いたい場合は.BaggingRegresserクラスを使用する)
    1. インスタンス生成
      引数
      • estimator:sklearnのモデルインスタンス(※デフォルトでは決定木)
      • n_estimators:モデルの数(※デフォルトでは10)
    2. .fit(X, y)で学習
    3. .predict(X)で予測
      estimator.predict_proba()が実装されていればsoft votion(確率の平均)、そうでなければhard voting(多数決)
Bagging
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)

  • バギングに一手間加えたアンサンブル学習で、簡単に言うと「バギング+決定木+特徴量のランダム選択」
    -> 一部のデータと特徴量を使わないことでそれぞれ少し異なる決定木を作る

image.png

もし決定木について詳しく知りたい場合は、過去記事を参照してください
Python初心者の備忘録 #21 ~機械学習入門編07~

Pythonでランダムフォレスト

  • sklearn.ensemble.RandomForestClassifierクラスを使用する
    (※回帰を行いたい場合は.BaggingRegresserクラスを使用する)
    1. インスタンス生成
      引数
      • n_estimators:決定木の数(※デフォルトでは100)
      • max_features:使用する特徴量数(※デフォルト$\sqrt{n}$)
      • 他にもDecisionTreeClassifierクラスに使うmax_depthmin_samples_splitが使用可能
    2. .fit(X, y)で学習
    3. .predict(X)で予測
Random forest
# 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
追加情報1
# 特徴量の重要度
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_)  

image.png

追加情報2
# 弱学習器の木構造を描画
plot_tree(rf.estimators_[1])

image.png

▶ブースティング(Boosting)

  • バギングやランダムフォレストとは違い、1つずつ順番に弱学習器を作る手法(並列ではなく直列)
  • うまく識別できないデータに重みをつけて再学習することで、改善を目指す
    (※予測時にはモデル毎に重みをつける場合がある)
  • うまくいけばbiasとvarianceの両方を下げることができる
    -> バギングよりも精度が高くなることが期待できる

image.png

▶AdaBoost(Adaptive Boosting)

  • ブースティングの中でも最も有名なアルゴリズムの一つ
  • うまく識別できなかったデータに重みをつけて次の学習で重点的に学習できるようにする

参考
大まかな操作は下記のようになっている
タイトルなし.png

PythonでAdaBoost

  • sklearn.ensemble.AdaBoostClassifierクラスを使用する
    (※回帰を行いたい場合は.BaggingRegresserクラスを使用する)
    1. インスタンス生成
      引数
      • estimator:使用する弱学習器のモデルクラス
        通常はDecisionTreeClassifierを使用(デフォルトはmax_depth=1)
      • n_estimators:ブースティングの最大イテレーション数
      • learning_rate:学習率(高いと過学習、低すぎると学習が終わらない)
    2. .fit(X, y)で学習
    3. .predict(X)で予測
AdaBoost
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_)

image.png

■勾配ブースティング(gradient boosting)

▶勾配ブースティングとは

  • 勾配情報を使用して徐々に残差を減らしていく手法で、通常は弱学習器に決定木を使う(GBDT:Gradient Boosted Decision Tree)
  • 勾配ブースティング木は最もよく使われる機械学習アルゴリズムの一つでニューラルネットワークと双璧を成す高い精度を誇る

image.png

▶勾配ブースティング(回帰)

前提
$F_b$:b回目の全体モデル(実際は$F_b(x)$だが、$(x)$は省略)
$f_b$:b回目に作成した決定木
$r_b$:b回目の残差
$\eta$:shrinkage係数(学習率)

操作イメージ
image.png

  • 各決定木は損失の勾配を学習しており、それが残差で表されている
    (※勾配降下法に似ている)

Pythonで勾配ブースティング

※スクラッチで勾配ブースティングを実装した場合
  1. クラス作成
  2. コンストラクタ(__init__)作成
    • 引数:学習率、イテレーション数、木の深さ、ランダムシード
  3. 学習メソッド(.fit)の作成
    • 引数:X, y
    • 初期モデル$F_0$
    • 各決定木($f_b(x)$)を学習
  4. 予測メソッド(.predict)作成
    • 学習済みの決定木$f_{1~b}(x)$および$F_0$から最終的な予測値$F_b$を計算
スクラッチでgradient boosting
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'])

image.png

動作チェック
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クラスを使用する
    1. インスタンス生成
      引数
      • n_estimators:モデルの数
      • learning_rate:学習率(高いと過学習、低すぎると学習が終わらない)
    2. .fit(X, y)で学習
    3. .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'])

image.png

gradient boosting
# 勾配ブースティング
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)

image.png

追加情報②
# 予測結果が弱学習器の組み合わせになっていることを確認する
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)

image.png

▶勾配ブースティング(分類)

  • 最終的な予測値(確率)$p$はシグモイド関数を使用して確率の形にする
    $p=\frac{1}{1+e^{-Fb}}$

操作イメージ
image.png
上記を一般化すると、下記のようになる
image.png

Pytonで勾配ブースティング分類

※スクラッチで勾配ブースティング分類を実装した場合
  1. クラス作成
  2. コンストラクタ(__init__)作成
    • 引数:学習率、イテレーション数、木の深さ、ランダムシード
  3. 学習メソッド(.fit)の作成
    • 引数:X, y
    • 初期モデル$F_0$
    • 各決定木($f_b(x)$)を学習
  4. 予測メソッド(.predict_proba)作成
    • 学習済みの決定木$f_{1~b}(x)$および$F_0$から最終的な予測値$p$を計算
スクラッチでGradient Boosting Classifier
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クラスを使用する
    1. インスタンス生成
      引数
      • n_estimators:モデルの数
      • learning_rate:学習率(高いと過学習、低すぎると学習が終わらない)
    2. .fit(X, y)で学習
    3. .predict_proba(X)で予測
Gradient Boosting Classifier
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)

image.png

追加情報②
# 弱学習器を全て足し合わせる
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)

image.png

■有名な勾配ブースティング決定木アルゴリズム

▶XGBoost(eXtreme Gradient Boosting)

  • 非常に精度が高いアルゴリズムで、GBDTに正則化項を加えることで過学習を防ぐ
    • 木が大きくなりすぎないようにする
    • 1つのモデルで残差を大きく減らさないようにする

image.png
$\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_typetotal_gainを指定する(デフォルトではweight(頻度))
  • .fit(X, y)で学習
    引数
    • eval_set:early stoppingを設定している場合に指定する。各イテレーション毎に評価したいデータ(例:[(X_val, y_val)])
    • verbose:Trueを指定すると各イテレーションの評価結果をprintする
  • .predict(X)で予測
  • xgboost.plot_tree(model, num_trees)で任意の木構造を描画
XGBoost
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)

image.png

学習データ/検証データ/テストデータ
上記ではテストデータで学習し、そのテストデータに対して予測を行っているのでAUCが高くなるのは当たり前であり、実際の業務では学習データとテストデータのほかに検証データを用意するのが普通となる。

  • 学習データ:.fitに入れるデータ
  • 検証データ:イテレーション時に毎回の評価に使用するデータ(early stoppingなどに使用)
  • テストデータ:学習の過程で正解ラベルにアクセスできないデータ、最後のモデル評価にだけ使用する

image.png

▶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)
    左のノードから順に分割し、その階層の分割が全て終了したら次の階層に行くようにして決定木を構築していくやり方で、一般的な方法

image.png

  • leaf wise(best first)
    最も損失が小さくなるようなノードから分割をしていくやり方で、とても速い

image.png

histgram based algorithm

  • データをヒストグラム化し、bin単位で分割することで高速化する
    計算量が減るので、その分早くなる

image.png

Gradient-based One-Side Sampling(GOSS)

‐ 残差が小さいデータ(うまく学習できているデータ)の一部を学習に使わないことで高速化する

image.png

Exclusive Feature Bundling(EFB)

  • 互いに排他的な複数の特徴量を1つにまとめる
    ※排他的:同時に0ではない値を取らない -> [0, 1, 0, 0] ($\therefore$one-hot-vectorは互いに排他的)
    • 特徴量が多い場合は欠損地が多いスカスカ(疎:sparse)なデータであることが多い
      -> bundleにしてまとめることで高速化
    • 衝突が少ない特徴量同士をbundle化する
    • それぞれの特徴量の値の範囲が被らないように値をずらす

下図だと、aは1~6の値を取っているので、bのすべてに6を足してずらすことで、それぞれの特徴量の値が被らない形でbundleできる。
※一部排他的でない特徴量もあるが、閾値を決めてある程度の数は許容する(今回は単純にbで上書きしている)
image.png

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)で任意の木構造を描画
LigthGBM
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)

image.png

追加情報②
# 弱学習器の木構造を描画
lgb.plot_tree(lgbmc, tree_index=0)

image.png

▶CatBoost(Category Boosting)

  • XGBoostやLightGBMより後発で、使い勝手がよく高精度が期待できる
  • 工夫している点
    • カテゴリかる変数の扱い -> 前処理が不要
    • 決定木の葉の値の計算 -> 過学習を防ぐ、推論の高速化

カテゴリカル変数のencodeing

  • one-hot eoncoding
    -> 計算量が増加し、決定木が複雑になるので、決定木では悪手

image.png

  • target encoding(TS:target statistics)
    各カテゴリ毎のtarget(目的変数)の統計量(TS)で代替(下図では単純に平均を取っている)
    -> 過学習しやすかったり、本来正解ラベルとなるtargetを使用するのでtarget leakageが起こる

image.png

★CatBoostでは次の方法を取っている

Ordered TS(Ordered Target Statistics)

  • 加工のデータからTSを算出する
    • 実際には寺家列ではないのでランダムに順序を決める
    • 過学習を防ぐことが可能
    • one-hot encodingに比べ、データが圧縮される

image.png

  • 一度のOrdered TSだけではhigh varianceになるので、サンプル数を変えて複数回行う
    -> Random Permutation

image.png

他にもOrdered BoostingやSymmetric Treesを使用している

Ordered Boosting

  • 従来の勾配ブースティングは勾配の推定で過学習気味になる
    ※勾配の推定で、そのデータを使って学習した学習器を使うので実際の分布とずれる
  • そのため、残差の計算に使うモデルを、推論に使うモデル群と分ける
    -> 一つ前までのデータを使って学習することで過学習を避ける

image.png

Symmetric Trees

  • 同じ階層ではすべてのノードで同じ特徴量の同じ条件で分割をする
  • 全てのデータに対して共通の処理を行えるので効率が良い
  • 特に推論の高速化が期待できる(並列化との相性◎)

image.png

上記だと弱い学習器になってしまうが、アンサンブル学習では弱学習器でいいので問題ない

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)で任意の木構造を描画
CatBoost
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)

image.png

■スタッキング(stacking)

▶スタッキングとは

  • 複数段階に分けて学習と予測をする手法で、それぞれのモデルにアンサンブルのアルゴリズムを使うことも多い

image.png

  • それぞれのデータの予測に使用するモデルは、そのデータを使って学習してはいけない
    つまり、データ[A,B,C,D,E]があるとして、Aを予測する場合はそれ以外の[B,C,D,E]を使用して作成したモデルAを使用して予測する
  • CVで学習をし、それぞれのfoldの予測をそれ以外のfoldで学習したモデルを使って行う

image.png

  • モデルAで全てのデータ(全てのfold)の予測値を作成したら、モデルB, モデルC...と繰り返していく
  • テストデータの各モデルによる予測には2種類の方法がある
    • 全データで再度学習して予測する
    • 各foldのモデルの予測値を平均する

image.png

  • 2層目のモデルでは、1層目の予測値を特徴量にしてモデルを学習する
    (※学習時に元の特徴量を入れることも可能)

image.png

Pythonでスタッキング

sklearnにスタッキング用のsklearn.emsemble.StakingClissifierクラスがあるが、こちらはCV(クロスバリデーション)に対応しておらず、若干過学習気味になってしまう。
そのため、以下でCVに対応したスタッキングのコード例を示す。

Stacking
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

次の記事

Python初心者の備忘録 #23 ~機械学習入門編09~

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?