ランダムフォレストのプログラムを自作してみた

シェアする

forest

機械学習を勉強していますが,有名なランダムフォレストのプログラムを勉強のために自作してみました.自作と言っても,決定木そのもののプログラムはsklearnを使用してます.

ランダムフォレストは,「決定木のバギング」に特徴量選択を加えたものかなと思ったので,その考え方をそのままコードにしました.また,比較のため単純にバギングしただけのもの(特徴量選択を行なっていない)も作成しました.

使用したデータは有名なBreast Cancer(30次元,データ数569)を使用してます.irisだと次元もデータ数も低すぎるので,ランダムフォレストの良さが出てきませんでした.

最初にクラス定義をして,その後「データ読み込み」と書かれたコメント行からメインのプログラムが開始します.

以下,Python3であればコピペでそのまま動くと思います.

import matplotlib.pyplot as plt
import sklearn.datasets
import sklearn.ensemble
import sklearn.model_selection
import numpy as np
import scipy.stats as st

# 自前で作成した決定木のバギング
# (決定木そのものはsklearnを使用)
class MyBaggedDecisionTree:
# コンストラクタ(パラメータは決定木の数のみ)
 def __init__(self, n_estimators=100):
 self.n_estimators = n_estimators
 self.cart = []
 for n in range(self.n_estimators):
  self.cart.append(sklearn.tree.DecisionTreeClassifier())

# 学習
def fit(self, X, y):
 for n in range(self.n_estimators):
  boot_num = [np.random.randint(0,len(y)) for n in range(len(y))]
  self.cart[n].fit(X[boot_num], y[boot_num])

# 予測
def predict(self, X):
 pred_all=[]
 for n in range(self.n_estimators):
  pred_all.append(self.cart[n].predict(X))
 return st.mode(pred_all)[0][0]

# 精度(accuracy)を計算
def score(self, X, y):
 pred = self.predict(X)
 return list(pred==y).count(True)/len(y)

# 自前で作成したランダムフォレスト
# (決定木そのものはsklearnを使用)
class MyRandomForest:
# コンストラクタ(パラメータは決定木の数のみ)
 def __init__(self, n_estimators=100):
  self.n_estimators = n_estimators
  self.cart = []
  for n in range(self.n_estimators):
   self.cart.append(sklearn.tree.DecisionTreeClassifier())

# 学習
def fit(self, X, y):
 dim = X.shape[1]
 dim_max = int(np.floor(np.sqrt(dim))) #選択する特徴量の数=floor(元の次元のルート)

 # どの特徴量を使用するのか決定木ごとにランダムに設定
 self.boot_dim = []
 for n in range(self.n_estimators):
  val = [np.random.randint(0,dim) for d in range(dim)] # 先ずは乱数ベクトルを生成
  self.boot_dim.append(np.argsort(val)[0:dim_max]) # 乱数ベクトルをソートし,そのインデックスをdim_max+1個だけ取得(こうすると番号が重複しない)
  self.boot_dim = np.array(self.boot_dim)

# ブートストラップサンプリングしてすべての決定木を学習
for n in range(self.n_estimators):
 boot_num = [np.random.randint(0,len(y)) for n in range(len(y))] #こちらは番号が重複してもOK(復元抽出なので)
 X2 = X[boot_num]
 y2 = y[boot_num]
 self.cart[n].fit(X2[:, self.boot_dim[n]], y2) # 特徴量も減らしていることに注意!

# 予測
def predict(self, X):
 pred_all=[]
 for n in range(self.n_estimators):
  pred_all.append(self.cart[n].predict(X[:, self.boot_dim[n]]))
 return st.mode(pred_all)[0][0] # mode関数は多数決を行う

# 精度(accuracy)を計算
def score(self, X, y):
 pred = self.predict(X)
 return list(pred==y).count(True)/len(y)

# ↑classの定義はここまで

# データの読み込み
bc = sklearn.datasets.load_breast_cancer()
features = bc.data
target = bc.target

acc_rf = [] # ランダムフォレストの精度
acc_ct = [] # 決定木の精度
acc_mybag = [] # MyBaggedDecisionTreeの精度
acc_myrf = [] # MyRandomForestの精度

# 50回実行した平均で評価
for iter in range(50):
 train_x, test_x, train_y, test_y = sklearn.model_selection.train_test_split(features, target, test_size=0.3)

# 決定木単独
cart = sklearn.tree.DecisionTreeClassifier()
cart.fit(train_x, train_y)
accuracy = cart.score(test_x, test_y)
acc_ct.append(accuracy)

# ランダムフォレスト
rf = sklearn.ensemble.RandomForestClassifier()
rf.fit(train_x, train_y)
accuracy = rf.score(test_x, test_y)
acc_rf.append(accuracy)

# バギングした決定木(自作)
mybag = MyBaggedDecisionTree()
mybag.fit(train_x, train_y)
accuracy = mybag.score(test_x, test_y)
acc_mybag.append(accuracy)

# ランダムフォレスト(自作)
myrf = MyRandomForest()
myrf.fit(train_x, train_y)
accuracy = myrf.score(test_x, test_y)
acc_myrf.append(accuracy)

# 50回実行した平均のaccuracyを出力
print('the average of accuracy using Decision Tree {0:.2%}'.format(np.average(acc_ct)))
print('the average of accuracy using Random Forest {0:.2%}'.format(np.average(acc_rf)))
print('the average of accuracy using MyBaggedDT {0:.2%}'.format(np.average(acc_mybag)))
print('the average of accuracy using MyRandomForest {0:.2%}'.format(np.average(acc_myrf)))

実行した結果は以下のようになりました.

もちろん,ブートストラップしてるし学習データとテストデータの分割も毎回変えてるので,この数値はやる度変わりますが,ランダムフォレストがsklearnでも自作でもいい値になってるのが分かります.

自作のランダムフォレストの方がsklearnのランダムフォレストよりやや値が低いですが,これは自作の方でまだ設定しなきゃいけないパラメータがあるためかな?と思います.このプログラムでは n_estimators しか設定してませんので.

MyBaggedDecisionTreeを継承して,MyRandomForestを作成した方がスッキリするでしょうね.いつか時間があったときに修正します(←いつだよw).

ご参考までに.