PyLearnMLCR 01: 決定木

人工知能を構築する手法の一つに、決定木と呼ばれる手法があります。決定木はIf-Then節のルールベースで分類条件が表されますので、数ある人工知能の中で、なぜその予測結果になるのか、人間が解釈しやすいという大きなメリットがあります。データマイニングとしても用いられることも多い、大変便利な分析手法です。例えば、

  • どういう条件を満たした商品が売れるのか?
  • どういう条件を満たしたお店にお客さんがたくさん来るのか?

といった疑問に対して、「もし、製品が[条件A]かつ[条件B]を満たすならば、そのとき、その製品は売れる」「もし、お店が[条件C]を満たしていれば、お客さんがこない」といった出力を返してくれます。ここでは、pythonを活用して決定木を構築する手法について述べます。

分類問題

ダミーデータの生成

決定木は、データを集めて、そのデータから分類規則を獲得することでモデルが完成します。今回は手元にデータがありませんので、ダミーデータを使用します。以下のコードをコピペして実行してください。

In [1]:
import numpy as np
np.random.seed(1) # 擬似乱数シード: 毎回同じ乱数を出す

# Class1: 成績上位群
mu = [7, 7]
sigma = [[0.5, 0.05], [0.05, 0.5]]
Dat01 = np.random.multivariate_normal(mu, sigma, 100)
Label01=[]
for i in range(0, len(Dat01)):
    Label01.append("Good")

# Class 2: 成績中位群
mu = [5.5, 4]
sigma = [[0.8, -0.5], [-0.5, 0.8]]
Dat02 = np.random.multivariate_normal(mu, sigma, 100)
Label02=[]
for i in range(0, len(Dat02)):
    Label02.append("Middle")

# Class 3: 成績下位群
mu = [2, 5]
sigma = [[0.3, 0.0], [0.0, 3]]
Dat03 = np.random.multivariate_normal(mu, sigma, 100)
Label03=[]
for i in range(0, len(Dat01)):
    Label03.append("Bad")

print(np.shape(Dat01))
print(np.shape(Label01))
(100, 2)
(100,)

散布図によるデータ確認

今回は例として、勉強方法から成績を分類する予測モデルを立ててみます。Dat01, 02, 03 はそれぞれ、成績上位群(Good)、中位群(Middle)、下位群(Bad)の「X1:勉強時間」と「X2:理解傾向」が格納された2次元、100人分データとなります。Dat01, 02, 03の1列目がX1、2列目がX2となります。Label01, 02, 03はGood, Middle, Badが100個ずつ並んでいます。訳が分からなくなる前に、printで確認してみてください。合計300人分のデータとなります。

今回は、X1とX2からどのくらいの成績を取れそうなのか、予測するモデルを作ってみます。入力が2次元ですので、散布図としてデータを確認することができますね。まずは確認してみましょう。

In [3]:
import matplotlib.pyplot as plt

x = [Dat01[:,0], Dat02[:,0], Dat03[:,0]]
y = [Dat01[:,1], Dat02[:,1], Dat03[:,1]]

plt.figure(figsize=(5, 4)) # figureの縦横の大きさ

# Goodの散布図
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

# Middleの散布図
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

# Badの散布図
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

plt.title("Relationship: X1, X2 and Score")  # タイトル
plt.xlabel("X1: Studying Time") # 軸名
plt.ylabel("X2: Understanding") #軸名
plt.grid(True)      #グリッド線(True:引く、False:引かない)
plt.xlim(0, 10)  # 横軸最小最大
plt.ylim(0, 10)  # 縦軸最小最大

plt.legend(["Good", "Middle", "Bad"], loc="upper right") # 凡例

plt.show() # グラフの表示

散布図を見ると、どういう人が良い成績を取りやすいのか、考察しやすい形になっていることがわかります(練習用の例ですので、わざわざそういうように乱数を生成させただけです)。その通りに、決定木は学習してくれるでしょうか?

教師データセットの作成

今回形成させたい知能は、X1:勉強時間とX2:理解傾向の二つの数字を与えたとき、試験結果がGood, Middle, Badの中でどれなのか判定させるというものです。この知能を形成させるためには、問題(2つの数字)とその答え(Goodなど)のペアにより構成されるデータセットです。機械学習の分野では、これを教師データと言います。

Dat01, Dat02, Dat03はそれぞれ勉強時間と理解傾向の2つが格納された100人分のデータです。ですから、これが「問題」になるわけです。問題を一つのセットにしたものを変数Xとしてひとまとめにして見ます。なお、機械学習の分野では、推定モデルに入力するデータ(勉強時間と理解傾向)を、特徴量ということがあります。

In [4]:
X=np.concatenate([Dat01, Dat02, Dat03])
np.shape(X) # 気になる人はprintで見ること
Out[4]:
(300, 2)

300人分の勉強時間と理解傾向のデータがあるわけですね。次に、この「問題」に対する「正解」データを用意してあげます。問題が300問あるわけですから、300この正解が必要です。上100人がGood、中央100人がMiddle、下100人がBadの成績を取っています。Label01にGoodが100個、Label02にMiddleが100個、Label03にBadが100個入っているので、それを縦につなげれば良いだけです。

In [5]:
Y=np.concatenate([Label01, Label02, Label03])
np.shape(Y) # 気になる人はprintで見ること
Out[5]:
(300,)

決定木の学習

それではいよいよ決定木により、推定モデルを構築してみます。XとYが機械学習のための教材となります。Xが問題でYが正解になります。Xを入れたとき、Yという出力が得られるように知能が形作られます。

In [6]:
from sklearn import tree
TreeModel = tree.DecisionTreeClassifier(criterion='gini', min_samples_leaf=1)
TreeModel = TreeModel.fit(X, Y)

DecisionTreeClassifierで決定木の学習条件を設定、fitで実際に学習を実施することができます。学習条件にはいろいろな種類がありますが、意識すべきパラメータは以下です。上の例では、criterionとmin_samples_leafのみを指定しています。

  • criterion: 'gini' or 'entropy'
    • 各サンプルを分割したとき、クラスが明瞭に分かれる度合いを算出し、最も明瞭に分割される特徴量を探索します。このとき、「明瞭に分かれた度合い」をジニ係数、情報エントロピー、どちらを採用するかを決めるパラメータです。基本的にどちらを採用しても問題ありません。学習結果が不適切だと感じたとき、変えてみるといいと思います。個人的にはエントロピーの方が、評価関数が好みです。
  • max_depth: 整数

    • 木の深さを指定するパラメータです。一般に、深い木は過学習の恐れが極めて強くなるので、オススメしません。ただし、浅い木は単純な条件しか獲得できなくなるので、ある程度深いことも必要です、トライアンドエラーでいい値を探しましょう。
  • min_samples_leaf: 整数

    • 決定木は、サンプルを2分割し続けていくことで学習が実施されていきます。2分割されていった最先端のノードをリーフノードといいます。リーフノードにあるサンプルの中で、最も多いクラスが、判定クラスとなります。そのため、リーフノードには一定数以上のサンプルがなければ、判定クラスに信頼がおけなくなってしまいます。このため、このパラメータによりリーフノードのサンプル数の最小値を決定します。教師データと相談する必要がありますが、ある程度大きな値を指定することが必要でしょう。この値に1を与えたとき、多くの場合、すべての教師データが正しく分類される分岐条件が獲得されるので、過学習となります。
  • class_weight: None or 'balanced'

    • 教師データのクラスラベルに偏りがあるとき、偏りが大きいクラスと回答した方が正答率が上がってしまいます。このように、クラスラベルに偏りが生じていると、学習が不適切なものになる場合があります。これを解消するために、クラスラベルに重みをつけることができます。class_weightはこのためのパラメータです。もし、クラスラベル間に大きな偏りがあると感じる場合は、'balanced'を指定してください。重み付けをしない場合は、何も記載しないか、Noneを記載します。

ツリー構造の可視化

続いて、学習したモデルを可視化してみます。画像ファイル、pdfファイル、jupyterで表示したい場合で、いろいろな書き方があります。

In [7]:
import pydotplus
dot_data = 'Tree.dot'
tree.export_graphviz(TreeModel, out_file=dot_data) # モデル入力
graph = pydotplus.graphviz.graph_from_dot_file(dot_data)

# 保存
graph.write_png('Tree.png') # pngで保存
graph.write_pdf('Tree.pdf') # pdfで保存

# jupyterで表示
from IPython.display import Image
Image(graph.create_png())
Out[7]:

valueの部分が、各クラスのサンプル数です。一番上(ルートノード)は100, 100, 100となっていますね。これは、Bad, Good, Middleのサンプル数です。まったく分割していませんので、全データがあります。2段目の左側のボックスを見てみます。これは、最先端のノードなので、実際の判定クラスとなるリーフノードを意味します。Badクラスが99例もあ流ので、当然Badと判定されます。このノードへは、「X[0]が3.196以下」が「True(真、肯定)」のときいくわけですね。一番最初の特徴量は勉強時間ですから、勉強時間が大体3時間より少ないと成績が悪いよ、と言っているわけです。他のノードも同じような読み方をしていきますので、自分で読んでみてください。

分類結果の取得

この決定木の推定結果を得る方法を以下に記載します。学習済みモデルを何らかのシステムに実装するとき必要になります。

In [8]:
InputDat = [[4, 5]]
OutputDat = TreeModel.predict(InputDat)
print(OutputDat)
['Middle']

勉強時間が4で、理解傾向が5だと、成績は中くらいと予測されました。これが正しいかはまた別の問題ですが、決定木の出力を得ることができました。

特徴量空間の可視化

別の可視化方法として、特徴量によって構成される空間(特徴量空間)そのものみる、という手段もあります。ただしこちらは、特徴量が2つの場合、つまり2次元空間の場合のみしかできません(少し工夫すれば4次元空間くらいまでは見れなくもないです)。基本的なアプローチとしては、

  • (X, Y)=(0, 0), (X, Y)=(0, 1), (X, Y)=(0, 2)...(X, Y)=(10, 10)というように、格子状の入力を決定木に与え、判定クラスを得る。
  • 判定クラスごとに色を塗り、空間を可視化する。 となります。

まずは、格子状の入力データを用意して見ます。

In [9]:
Xmin, Ymin, Xmax, Ymax = 0, 0, 10, 10 # 空間の最小最大値
resolution = 0.1 # 細かさ
x_mesh, y_mesh = np.meshgrid(np.arange(Xmin, Xmax, resolution),
                             np.arange(Ymin, Ymax, resolution))
MeshDat = np.array([x_mesh.ravel(), y_mesh.ravel()]).T
print(np.shape(MeshDat))
print(MeshDat)
(10000, 2)
[[0.  0. ]
 [0.1 0. ]
 [0.2 0. ]
 ...
 [9.7 9.9]
 [9.8 9.9]
 [9.9 9.9]]

ここは少し難しいので、頑張って解読して見てください。printを活用して一つ一つ見て見るといいです。MeshDatを見ると、(0, 0), (0.01, 0) ... (9.99, 9.99)というデータが用意されていることがわかります。これで、先ほど示した散布図の全領域のデータが揃いました。これを、決定木に判別させます。

In [10]:
z = TreeModel.predict(MeshDat) # 決定木に判別させる
z = np.reshape(z, (len(x_mesh), len(y_mesh))) # 空間のためにデータ整形
print(z)
[['Bad' 'Bad' 'Bad' ... 'Middle' 'Middle' 'Middle']
 ['Bad' 'Bad' 'Bad' ... 'Middle' 'Middle' 'Middle']
 ['Bad' 'Bad' 'Bad' ... 'Middle' 'Middle' 'Middle']
 ...
 ['Bad' 'Bad' 'Bad' ... 'Good' 'Good' 'Good']
 ['Bad' 'Bad' 'Bad' ... 'Good' 'Good' 'Good']
 ['Bad' 'Bad' 'Bad' ... 'Good' 'Good' 'Good']]

学習したモデルに対し、predictをつけ、判定させたい入力を入れると、判別結果が返ってきます。MeshDatは行が長いのデータなので、判別結果が格納された変数zにも、行に長い結果が入っています。これでは空間になりませんので、サイズ変形させます。

これで準備が揃いました。可視化して見ます。

In [11]:
import matplotlib.pyplot as plt

# Bad, Middle, Good領域
plt.scatter(x_mesh[z=='Bad'], y_mesh[z=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z=='Middle'], y_mesh[z=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z=='Good'], y_mesh[z=='Good'], s=5, alpha=0.3, c='blue')

plt.title("Feature space by the Decision Tree")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)
plt.legend(["Bad", "Middle", "Good"], loc="upper right", bbox_to_anchor=(1.3, 1))
Out[11]:
<matplotlib.legend.Legend at 0x10fb61cf8>

これが、決定木の学習によって形成された知能の出力です。赤がBad, 黄がMiddle, 青がGoodです。勉強時間が3時間未満だと成績が悪いと判断しています。勉強を5時間以上し、理解傾向が5以上だと、成績が良いと判断することもわかります。それ以外が成績は中程度です。比較的解釈しやすい結果ですが、理解傾向が2だと、どれだけ勉強していても成績が悪いと判断されてしまう空間になっていることがわかります。それで、理解傾向を1に下げると成績が上がることになりますね。これは納得できない結果です。なぜこのような結果が出たのか、教師データをこの空間にプロットし、考察して見ます。

In [12]:
# 決定木の判定結果(Bad, Middle, Good領域)
plt.scatter(x_mesh[z=='Bad'], y_mesh[z=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z=='Middle'], y_mesh[z=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z=='Good'], y_mesh[z=='Good'], s=5, alpha=0.3, c='blue')

# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

plt.title("Feature space by the Decision Tree")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)
plt.legend(["Bad", "Middle", "Good",
            "Bad (TrainData)", "Middle (TrainData)", "Good (TrainData)"], 
           loc="upper right", bbox_to_anchor=(1.5, 1))
Out[12]:
<matplotlib.legend.Legend at 0x10fc83240>

勉強時間が4、理解傾向が2のところに、一つだけBadラベルが付与されたデータがあります。決定木はこれを正しく分類しようとしたために、おかしい判定条件が形成してしまった、というのが原因です。このように、教師データを正しく分類し過ぎようとして、未知のデータを正しく分類できないような条件が形成される状態を、過学習(Over learning)と言います。教師データの精度を高めることは大変重要なことですが、教師データの精度は高めすぎると良くない、と言われる所以です。

過学習の回避

過学習が起きてしまった理由は、リーフノードの最小サンプル数を1に設定したことです。データが1つになるまできれいに分類することを許可してしまっています。これは、極めて稀な異常なデータを分類することを許容することを意味します。過学習を回避したモデルを作って見ます。

In [13]:
# リーフノードの最小サンプル数を10に設定
TreeModel2 = tree.DecisionTreeClassifier(criterion='gini',
                                         min_samples_leaf=10)
# 学習
TreeModel2 = TreeModel2.fit(X, Y)

# 可視化用データ整形
z2 = TreeModel2.predict(MeshDat) # 決定木に判別させる
z2 = np.reshape(z2, (len(x_mesh), len(y_mesh))) # 空間のためにデータ整形

# 可視化
# 決定木の判定結果(Bad, Middle, Good領域)
plt.scatter(x_mesh[z2=='Bad'], y_mesh[z2=='Bad'], s=5, alpha=0.3, c='red')
plt.scatter(x_mesh[z2=='Middle'], y_mesh[z2=='Middle'], s=5, alpha=0.3, c='yellow')
plt.scatter(x_mesh[z2=='Good'], y_mesh[z2=='Good'], s=5, alpha=0.3, c='blue')

# 教師データ(Bad, Middle, Good)
plt.scatter(Dat03[:,0], Dat03[:,1], s=50, c='red', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat02[:,0], Dat02[:,1], s=50, c='orange', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')
plt.scatter(Dat01[:,0], Dat01[:,1], s=50, c='blue', marker='s', 
            alpha=0.8, linewidths=0.5, edgecolors='black')

plt.title("Feature space by the Decision Tree")
plt.xlabel("X1: Studying Time")
plt.ylabel("X2: Understanding")
plt.grid(True)
plt.xlim(0, 10)
plt.ylim(0, 10)
plt.legend(["Bad", "Middle", "Good",
            "Bad (TrainData)", "Middle (TrainData)", "Good (TrainData)"], 
           loc="upper right", bbox_to_anchor=(1.5, 1))
Out[13]:
<matplotlib.legend.Legend at 0x10fdc89e8>

先ほどとは異なり、教師データを正しく分類出来ていない領域があります。しかし、かなりシンプルな識別境界が生成され、解釈も容易です。これが正しいモデルであるという証明はできませんが、明らかな誤りがないので、過学習はしていないと判断することができます。このように、過学習を起こさないモデルの構築を心がけましょう。

回帰問題

決定木は、入力変数から状態を判断するクラス分類問題以外にも、入力変数から実数値を判断する回帰問題も解くことができます。

In [14]:
from sklearn import tree

# 教師データ
Xreg = [[1,2], [3, 4], [2, 1], [3, 5]]
Yreg = [15,     24,     13,     55]

# 決定木の学習
TreeModelReg = tree.DecisionTreeRegressor(min_samples_leaf=1)
TreeModelReg = TreeModelReg.fit(Xreg, Yreg)

# 推定
InputDat = [[2, 2]]
OutputDat = TreeModelReg.predict(InputDat)
print(OutputDat)
[13.]

2次元の入力に対し、実数値を出力する回帰モデルを構築して見ました。2, 2を入力すると15が出力されていることがわかります。