GitHubコードから読み解く!DecisionTreeClassifier@scikit-learnの各種オプション解説

Taketo Kimura
MICIN Developers
Published in
58 min readApr 25, 2019

--

はじめに

最も直感的な機械学習のアルゴリズムとして、「決定木」が存在します。
Random ForestやBoostingといったアンサンブル手法の基礎アルゴリズムになります。

source:wiki

決定木の学習アルゴリズム

比較的分かりやすいアルゴリズムで学習されるのが、決定木です。
アルゴリズムの全容は、以下となります。

【決定木の学習アルゴリズム】
① ランダムにデータ項目値を選択
② ランダムな閾値を選択
③ ②の閾値境界によるデータ分類の具合から、不純度を計算
④ 任意回数だけ③を繰返した中から、最も不純度を低くした①②の組合せを決定木条件として採用
⑤ ④で選定した条件にて分かれた2つのデータセットに対して、①〜⑤を繰返す

アルゴリズムを経て、仕上がった決定木イメージが、例えば以下となります。

source:scikit-learn

コンピューターの計算力をフル活用して、試行錯誤をひたすら繰返す強引な手法です。
初歩的な機械学習手法であるモンテカルロ手法の応用という感じです。

一意な解析解を求めるような機械学習アルゴリズムを除けば、機械学習全般が試行錯誤による最適化を実現するものですが、決定木は特に試行錯誤感が強いです。
つまり、random seedによって、結果が大きく変わる形です。

その為、精度を安定させるために、Random Forestのような、決定木を沢山作って投票形式で予測を行うような、発送の工夫が生まれるのかと思われます。

DecisionTreeClassifier@scikit-learnについて

決定木の学習アルゴリズムをフォローしているライブラリは様々存在し、scikit-learnもその1つです。
尚、scikit-learnの決定木は以下の特徴を持ちます。

【scikit-learn決定木の特徴】
① 木構造に関しては、CARTアルゴリズムのみを採用、C4.5等のアルゴリズムは現時点で非採用
② 学習用の入力データは数値のみが許され、カテゴリカル値などを用いた学習は実施不可
③ 精度向上に向けた細かいパラメータチューニングが可能であり、それがRandom ForestやBoostingなどにも継承される

特徴の詳細については、以下リンクを参照下さい。

DecisionTreeClassifier@scikit-learnのオプション指定について

DecisionTreeClassifier@scikit-learnの学習は、ソースを見て見る限り、以下アルゴリズムにより実現されているようです。

【決定木の学習アルゴリズム】
① ランダムにデータ項目値を選択
② ランダムな閾値を選択
③ ②の閾値境界によるデータ分類の具合から、不純度を計算
④ 任意回数だけ③を繰返した中から、精度向上に向けたオプション指定条件を満たしつつ、最も不純度を低くした①②の組合せを決定木条件として採用
⑤ ④で選定した条件にて分かれた2つのデータセットに対して、①〜⑤を繰返す

以上。

基本的な決定木のアルゴリズムに、「精度向上に向けたオプション指定条件を満たしつつ、」という文言を追加しました。
DecisionTreeClassifier@scikit-learnには、精度向上のためのオプションが以下の種類だけ存在します。

【精度向上に向けたオプション各種】
① criterion
② splitter
③ max_depth
④ min_samples_split
⑤ min_samples_leaf
⑥ min_weight_fraction_leaf
⑦ max_features
⑧ max_leaf_nodes
⑨ min_impurity_decrease
⑩ class_weight

以上です。
沢山ありますね。

これより以降、これらオプションの意味について、説明をしていきます。
また、精度貢献に対するHot度合を、🌶から🌶🌶🌶で表現します。笑

① criterion(Hot度:🌶)

「criterion」オプションは、不純度計算に用いる式の指定です。
不純度は、値が小さいほど、データセット内でのクラス属性がワンパターンであることを示します。
決定木の条件分岐は、不純度を下げるべく設けていくものです。
データが綺麗に分割できた場合、不純度はZEROとなります。

scikit-learnにて用意されている「criterion」の指定値は、以下2パターンです。

 ⑴ criterion = ‘gini’
⑵ criterion = ‘entropy’

それぞれの式は以下です。

source:THAT-A-SCIENCE

式中の「n」はクラス属性数です。
p(cᵢ)」は、参照データ群のクラス属性が「cᵢ」である可能性を示しています。
或いは、クラス属性「cᵢ」の割合です。
例えば、100人分のデータ群があり、男性「c₁」が30人、女性「c₂」が70人という内訳であると、「p(c₁) = 0.3」「p(c₂) = 0.7」となります。

その際の不純度は、以下となります。

Gini = 1 – {p(c₁)² + p(c₂)²} = 1 − {0.3² + 0.7²} = 0.42
Entropy = { – p(c₁) log₂ p(c₁)} + { – p(c₂) log₂ p(c₂)} = { – 0.3 × log₂ 0.3}+{ – 0.7 × log₂ 0.7} = 0.88

尚、クラス属性数が2である場合には、GiniとEntropyのグラフは以下となります。

2つの不純度指標は、概ね同じ形をしていますね。
左右対称ですので、横軸を「p(c₁)」から「p(c₂)」に切り替えても、グラフの形状は変わりません。
上記グラフの描画コードも一応載せます。

p   = np.arange(101) / 100
eps = 1e-10
g = 1 - (p**2 + (1 - p)**2)
e = (-(p+eps) * np.log2(p+eps)) + (-(1-p+eps) * np.log2(1-p+eps))
fig = plt.figure(figsize=(6,4),dpi=100)
#
plt.plot(p, g, label='Gini', linewidth=3)
plt.plot(p, e, label='Entropy', linewidth=3)
plt.xlabel('p(c_1) = 1-p(c_2)')
plt.ylabel('impurity')
plt.rcParams["font.size"] = 12
plt.grid(True)
plt.legend(loc='lower center')

私の感覚では、「criterion」がGiniだろうが、Entropyだろうが、あまり精度への影響はありません。
default設定のGiniを使っておけば良いかと思います。

以下、検証コードにて、Gini、Entropyによる決定木を比較します。

(先ず、データ準備)

# import lib
import numpy as np
import matplotlib.pyplot as plt
import graphviz
import pydotplus as pdp
import matplotlib.gridspec as gridspec
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from IPython.display import Image, display_png
# data load
data = load_breast_cancer()
X = data.data
y = data.target
X_title = data.feature_names
print('np.shape(X) = [%d, %d]' % np.shape(X))
print('np.shape(y) = [%d]' % np.shape(y))
print('len(X_title) = [%d]' % len(X_title))
print('np.unique(y) = %s' % np.unique(y))
# preparation...
np.random.seed(0)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.7, random_state=0)
file_name = './tree_visualization.png'

(パラメータの異なる決定木作成)

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(criterion = 'entropy',
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(criterion = ‘gini’ [default] )

(criterion = ‘entropy’)

② splitter(Hot度:🌶🌶)

「splitter」は、条件探索アルゴリズムを選択するオプションです。
分岐条件を探索する際に、どう探索するかの方法を設定します。
設定できるのは、以下の2パターンになります。

 ⑴ splitter = ‘best’
⑵ splitter = ‘random’

条件探索方法「best」は、概ね全項目、全閾値を試すという、文字通りbestな方法です。
「概ね」を付けたのは、「max_features」というパラメータ値が1でない場合には、そのパラメータの制御によって、全項目探索を行わない為です。(※詳細は、以降の「⑦ max_features」にて説明します。)

条件探索方法bestの特徴として、隈なく探索を行なっている為に、都度の学習結果(分岐条件)が似通うという特徴があります。
この辺りはBagging適用する場合に、結構議論になるポイントかと思いますが、RandomForestClassifier、ExtraTreesClassifierにおいては、splitterの指定は出来ず、必ず条件探索方法bestが採用されています。
ただし、それらクラスにおける「max_features」のdefault値が1でない為に、上手くensembleが実現されているようです。

条件探索方法「random」は、DecisionTreeClassifierを直接使用する時だけ、選択できるオプションとなります。
この条件探索方法は、文字通り、分岐条件となる項目と閾値をrandomに探索するものです。
特徴として、random探索に付き、決定木構造が深くなりやすい点があります。

以下、検証コードにて、条件探索方法の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(splitter = 'random',
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(splitter = ‘best’ [default] )

(splitter = ‘random’)

③ max_depth(Hot度:🌶🌶🌶)

「max_depth」は決定木構造の深さを調整するパラメータです。
木が過度の深く作られ、過学習を引き起こしている場合は、このパラメータにて、正則化が実現できます。

default設定はNoneとなっており、「限界を設けない」という意味。
深さ指定は、int値を引数に与えてあげれば実現してくれます。

以下、検証コードにて、深さ指定の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(max_depth = 2,
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(max_depth = None [default] )

(max_depth = 2)

④ min_samples_split(Hot度:🌶🌶)

「min_samples_split」は、分岐条件を作成する条件として、分岐元のデータ数として、最低限必要な数を指定するものです。
よって、このパラメータ値以上のデータ数を持たない分岐先は、それ以上の条件分岐がされず、葉となります。
細かすぎる条件分岐は、ノイズへの過敏反応であり、テストデータには通用しなそうである、という発想です。

パラメータ値には、int値を与えるとデータ絶対数にて制御を、float値を与えると全データ数に対する割合にて制御をしてくれます。
default値は、int値2です。

以下、検証コードにて、深さ指定の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(min_samples_split = 0.1,
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(min_samples_split = 2 [default] )

(min_samples_split = 0.1)

⑤ min_samples_leaf(Hot度:🌶🌶)

「min_samples_leaf」は、分岐条件を作成する条件として、分岐先へのデータ数として、最低限必要な数を指定するものです。
よって、このパラメータ値以上のデータ数を持たない分岐先は、作られないこととなります。
細かすぎる条件分岐は、ノイズへの過敏反応であり、テストデータには通用しなそうである、という発想です。

パラメータ値には、int値を与えるとデータ絶対数にて制御を、float値を与えると全データ数に対する割合にて制御をしてくれます。
default値は、int値1です。

尚、このパラメータは「min_sample_split」と非常に概念が似ています。
が、概念的には、min_sample_leafの方が強いです。
というのも、min_samples_splitは、分割前の状態で明らかにニッチな過学習条件が作られる場合にだけ有効である為です。
一方、min_sample_leafは、例えば、急に予想外的に作成されるニッチな条件に対しても反応できる強さがあります。
その意味での推奨は、min_samples_splitよりも、min_sample_leafで制御する方が、旨味が多いかと思います。
(※ここ、実は知り合いからの受け売りです😅)

以下、検証コードにて、min_sample_leaf指定の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(min_samples_split = 0.1,
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(min_samples_leaf = 2 [default] )

(min_samples_leaf = 0.1)

⑥ min_weight_fraction_leaf(Hot度:🌶)

「min_weight_fraction_leaf」は、「sample_weight」を考慮した上での「min_samples_leaf」です。
「sample_weight」については、別途記事を書いていますので、そちらを参照ください。

要するには、データ1つ1つに対して振られている重みがあれば、その総和でもって、条件分岐の生成を抑制する形です。
最低限でも、指定した値以上のweight総和が無いといけない訳です。

パラメータ値には、float値を与えると全データ数に対する割合にて制御をしてくれます。
default値は、ZEROです。

以下、検証コードにて、min_weight_fraction_leaf指定の異なる決定木を比較します。
尚、以下は、sample_weightを設定していない例ですので、min_sample_leafで同じ値を設定した時と、結果が合致します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(min_weight_fraction_leaf = 0.1,
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(min_weight_fraction_leaf = 0.0 [default] )

(min_weight_fraction_leaf = 0.1)

⑦ max_features(Hot度:🌶🌶)

「max_features」は、条件探索を行う際に、説明変数の項目を最大で幾つ参照しに行くかという指定です。

パラメータ指定方法は幾つか存在します。
default設定は、Noneです。
Noneを設定すると、説明変数項目の数が設定されます。
つまり、全項目を参照するという指定です。
int値での設定は、説明変数項目の最大参照数を絶対数で制御します。
float値での設定は、説明変数項目数に対する割合にて、説明変数項目の最大参照数を制御します。

文字列でも以下パターンの設定が可能です。

 ⑴ max_features = ‘auto’
⑵ max_features = ‘sqrt’
⑶ max_features = 'log2'

説明変数項目の数をDとした場合、「sqrt」指定は、max_featuresに「√D」が設定されます。
D=100」ならば、「√D=10」です。
「log2」指定は、「log₂(D)」が設定されます。
D=32」ならば、「log₂(D)=5」です。

「auto」指定は、基本的に「sqrt」指定と同様なようです。
DecisionTreeClassifier、RandomForestClassifier、ExtraTreesClassifier、GradientBoostClassifier等において、そのようでした。
scikit-learn推奨の方法が、設定されている感覚かと思います。

尚、DecisionTreeClassifierのmax_featuresのdefault値はNone(全項目参照指定)ですが、その他ensemble系のアルゴリズムのdefault値は「auto」でした。
RandomForestなどは特に、各決定木が全項目参照していたら、似通った決定木ができてしまいensemble効果が薄れるので、default設定値に納得です。

以下、検証コードにて、max_features指定の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(max_features = 'sqrt',
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(max_features = None [default] )

(max_features = ‘sqrt’)

⑧ max_leaf_nodes(Hot度:🌶🌶🌶)

「max_leaf_nodes」は、最終的に出来上がる決定木の葉の数を、設定値以下に制御するパラメータです。
別パラメータの「max_depth」と深く関連します。

scikit-learnの決定木可視化において、分岐元や分岐先は総じてnodeと表現されます。
nodeの内、分岐元となっていないnode、つまりそれ以降の条件分岐がされていないnodeのことを決定木の「葉」と呼びます。
nodeが「葉」となる場合は2つあり、完全に分岐し切ってしまった場合(下記の「node #3」や、「node #6」)と、オプション指定によってなどがそうです。
木の深さ2の時点で葉となっています。
「node #2」や「node #5」は、まだ分岐しきれていない為、max_depthを3以上に設定すれば、その下に新しい分岐先が生まれることになります。
ですので、これらはオプション指定のコントロールによって、葉となっているnodeとなります。

max_leaf_nodesは、決定木の深さでなく、葉の数で決定木構造のサイズをコントロールするオプション指定です。
例えば、max_leaf_nodesを4に設定すると、決定木構造は以下のようになります。

葉で指定した場合は、不純度が高い葉から順に手当をしていくという特徴があります。
ここでいう「不純度」は、上記図上の「gini × samples」のことです。
この値が大きい葉から順に、条件分岐を追加していきます。
以下、順々にmax_leaf_nodesを増やしていった場合も載せます。

(max_leaf_nodes = 2)

(max_leaf_nodes = 3)

(max_leaf_nodes = 4)

(max_leaf_nodes = 5)

(max_leaf_nodes = 6)

という感じです。
決定木系のアルゴリズムでパラメータチューニングをする際には、max_depthでチューニングする人が多いですが、個人的には、より分けづらいデータにフォーカスするmax_leaf_nodesでのチューニングの方が、筋が良いのではないかと思っています。

パラメータ値には、int値を与えるとnodeの絶対数にて制御をしてくれます。
Noneを与えると、制限なしにnodeを作成していきます。
default値は、Noneです。

以下、検証コードにて、max_features指定の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(max_leaf_nodes = 8,
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(max_leaf_nodes = None [default] )

(max_leaf_nodes = 8)

⑨ min_impurity_decrease(Hot度:🌶🌶)

「min_impurity_decrease」は、分岐元から分岐先に分かつ際に、あまりimpurityが下がらないようならば、その分岐を抑制するためのオプション指定です。

パラメータ値には、float値にて、条件分岐をするために最低限必要なimpurity低下を設定します。
尚、default値はZERO、制限なしとなります。

impurity低下度合については、以下「不純度減少方程式」にて求めます。
この算出値と、min_impurity_decreaseの値とを比較します。

source:sklearn.tree.DecisionTreeClassifier

ここで、「N」は全データの絶対数、または全データの割合「100%」です。
大事なのは各種比率ですので、「N」は絶対数で統一するか、割合で統一するかのどちらでも同じ結果が得られます。
以下、図を使いながら、割合にて説明します。

「N_t」は、分岐条件元でのデータ数割合です。
例えば、「node #2」を分岐条件元と捉えれば「56.5%」です。
「N_t_R」は、分岐条件先でのデータ数割合です。
例えば、「node #4」を分岐条件元と捉えれば「1.2%」です。
「N_t_L」は、分岐条件先でのデータ数割合です。
例えば、「node #3」を分岐条件先と捉えれば「55.3%」です。「impurity」は、分岐条件元での不純度です。
例えば、「node #2」を分岐条件元と捉えれば「0.020616…」です。
「right_impurity」は、分岐条件先での不純度です。
例えば、「node #4」を分岐条件先と捉えれば「0.5」です。
「left_impurity」は、分岐条件先での不純度です。
例えば、「node #3」を分岐条件先と捉えれば「0.0」です。
式に当てはめて計算してみると、凡そ…
56.5 / 100 * (0.021 - 1.2 / 56.5 * 0.5 - 55.3 / 56.5 * 0.0) ≒ 0.0057
となります。

要するには、「データ数×impurity」の低下が大きい条件分岐ほど、決定木精度向上への貢献が高く、逆に、「データ数×impurity」の低下が非常に小さい条件分岐は、学習データへの過学習の疑い強く、汎用性低かろう、という仮説の下、考えられたパラメータという訳です。
尚、上記「データ数」については、「sample_weight」が設定されている場合、「重み付きデータ数」となります。

以下、検証コードにて、min_impurity_decrease指定の異なる決定木を比較します。

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(min_impurity_decrease = 0.01,
random_state = 0)
model.fit(X_train, y_train)
print("acc of test data = %.3f" % model.score(X_test, y_test))
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = X_title,
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True,
precision = 10)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(min_impurity_decrease = None [default] )

(min_impurity_decrease = 0.006)

⑩ class_weight(Hot度:🌶🌶🌶)

「class_weight」は、クラスラベルに対して重みを設定するオプションです。
例えば、2クラス識別問題であれば、パラメータは以下の2パターンで設定します。

 ⑴ class_weight = {0:0.5, 1:1}
⑵ class_weight = 'balanced'

そもそも、なぜこのようなオプションがあるのか、理由を説明します。
そのために、今一度、不純度の計算式を見直しましょう。

source:THAT-A-SCIENCE

不純度は、決定木の分岐条件を選定する際に、参考とする指標です。
この指標を軸に、不純度をより低下させる条件を探索し、決定技条件を採用します。

また、クラス属性数が2である場合には、GiniとEntropyのグラフは以下となります。

Giniで言えば、最大値が0.5です。
それは、データ群のクラス別データ数が、ちょうど半々の状態です。
つまり、「1 - (1 / 2)² - (1 / 2)² = 0.5」という状態で、これが一番不純だということです。
最も不純でない状態は、データ群に片側クラスしか存在しない状態です。
決定木の学習としては、不純度を参考にしながら、葉の不純度がなるべく小さくなることを目指します。

そんな不純度指標ですが、実は不均衡データにおいては、上手く機能しないという問題があります。
尚、当該不純度指標に限らず、機械学習全般に言えることですが、機械学習は基本的に、別クラスのデータは同数が存在する前提となっています。
例えば、2クラス属性データが100個あれば、クラス1のデータが50個、クラス2のデータも50個という想定です。
データ割合「50%:50%」ですね

しかし、仮に、2クラス属性データが100,000個存在した中で、クラス1のデータが99,500個、クラス2のデータが500個だった場合はGini係数が上手く機能せず、クラス2のデータが軽視されるような結果となってしまいます。
データ割合は「99.5%:0.5%」です。
これは、決定木にて条件分岐をする前の状態で、全てのデータをクラス1と予測すれば、「99.5%」の正解率が発揮できる、ということになります。

そして、上記の軽視問題を解決するのが、class_weight問題です。
「class_weight = ‘balanced’」です。
これは、Gini係数での評価を行う際に、多数クラスデータの「sample_weight」を軽く、少数クラスデータの「sample_weight」を重くすることで、あたかもデータ割合「50%:50%」で学習しているような、公平な不純度評価をしてくれる、というものです。

class_weightの算出方法は以下です。

source:sklearn.tree.DecisionTreeClassifier

ここで、「n_samples」は全てのデータ数のことです。
「n_classes」は、クラス数。
「np.bitcount(y)」は、各クラス別のデータ数です。
前述のデータ割合「99.5%:0.5%」の場合…
① クラス1のclass_weight:「100000 / (2 × 99500) = 0.50251…
② クラス2のclass_weight:「100000 / (2 × 500) = 100
となります。

尚、「(① × クラス1データ数)+(② × クラス2データ数)=全データ数」となります。
(0.50251… × 99500) + (100 × 500) = 100000」という感じです。
「sample_weight」を適用しない場合、全てのデータに「weight=1」が振られる為、そのケースと「sample_weight × データ数」の総和にて合致する形です。

それでは、実際に「class_weight = ‘balanced’」を適用してみます。

(データ準備)

# import lib
import numpy as np
import matplotlib.pyplot as plt
import graphviz
import pydotplus as pdp
import matplotlib.gridspec as gridspec
from sklearn import metrics
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from IPython.display import Image, display_png
# prepare data
np.random.seed(0)
N = 100000
N_pos = 500
X = np.random.normal(0, 1, [(N - N_pos), 2]) + np.array([5, 5])
X = np.concatenate([X, np.random.normal(0, 1, [N_pos, 2]) + np.array([8, 5])], axis=0)
y = np.zeros(len(X))
y[-N_pos:] = 1
print('np.shape(X) = [%d, %d]' % np.shape(X))
print('np.shape(y) = [%d]' % np.shape(y))
print('len(X_title) = [%d]' % len(X_title))
print('np.unique(y) = %s' % np.unique(y))
print('np.sum(y == 0) = %s' % np.sum(y == 0))
print('np.sum(y == 1) = %s' % np.sum(y == 1))
# import lib
import numpy as np
import matplotlib.pyplot as plt
import graphviz
import pydotplus as pdp
import matplotlib.gridspec as gridspec
from sklearn import metrics
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from IPython.display import Image, display_png
# prepare data
np.random.seed(0)
N = 100000
N_pos = 500
X = np.random.normal(0, 1, [(N - N_pos), 2]) + np.array([5, 5])
X = np.concatenate([X, np.random.normal(0, 1, [N_pos, 2]) + np.array([8, 5])], axis=0)
y = np.zeros(len(X))
y[-N_pos:] = 1
print('np.shape(X) = [%d, %d]' % np.shape(X))
print('np.shape(y) = [%d]' % np.shape(y))
print('len(X_title) = [%d]' % len(X_title))
print('np.unique(y) = %s' % np.unique(y))
print('np.sum(y == 0) = %s' % np.sum(y == 0))
print('np.sum(y == 1) = %s' % np.sum(y == 1))
#
fig = plt.figure(figsize=(12,8),dpi=100)
gs = gridspec.GridSpec(9,9)
plt.subplot(gs[:6, :6])
plt.scatter(X[(y == 0), 0], X[(y == 0), 1], alpha=0.5)
plt.scatter(X[(y == 1), 0], X[(y == 1), 1], alpha=0.5)
plt.title('principal component')
plt.xlabel('X_1')
plt.ylabel('X_2')
plt.rcParams["font.size"] = 12
plt.grid(True)
# plot histogram
plt.subplot(gs[-2:, :6])
plt.hist(X[(y == 0), 0], alpha=0.5, bins=20, density=True)
plt.hist(X[(y == 1), 0], alpha=0.5, bins=20, density=True)
plt.xlabel('X_1')
plt.rcParams["font.size"] = 12
plt.grid(True)
plt.subplot(gs[:6, -2:])
plt.hist(X[(y == 0), 1], alpha=0.5, bins=20, density=True, orientation="horizontal")
plt.hist(X[(y == 1), 1], alpha=0.5, bins=20, density=True, orientation="horizontal")
plt.xlabel('X_2')
plt.gca().invert_xaxis()
plt.rcParams["font.size"] = 12
plt.grid(True)
plt.show()# preparation...
file_name = "./tree_visualization.png"

2次元データで、データ数割合は「99.5%:0.5%」です。
ヒストグラムは、クラス別の頻度割合になります。

次に、「class_weight = None [default]」と、「class_weight = ‘balanced’」とでの決定木学習ケースを比較します。

(学習コード)

# --------------------------------------------------
# at 1st, no option
model = DecisionTreeClassifier(max_leaf_nodes = 6,
random_state = 0)
model.fit(X, y)
# measure
fpr, tpr, _ = metrics.roc_curve(y, model.predict_proba(X).T[1])
auc = metrics.auc(fpr, tpr)
# plot orc
plt.plot(fpr, tpr, label=('ROC@class_weight=None (AUC = %.2f)' % auc), alpha=0.3, linewidth=5)
plt.legend()
plt.title('ROC curve')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.grid(True)
plt.show
# visualize tree
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = ['X_1', 'X_2'],
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))
# --------------------------------------------------
# at 2nd, set option
model = DecisionTreeClassifier(max_leaf_nodes = 6,
class_weight = 'balanced',
random_state = 0)
model.fit(X, y)
# measure
fpr, tpr, _ = metrics.roc_curve(y, model.predict_proba(X).T[1])
auc = metrics.auc(fpr, tpr)
# plot orc
plt.plot(fpr, tpr, label=('ROC@class_weight=\'balanced\' (AUC = %.2f)' % auc), alpha=0.3, linewidth=5)
plt.legend()
plt.title('ROC curve')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.grid(True)
plt.show
# visualize tree
file_name = "./tree_visualization.png"
dot_data = export_graphviz(decision_tree = model,
out_file = None,
max_depth = None,
feature_names = ['X_1', 'X_2'],
class_names = None,
label = 'all',
filled = True,
leaves_parallel = False,
impurity = True,
node_ids = True,
proportion = True,
rotate = False,
rounded = True,
special_characters = True)
graph = pdp.graph_from_dot_data(dot_data)
graph.write_png(file_name)
display_png(Image(file_name))

(作成された決定木@class_weight = None [default] )

(作成された決定木@class_weight = ‘balanced’)

(ROC比較)

決定木上のvalueの値を見てもらうと、class_weightオプションが適用されていることが、直感的に捉えられると思います。

ROCカーブ、AUCにおいては、class_weightオプションを適用した場合の方が、良好であることが確認できます。

おわりに

以上で、DecisionTreeClassifier@scikit-learnのオプション指定について、説明を終えます。
様々な種類が存在し、様々な正則化アプローチ等が可能な、なんとも有り難いツールです。

また、これがRandomForestやBoostingにも共通するので、一度覚えてしまえば、知識として重宝します。

P.S.

この記事を書くキッカケを与えてくれた、かつ、当該記事を仕上げるに当たって多大なる貢献をしてくれたO氏に感謝します。🙇‍♂️
重ねて、記事のブラッシュアップに貢献してれたS氏に感謝します。️️🙇‍♂️️️

--

--