ytyt blog

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

【素人が書く】単純パーセプトロンの学習アルゴリズム Pythonうんぬん

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

 

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

 

今回は、その中でも、「教師あり学習」の中のデータをクラスラベル( -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)