読者です 読者をやめる 読者になる 読者になる

白猫のメモ帳

JavaとかJavaScriptとかHTMLとか機械学習とか。

単純パーセプトロンをJavaで作る その②

Java 機械学習

こんばんは。

歩きながらスマホを見るのは危ないのでやめましょうね。

さて、単純パーセプトロンについての記事の2つ目です。

その① 機械学習の基本とパーセプトロンでできること
その② 単純パーセプトロンの仕組みを簡単に解説    ←今回はコレ
その③ 単純パーセプトロンのJavaでの実装

式とか出てきますが、そんなに難しいことは書いていないので気楽にどうぞ。

単純パーセプトロンの仕組み


単純パーセプトロンの仕組みを見ていきましょう。
パーセプトロンは動物の神経を参考にして考案された層状のネットワークです。
(「形式ニューロン」とかで調べると生物学的なあれこれが見つかるかも。ここでは触れません。)
その中で、一番シンプルな入力層と出力層の2層のみで構成されているものを単純パーセプトロンと呼びます。

図にするとこんな感じです。

f:id:Shiro-Neko:20160722223726p:plain

①まずは、入力として何かしらの特徴量を受け取ります。
 このとき、特徴量の数を「次元」といいます。(3次元の特徴量とか)

②次に、それぞれの特徴量に係数を掛けます。
 この係数を「重み」といいます。重みは負数でも構いません。
 イメージとしてはその特徴量が出力に与える影響の大きさと考えるとわかりやすいです。

③②をすべて合計します。

④③の値が0以上の場合は「1」を、0未満の場合には「-1」を出力します。
 この処理を行う関数を「活性化関数」といいます。
 二値分類問題では出力値の種類が二種類でなくてはならないので、「1」か「-1」の二値に変換する作業です。

もうちょっとロジックとして組み立てやすいように、数式っぽくしてみましょう。

f:id:Shiro-Neko:20160730101358p:plain

むしろこちらの方がわかりやすい方もいるでしょうか。
xが入力値、wが重み、uが入力値×重みの合計で、f(u)が活性化関数、yが出力ですね。

さてさて、ここでちょっと工夫を加えます。
x_0を「1」と置いてみましょう。

f:id:Shiro-Neko:20160726100720p:plain

するとこの「1」にかかる重みの大きさ(w_0)によって、入力に関係のない固定の値を足すことができます。
この定数項をバイアス」といいます。
これは基本料金みたいなものだと思ってもらうといいかも知れません。

f:id:Shiro-Neko:20160726105856p:plain

ところで、xとwを1次元のベクトルだと考えると式が簡単になります。ベクトルは太字で書きます。
(ベクトルとか見ると動悸が激しくなる方は、別に気にしなくてよいです)

これって一次方程式?


バイアスを設定した式、
 {
w_0 + w_1 \cdot x_1 + w_2 \cdot x_2
}

これってどこかで見たことありませんか?
これです。
 {
ax + by + c = 0
}

一次関数の方程式です。
あ、これも一緒ですよ。
 {
y = ax + b
}

式の表し方はいろいろあるので、見てみると楽しいかも?
平面における直線の標準形 - Wikipedia


で、結局はこういうことです。
ちょうど線形分離する直線が、 {u = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 = 0}です。

f:id:Shiro-Neko:20160730101446p:plain

 {u = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 > 0}のときには、活性化関数によって出力は「1」になります。

 {u = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 < 0}のときには、活性化関数によって出力は「-1」になります。

直線上もどちらかに分類しなくてはならないので、「1」側に倒しています。(「0以上の場合」としているので)


つまりは線形分離するための直線の方程式を求めることになるわけですね。

単純パーセプトロンの学習


ここまでで、パーセプトロンによって一次関数を表現することができました。
しかし、重みによってこの直線の傾きや切片は変わるのでしたね。

ということは、この直線はうまく線形分離できるかもしれないし、できないのかもしれません。
うまくいくまでがんばって乱数生成しますか?ちょっと現実的ではないですよね。

前回の例では入力とその答え(教師データ)を与えることによって学習をしていましたね。
つまり、入力に対するパーセプトロンの解答が教師ラベルと一致しているかどうかを確認し、
一致しなかった場合に(正解に近づくような)何らかの手を加えれば、そのうち正解できるようになるはずです。

f:id:Shiro-Neko:20160720235900p:plain

その重みの更新式はずばり以下の式になります。

 {
w_n := w_n + ρtx_n
}

ただし、tは教師データの正解ラベル( t = \{+1, -1\})で、
出力の結果yがtと一致しない場合のみ更新を行います。

また、ρは学習率といって通常は1以下の値を設定します。
これは1回のイテレーションでどれくらいずつ更新していくかの係数になります。
ρを大きくすると早く収束しますが、うまく収束しきらないことがあります。
ρを小さくすると収束に時間がかかりますが、収束しないリスクが下がります。

なぜこの式で重みを更新できるかは急に難しくなってしまいそうなので、ここでは触れないことにします。
ひょっとしたら次回以降の記事で解説するかもしれません。

まとめ


単純パーセプトロンの実装に必要な式がいくつかでてきたのでまとめた上で、
疑似コードを使ってアルゴリズムをまとめてみましょう。

入力⇒出力


①入力値に重みを掛けたものをすべて足し合わせる。
 {
u = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 + ・・・ + w_n \cdot x_n = {\boldsymbol w^Tx} \\
w : 重み \\
x : 入力
}


②出力には活性化関数を通す。
 {
y = f(u) \\
y    : 出力 \\
f(u) : 活性化関数
}


③活性化関数は入力が0以上の場合は「1」を、0未満の場合には「0」を返す。
 {
\displaystyle f(u) = \!\begin{cases}
\ +1 & (u \geq 0) \\
\ -1 & (u \lt 0)
\end{cases}
}


重みの更新

①出力の結果yが正解ラベルtと一致しない場合のみ更新する。
 {
w_n := w_n + ρtx_n \\
ρ : 学習率 \\
t : 正解ラベル
}


アルゴリズム

重み w = {w_0, w_1, w_2, ・・・, w_n} を乱数で適当に決める。(固定値でも可)
do while すべての重みが更新されなくなるまで
  foreach 教師データ
    出力値yを計算する
    if 出力値y != ラベルt
      重みを更新する
    end if
  end for
end do

こんな感じですね。
(疑似コードって文法がよくわからない…)

線形分離不可なデータが教師データとして渡された場合には解はないため、収束しません。
そのため、実際にはある一定回数重み更新をしても収束しなかった場合にはあきらめる処理が必要になります。



さて、単純パーセプトロンの仕組みとその実装方法はイメージできるようになったでしょうか。
何となく難しそうですが、わかってしまえばその仕組みはとてもシンプルなものです。

次回はいよいよJavaによる実装です。
それではまた。