ytyt blog

気まぐれで更新します。。

Python scikit-learn を使って決定木学習

 

この記事では、できるだけ詳しく説明をつけながら、手軽に学習を実行できる"Python scikit-learn を使って決定木学習"をやっていきたいと思います。

 

決定木を用いた分類は、分類されたデータの得られた結果の意味を理解しやすいか(意味解釈可能性)に配慮する場合に魅力的なモデルです。このモデルでは、データの特徴区間を矩形(角が直角になるような形)に分割することで、データの決定境界を構築するために、データに対していくつかの質問(分類基準)を行い、その結果からそれぞれどのクラスラベルに含まれるかを判断し、データを分類します。

 

 

 

といっても、わかりにくいので、図で極端に書くと以下のような感じです。

ここでは、私の大学に居るような大学生を例に描いています。

       f:id:xedvc3-phone-8:20170123110341p:plain

大量の大学生から、データをとり、そこから作成した決定木分類器が上のようにあるとします。

例えば、学生Aくんは、[今日は講義がある|課題は終わっている|やる気はない]の条件を持つとします。この場合、Aくんはrootの"今日は講義があるか?"から判別にかけられて、1段階目では、講義があるので、次の"課題は終わっているか"の2段目の子ノードへ流れます。Aくんは課題は終わっているので、ここでは、"もっと、寝よう!!!"の子ノードへ移動します。

この結果から、Aくんは、本日は"もっと、寝よう!!!"に分類(予想)されます。これが、人の行動予測であれれば、1日中寝る日と分類されたわけです。
 そんなの、寝るべき一択だろ!!

 

ここで、決定木学習の 過学習 落とし穴に触れておきます。
(以下、過学習の説明なので読み飛ばしてもらっても構いません)

 

決定木学習では、データサンプルから決定木を作成するのですが、例えば、先ほどの例では、質問が3つと少ないですよね? また、その質問に対しての答えも2つしか(1つの親ノードあたりに子ノードが2つしか)ないです。
大学生の中には意識高い人であれば、講義や課題がなくても大学に行く人、寝るだけではなく、もっとサークルやバイトなど様々な人が居ると思います。そのような人をこの分類器にかけるとうまく分類出来ませんね。

では、このような人たちもデータサンプルとして決定木作成を行うことを考えましょう。

決定木の親ノードでの判別にはデータの持つ特徴量の大小などで判断します。では、データサンプルが100人分10項目のデータがあったとしましょう。このデータで決定木を作成するとして完全にフィットした決定木を作成すると、1段目のrootで複数個の子ノードが作成され、その子ノードそれぞれで、更に複数個の子ノードが作成される。このようなことが連鎖的に行われ、結果として決定木が非常に大きなものとなります。これでは、決定木を作成するのに非常に時間がかかりますし、データが完全に別れてしまうので、関連性など人間が見て分かるものが少ないのです。

更に、その決定木に未知のデータを通した時、学習用のデータセットに例がなければうまく分類することが出来ないことも挙げられます。

このように、決定木によって学習用のデータセットに対してはほぼ100%の精度で分類することが、未知のデータに対してはうまく分類が出来ない状態を 過学習 といいます。
過学習は多くの場合で、好まれない状況です。

もちろん過学習を防ぐことは、学習用のデータセットの分類精度を100%から下げる 代わりに、未知のデータの分類精度を上げる ことになりますが、現実には未知のデータに対しての評価を行うことがほとんどなので、むしろ未知のデータに柔軟に対応でき、分類精度を高める方が望ましいと基本的にはされています。

 

 

 

では、前置き・過学習に関してはこのあたりまでとして、 そろそろ実際にやっていきましょう。

 

今回使用するライブラリは以下の3つです。

  • scikit-learn (1.18.1)
  • matplotlib (2.0.0)
  • myplot (自作)

 

まず、実装したプログラムは以下のものです。

3-6.py

 

では、コードをおおまかに解説します。

1〜7行目:使用するライブラリについて記述しています。ここで7行目に、"myplot"という見慣れないものがありますね。これは、自分で作成したライブラリmyplot.pyを使用しています。ここで詳しく実装内容については説明しませんが、このライブラリでは、 結果データをプロットしているもの と理解していただければそれだけでOKです。
更に、ここでは、題にもなっている scikit-learn の tree を用いていることも重要です。

 

11〜13行目:ここでは、使用する分類器作成に使用するトレーニングデータ と、そのトレーニングされた分類器にかけるテストデータをirisから整形しています。 12, 13行目をもっと具体的に説明すると、12行目では、irisのデータセットの中から、データラベル(X)とクラスラベル(Y)を取り出しています。13行目では、train_test_split()を用いて、X,Yの中からランダムに0.3:0.7[train:test]の割合でトレーニングデータとテストデータを分割しています。
まとめると

  • x_train, y_train:データラベルX,クラスラベルYの分類器作成のためのトレーニングデータ
  • x_test, y_test:データラベルX,クラスラベルYのテストグデータ

です。ちなみに、Xは 2×150データ([x0,y0], [x1,y1],・・・,[x149,y149])で、YにはXの列分に対応するクラスラベルが150個入っています。また、Yはクラスラベル(0,1,2)の3値を取ります。

 

16〜17行目:ここでは、具体的に決定木学習を行う際の設定をDecisionTreeClassifier()に対しておこなっています。この引数について少し説明しようと思います。第一引数では、決定木学習の分岐指標を設定しています。Treeは親ノードから子ノードに分岐するような形態を取るのですが、どのような条件で分岐するのか?をここでは平均情報量である"entropy"を用いています。この分岐条件には他にも、ジニ分散指標 などいくつかあるので、詳しくは公式ページを参照してくだい。次に第二引数(max_depth)では、作成される木の深さ(レベル)を設定しています。今回ではレベルは3で設定しています。この値ももちろん変更することができ、データ数を超えないほどに大きな値にすればある程度精度は良くなります。かといって、大きすぎる値になるとあまりうまくいきません。このことに関しては後の実行結果あたりで記述します。第三引数のrandom_state()では、内部で使用する乱数についてのseed値の設定です。17行目では、先ほどの設定にトレーニングデータを通して決定木を作成(学習)させています。更に、本来であれば、ここではテストデータのみを通せばいいのですが、あえて全データX, Yをそのまま通します。

 

18〜22行目:ここでは、決定木学習器に通したデータを描画しています。それだけです。

 

24行目:ここでは、分類したデータをより見やすく描画できるように".dot"ファイルで出力しています。後の実行結果でどのように使用されているのか説明・可視化しているので詳しくはそちらで。

 

 

 

ではでは、コードの説明もこのへんとして、ここからは実際の実行結果について書いていきます。

 

3-6.py を実行した結果、以下のようなグラフと、24行目で出力設定した"tree.dot"のバイナリファイルができます。

f:id:xedvc3-phone-8:20170213165939p:plain

データラベルXの要素、( width, length)からなるグラフで、単に横軸:width, 縦軸:length であってそれが何かではなく、とりあえずその2つの要素からなるものと思ってください。

このグラフでは、色付けしてデータのラベル予測した範囲と、実データの様子を示しています。つまり、width の大きさにかかわらず、length が大体0.75以下であれば、赤色のラベルに分類されるということを示しています。青色、緑色も同様に読み取ることができます。

精度評価ですが、赤色の部分はうまく分類できていることがわかります。また、青色に関して一部緑色の領域に青色のマーカーがあることから、完全には分類できていないことがわかります。緑色からみた評価も同様です。

 

また、プログラムこのバイナリデータを25行目にあるコマンドで展開すると、以下のようなグラフが出力されました。

f:id:xedvc3-phone-8:20170213162554p:plain

 

まず、16行目で設定している "max_depth=3"より、レベルが3で設定通り出力分類ができていることが ぱっと 見た感じでわかりますね。

一番上のレベル0ノードを見てみると、length が0.75以下であれば、赤色のノードの分類されることをここでは示しています。グラフの考察と一致してますね。正確には、平均情報量(entropy)の値に従って変化させた結果、そのように分類される結果となったと捉えます。

 

レベル1のノードでは、length が 4.95を基準に青色、緑色に分類されていることがわかります。本来では、この分類で、赤、青、緑 の3つのクラスラベルに分類されているので、いいように思えますが、このまま描画すると以下のようになってしまい、まだまだ上手く分類できてないことがわかります。

f:id:xedvc3-phone-8:20170213170732p:plain

レベル2のノードでは、更に細かく、length, width の条件を追加するように分類されていることがわかります。この分割後の結果をグラフに表したものが最初にプロットされた図です。mat_depth=3 にした効果が出ていると実感できるのが、このレベル2のノードを見ると、緑色のノードから、緑色と青色の両クラスのノードが作成されていることがわかります。こればより細かく分割できているので制度的にお良くなった捉えることができ、今回ではmax_depth も 2 より 3 のほうが分類結果は良いとわかります。

 

 

実行は、異常なのですが、max_depth の値を大きくすれば精度が良くなるとすれば、どのくらいが妥当なのでしょうか?大きい値を使用するほど精度が良くなると言えるのでしょうか?

 

先に答えを述べるとそれは 99%No です。

まず、No の理由ですが、最初らへんで 過学習 について触れました。まさにここでは、過学習を起こしてしまう可能性があるので、max_depth が大きれば良いというのは一般的に正しいとは言えません。また、max_depth を大きくするということは、決定木が大きくなることを示し、結果として、過学習になるし、計算に時間かかるしで、あまり良いことはないと私は考えます。

 

99%の理由は何でしょうか?それは、場合によっては、うまくいこともあるのです。なので、可能性を残すような書き方をさせていただきました。しかし、一般的には良くはないです。

 

では、具体的にmax_depth はどのくらいに設定するのが良いのでしょうか?今回使用したのを例に説明します。まずは、max_depth = 4の図を載せます。

f:id:xedvc3-phone-8:20170213162712p:plain

さぁ、どうでしょう。最下層ノードでは、青色から、青色、または、はっきりと分類できてない白色になりました。これはmax_depth=3 と比べてどうでしょう。私が考えるに、精度向上の可能性はあるがあまり意味のない分類と考えます。結果として、これをグラフ化しても、max_depth=3 と分類はほとんど変わらないグラフが出力されます。

 

先程も述べたように、計算量を増やしても、一つ前より大きな変化が見られない場合は、そのレベルでの計算は必要ないと私なら考えます。

 

実際、このmax_depthのような閾値をどこで引くのかが、技術者の腕でもあります。私の閾値の引き方も、人から見れば正しくないと捉えることもあります。みなさんがどのように引くのかは、ご自分で選んでください。私の引き方は参考程度にしてください。

 

 

 

なんか、堅苦しくなってしましましたが、今回はこのへんで、、、

 

 

 

非常に参考にさせていただきました。

 

Rosenblatt の単純パーセプトロンの学習アルゴリズム うんぬん

初めて技術系ブログというのを書くのでいろいろお許しください。。

 

最近流行りの「学習」には、入力するデータのクラスラベルがあらかじめ用意されている「教師あり学習」と、そのクラスラベルがない「教師なし学習」というものがあります。

 

今回は、その中でも、「教師あり学習」の中のデータをクラスラベル( -1, 1) のどちらかの2値に分類する、基礎的な Rosenblatt の単純パーセプトロンの学習アルゴリズムPythonで実装 まで目標とします。

2値分類なのでデータが線形分離可能な場合のみ適応出来ます。データが非線形分離の時は、単純パーセプトロンは適応できないため、別の多層パーセプトロンを適応する必要があります。

 

 

 

で、"Rosenblatt の単純パーセプトロンの学習アルゴリズム" と言われても ぱっ としないので、自分なりに説明を交えながら、やっていきたい思います。

 

前提としてここでは、入力値を\boldsymbol{x}、それに掛かる重みベクトルを\boldsymbol{w}とします。 

 

\[
\boldsymbol{w}
=
\left[
\begin{array}{c}
w_{1} \\
w_{2} \\
\vdots \\
w_{m} \\
\end{array}\right]
\ ,\ \
\boldsymbol{x}
=
\left[
\begin{array}{c}
x_{1} \\
x_{2} \\
\vdots \\
x_{m}\\
\end{array}
\right]
\]

\[
Z
=
w_{0}x_{0} + w_{1}x_{1} + w_{2}x_{2} + \dots + w_{m}x_{m} = \boldsymbol{w}^{\boldsymbol{T}}\boldsymbol{x}
\]

 

Zは入力値とそれに掛かる重みの総入力の式です。 はい、w_{0}x_{0}がいきなり入っていますね。このw_{0}x_{0}のうち、w_{0}は予測判定閾値の変動の役割を果たしていて、このw_{0}によって予測判定の結果が変化します。x_{0}はそれに掛かる変数と形式上記述しています。

少し先に説明しますと、この総入力Zから、クラスラベルを判定する関数は以下のような式で表現することができます。

 

\begin{eqnarray}
\Phi(Z)=\left\{ \begin{array}{ll}
1 & (Z \geq \theta) \\
-1 & (Z < \theta) \\
\end{array} \right.
\end{eqnarray}

 

この式は、総入力Zの値とその判別基準任意の閾値{\theta}の関係性で、予測値が決まるような関数です。このように決まった値のみを返す恒等関数を別名活性化関数といいます。更に、活性化関数の返り値が 0, 1 のとき、特に単位ステップ関数、もしくはヘビサイド関数とも呼ばれます。 

 

総入力Zに話を戻しましょう。閾値の変動は\boldsymbol{w}重み更新があった時のみ、その更新した分を反映させるものなので変数x_{0} = 1として、x_{0}自身による変動量はないものと考えます。x_{0} = \theta は前提のもとここでは、(w_{0}, x_{0}) = ( \theta, 1)として活性化関数を考えます。つまりZ閾値 0 で(1, -1)に分類されます。

この条件では、Zに関する活性化関数は単純になり、Zが 0以上で1, 0未満で-1 と理解がしやすいと思います。(Z\thetaを右辺へ移項すれば\theta - \theta = 0になるので)


\[
Z = 0 + w_{1}x_{1} + w_{1}x_{1} + \dots + w_{m}x_{m} \\
\]
\[
\Phi(Z)=\left\{ \begin{array}{ll}
1 & (Z \geq 0) \\
-1 & (Z < 0) \\
\end{array} \right.
\]

 

ちなみに、この活性化関数で入力値のクラスラベルを予測してデータを線形分離するので、活性化関数は非常に大切なものです。 

 

ここからは、題にある"学習アルゴリズム"について触れていきたいと思います。

簡単にまとめると以下のようにまとめられます。

  1. 重みを0又は値の小さい乱数で初期化する。
  2. トレーニングサンプルx_{i・}ごとに以下の手順を実行する。
    ① 出力値\hat{y}を計算する。
    ② 重みを更新する。

出力値\hat{y}は活性化関数によって予測されるクラスラベルを指しています。また、以降の説明で出てくるx_{j}は、入力値行列のi行目を指しています。

 

重みの更新に関して、簡単に説明をします。
重みベクトル\boldsymbol{w}の各重みw_{j}の更新は、以下の式で行われます。

\begin{eqnarray}
w_{j} = w_{j} + \Delta{w_{j}}
\end{eqnarray}

\Delta{w_{j}}w_{j}の更新に使用される重みの変化量で、式で表現すると、

\begin{eqnarray}
\Delta w_{j} = η\left( y_{j} - \hat{y}_{j} \right)x_{j} \ \ \ \  \left( 0 < η \leq 1 \right)
\end{eqnarray}

ここでηは学習率を指しています。 この式が重み更新の要で、一見するとただの式なのですが、具体的に値を代入してみるとわかりやすいかと思います。

今、( 学習率100%, クラスラベル, 予測値, 入力値) = (η, y_{i}, \hat{y}_{i}, x_{j}) = ( 1.0, 1, -1, 0.5)として考えると、

\begin{eqnarray}
\Delta w_{j} = 1.0 \left( 1 - (-1) \right)0.5 = 1
\end{eqnarray}

で、クラス予測値\hat{y}_{i}が -1 で間違えた値で、最終的な計算結果は\Delta w_{ij}は +1 となりました。この結果は、次回の予測値\hat{y}_{i}が正しく分類できるように、この要素では、値が正の方向へ働きかけるような作用をもたらしています。この操作を各要素で行うことで、全体として予測の精度が上がるようになっています。 

今度はクラスラベルと予測値の政府が逆だった場合を考えてみましょう。今度は、( 学習率100%, クラスラベル, 予測値, 入力値) = (η, y_{j}, \hat{y}_{j}, x_{j}) = ( 1.0, -1, 1, 0.5)なので、計算は以下のようになります。

\begin{eqnarray}
\Delta w_{j} = 1.0 \left( -1 - 1) \right)0.5 = -1
\end{eqnarray} 

今度は、\Delta w_{j}は負の方向へ働き書けるような作用をもたらしていることがわかります。このように、値に応じて、また予想結果に応じて、次回の計算でうまく予測できるような仕組みになっています。

 

 

 

主な学習アルゴリズムは以上になります。

 

 

次は、これを元にPython で実装してみます。
なお、分からないライブラリ関数等の動きは自分で調べてください。

 今回使用するライブラリは以下の1つです。

  • numpy  (1.11.1)

 

1.実際に学習する Perceptronクラス を作成します。

まず、Perceptronクラスを定義します。以下ではクラス内のメソッドについて説明しています。

コンストラクタは __init__メソッド として定義します。第一引数が、selfで、それぞれのインスタンス自身を指します。第二引数以降で学習率(eta)、学習回数(n_iter)をセットします。(2〜4行目)

 次に fit というメソッドを定義します。こちらも、第一引数がselfでそれぞれのインスタンス自身を差しています。第二引数以降で入力値X、クラスラベルyをセットしています。(6行目)

 また、そのメソッドの中で総入力Z内での重みである w_ と、それぞれ(n_iter回分)試行で間違えて判定した回数を保持する error_ の2つを定義しています。(7~8行目)

 その次のfor文では n_iter 回分、入力値Xデータ分の重み更新をおこなっています。続いて、w_{0}ではクラスラベルを判定するために必要な閾値を更新しています。(13~15行目)

 次に、それぞれで計算したupdateが 0 ではない時に、上手くクラスラベルが判定出来ていないとして、errors に1カウントします。その下の行、errors_は n_iter 回目のそれぞれの試行での数を保持したいため、1回の試行あたりのエラー数 errors を .append()で errors_ に追加しています。(16~17行目)

 20~24行目は13行目で呼び出されている predictメソッド に関して記述してます。
predict, net_input両メソッド では、インスタンス自身を指す self を第一引数にとり、入力値X を第二引数としています。

 まずは、net_inputメソッドを説明すると、ここでは総入力Zを計算して返しています。なお、閾値に掛かる第一項目 w_{0}x_{0}x_{0}は 1 として扱います。

 次に net_input を呼び出している predictメソッドは、総入力Zの値と、閾値\theta = 0.0を比較して、閾値以上か未満かの判断でクラスラベル(1, -1)を where() で返しています。

 

 

以上が、Perceptronクラス の全体的な説明です。 

あとは、このインスタンスを作成して、入力データ、そのクラスラベルをfitメソッドに入れてやればOKです。

 

 

実際に、自分でやってみた結果を簡単に紹介しようと思います。

有名すぎてここでは具体的に説明はしませんが、花に関するデータベース irisのデータを用いて、実行しました。

 

データを読み込むのと、この先に結果をグラフで表示させるので、追加でライブラリを読み込ませる必要があります。追加したのは以下の2つです。

  • matplotlib   (1.5.3)
  • scikit-learn   (0.15.0)

まず、scikit-learnから、dataset.load()でデータ全体を読み込みます。次に読み込んだデータを必要に応じて 入力値 X と クラスラベルy についてデータ整形します。ちなみに、今回の場合だと、クラスラベルが(1, 0)だったので(1, -1) に変換しました。

 

そして、実行したところ、以下のような結果になりました。

 まずはデータの散布図です。データはクラスラベル1は赤色の"A"という品種、クラスラベル-1は青色の"B"という品種です。それぞれ(x, y)という値を持つものです。目で見ると、だいたいの境界線が分かりますね。では、これをPerceptronに入れてみましょう。

f:id:xedvc3-phone-8:20161216173829p:plain

 

まずは、各ステップによる誤分類です。横軸"Epochs"は試行回数を、縦軸は、その試行毎の誤分類数を表しています。最初の1ステップ目は重みは 0 で初期化している中で、誤分類数は 2つと初めは結構いい感じですね。そして重み更新し、3ステップでは3つ、それ以降は誤分類数も減っていき、グラフを見る限りでは、6ステップ目では誤分類数は 0 で全てうまく分類できていると目で見てわかりますね。

f:id:xedvc3-phone-8:20161216171347p:plain

以下は、最終的な分類結果を表示しています。

うん、うまく分類できていますね。
f:id:xedvc3-phone-8:20161216171350p:plain

 

 最終的な重みは、(w_{0}, w_{1}, w_{2}) = (-0.4, -0.68, 1.82) でした。

 

 

この重みを使って、例えば未知の値を入れてみてうまくクラスラベルが分類できるかなど etc.
やれそうなことはまだありそうですね。

ですが、今回はここらへんまでにしときますね。

 

使ったコード 

gist935401beaeaa6423387bf4a77f352297

 

 

最後に、非常に参考にさせていただきました。

Python機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)

Python機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)