記事の概要
簡単なデータセットを使って決定木を用いた分類を行うことで、決定木の仕組みを改めて理解し直すための記事。参考にした本は、scikit-learn、Keras、TensorFlowによる実践機械学習 第2版(以下、実践機械学習)。
※ pythonのバージョンは3.12を使用している。
なぜ本記事を書いたのか
研究でランダムフォレストを使って分類を行っているのだが、決定木の仕組みがしっかり分かっていなかったため、改めて勉強し直す必要があると感じた。
決定木とは
実践機械学習の第6章に取り上げられている。決定木は教師あり学習で、分類と回帰の両方に使える。損失関数はジニ不純度などが使われ、一番不純度が低くなるような閾値を貪欲法で探索する。
メリットとしては、特徴量スケーリングが不要な点と、どの特徴量を重要視しているかの可視化が可能な点が挙げられる。デメリットとしては、木の深さを制限しなければ、簡単に過学習してしまうと言う点と、訓練セットの微量な変化にも敏感に反応してしまうと言う点が挙げられる。
これだけでは理解が難しいため、実際にライブラリを使って分類を行ってみる
決定木を使った分類を行う
実践機械学習を参考に、scikit-learnのirisデータセットを使って分類を行う。
以下のコードを実行すると、irisのデータを取得できる。
※irisはアヤメ花のことで、3種類のアヤメ花を分類するためのデータセットである。特徴量としては、花弁の長さ、花弁の幅、がく片の長さ、がく片の幅が含まれている。ここでは特徴量を可視化しやすいように、花弁の長さと花弁の幅の2つだけを使う。
from sklearn.datasets import load_iris
# Irisデータセットをロード
iris = load_iris()
# 視覚化しやすいように、2つの特徴量だけを使用
X = iris.data[:, 2:]
y = iris.target
print("X[:5]:", X[:5])
print("y:", y)
# 出力
# X[:5]: [[1.4 0.2]
# [1.4 0.2]
# [1.3 0.2]
# [1.5 0.2]
# [1.4 0.2]]
# y: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
# 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
# 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
# 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
# 2 2]
次に、決定木を使って分類を行う。以下がそのコードである。また、export_graphvizを使うことで、決定木の可視化を行うことができる。つまり、このモデルがどの特徴量を重要視して分類を行っているかということを人の目で確認することができる
from sklearn.tree import DecisionTreeClassifier
# max_depthを変えることで、木の深さを変更できる
tree_clf = DecisionTreeClassifier(max_depth=2)
# 学習
tree_clf.fit(X, y)
from sklearn.tree import export_graphviz
export_graphviz(
tree_clf,
out_file="iris_tree_class.dot",
feature_names=iris.feature_names[2:],
class_names=iris.target_names,
rounded=True,
filled=True
)
上記のコードを実行すると、iris_tree_class.dot
というファイルが生成される。以下のコマンドを使うことで、.dotファイルをpng画像に変換することができる。dotコマンドのインストールはこちらを参照。
dot -Tpng iris_tree_class.dot -o iris_tree_class.png
それぞれ囲まれた部分はノードと呼ばれ、白い部分はまだ分類が完了していない部分を示している。一方色付けされた部分は葉ノードと呼ばれ、その部分に分類されたクラスが表示されている。いろいろ文字が書いてあるが、以下で簡単に説明する。
- petal width (cm) <= 0.8: 花弁の幅が0.8cm以下の場合
- samples: そのノードに属するサンプルの数
- value: そのノードに属するサンプルのクラスごとの数
- gini: ジニ不純度(詳細はこちらを参照)
- class: そのノードに属するサンプルのクラス
一番上のルートノードから始まって、花弁の幅が0.8cm以下の場合は、左側の子ノードに行き、そこから分岐していないのでsetosaと分類される。花弁の幅が0.8cmより大きい場合は、さらに花弁の幅が1.75以下の場合はversicolorと分類される。そうでなかった場合はvirginicaと分類される。
ニューラルネットワークなどでは、どの特徴量が重要視されて結果が出ているのかを理解することはほぼできない(ブラックボックス)。このように結果の要因が目で確認できる(ホワイトボックス)ことは、データ分析を行う上でのヒントになり得るため、非常に重宝する。
また、決定木は回帰にも使える。以下のコードを実行すると、決定木を使って回帰を行うことができる。
from sklearn.tree import DecisionTreeRegressor
# ClassifierからRegressorに変更
tree_reg = DecisionTreeRegressor(max_depth=2)
tree_reg.fit(X, y)
export_graphviz(
tree_reg,
out_file="iris_tree_reg.dot",
feature_names=iris.feature_names[2:],
class_names=iris.target_names,
rounded=True,
filled=True
)
再度dotコマンドを使うことで、以下の画像を生成できる
- value: そのノードのターゲット変数の平均値(あまり気にしなくても良い)
- squared_error: そのノードのターゲット変数の平均値からの二乗誤差
※ ここでターゲット変数はクラスを区別するための0, 1, 2のような数。とあるノードに属しているクラスの平均がvalueとして表記されている。試しに一番下のノードのvalueを見ると、それぞれ0、1、2に近い値になっていることがわかり、ある程度正しく分類されていそうなのがわかる。
補足として、計算量の説明をする。閾値を見つけるためにデータ量の数だけ探索する必要があり、さらにそれを特徴量の数だけ繰り返す必要があるため、nを特徴量、mをデータ数とすると、O(n * m log(m))となる。
終わりに
決定木を改めて使ってみることによって、この手法のメリットデメリットをある程度理解することができた。しかし、ゼロから作るDeep Learningを使って勉強している時にも感じたが、ライブラリに頼らず0から作った方がより手法についての理解を深めることができると思っているため、近いうちに実装してみたい。そこまで複雑な手法ではないはず…