初心者データサイエンティストの備忘録

調べたことは全部ここに書いて自分の辞書を作る

AdaBoostを深く理解する~スクラッチ実装~

サマリ

 AdaBoostについて勉強しています。本記事では、AdaBoostを実装する際に躓いたポイントを備忘録的に残しておこうと思います。本記事のサマリは下記です。

  • 二値分類問題用のトイデータを作成した。
  • クラッチ実装したAdaBoostを使い、上記のトイデータにおける二値分類問題を解いてみた。スクラッチ実装したAdaBoostのクラスは、AdaBoostHandmadeとした。
    • 当初、全ての弱分類器が同じになってしまうという課題が発生した。原因は、弱分類器としてscikit-learnDecisionTreeClassifierインスタンスAdaBoostHandmadeに渡していることにあった。
    • DecisionTreeClassifierクラスと、DecisionTreeClassifierの引数を可変長引数としてAdaBoostHandmadeに渡すことで、正しく動くようになった。

 本記事では、まず最初にAdaBoostのアルゴリズムの概要を説明し、次にトイデータの設定を説明します。その後、弱分類器が全て同じになってしまったAdaBoostHandmadeを提示し、どこに課題があったのか、どのように修正したのかについて説明します。

AdaBoostの概要

 AdaBoostとは、二値分類問題を解くためのアルゴリズムの一つです。図1にアルゴリズムの内容を示します。

図1:AdaBoostのアルゴリズム

 詳細については、下記記事に書いてあります。興味があったらご覧ください。 aisinkakura-datascientist.hatenablog.com

aisinkakura-datascientist.hatenablog.com

トイデータの 作成

 下記の設定でトイデータを作成しました。

  • 特徴量
    • 標準正規分布にしたがう10個の独立な変数。つまり、一つのサンプルに対して、X _ i \sim N(0, 1)\ \ (i=1, \cdots, 10)とする。
  • 目的変数
    • 各特徴量を二乗して和を取ったのち、自由度10のカイ二乗分布の中央値より大きい場合1、小さい場合0とした。つまり、\displaystyle{\sum _ {i=1} ^ {10}} X _ i ^ 2 \geq  \chi _ {(10)} ^ 2(0.5)ならば、Y=1\displaystyle{\sum _ {i=1} ^ {10}} X _ i ^ 2 \lt  \chi _ {(10)} ^ 2(0.5)ならば、Y=0とする。
  • サンプルサイズ
    • 2000個とした。

 以上の条件でPythonを用いてトイデータを作成すると、下記のようになります。

AdaBoostの実装

弱分類器が全て同じになってしまった悪い例

 一番最初に実装したスクリプトが下記です。

 上記スクリプトのIn[3]の出力結果をご覧ください。[1, 0, 0, ..., 0,0,1]が並んでいます。これらは、100個の弱分類器で2000個のデータの予測をした結果です。[1, 0, 0, ..., 0,0,1]が並んでいるということは、これら100個の弱分類器が同じ予測をしてしまっていることを示しています。これでは、アンサンブル学習をした意味がありません。
 なぜこのようなことになってしまったのか?理由はシンプルで、for文内で.fitをしても適切にインスタンスが更新されていないからです。具体的には、下記の箇所に問題があります。

# モデル作成するためのメソッド
    def fit(self, X, y):
        # 省略

        # 弱分類器を連続して作成する
        for m in range(self.n_estimators):
            
            # 弱分類器の学習
            self.weak_learner.fit(X, y, sample_weight=weight)

 weak_learnerは、DecisionTreeClassifier(max_depth=1, random_state=100)(=インスタンス)を指しています。for文の中で毎回sample_weightの値を変えて.fitをしたかったのですが、ここが上手くいきませんでした。詳細はわからないのですが、一度.fitしたインスタンスは、再度.fitしてもインスタンスの中身が変わらないようです。したがって、for文内でn_estimators回の.fitをしても、結局1回目に.fitした結果が保存されてしまいます。そのため、弱分類器が全て同じになってしまいました。

ハードコーディングをして一旦解消

 上記のように、一度インスタンスを作成し.fitをしてしまうとfor文内でインスタンスが更新されません。ならば、for文内でDecisionTreeClassifierインスタンスを毎回作成するという解決策を思いつきました。
 具体的には、下記のように変更します。

# モデル作成するためのメソッド
    def fit(self, X, y):
        # 省略

        # 弱分類器を連続して作成する
        for m in range(self.n_estimators):
            
            # 弱分類器の学習
            weak_learner = DecisionTreeClassifier(max_depth=1) # 追加した行!!
            self.weak_learner.fit(X, y, sample_weight=weight)

 これにより、複数の異なる弱分類器を作成できるようになりました。実際に動かしてみた結果は下記です。predictの結果が同じになっていないですし、accuracyも0.5を上回っているので成功です。

ハードコーディングを避ける

 ハードコーディングをすることで、正しく動くようにはなりました。しかし、DecisionTreeClassifierを弱分類器としてハードコーディングしているので、柔軟性がありません。弱分類器をもっと深い決定木に変えたり、ロジスティック回帰にするためには、面倒な書き換えが必要です。
 そこで、AdaBoostHandmadeに対して、DecisionTreeClassifierの(インスタンスではなく)クラスと引数を渡すことにしました。その結果、下記のようなスクリプトになりました。

class AdaboostHandmade:
    '''
    n_estimetors:学習する弱分類器の個数
    weak_learner:弱分類器(DecisionTreeClassifier)を想定
    params:weak_learnerのパラメータ
    '''
    
    def __init__(self, n_estimators, WeakLearner, **params):
        self._n_estimators = n_estimators
        self._WeakLearner = WeakLearner
        self._params = params
    
    def fit(self, X, y):
        # 省略

        # 弱分類器を連続して作成する
        for m in range(self.n_estimators):
            # 弱分類器のインスタンスを作成する
            weak_learner = self._WeakLearner()
            weak_learner.set_params(**self._params)

 これにより、AdaBoostHandmade内に弱分類器をハードコーディングすることなく、異なる複数の弱分類器を作れるようになりました。全体としては、下記のようなスクリプトになります。

 学習・テストデータを分割していないので、いい加減なacurracyですが、一応0.843にまで上げることができました。

次回の記事のトピック

 ここまでで、一応動くAdaBoostのスクリプトを書くことができました。次回は、下記の二冊を参考にスクリプトをキレイ化していこうと思います。