MasaのITC Life

夢は起業家!全てにおいて凡人だけど頑張ることだけはいっちょ前!

AI/機械学習 プログラミング

【機械学習】Go言語でゼロから多層パーセプトロン(MLP)を実装する

投稿日:2021年7月3日 更新日:



標準ライブラリのみでニューラルネットワーク、

今回は多層パーセプトロンを実装します。

最後に学習精度をグラフ化していますが、

そこは有志のライブラリを使用します。



今回はシンプルに

排他的論理和を学習させます。

学習データとラベルです。



それではLet's go!


多層パーセプトロンの概要


多層パーセプトロンの詳細は他サイトに任せて、

ここでは実装のことを考えて、

命名規則に沿って簡単に話していきます。



まず言葉の定義の基準として、

どの層にまつわる値かすぐにわかるように、

名前にin, hidden, outのいづれかを付けます。



〇〇Input: 入力層の何か
〇〇Hidden: 隠れ層の何か
〇〇Output: 出力層の何か



またそれぞれに対して以下を基にします。

val○○○: 各ノードの値
delta○○○: 各ノードの誤差
wei○○○: 重みの値
node○○○: 該当する層のノード数
lay○○○: 層の数(隠れ層のみ該当)


こんな感じです。



画像に書き起こすとこんな風になっています。




今回の場合、隠れ層が1層のみなので、

weiHiddenは存在しません。

weiHiddenは隠れ層と隠れ層を結ぶ重みなので、

隠れ層が2層以上のときのみ存在します。




実装に入る前に少し、

多層パーセプトロンについて考えてみます。



なぜ多層になると非線形に分けられるのか?



単層=線形分離のみ

多層=非線形分離可能

について考えてみましょう。



そもそも線形分離とは、

線で出力値を区切ることですが、

それを何個に区分けるか、

ということは問ていません。







ここで単純パーセプトロンを

簡単に定義してみましょう。

z = func(1*x + 1*y)



funcは活性化関数です。

活性化関数の中に入るのは、

ノードの値と重みが線形結合された式です。



例えば、sigmoid関数を例に見てみましょう。

分かりやすくするためにxとyを式に入れています。

Sigmoid(x+y) = 1 / (1 + e -(x+y) )



Sigmoid関数の形自体は非線形で表されているのに、

なぜ単層パーセプトロンは、

線形分離しかできないのでしょうか?




後述しますがよくSigmoid関数が出てくるのは、

下画像のように出力を2値に、つまり

"ほぼ"一本線ではっきりと分けられるからです。



右上のグラフが先ほどの式をグラフ化したものです。

確かに形自体は非線形になっており、

単層=線形分離が少し違和感を感じますよね。




では、ノードの値と重みを線形結合した式を

活性化関数に通す意味は何だったのでしょうか?



そうです。sigmoidの場合は0と1ですが、

活性化関数にある入力が来たら0を出力して、

ある入力が来たら1を出力するものでした。

つまり入力によって0か1を分けるのでした。



ここで↑画像中の右下グラフを見てみましょう。

これはsigmoid関数を上からみたグラフです。

なじみのある言葉で表現すると等高線です。



入力の組み合わせによって、標高が決まります。

標高、その高さが活性化関数の出力になります。

どうですか?線形になっていますでしょう?



真ん中斜めに走っている線が、

まさに0か1かを分ける線になります。

この線が少し広がっているのは、

sigmoid関数が少し滑らかになっているからです。



重みを大きくすると以下のように細くなり、

その変化も急峻になります。


また、重みのバランスを変えても、

0と1に分ける線の方向が変わります。

これが重みを調整する意義です。



ではsigmoid関数以外はどうでしょうか?

cosine、2次関数、ReLuについて見てみましょう。

活性化関数にcosineを使う事例は見かけないですが。。。

重みは1で計算してあります。



見てわかるように、

活性化関数に何を使用しようと、

線形分離の形になっていることが分かります。



但し活性化関数によってはcosineのように、

どの入力の組み合わせによって、

0なのか1なのか視覚的に分かりにくくなります。




加えてSigmoidのようにほぼ一本線でない場合、

n個の線によって0と1以外のグループが入ることで

区分けが細かくなっていきます。



また、ReLu関数のように活性化関数の値域が

0と1の範囲にないものもあります。

いずれにせよ、活性化したい入力が来たら正の値で、

逆は負の値(もしくは0)という2値が基本です。




よく説明でsigmoidの事例が出てくるのは、

綺麗に真っ二つに分けられるからですね。




それでは多層パーセプトロンになると、

グラフはどうなるのでしょうか。


隠れ層を1層追加してみましょう。

活性化関数はsigmoid関数で、

重みはグラフが分かりやすくなるように、

適当に調整しました。



どうでしょうか?

右上のグラフはsigmoid関数の

重ね合わせで表現されています。



単層(sigmoidが1つだけ)だと、

0と1の境界が極端に分かれていましたが、

sigmoidを重ね合わせていくと、

0と1以外の値でも広い領域を持つようになりました。



右下のグラフも形が非線形になっていますね!

これが多層の意義になります。

層を追加していくことで、

より複雑な形で分けることが可能になるというわけです。




それでは実装していきましょう!



多層パーセプトロンをゼロから作る!


※記事最後にgithubへのリンク貼ってあります。



構造はmlp という構造体を定義して、

その中に入力層、隠れ層、出力層それぞれの

重み、伝播する誤差、バイアスを用意します。



つまりmlp構造体がネットワーク絶対の設定を

保持する形になっています。



バイアスはノードの一部としてみなして、

ノード数をセットする際に+1します。



ハイパーパラメータは以下で定義します。


エポック数は5千で、学習率は0.6です。

自由に変えてみてください。

今回はこの値で行ってみましょう!




続いてmain文で全体の流れを見ます。


patterns配列のように、

データとラベルを用意します。



set関数で各層のノードの数と隠れ層の数を指定し、

ネットワークの全体構造を取得します。



そしてTrainメソッドとTestメソッドを実行します。

最後に誤差をグラフ化をします。



まずは、setを見てみましょう。


引数にノードの数と隠れ層の数を受け取ります。

入力層と隠れ層はバイアス分があるので1を足します。



次に入力層、隠れ層、出力層の全ての重みを

-1から1の範囲で初期化します。



今回は隠れ層は1層のみなので隠れ層の重みはなしです。

隠れ層の重み(weiHidden)は隠れ層同士をつなぐ重みです。



最後にノードの値は1をセットして終了です。

バイアスノードの値はずっと1なので、

0をセットするのではなく1をセットします。




続いて、Trainを見てみましょう。



エポック数を満たすまで、

入力層から順にノードの値を再計算して、

バックプロパゲーションを繰り返します。



ノードの値の再計算は以下になります。


入力層→隠れ層→出力層と、

順にfor文回して計算しているだけです。



バックプロパゲーションの実装は以下になります。



誤差は最小2乗誤差を基に計算して、

重みの更新はSGD(勾配降下)を適用してあります。



出力層→隠れ層→入力層と、順に計算していきます。

計算は微分した式を基にしていますので、

最小2乗誤差を偏微分した形を知っておく必要があります。


E = 1/2 Σ ( zi - li )2

∂E/ ∂zi = zi - li


あとは微分の連鎖率を使用して、

該当するノードの偏微分を求めていきます。




次に、Testを見てみましょう。




最後にグラフ化するコード(おまけ)です。





最後に結果を見てみましょう!


学習結果


誤差の推移をグラフ化しました。

横軸がエポック数で、

以下のグラフは全て5000回です。



学習率=0.2



学習率=0.6



学習率=0.8


0.2だと学習が遅いですが、

0.6と0.8とではほとんど変わらないですね。



0.6も0.8もどちらも1000回学習させると、

誤差が底になる感じですね。






以上になります。

最後まで読んでいただきありがとうございました。




今回紹介したコードはgithub上においてあります。





参考:

以下をベースにしてあります。

オリジナル
https://github.com/goml/gobrain








-AI/機械学習, プログラミング

Copyright© MasaのITC Life , 2023 All Rights Reserved Powered by STINGER.