Random Forest(Classifier)
今回の記事では、分類器の一つである決定木を応用したランダムフォレストについて学んだ知見をまとめます。
ランダムフォレストのアルゴリズム
ランダムフォレストとは決定木の一つの手法です。決定木とは前回の交差検証の記事で示したようにノードのサンプルの不純度を最小とするようにサンプルを分類する手法でした。しかし、決定木では一つの木のみしか作成されず、分類する基準は下図に示すように当該ルートのサンプルによって一意に定められてしまうため、未知データに対する汎化性が乏しくなるという問題があります。(左側ノードのperFare≦26.144の分類基準は既知データにフィットしすぎている可能性がある)
そこで、決定木を発展させたランダムフォレストという手法により、決定木を複数作成し、決定木間で多数決を取ることで汎化性を向上させた分類基準を得ることが可能となりました。また、ランダムフォレストの決定木はブートストラップによる再標本化により作成されます。
ブートストラップ
ブートストラップとは再標本化の一つの手法であり、人為的に母集団と似た標本を作成するという考え方です。具体的には、母集団からランダムに要素を抽出、またその際、要素の重複を許容します。このようにして、任意サイズの標本を作成することで母集団と似た傾向を持つがそれぞれ異なる標本群を作成することが可能となります。(ランダムフォレストの場合、標本サイズは母集団サイズの0.62倍と決まっています)
ランダムフォレストの実装
今回もいつもと同様タイタニック号のデータセットを用いランダムフォレストを実装します。 まずは、特徴量エンジニアリングをします。
train['Sex'] = train['Sex'].replace('male', 0) train['Sex'] = train['Sex'].replace('female', 1) test['Sex'] = test['Sex'].replace('male', 0) test['Sex'] = test['Sex'].replace('female', 1) train['Embarked'] = train['Embarked'].fillna('C') train['Embarked'] = train['Embarked'].map({'S':0, 'Q':1, 'C':2}) test['Embarked'] = test['Embarked'].map({'S':0, 'Q':1, 'C':2}) train['Parch'] = train['Parch'].astype(int) train['SibSp'] = train['SibSp'].astype(int) train['Familysize'] = train['Parch'] + train['SibSp'] + 1 test['Parch'] = test['Parch'].astype(int) test['SibSp'] = test['SibSp'].astype(int) test['Familysize'] = test['Parch'] + train['SibSp'] + 1 test_Fare = train[(train['Pclass'] == 3) & (train['Age'] > 40)]['Fare'].mean() test['Fare'] = test['Fare'].fillna(test_Fare) train['perFare'] = train['Fare'] / train['Familysize'] test['perFare'] = test['Fare'] / test['Familysize'] train['honorific'] = train['Name'].str.split(',', expand = True)[1].str.split('.', expand = True)[0] train['honorific'] = train['honorific'].str.replace(' ', '') honorific_list = train[train['Age'].isnull()]['honorific'].values.tolist() age_list = [] for honorific in honorific_list: age_avg = train[train['honorific'] == honorific]['Age'].mean() age_std = train[train['honorific'] == honorific]['Age'].std() age_list.append(random.uniform(age_avg - age_std, age_avg + age_std)) train['Age'][np.isnan(train['Age'])] = age_list train['Age'] = train['Age'].astype(int) test['honorific'] = test['Name'].str.split(',', expand = True)[1].str.split('.', expand = True)[0] test['honorific'] = test['honorific'].str.replace(' ', '') honorific_list = test[test['Age'].isnull()]['honorific'].values.tolist() age_list = [] for honorific in honorific_list: age_avg = train[train['honorific'] == honorific]['Age'].mean() age_std = train[train['honorific'] == honorific]['Age'].std() age_list.append(random.uniform(age_avg - age_std, age_avg + age_std)) test['Age'][np.isnan(test['Age'])] = age_list test['Age'] = test['Age'].fillna(28) test['Age'] = test['Age'].astype(int)
説明変数と目的変数を定義し、訓練データを訓練データと検証データに分割します。
trainE = train.loc[:, ['perFare', 'Pclass', 'Embarked', 'Sex', 'Familysize']] trainO = train['Survived'] # データの分割 from sklearn.model_selection import train_test_split x_train, x_test, y_train, y_test = train_test_split(trainE, trainO, test_size=0.25, random_state=42)
ホールド・アウトにより分割した訓練データに対しランダムフォレストを適用します。そして、学習した決定木を用いて、オリジナルデータ(母集団)、学習に用いた訓練データ、検証データ(未知データ)に対し予測精度を算出しました。
# ランダムフォレストの作成 from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier(random_state = 42) clf = rf.fit(x_train, y_train) print('n_estimators : 10') print('score(オリジナルデータ): {:.3%}'.format(clf.score(trainE, trainO))) print('score(訓練データ0.75): {:.3%}'.format(clf.score(x_train, y_train))) print('score(検証データ0.25): {:.3%}'.format(clf.score(x_test, y_test)))
[out] n_estimators : 10 score(オリジナルデータ): 89.675% score(訓練データ0.75): 92.665% score(検証データ0.25): 80.717%
ここで、前回作成した決定木と予測精度の比較を行います。
# 決定木の作成 my_tree = tree.DecisionTreeClassifier() clf = my_tree.fit(x_train, y_train) print('score(オリジナルデータ): {:.3%}'.format(clf.score(trainE, trainO))) print('score(訓練データ0.75): {:.3%}'.format(clf.score(x_train, y_train))) print('score(検証データ0.25): {:.3%}'.format(clf.score(x_test, y_test)))
[out] score(オリジナルデータ): 89.562% score(訓練データ0.75): 92.665% score(検証データ0.25): 80.269%
ランダムフォレストと決定木ではほとんど予測精度に差違を確認できませんでした。要因の一つとしてランダムフォレストのデフォルトの設定では、決定木が10個しか作成されないため、差が生じなかったのではないかと考えられます。
そこで、作成する決定木の数と検証データの予測精度の関係を算出しました。
rfclf = [] for i in range(5, 100, 5): rf = RandomForestClassifier(n_estimators = i, random_state = 42) clf = rf.fit(x_train, y_train) rfclf.append(clf.score(x_test, y_test)) dx = 0.1 xmin, xmax = 0, 100 x = np.arange(xmin, xmax+dx, dx) plt.plot(range(5,100,5), rfclf) plt.hlines(0.80269, xmin, xmax, "blue", linestyles='dashed') plt.title('Relationship between n_estimators and prediction accuracy') plt.xlabel('n_estimators') plt.ylabel('Verification Score')
図中の破線は単純な決定木の予測精度を表しています。この結果から、たしかに初期段階(n_estimators <25)では、作成する決定木の数により予測精度が向上する傾向が確認できます。しかし、それ以降数を増加させても予測精度は低下しました。これは母集団に類似した標本群に対し過学習してしまったためだと考えられます。そのため、ランダムフォレストで作成する決定木の数は十分に検討する必要があります。
実際にn_estimators = 20のときのランダムフォレストを可視化してみました。このようにそれぞれの決定木の形状変化が見て取れるため、ランダムフォレストを適用する意味を視覚的にも認識できます。
feature_importances
ランダムフォレストの機能の一つに、feature_importancesがあります。これは、説明変数がどの程度目的変数に影響しているかを示す特徴量重要度です。feature_importancesはルートノードが分岐する際の不純度の変化量を全ノードに対し計算し総和で除すことで任意の特徴量の重要度を算出しています。
実際には、不純度変化量は下記の式で計算され
feature_importancesは下記式で計算されます。
実際にpythonで実装します。
rf = RandomForestClassifier(n_estimators = 25, random_state = 45) clf25 = rf.fit(x_train, y_train) features = trainE.columns importances = clf.feature_importances_ indices = np.argsort(importances) # importancesを並び替える plt.barh(range(len(indices)), importances[indices], align='center') plt.yticks(range(len(indices)), features[indices]) plt.show()
この結果から、最も重要度が高い特徴量はperFareであることがわかりました。しかし、実際にはランダムフォレストでの特徴量重要度の計算は分岐に使用される回数が多い特徴量ほど不純度変化量が増加してしまいます。そして、perFareという量的データ(Familysizeも量的データだがその範囲は小さいため除く)は分岐の基準となりやすいです。反対に、質的データであるSexはトップの分岐にて0か1で完全に分類されるため決定木での登場は一度のみしかないという不利な点があります。そのため、feature_importancesは特徴量選定の一つのメソッドですが、変数の種類により重要度に差が生じることは留意する必要があります。