PythonでDeep Forestを実行し、理解する

スポンサーリンク

 Deep ForestはDeep Learningという巨頭に立ち向かう、まるでラグビー日本代表のような物語が背後にあることがわかりました。
 いまや予測モデルを構築する際は、ニューラルネットワークによるDeep Learning一択と言っても過言ではありません。しかし、Deep LearningにもBlack Boxやハイパーパラメータのチューニングの困難性など問題があります。


 そこで、ニューラルネットワークによるDeep Learningの良さを残しつつ、デメリットも克服するという目標を掲げてDeep Forestが構築されました。夢がありますね。


 本エントリーでは、Deep Forestを実行し、XGBoostとランダムフォレストと比較することで精度が高いのかを検証しています。また、参考にしたこの論文を要約する形で、Deep Forest構築の背景も記載しています。
 以下のグラフは、MNISTにおける各モデルの比較結果です。実行部分では、この結果ができるまでを行っています。


f:id:dskomei:20191022174518p:plain


 今回のコードはここ(Github)に置いてあります。また、Deep Forestのモジュールは以下のコードを使わせてもらいました。
github.com

Deep Forestの構築

 まずはDeep Forestを理解するために、実行して学習する様子を見てみます。
 最初に必要モジュールをimportします。この際に、Deep Forestのモジュールもimportしています。

from pathlib import Path
import numpy as np
import random
import uuid
import time

from keras.datasets import mnist
from sklearn.ensemble import ExtraTreesClassifier, RandomForestClassifier
from sklearn.metrics import *
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import KFold
from sklearn.model_selection import StratifiedKFold
from sklearn import datasets
import xgboost as xgb

from deep_forest import MGCForest

result_dir_path = Path("result")
if not result_dir_path.exists():
    result_dir_path.mkdir(parents=True)


 次に、Deep Forestを構築します。論文では、カスケードフォレストのランダムフォレストの「n_estimators」を1000にしていますが、計算機の都合上今回は100に設定しています。とにかく実行することを目標にしてるので、どうぞあしからず。

n_estimators_mgs = 30
n_estimators_cascade = 100

mgc_forest = MGCForest(
    estimators_config={
        'mgs': [{
            'estimator_class': ExtraTreesClassifier,
            'estimator_params': {
                'n_estimators': n_estimators_mgs,
                'n_jobs': -1,
            }
        }, {
            'estimator_class': RandomForestClassifier,
            'estimator_params': {
                'n_estimators': n_estimators_mgs,
                'n_jobs': -1,
            }
        }],
        'cascade': [{
            'estimator_class': ExtraTreesClassifier,
            'estimator_params': {
                'n_estimators': n_estimators_cascade,
                'max_features': 1,
                'n_jobs': -1,
            }
        }, {
            'estimator_class': ExtraTreesClassifier,
            'estimator_params': {
                'n_estimators': n_estimators_cascade,
                'max_features': 'sqrt',
                'n_jobs': -1,
            }
        }, {
            'estimator_class': RandomForestClassifier,
            'estimator_params': {
                'n_estimators': n_estimators_cascade,
                'max_features': 1,
                'n_jobs': -1,
            }
        }, {
            'estimator_class': RandomForestClassifier,
            'estimator_params': {
                'n_estimators': n_estimators_cascade,
                'max_features': 'sqrt',
                'n_jobs': -1,
            }
        }]
    },
    stride_ratios=[1.0 / 4, 1.0 / 9, 1.0 / 16],
    verbose=False
)


 上記のコードで、multi-grained scanning用の2つのランダムフォレストとカスケードフォレスト用の4つのランダムフォレストを設定しています。
 次に、Deep Forestと比較するようの予測モデルのランダムフォレストとXGBosstを構築しておきます。

rf_model = RandomForestClassifier(max_depth=10, n_estimators=100)
xgb_model = xgb.XGBClassifier(max_depth=10, n_estimators=100)

model_dict = {
    "rf" : rf_model,
    "xgb" : xgb_model,
    "mgc_forest" : mgc_forest
}


 Deep Forestの精度を検証するために、交差検証法による正答率で比較してみます。そこで、各モデルを交差検証する関数を設計しておきます。

def cross_val_scores_fn(model, X_data, y_data, n_splits, shuffle=False):
    
    kfold = StratifiedKFold(n_splits=n_splits, shuffle=shuffle)

    cvs_list, elapsed_time_list = [], []
    for train_indexes, test_indexes in kfold.split(X_data, y_data):

        X_train = X_data[train_indexes]
        y_train = y_data[train_indexes]
        X_test = X_data[test_indexes]
        y_test = y_data[test_indexes]
        
        start_time = time.time()
        
        model.fit(X_train, y_train)
        
        end_time = time.time()
        
        pred = model.predict(X_test)

        cvs_list.append(accuracy_score(y_test, pred))
        elapsed_time_list.append(end_time - start_time)

    return np.round(cvs_list, 3).tolist(), np.round(elapsed_time_list, 2).tolist()


def estimate_models(model_dict, X_data, y_data, n_splits=5, shuffle=True):
    
    results = []
    for model_name, model in model_dict.items():

        cvs_list, elapsed_time_list = cross_val_scores_fn(
            model, 
            X_data=X_data,
            y_data=y_data,
            n_splits=n_splits,
            shuffle=shuffle
        )

        print("{} : cv score mean : {:.0f}%, scores : {}".format(
            model_name,
            np.mean(cvs_list) * 100,
            cvs_list
        ))
        results.append([model_name, len(X_data), np.mean(cvs_list), str(cvs_list), np.mean(elapsed_time_list), str(elapsed_time_list)])

    results = pd.DataFrame(results, columns=["model_name", "data_size", "cvs_mean", "cvs_list", "elapsed_time_mean", "elapsed_time_list"])
    return results


Deep Forestの実行

 それでは、Deep Forestを実行してみます。まずはさくっとやるためにirisデータで試してみます。

from sklearn.datasets import load_iris, load_digits

iris = load_iris()
X_data = iris.data
y_data = iris.target

results = estimate_models(
    model_dict=model_dict,
    X_data=X_data,
    y_data=y_data,
    n_splits=5,
    shuffle=True
)
results.to_csv(result_dir_path.joinpath("model_estimate_iris.csv"), index=False)

 実行結果

f:id:dskomei:20191022172112p:plain:w750


 なんと、交差検証法の正答率ではDeep Forestが一番低い結果となりました。1回あたりの学習時間も100倍以上です。
 もう少しデータサイズが大きい学習データでやってみます。乳がんのデータに替えてみます。

breast_cancer_data = datasets.load_breast_cancer()
X_data = breast_cancer_data.data
y_data = breast_cancer_data.target

rf_model = RandomForestClassifier(max_depth=10, n_estimators=100)
xgb_model = xgb.XGBClassifier(n_estimators=100)

model_dict = {
    "rf" : rf_model,
    "xgb" : xgb_model,
    "mgc_forest" : mgc_forest
}


results = estimate_models(
    model_dict=model_dict,
    X_data=X_data,
    y_data=y_data,
    n_splits=5,
    shuffle=True
)
results.to_csv(result_dir_path.joinpath("model_estimate_breast_cancer.csv"), index=False)

 実行結果

f:id:dskomei:20191022173028p:plain:w750


 irisのデータと比べてデータサイズは3.8倍ほど多くなりました。交差検証法の正答率はDeep Forestが一番高くなりました。やはり、データサイズが大きくくなったときに、Deep Forestの良さが出てくると思います。それでは、MNISTのデータを使って、データサイズを更に大きくしてみます。

(X_train, y_train), (X_test, y_test) = mnist.load_data()

# 画像形式をベクトルに変換
X_train = X_train.reshape((len(X_train), -1))
X_test = X_test.reshape((len(X_test), -1))

X_test = X_test[:1000]
y_test = y_test[:1000]


rf_model = RandomForestClassifier(n_estimators=100, max_depth=10)
xgb_model = xgb.XGBClassifier(n_estimators=100, max_depth=10)

model_dict = {
    "rf" : rf_model,
    "xgb" : xgb_model,
    "mgc_forest" : mgc_forest
}

results = []
for model_name, model in model_dict.items():
    
    for data_size in [100, 200, 500, 1000, 2000]:
        
        X_train_ = X_train[:data_size]
        y_train_ = y_train[:data_size]

        start_time = time.time()

        model.fit(X_train_, y_train_)

        end_time = time.time()

        acc = accuracy_score(y_test, model.predict(X_test))
        results.append([model_name, data_size, end_time - start_time, acc])
        print("End {} {} : {:.0f}%".format(model_name, data_size, acc*100))

results = pd.DataFrame(results, columns=["model_name", "data_size", "elapsed_time", "accuracy"])
results.to_csv(result_dir_path.joinpath("model_estimate_mnist.csv"), index=False)


 上記のコードでは、テストデータの正答率によってモデルの精度を比較しています。各モデルにおいて、データのサイズを替えたときに正答率がどのように変わるかを下のグラフで見てみます。


f:id:dskomei:20191022174518p:plain:w750


 上記のグラフを見ると、データサイズが2000のときにはDeep Forestの正答率が高いのはそうですが、データサイズが100のときでもDeep Forestが一番高いです。これは、MNISTの特徴量が784個と先程の2つのデータに比べて段違いで多いからだと思われます。


f:id:dskomei:20191022180315p:plain:w400


 ただ計算時間は、データサイズ2000のときに2800秒であるため、モデルの精度と学習時間がトレード・オフの関係になってしまっています。

Deep Forest実装の背景

 まずはDeep Forestが実装されるに至った背景を記載します。歴史も年表だけを覚えるよりも大河ドラマで人間物語としてストーリーを見たほうが理解しやすいですね。それはさておき、Deep Forestは現在成功しているディープラーニングの良い部分を取り入れて作られています。なので、ディープラーニングの良さの部分からお話しします。


 今日の精度の高い予測モデルは、ディープラーニングと呼ばれるニューラルネットワークを多層に重ねたモデルで構築されることが多いです。このモデルで構築することである程度の精度が保証されます。そして、そこには3つの成功の秘訣があると考えられています。下記では、一般的な呼称のディープラーニングをニューラルネットワークによリ構築されたものという意味で、ディープニューラルネットワークと呼ぶこととします。

  1. 層を重ね合わせた多層化
  2. モデル内での特徴量の変形
  3. モデルの複雑性


 ディープニューラルネットワークは、層を重ねることで上位層ほど抽象的な情報に反応するようになり、次層への入力データは演算処理が加わることで変形されて伝達されます。XGBoostやランダムフォレストのような決定木モデルでは、入力データはオリジナルなデータであり、モデル内でデータが変形して伝達されてはいないです。そしてモデルの複雑性が高いことで、大規模なデータの特徴を含むことできます。しかしながら、ディープニューラルネットワークにはデメリットもあります。

  1. ハイパーパラメータのチューニングは科学よりもアート
  2. 学習済みモデルを理論的に解析するのは困難
  3. 学習する前にモデルの構造が決まってしまう

 
 ディープニューラルネットワークにおいて、ハイパーパラーメータのチューニングは組合わせが多すぎることから膨大な労力を要します。そのため、科学的にチューニングを行うことは困難です。また、先行研究において良い結果を生み出したDNNsを見るとモデルの構造がバラバラであることがわかります。つまり、ディープニューラルネットワークにおいてモデルの構築は、感性が入るアートに近いです。


 またモデルが複雑であるがゆえに内部はBlack Box化し、学習過程を理論的に理解するのは難しいです。更に、学習する前にモデルの構造が決まってしまうため、必要な複雑性以上の複雑なモデルになってしまうこともあります。そのため、DNNsにおいては、モデルの複雑性を減少させる方法(shortcut connection, pruning, binarization)も提案されています。


 Deep Forestでは、ディープニューラルネットワークのメリットを生かし、デメリットを克服するように作られています。ハイパーパラメータが少なく、理論的解析可能な決定木モデルのアンサンブル学習を多層化し、学習時にモデルの複雑性をデータに依存して自動的に決められるようにしています。

Deep Forestの主要部分

 Deep Forestはカスケードフォレスト構造とmulti-grained scanningの2点が重要です。カスケードフォレスト構造は、2つのランダムフォレストと2つの完全ランダムフォレストを1つのアンサンブルとし、層化することです。これによりモデルに多様性が生まれます。

f:id:dskomei:20191022161059p:plain:w400
(引用:Deep Forest / https://arxiv.org/pdf/1702.08835.pdf


 multi-grained scanningはデータ間の関連性をデータに含められるように、スライドしたデータをカスケードの出力に結合して次層の入力とする方法です。DNNsでは画像において周辺ピクセルの情報を抽出し、LSTMにおいては要素の順番で時系列の情報を抽出しています。これに習い、入力データをスライドしてデータを順々に塊にし、そのデータを学習した結果を次層の入力データに結合することで、データ間の関連性も学習できるようにしています。

f:id:dskomei:20191022163047p:plain:w400
(引用:Deep Forest / https://arxiv.org/pdf/1702.08835.pdf


 上の図では、400次元の入力データに対して、100次元のスライドでデータを固まりにして301インスタンスを作り、それを2つの完全ランダムフォレストで学習し(今回の例では教師データは3つのラベル)、それを結合したベクトルを次層の入力データに結合します。

終わりに

 アンサンブル学習に関しては、こちらの本が参考になります。

作ってわかる! アンサンブル学習アルゴリズム入門

作ってわかる! アンサンブル学習アルゴリズム入門