ランダムフォレストの解釈

『ランダムフォレストの解釈』を魅力的にする方法

ランダムフォレストアルゴリズムの包括的なガイドとその解釈方法

写真提供:Sergei A on Unsplash

最近、大規模な言語モデルについての話題が多いですが、それは古いスクールの機械学習アプローチが今では絶滅の危機に瀕しているということを意味するものではありません。数百もの数値特徴量を持つデータセットを与えてターゲット値を予測するようにChatGPTに依頼しても役に立つとは思えません。

ニューラルネットワークは通常、非構造化データ(例:テキスト、画像、音声)の場合に最適な解決策です。しかし、表形式のデータの場合は、古いがよいランダムフォレストの恩恵を受けることができます。

ランダムフォレストアルゴリズムの最も重要な利点は次のとおりです:

  • わずかなデータの前処理しか必要ありません。
  • ランダムフォレストでは上手くいかないことが非常に少ないです。アンサンブルに十分な数の木があれば、過学習の問題に直面することはありません。また、木を追加すると誤差が減少します。
  • 結果を解釈するのは簡単です。

このため、ランダムフォレストは表形式のデータを使用して新しいタスクを開始する際の最初のモデル候補になることがあります。

この記事では、ランダムフォレストの基礎をカバーし、モデルの結果を解釈するアプローチについて説明します。

次の質問について回答する方法を学びます:

  • どの特徴量が重要であり、どの特徴量が冗長で削除できるのか?
  • 各特徴量の値が目標指標にどのように影響するのか?
  • 各予測の要因は何ですか?
  • 各予測の信頼度を推定するにはどうすればよいですか?

前処理

この解説では、ワインの品質データセットを使用します。これは、異なるポルトガルの「ヴィニョー・ヴェルデ」ワインバリアントの物理化学的テストとワインの品質との関係を示しています。ワインの特性に基づいてワインの品質を予測しようとします。

決定木を使う場合、前処理をほとんど行う必要はありません:

  • アルゴリズムが自動的に対応できるため、ダミー変数を作成する必要はありません。
  • 正規化や外れ値の取り扱いも必要ありません。順序のみ重要です。したがって、決定木ベースのモデルは外れ値に対して頑健です。

ただし、scikit-learnの決定木の実装はカテゴリ変数や欠損値を扱うことができません。そのため、自分で対処する必要があります。

幸いなことに、データセットには欠損値がありません。

df.isna().sum().sum()0

そして、type変数(’red’または’white’)をstringからintegerに変換する必要があります。これにはpandasのCategorical変換を使用できます。

categories = {}  cat_columns = ['type']for p in cat_columns:    df[p] = pd.Categorical(df[p])        categories[p] = df[p].cat.categoriesdf[cat_columns] = df[cat_columns].apply(lambda x: x.cat.codes)print(categories){'type': Index(['red', 'white'], dtype='object')}

これで、赤ワインに対してdf['type']が0、白ワインに対して1になりました。

前処理のもう一つの重要な部分は、データセットをトレーニングセットと検証セットに分割することです。そのため、モデルの品質を評価するための検証セットを使用できます。

import sklearn.model_selectiontrain_df, val_df = sklearn.model_selection.train_test_split(df,     test_size=0.2) train_X, train_y = train_df.drop(['quality'], axis = 1), train_df.qualityval_X, val_y = val_df.drop(['quality'], axis = 1), val_df.qualityprint(train_X.shape, val_X.shape)(5197, 12) (1300, 12)

前処理ステップが完了し、モデルのトレーニングという最も興味深い部分に移行する準備が整いました。

決定木の基本

トレーニングに入る前に、ランダムフォレストの仕組みを理解するために少し時間をかけましょう。

ランダムフォレストは、決定木のアンサンブルです。したがって、まずは基本的な構成要素である「決定木」から始める必要があります。

ワインの品質予測の例では、回帰タスクを解決しますので、回帰から始めましょう。

決定木:回帰

デフォルトの決定木モデルを当てはめましょう。

import sklearn.treeimport graphvizmodel = sklearn.tree.DecisionTreeRegressor(max_depth=3)# 可視化のために max_depth を制限していますmodel.fit(train_X, train_y)

決定木の最大の利点の一つは、これらのモデルを簡単に解釈できることです。それはただの一連の質問です。可視化してみましょう。

dot_data = sklearn.tree.export_graphviz(model, out_file=None,                                       feature_names = train_X.columns,                                       filled = True) graph = graphviz.Source(dot_data) # ツリーを png ファイルに保存します。png_bytes = graph.pipe(format='png')with open('decision_tree.png','wb') as f:    f.write(png_bytes)
Graph by author

ご覧の通り、決定木は2つの二分割で構成されています。各ノードでデータセットを2つに分割しています。

最後に、葉ノードの予測値はこのノード内のすべてのデータポイントの平均です。

備考: 決定木は葉ノードのすべてのデータポイントの平均を返すため、外挿にはかなり悪い性能です。トレーニングと推論の間に特徴量分布に注意を払う必要があります。

データセットの最適な分割方法を特定する方法について考えてみましょう。1つの変数から始めて、最適な分割方法を定義することができます。

4つのユニークな値(1、2、3、4)を持つ特徴量の場合、その間には3つの可能な閾値があります。

Graph by author

次に、各閾値でデータの予測値を計算し、葉ノードの平均値とします。その後、これらの予測値を使用して、各閾値のMSE(平均二乗誤差)を取得します。最も低いMSEを持つものが最適な分割です。scikit-learnのDecisionTreeRegressorは、デフォルトでMSEを基準としています。

より良い理解のために、sulphates特徴量の手動で最適な分割を計算してみましょう。

def get_binary_split_for_param(param, X, y):    uniq_vals = list(sorted(X[param].unique()))        tmp_data = []        for i in range(1, len(uniq_vals)):        threshold = 0.5 * (uniq_vals[i-1] + uniq_vals[i])                 # 閾値でデータセットを分割        split_left = y[X[param] <= threshold]        split_right = y[X[param] > threshold]                # 各分割の予測値を計算        pred_left = split_left.mean()        pred_right = split_right.mean()        num_left = split_left.shape[0]        num_right = split_right.shape[0]        mse_left = ((split_left - pred_left) * (split_left - pred_left)).mean()        mse_right = ((split_right - pred_right) * (split_right - pred_right)).mean()        mse = mse_left * num_left / (num_left + num_right) \            + mse_right * num_right / (num_left + num_right)        tmp_data.append(            {                'param': param,                'threshold': threshold,                'mse': mse            }        )                return pd.DataFrame(tmp_data).sort_values('mse')get_binary_split_for_param('sulphates', train_X, train_y).head(5)| param     |   threshold |      mse ||:----------|------------:|---------:|| sulphates |       0.685 | 0.758495 || sulphates |       0.675 | 0.758794 || sulphates |       0.705 | 0.759065 || sulphates |       0.715 | 0.759071 || sulphates |       0.635 | 0.759495 |

私たちは、sulphatesに対して、最も優れたしきい値は0.685であり、最も低いMSEを示すことがわかります。

次に、私たちは全ての特徴量に対してこの関数を使用し、最適な分割を定義することができます。

def get_binary_split(X, y):    tmp_dfs = []    for param in X.columns:        tmp_dfs.append(get_binary_split_for_param(param, X, y))            return pd.concat(tmp_dfs).sort_values('mse')get_binary_split(train_X, train_y).head(5)| パラメータ   |   閾値 |      MSE ||:--------|------------:|---------:|| アルコール |      10.625 | 0.640368 || アルコール |      10.675 | 0.640681 || アルコール |      10.85  | 0.641541 || アルコール |      10.725 | 0.641576 || アルコール |      10.775 | 0.641604 |

私たちは、初期の決定木とまったく同じ結果を得たで、最初の分割はアルコール <= 10.625です。

全ての決定木を構築するためには、データセットアルコール <= 10.625アルコール > 10.625に対して再帰的に最適な分割を計算し、次のレベルの決定木を取得します。そして、繰り返します。

再帰の停止基準は、深さまたはリーフノードの最小サイズのいずれかです。ここには、リーフノードに少なくとも420のアイテムがある場合の決定木の例があります。

model = sklearn.tree.DecisionTreeRegressor(min_samples_leaf = 420)
Graph by author

モデルの性能を把握するために、検証セットで平均絶対誤差(MAE)を計算しましょう。外れ値の影響が少ないため、MAEはMSE(平均二乗誤差)よりも好まれます。

import sklearn.metricsprint(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))0.5890557338155006

決定木:分類

回帰の例を見てきましたが、分類の場合は少し異なります。この記事では分類の例に詳しく触れることはしませんが、基本的な考え方について話し合う価値はあります。

分類では、平均値の代わりに、各リーフノードの最も一般的なクラスを予測値として使用します。

分類においては、分割の品質を評価するためにジニ係数を使用することが一般的です。サンプルからランダムにアイテムを抽出し、別のアイテムを抽出した場合の確率がジニ係数に等しいとします。

例えば、二つのクラスのみが存在し、最初のクラスのアイテムの割合がpである場合、以下の式を用いてジニ係数を計算することができます。

分類モデルが理想的な場合、ジニ係数は0に等しいです。最悪の場合(p = 0.5)、ジニ係数は0.5に等しいです。

二つの部分(左側と右側)のためにジニ係数を計算し、それぞれのパーティション内のサンプル数で正規化することで、二値分割の評価指標を計算することができます。

その後、異なるしきい値に対して最適化指標を同様に計算し、最適なオプションを選択することができます。

シンプルな決定木モデルを訓練し、その動作について説明しました。ここで、ランダムフォレストに進む準備が整いました。

ランダムフォレスト

ランダムフォレストは、バギングの概念に基づいています。アイデアは、いくつかの独立したモデルを適合させ、それらの平均予測値を使用することです。モデルは独立しているため、エラーは相関していません。モデルに系統的なエラーがないと仮定し、多くのエラーの平均値がゼロに近づくはずです。

どのようにたくさんの独立したモデルを手に入れることができるのでしょうか?実際には非常にシンプルです。ランダムなサンプルセットと特徴量の決定木をトレーニングすることができます。それがランダムフォレストです。

ベーシックなランダムフォレストを100個の木と、葉ノードの最小サイズを100としてトレーニングしてみましょう。

import sklearn.ensemble
import sklearn.metrics

model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)
model.fit(train_X, train_y)
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408

ランダムフォレストを使用すると、1つの決定木よりもはるかに優れた品質を実現できました:0.5592 vs. 0.5891。

過学習

重要な質問は、「ランダムフォレストは過学習する可能性があるか」ということです。

実際には、過学習することはありません。相関のないエラーの平均化を行っているため、ツリーを追加することによってモデルを過学習することはできません。ツリーの数が増えると品質は漸近的に向上します。

Graph by author

ただし、ツリーが深く、十分な数がない場合は過学習が発生する可能性があります。1つの決定木では簡単に過学習することができます。

Out-of-bagエラー

ランダムフォレストでは、各ツリーに使用される行の一部のみを使用してエラーを推定することができます。各行に対して、その行が使用されていないツリーのみを選択し、それらを使用して予測を行うことができます。その後、予測に基づいてエラーを計算することができます。このアプローチは「out-of-bagエラー」と呼ばれます。

OOBエラーはバリデーションセットのエラーと比較して非常に近いことがわかります。これは、良い近似値であることを意味します。

# OOBエラーを計算するにはoob_score = Trueを指定する必要があります
model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100, oob_score=True)
model.fit(train_X, train_y)

# バリデーションセットのエラー
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408

# トレーニングセットのエラー
print(sklearn.metrics.mean_absolute_error(model.predict(train_X), train_y))
0.5430398596179975

# OOBエラー
print(sklearn.metrics.mean_absolute_error(model.oob_prediction_, train_y))
0.5571191870008492

モデルの解釈

冒頭で述べたように、決定木の大きな利点は、それらを解釈するのが簡単であることです。モデルをよりよく理解してみましょう。

特徴の重要度

特徴の重要度の計算は非常にシンプルです。アンサンブル内の各決定木と各バイナリ分割を調べ、私たちのメトリック(この場合はsquared_error)への影響を計算します。

初期の決定木のalcoholによる最初の分割に注目しましょう。

次に、すべての決定木のすべてのバイナリ分割に対して同じ計算を行い、すべてを合計し、正規化して各特徴の相対的な重要度を得ることができます。

scikit-learnを使用する場合、手動で特徴の重要度を計算する必要はありません。単にmodel.feature_importances_を取得すればよいです。

def plot_feature_importance(model, names, threshold=None):
    feature_importance_df = pd.DataFrame.from_dict({'feature_importance': model.feature_importances_,
                                                    'feature': names})
            .set_index('feature').sort_values('feature_importance', ascending=False)
    if threshold is not None:
        feature_importance_df = feature_importance_df[feature_importance_df.feature_importance > threshold]
    fig = px.bar(
        feature_importance_df,
        text_auto='.2f',
        labels={'value': 'feature importance'},
        title='Feature importances'
    )
    fig.update_layout(showlegend=False)
    fig.show()

plot_feature_importance(model, train_X.columns)

最も重要な特徴は、アルコール揮発性酸度です。

作者によるグラフ

偏関数

各特徴が目標指標にどのような影響を与えるかを理解することは、興奮をもって非常に有益です。たとえば、アルコールの量が増えると品質が上昇するのか、より複雑な関係が存在するのか、といったことです。

データセットからデータを取得し、アルコールによる平均値をプロットすることもできますが、相関関係がある場合には正確ではありません。たとえば、データセットの中でアルコールの量が多い場合は、糖分が多く品質が良いことにも対応しているかもしれません。

アルコールの影響を推定するために、データセットのすべての行を取り、機械学習モデルを使って、アルコールの異なる値(9、9.1、9.2など)に対する各行の品質を予測します。その結果を平均化し、アルコールレベルとワインの品質の実際の関係を得ることができます。つまり、データはすべて同じであり、アルコールレベルだけを変化させています。

このアプローチは、ランダムフォレストだけでなく、他の機械学習モデルでも使用することができます。

この関係を簡単にプロットするために、sklearn.inspectionモジュールを使用することができます。

sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X, range(12))

これらのグラフからは、さまざまな洞察を得ることができます。たとえば:

  • 自由亜硫酸塩の増加に伴いワインの品質も向上しますが、30を超えると安定します。
  • アルコールの量が多いほど品質が高くなります。

2つの変数の関係も見ることができます。これは非常に複雑な場合もあります。たとえば、アルコールレベルが11.5以上の場合、揮発性酸度は影響しません。しかし、より低いアルコールレベルでは、揮発性酸度が品質に大きな影響を与えます。

sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X, [(1, 10)])

予測の信頼性

ランダムフォレストを使用することで、各予測の信頼性を評価することもできます。そのために、アンサンブル内の各木から予測を計算し、分散や標準偏差を確認することができます。

val_df['predictions_mean'] = np.stack([dt.predict(val_X.values) for dt in model.estimators_]).mean(axis = 0)val_df['predictions_std'] = np.stack([dt.predict(val_X.values) for dt in model.estimators_]).std(axis = 0)ax = val_df.predictions_std.hist(bins = 10)ax.set_title('予測の標準偏差の分布')

標準偏差が低い(0.15未満)予測値と、stdが0.3を超える予測値があることが分かります。

ビジネス目的でモデルを使用する場合には、このような場合を異なる方法で処理することができます。たとえば、stdがある閾値(X)を超える場合には予測を考慮しない、または顧客にパーセンタイル25%と75%の間隔を表示することができます。

予測の方法

各予測の方法を理解するために、treeinterpreterおよびwaterfallchartsパッケージも使用することができます。これは、例えば、顧客になぜクレジットが却下されたのかを説明する必要があるビジネスケースで便利です。

私たちは、ワインの1つを例に見てみましょう。それは比較的低アルコールであり、揮発性酸度が高いです。

from treeinterpreter import treeinterpreterfrom waterfall_chart import plot as waterfallrow = val_X.iloc[[7]]prediction, bias, contributions = treeinterpreter.predict(model, row.values)waterfall(val_X.columns, contributions[0], threshold=0.03,           rotation_value=45, formatting='{:,.3f}');

グラフは、このワインが平均よりも優れていることを示しています。品質を高める主な要素は揮発性酸度の低いレベルであり、主な欠点は低いアルコールレベルです。

Graph by author

したがって、データとモデルをより良く理解するのに役立つ便利なツールがたくさんあります。

木の数を減らす

Random Forestのもう1つの素晴らしい機能は、任意の表形式データの特徴量の数を減らすために使用できることです。Random Forestを素早く適合させ、データの意味のある列のリストを定義できます。

大量のデータが常により良い品質を意味するわけではありません。また、トレーニングや推論中にモデルのパフォーマンスに影響を与える可能性があります。

初期のワインデータセットには12つの特徴量しかなかったため、この場合はやや大きなデータセットを使用します – Online News Popularity

特徴量の重要性を確認

まず、Random Forestを構築し、特徴量の重要度を確認しましょう。59個の特徴量のうち、34個の重要度が0.01未満です。

それらを削除して精度を確認してみましょう。

low_impact_features = feature_importance_df[feature_importance_df.feature_importance <= 0.01].index.valuestrain_X_imp = train_X.drop(low_impact_features, axis = 1)val_X_imp = val_X.drop(low_impact_features, axis = 1)model_imp = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)model_imp.fit(train_X_sm, train_y)
  • すべての特徴量に対する検証セット上のMAE:2969.73
  • 25個の重要な特徴量に対する検証セット上のMAE:2975.61

品質の差はそれほど大きくありませんが、トレーニングおよび推論のステージでモデルをより高速にすることができます。初期の特徴量の60%以上をすでに削除しました – いい仕事です。

冗長な特徴を見る

残りの特徴について、冗長(強く相関する)ものがあるかどうかを見てみましょう。そのために、Fast.AIツールを使用します:

import fastbookfastbook.cluster_columns(train_X_imp)

次の特徴量が近いことがわかります:

  • self_reference_avg_sharessself_reference_max_shares
  • kw_min_avgkw_min_max
  • n_non_stop_unique_tokensn_unique_tokens

これらも削除しましょう。

non_uniq_features = ['self_reference_max_shares', 'kw_min_max',   'n_unique_tokens']train_X_imp_uniq = train_X_imp.drop(non_uniq_features, axis = 1)val_X_imp_uniq = val_X_imp.drop(non_uniq_features, axis = 1)model_imp_uniq = sklearn.ensemble.RandomForestRegressor(100,   min_samples_leaf=100)model_imp_uniq.fit(train_X_imp_uniq, train_y)sklearn.metrics.mean_absolute_error(model_imp_uniq.predict(val_X_imp_uniq),   val_y)2974.853274034488

少しでも品質が向上しています。したがって、特徴量の数を59から22に減らし、エラーをわずか0.17%増加させました。これは、このようなアプローチが機能することを証明しています。

GitHubで詳細なコードを見つけることができます。

概要

この記事では、Decision TreeとRandom Forestアルゴリズムの動作について説明しました。また、以下の内容を学びました:

  • 最も重要な特徴量のリストを取得し、モデルのパラメータ数を減らすために特徴重要度を使用する方法。
  • 各特徴量の値がターゲットメトリックに与える影響を部分依存を使用して定義する方法。
  • treeinterpreterライブラリを使用して、異なる特徴量が各予測に与える影響を推定する方法。

この記事を読んでくれて本当にありがとうございます。あなたにとって有益であったことを願っています。追加の質問やコメントがあれば、コメントセクションにお願いします。

参考文献

データセット

  • Cortez,Paulo, Cerdeira,A., Almeida,F., Matos,T., and Reis,J.. (2009). Wine Quality. UCI Machine Learning Repository. https://doi.org/10.24432/C56S3T
  • Fernandes,Kelwin, Vinagre,Pedro, Cortez,Paulo, and Sernadela,Pedro. (2015). Online News Popularity. UCI Machine Learning Repository. https://doi.org/10.24432/C5NS3V

参考情報

この記事は、Fast.AI Deep Learning Courseに触発されました。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more