乱れた森林再生委員会(学習集会)

What I cannot create, I do not understand.(作ることができないなら,理解できていないということだ) - R. Feynman(物理学者)

木を見たので森を見る:ランダムフォレストを作る(ジコリューで行こう!#4)

御機嫌よろしくて,森林門郎(もりりんもんろう)ですの。*1

f:id:tatamiyatamiyatatatamiya:20190209162707p:plain

tatamiya.hatenablog.com

tatamiya.hatenablog.com

tatamiya.hatenablog.com

前回までの記事では,決定木を作りました。

今回は木をたくさん作って多数決をとるランダムフォレストを作っていきます。

指針

前回までのあらすじと今回やること

決定木の実装は,GitHubの方に上げてあります。*2

こちらを使って,以下のように学習・予測を行うことができます。

import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split

from tree_modules.tree_base import MyTree


iris = datasets.load_iris()
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(X, y)

tree = MyTree()
tree.fit(X_train, y_train)
'''
=== node 0 (depth 1): arg_div -> 2, x_div -> 1.9 ===
=== node 2 (depth 2): arg_div -> 2, x_div -> 4.8 ===
=== node 3 (depth 3): arg_div -> 3, x_div -> 1.6 ===
=== node 6 (depth 3): arg_div -> 3, x_div -> 1.7 ===
'''

y_pred = tree.predict(X_test)
(y_pred == y_test).sum() / len(y_test)
'''
0.9473684210526315
'''

今回は,ランダムに条件を変えて作った複数の決定木を集め,その中で多数決をとります。

一本一本の木をどう作るか?

ただ単に同じデータ・同じ学習方法で決定木を作ると,何回やっても同じ答えが返ってくる木しか作れないので,多数決をとる意味がありません。

したがって,少しずつ違う性質を持つ木を作る必要があります。

そのために,ランダムフォレストでは以下の方法を用います。

  1. 使うデータをランダムに選択する
  2. 木を分割する際の特徴量選択にランダムネスを加える

このうち1.については,N個のデータの中から重複を許してm個のデータをランダムにとってきて新しいデータセットを作成します。 このような方法でデータセットを複製し統計量をとる手法を,ブートストラップサンプリングと呼びます。

2番目の分割特徴量についてですが,今までは各ノードで分割を行う際に,全特徴量ひとつひとつ調べて最も不純度が小さくなるものを探してきました。これに対してランダムフォレストでは,全部の特徴量は使わずにノードごとにランダムに選択し,ランダムサブセット(部分集合)の中から不純度が小さくなるものを探します。

これにより出来上がった一本一本の木の精度は,毎回全特徴量を使って探索した場合と比べて劣ってしまいます。 しかし,ランダムよりはマシだけど少し精度の低いモデル(弱学習器)をたくさん集めて多数決をとることで全体の精度を高めるのが,アンサンブル学習の考え方です。

実装

データの復元抽出ランダムサンプリング(ブートストラップ)

numpyのrandom.choiceメソッドをreplace=Trueに設定して使えば,重複を許しつつランダムにデータをとってくることができます。

index_resample = np.random.choice(len(y), len(y), replace=True)
X_resample, y_resample = X[index_resample], y[index_resample]

特徴量の選出

numpyのrandom.choiceは使用特徴量の選択を各ノードで行う際にも使えます。

num_features = X.shape[1]
max_features = int(np.sqrt(num_features))

index_feat_chosen = np.random.choice(num_features, max_features, replace=False)

X_chosen = X_resample[:, index_feat_chosen]

今回は一度に同じ特徴量を複数選択しないように,日復元抽出replace=Falseとしました。 ただ,replace=Trueにして選んでから重複を省くことで,指定した数より少ない特徴量から最適分割を探れるようにすることもできます。

なお,選択する特徴量の数ですが,一般には全特徴量数の平方根以下が好ましいとされているので,max_features = int(np.sqrt(num_features))としてあります。

決定木にランダム分割を組み込む

決定木の際は,各ノードでの分割をgo_on_dividing(X, y)という関数を再帰的に呼び出して行なっていました。

ランダムフォレスト用にこの部分を改造して,オプションに応じて全特徴量使うかランダム選択した中から分割するかを選べるようにします。

前回まで作った決定木はクラスに書き換えてあるので,go_on_dividing関数の代わりにMyTreeクラスの_go_on_dividingメソッドを書き換えます。中身は基本的に同じですが,global変数の代わりにself.から始まるクラス変数を使っています。

 def _go_on_dividing(self, X, y, depth=0):

     depth += 1
        
     if self.splitter == 'best':
         X_chosen = X
         index_feat_chosen = np.arange(X.shape[1])
        
     elif self.splitter == 'random':
            
         n_features = X.shape[1]
         num_feat_chosen = int(np.sqrt(n_features))
         index_feat_chosen = np.random.choice(n_features, num_feat_chosen, replace=False)
         X_chosen = X[:, index_feat_chosen]

     arg_div_tmp, x_div = self._divide(X_chosen, y)
     arg_div = index_feat_chosen[arg_div_tmp] # inevitable in case of RF

     # 以下省略

後述するMyTreeクラスインスタンスを作成する際にsplitterという引数をオプションに加え,全探索なら'best',ランダム選択なら'random'を選べるようにします(defaultは'best')。

学習と予測

学習の際には,こうしてランダム分割できるようにしたMyTreeを複数作成し,リストlist_treesに格納していきます。

n_estimatores=10

list_trees = []
length_data = len(y)
for i in range(0, n_estimators):
    
    index_resample = np.random.choice(length_data, length_data, replace=True)
    X_resample, y_resample = X[index_resample], y[index_resample]
    
    a_tree = MyTree(splitter='random', max_features=None)
    a_tree.fit(X_resample, y_resample)
    list_trees.append(a_tree)

予測の際は,list_treesから学習済みの木を一本ずつ取り出してデータを入れ,返ってきた予測ラベルをリストに入れていきます。

list_pred = []
for a_tree in list_trees:
    
    list_pred.append(a_tree.predict(X))

array_pred = np.array(list_pred)

あとで使いやすいように,numpy arrayとして持っておきます。

多数決

numpynのbincount関数とargmaxメソッドを使うことで,それぞれの木が出した予測ラベルの中で多数決を行います。

わかりやすくするために,まずは一件目の入力データに対する各木の予測ラベルを見てみます:

array_pred[:,0]
# array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 

今は木の数n_estimatorsを10に設定しているので,要素数10の一次元arrayとなっています。これをnp.bincount関数に入れることで,各ラベルの投票数が返ってきます。

np.bincount(array_pred[:,0], minlength=3)
# array([10,  0,  0])

minlenghth=3としているのは,予測ラベルの数が3種類(setosa, versicolour, virginica)あるからです。今回のように投票数が0のラベルが存在する場合,この指定をしないと何番目のクラスの投票数なのかわからなくなります。 *3 この中から最も投票数の多かったクラスラベルを得るには,.argmax()をとります。

今の操作を全データでいっぺんに行うために,今までも何回か登場したnumpyのapply_along_axis関数を使います。

pole_result = np.apply_along_axis(func1d=np.bincount, axis=0, arr=array_pred, minlength=3)

axis=0とすることで,各列について行方向にnp.bincountを作用させます。

さらにもう一度,axis=0方向にargmax()をとることで,最も投票数の多かったラベルを全データで得ることができます:

pole_result.argmax(axis=0)

なお,今回は最多投票数のラベルを返すようにしましたが,各ラベルへの投票率で返すこともできます:

(pole_result / n_estimators).T

まとめ

以上をまとめて,MyForestクラスにしてみました。

X_train, X_test, y_train, y_test = train_test_split(X, y)

myrf = MyForest(n_estimators=100)
myrf.fit(X_train, y_train)

y_pred = myrf.predict(X_test)

y_pred
# array([1, 1, 2, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 1, 1, 1, 1, 0, 1,
#        2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 1])

y_test
# array([1, 1, 2, 0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 2, 1, 1, 1, 1, 1, 0, 1,
#        2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 1])

(y_pred == y_test).sum() / len(y_test)
# 0.9473684210526315

trainとtestの分け方にも依存しますが,比較的安定して精度が出るようです。


決定木さえできれば,ランダムフォレストは比較的簡単に作ることができました。

今まではクラス分類を行ってきましたが,決定技・ランダムフォレストは連続値を持つデータに対して回帰を行うこともできます。

次回は,回帰の実装を行いたいと思います。

*1:筆者が昔通っていた塾に,森先生という人がいた。ある日,廊下の黒板に「森林門郎ケツでかい!!!!」とデカデカと落書きがされていた。どちらかというと痩せ身の先生だったのだが…

*2:前回までの内容から,一部改変が含まれますがご容赦願います。

*3:今回の例でいうと,minlengthの指定がない場合array([10])のように出力される。しかし2番目のクラスのみ,もしくは3番目のみに票が集中した場合にも同様に出力されてしまう。minlength=3としておけば,それぞれarray([0,10,0]), array([0,0,10])と出力されるので,どのクラスに票が入っているのか判別することができる。