前記事『
Javaで機械学習:単純パーセプトロンを実装してみる』に引き続き、今回は多層パーセプトロンを実装してみる
■ 多層パーセプトロン
多層パーセプトロンは3層以上のニューラルネットワークで、バックプロパゲーション学習と呼ばれる学習方法を用いる。3層以上となったこととバックプロパゲーション学習を取り入れたことにより、単純パーセプトロンではできなかった線形非分離な問題も解けるようになっている。
計算式
多層パーセプトロンのニューロンが利用する線形関数は単純パーセプトロンとほぼ同様であり、以下の式で表される。唯一の違いは、活性化関数が非連続な階段関数から連続なシグモイド関数に変更されていることである。
多層パーセプトロンを構成するニューロンの数式
【中間層のニューロン】
\begin{align}
h_i & = f( \sum_{k=0}^n v_{ik}x_k - \theta ) \\
& = f( v_{i1}x_1 + v_{i2}x_2 + \cdots + v_{in}x_n - \theta )
\end{align} |
【出力層のニューロン】
\begin{align}
o_i & = f( \sum_{k=0}^n v_{ik}h_k - \theta ) \\
& = f( v_{i1}h_1 + v_{i2}h_2 + \cdots + v_{in}h_n - θ )
\end{align} |
変数 |
内容 |
\(n\) |
入力値の個数。各層毎に異なってもよい |
\(x_i\) |
i番目の入力値(教師データ) |
\(v_{ij}\) |
各層内のi番目ニューロンで、入力jにかける加重パラメータ(結合荷重) |
\(h_i\) |
中間層のi番目ニューロンの出力値。(次の層の入力値にもなる) |
\(o_i\) |
出力層のi番目ニューロンの出力値 |
\(f\) |
活性化関数(シグモイド関数)
\begin{align}
f(x) & = \frac{1}{1+e^{-x}}
\end{align} |
\(\theta\) |
閾値 |
学習方法
多層パーセプトロンの学習方法は誤差逆伝播学習則(バックプロパゲーション)と呼ばれる。標準デルタ則と同様ちょっと難しい数式を解く必要があるが、プログラムで利用する分には以下の式だけ分かればよい。誤差逆伝播学習則でも最急降下法を利用してパラメータを更新するため局所解に陥りやすいという欠点がある。
\(v_{ij} = v_{ij} + \triangle v_{ij}\) |
ただし、 |
【出力層のi番目ニューロンの場合】
\begin{align}
\triangle v_{ij} & = \eta \times \delta_i \times h_j \\
\delta_i & = ( t_i - o_i ) o_i ( 1 - o_i ) \\
\end{align}
【中間層のi番目ニューロンの場合】
\begin{align}
\triangle v_{ij} & = \eta \times \delta_i \times x_{ij} \\
\delta_i & = h_i ( 1 - h_i ) \sum_{k=0}^n v_{ki}\delta_k
\end{align} |
変数 |
内容 |
\(n\) |
出力層の出力値の個数 |
\(v_{ij}\) |
各層内のi番目ニューロンで、入力jにかける加重パラメータ(結合荷重) |
\(h_i\) |
中間層のi番目ニューロンの出力値。(次の層の入力値にもなる) |
\(o_i\) |
出力層のi番目ニューロンの出力値 |
\(t_i\) |
i番目の出力値(教師データ) |
\(\eta\) |
定数 |
* 中間層のΣ計算で利用する変数は「\(v_{ki}\)= 次ニューロンの出力値を受け取る次の層のk番目ニューロンの加重パラメータ」「\(\delta_i\) = 次の層のi番目ニューロンの\(\delta\) 」を表している。
■ 多層パーセプトロンの学習の挙動確認
XOR計算を行う多層パーセプトロンの学習時の挙動を手計算で確認していく。まず、多層パーセプトロンを以下のように構成・初期化したとする。
\begin{align}
v_{11} & = 0.35 \\
v_{12} & = 0.50 \\
v_{21} & = 0.11 \\
v_{22} & = 0.93
\end{align} |
\begin{align}
v_1 & = 0.6 \\
v_2 & = 0.2 \\
\theta & = 0.5 \\
\eta & = 5 \\
\end{align} |
ここに教師データ(入力)\((x_1,x_2)=(1,0)\)を与えると、多層パーセプトロンの出力値は以下のように計算される。
\begin{align}
h_1 & = f( v_{11}x_{11} + v_{12}x_{12} - \theta ) \\
& = f( 0.35 \times 1 + 0.50 \times 0 - 0.5 ) \\
& = f( -0.15 ) \\
& = 0.46 \\
\\
h_2 & = f( v_{21}x_{21} + v_{22}x_{22} - \theta ) \\
& = f( 0.11 \times 1 + 0.93 \times 0 - 0.5 ) \\
& = f( -0.39 ) \\
& = 0.40\\
\\
o_1 & = f( v_1h_1 + v_2h_2 - \theta ) \\
& = f( 0.6 \times 0.46 + 0.2 \times 0.40 - 0.5 ) \\
& = f( -0.144 ) \\
& = 0.464
\end{align}
XOR計算では\((x_1,x_2)=(1,0)\)に対して\(o_1=1\)を出力してほしいので、いま許容誤差を0.1とすると、この出力値 \(1-0.464=0.536\)の誤差は大きすぎる。このため学習フェーズに入る。学習フェーズでは誤差逆伝播学習則に従って、まず出力層のパラメータを以下のように更新する(出力の教師データ\(t_i =1\))。
\begin{align}
\delta_1 & = ( t_i - o_i ) o_i ( 1 - o_i ) \\
& = ( 1 - 0.47 ) \times 0.47 \times ( 1 - 0.47 ) \\
& = 0.132 \\
\\
v_1 & = v_1 + \eta \times \delta_1 \times h_1 \\
& = 0.6 + 5 \times 0.132 \times 0.46 \\
& = 0.903 \\
\\
v_2 & = v_2 + \eta \times \delta_1 \times h_2 \\
& = 0.2 + 5 \times 0.132 \times 0.60 \\
& = 0.596 \\
\end{align}
次に中間層のパラメータを以下のように更新する
\begin{align}
\delta_1 & = h_1( 1 - h_1 ) \sum_{k=0}^n v_{k1}\delta_k \\
& = 0.46 ( 1 - 0.46 ) ( 0.903 \times 0.132 ) \\
& = 0.0030 \\
\\
v_{11} & = v_{11} + \eta \times \delta_1 \times x_{11} \\
& = 0.35 + 5 \times 0.0030 \times 1 \\
& = 0.365 \\
\\
v_{12} & = v_{12} + \eta \times \delta_1 \times x_{12} \\
& = 0.50 + 5 \times 0.0030 \times 0 \\
& = 0.50 \\
\\
\delta_2 & = h_2( 1 - h_2 ) \sum_{k=0}^n v_{k2}\delta_k \\
& = 0.60 ( 1 - 0.60 ) ( 0.596 \times 0.132) \\
& = 0.0019 \\
\\
v_{21} & = v_{11} + \eta \times \delta_2 \times x_{21} \\
& = 0.11 + 5 \times 0.0019 \times 1 \\
& = 0.120 \\
\\
v_{22} & = v_{12} + \eta \times \delta_2 \times x_{22} \\
& = 0.93 + 5 \times 0.0019 \times 0 \\
& = 0.93 \\
\\
\end{align}
更新後のパラメータをもとに再度出力値を求めると以下のようになる。
相変わらず不正解ではあるが、以前より正解に少し近づいていることが確認できる。このようにして入力値を変えて学習を繰り返せば、いつかは正解を出力するようになるのである。
\begin{align}
h_1 & = f( v_{11}x_{11} + v_{12}x_{12} - \theta ) \\
& = f( 0.365 \times 1 + 0.50 \times 0 - 0.5 ) \\
& = f( -0.38 ) \\
& = 0.466 \\
\\
h_2 & = f( v_{21}x_{21} + v_{22}x_{22} - \theta ) \\
& = f( 0.120 \times 1 + 0.93 \times 0 - 0.5 ) \\
& = f( -0.367 ) \\
& = 0.406\\
\\
o_1 & = f( v_1h_1 + v_2h_2 - \theta ) \\
& = f( 0.903 \times 0.466 + 0.596 \times 0.406 - 0.5 ) \\
& = f( 0.163) \\
& = 0.54
\end{align}
■ 実装プログラム
以下に多層パーセプトロンの実装プログラムを示す。実装した多層パーセプトロンはXOR計算(非線形な計算)を学習する。
◇コード
package application;
import java.io.PrintStream;
import java.util.Random;
import java.util.logging.Logger;
public class TestMultiLayerPerceptron
{
/**
* 多層パーセプトロンの実装
* @author karura
* @param args
*/
public static void main(String[] args)
{
new TestMultiLayerPerceptron();
}
/**
* 処理関数
*/
public TestMultiLayerPerceptron()
{
// OR計算の教師データ
// 入力データ配列 x =(入力1,入力2)の配列と,正解データ配列 answer
final double[][] x = { { 1.0f , 1.0f } ,
{ 1.0f , 0.0f } ,
{ 0.0f , 1.0f } ,
{ 0.0f , 0.0f } };
final double[] answer = { 0.0f ,
1.0f ,
1.0f ,
0.0f };
// パーセプトロンの動作確認
try
{
// 標準出力をファイルに関連付ける
String fileName = System.getProperty( "user.dir" )
+ "/"
+ "TestMultiLayerPerceptron.log";
PrintStream out = new PrintStream( fileName );
System.setOut( out );
// 多層パーセプトロンの作成
MultiLayerPerceptron mlp = new MultiLayerPerceptron( 2 , 2 , 1 );
mlp.learn( x , answer );
// ファイルを閉じる
out.close();
}catch( Exception e ){
e.printStackTrace();
}
}
}
/**
* 多層パーセプトロンを表すクラス
*
* ■x1 → ■m1 → H1
* θ
* × ■ → o1
* θ
* ■x2 → ■m2 → H2
* θ
*
* x:入力
* H,o:出力
* v:結合加重
* θ:閾値
* 誤差逆伝播学習則(バックプロパゲーション)を利用
* @author karura
*
*/
class MultiLayerPerceptron
{
// 定数
protected static final int MAX_TRIAL = 100000; // 最大試行回数
protected static final double MAX_GAP = 0.1; // 出力値で許容する誤差の最大値
// プロパティ
protected int inputNumber = 0;
protected int middleNumber = 0;
protected int outputNumber = 0;
protected Neuron[] middleNeurons = null; // 中間層のニューロン
protected Neuron[] outputNeurons = null; // 出力層のニューロン
// ロガー
protected Logger logger = Logger.getAnonymousLogger(); // ログ出力
/**
* 三層パーセプトロンの初期化
* @param input 入力層のニューロン数
* @param middle 中間層のニューロン数
* @param output 出力層のニューロン数
*/
public MultiLayerPerceptron( int input , int middle , int output )
{
// 内部変数の初期化
this.inputNumber = input;
this.middleNumber = middle;
this.outputNumber = output;
this.middleNeurons = new Neuron[middle];
this.outputNeurons = new Neuron[output];
// 中間層のニューロン作成
for( int i=0 ; i<middle ; i++ ){ middleNeurons[i] = new Neuron( input ); }
// 出力層のニューロン作成
for( int i=0 ; i<output ; i++ ){ outputNeurons[i] = new Neuron( input ); }
// 確認メッセージ
System.out.println( "[init] " + this );
}
/**
* 学習
* @param x
* @param answer
*/
public void learn( double[][] x , double[] answer )
{
// 変数初期化
double[] in = null; // i回目の試行で利用する教師入力データ
double ans = 0; // i回目の試行で利用する教師出力データ
double[] h = new double[ middleNumber ]; // 中間層の出力
double[] o = new double[ outputNumber ]; // 出力層の出力
// 学習
int succeed = 0; // 連続正解回数を初期化
for( int i=0 ; i<MAX_TRIAL ; i++ )
{
// 行間を空ける
System.out.println();
System.out.println( String.format( "Trial:%d" , i ) );
// 使用する教師データを選択
in = x[ i % answer.length ];
ans = answer[ i % answer.length ];
// 出力値を推定:中間層の出力計算
for( int j=0 ; j<middleNumber ; j++ )
{
h[j] = middleNeurons[j].output( in );
}
// 出力値を推定:出力層の出力計算
for( int j=0 ; j<outputNumber ; j++ )
{
o[j] = outputNeurons[j].output( h );
}
System.out.println( String.format( "[input] %f , %f" , in[0] , in[1] ) );
System.out.println( String.format( "[answer] %f" , ans ) );
System.out.println( String.format( "[output] %f" , o[0] ) );
System.out.println( String.format( "[middle] %f , %f" , h[0] , h[1] ) );
// 評価・判定
boolean successFlg = true;
for( int j=0 ; j<outputNumber ; j++ )
{
// 出力層ニューロンの学習定数δを計算
double delta = ( ans - o[j] ) * o[j] * ( 1.0d - o[j] );
// 教師データとの誤差が十分小さい場合は次の処理へ
// そうでなければ正解フラグを初期化
if( Math.abs( ans - o[j] ) < MAX_GAP ){ continue; }
else{ successFlg = false; }
// 学習
System.out.println( "[learn] before o :" + outputNeurons[j] );
outputNeurons[j].learn( delta , h );
System.out.println( "[learn] after o :" + outputNeurons[j] );
}
// 連続成功回数による終了判定
if( successFlg )
{
// 連続成功回数をインクリメントして、
// 終了条件を満たすか確認
succeed++;
if( succeed >= x.length ){ break; }else{ continue; }
}else{
succeed = 0;
}
// 中間層の更新
for( int j=0 ; j<middleNumber ; j++ )
{
// 中間層ニューロンの学習定数δを計算
double sumDelta = 0;
for( int k=0 ; k<outputNumber ; k++ )
{
Neuron n = outputNeurons[k];
sumDelta += n.getInputWeightIndexOf(j) * n.getDelta();
}
double delta = h[j] * ( 1.0d - h[j] ) * sumDelta;
// 学習
System.out.println( "[learn] before m :" + middleNeurons[j] );
middleNeurons[j].learn( delta , in );
System.out.println( "[learn] after m :" + middleNeurons[j] );
}
// 再度出力
// 出力値を推定:中間層の出力計算
for( int j=0 ; j<middleNumber ; j++ )
{
h[j] = middleNeurons[j].output( in );
}
// 出力値を推定:出力層の出力計算
for( int j=0 ; j<outputNumber ; j++ )
{
o[j] = outputNeurons[j].output( h );
}
System.out.println( String.format( "[input] %f , %f" , in[0] , in[1] ) );
System.out.println( String.format( "[output] %f" , o[0] ) );
System.out.println( String.format( "[middle] %f , %f" , h[0] , h[1] ) );
}
// すべての教師データで正解を出すか
// 収束限度回数を超えた場合に終了
System.out.println( "[finish] " + this );
}
@Override
public String toString()
{
// 戻り値変数
String str = "";
// 中間層ニューロン出力
str += " middle neurons ( ";
for( Neuron n : middleNeurons ){ str += n; }
str += ") ";
// 出力層ニューロン出力
str += " output neurons ( ";
for( Neuron n : outputNeurons ){ str += n; }
str += ") ";
return str;
}
/**
* 多層パーセプトロン内部で利用するニューロン
* @author karura
*/
class Neuron
{
// 内部変数
protected int inputNeuronNum = 0; // 入力の数
protected double[] inputWeights = null; // 入力ごとの結合加重
protected double delta = 0; // 学習定数δ
protected double threshold = 0; // 閾値θ
protected double eater = 1.0d; // 学習係数η
/**
* 初期化
* @param inputNeuronNum 入力ニューロン数
*/
public Neuron( int inputNeuronNum )
{
// 変数初期化
Random r = new Random();
this.inputNeuronNum = inputNeuronNum;
this.inputWeights = new double[ inputNeuronNum ];
this.threshold = r.nextDouble(); // 閾値をランダムに生成
// 結合加重を乱数で初期化
for( int i=0 ; i<inputWeights.length ; i++ )
{ this.inputWeights[i] = r.nextDouble(); }
}
/**
* 学習(バックプロパゲーション学習)
* @param inputValues 入力データ
* @param delta δ
*/
public void learn( double delta , double[] inputValues )
{
// 内部変数の更新
this.delta = delta;
// 結合加重の更新
for( int i=0 ; i<inputWeights.length ; i++ )
{
// バックプロパゲーション学習
inputWeights[i] += eater * delta * inputValues[i];
}
// 閾値の更新
threshold -= eater * delta;
}
/**
* 計算
* @param inputValues 入力ニューロンからの入力値
* @return 推定値
*/
public double output( double[] inputValues )
{
// 入力値の総和を計算
double sum = -threshold;
for( int i=0 ; i<inputNeuronNum ; i++ ){ sum += inputValues[i] * inputWeights[i]; }
// 活性化関数を適用して、出力値を計算
double out = activation( sum );
return out;
}
/**
* 活性化関数(シグモイド関数)
* @param x
* @return
*/
protected double activation( double x )
{
return 1 / ( 1 + Math.pow( Math.E , -x ) );
}
/**
*
* 入力iに対する結合加重を取得
* @param i
* @return
*/
public double getInputWeightIndexOf( int i )
{
if( i>=inputNumber ){ new RuntimeException("outbound of index"); }
return inputWeights[i];
}
/**
* 学習定数δの取得
* @return 学習定数δ
*/
public double getDelta()
{
return delta;
}
/**
* クラス内部確認用の文字列出力
*/
@Override
public String toString()
{
// 出力文字列の作成
String output = "weight : ";
for( int i=0 ; i<inputNeuronNum ; i++ ){ output += inputWeights[i] + " , "; }
return output;
}
}
}
◇実行結果
[init] middle neurons ( weight : 0.9838057333327339 , 0.06615224861858782 , weight : 0.3478404842040157 , 0.5539850908763432 , ) output neurons ( weight : 0.9919828208494069 , 0.6217470734164099 , )
Trial:0
[input] 1.000000 , 1.000000
[answer] 0.000000
[output] 0.630820
[middle] 0.729555 , 0.570786
[learn] before o :weight : 0.9919828208494069 , 0.6217470734164099 ,
[learn] after o :weight : 0.8848044069578419 , 0.5378934073211266 ,
[learn] before m :weight : 0.9838057333327339 , 0.06615224861858782 ,
[learn] after m :weight : 0.9581589432550048 , 0.040505458540858776 ,
[learn] before m :weight : 0.3478404842040157 , 0.5539850908763432 ,
[learn] after m :weight : 0.3284810521211755 , 0.534625658793503 ,
[input] 1.000000 , 1.000000
[output] 0.597608
[middle] 0.719317 , 0.561275
…中略…
Trial:21282
[input] 0.000000 , 1.000000
[answer] 1.000000
[output] 0.900362
[middle] 0.999381 , 0.525638
Trial:21283
[input] 0.000000 , 0.000000
[answer] 0.000000
[output] 0.099926
[middle] 0.485606 , 0.350517
Trial:21284
[input] 1.000000 , 1.000000
[answer] 0.000000
[output] 0.068134
[middle] 1.000000 , 0.679308
Trial:21285
[input] 1.000000 , 0.000000
[answer] 1.000000
[output] 0.940575
[middle] 0.999373 , 0.507795
[finish] middle neurons ( weight : 7.431611631033646 , 7.4451035204082165 , weight : 0.647951748801302 , 0.7194111046308782 , ) output neurons ( weight : 19.2735309553994 , -31.423720548177265 , )
◇解説
使用しているクラスは3つ。1つ目はプログラムを起動するためのTestMultiLayerPerceptronクラス(7行目)。2つ目は多層パーセプトロンを表すMultiLayerPerceptronクラス(77行目)。3つ目はニューロンを表すNeuronクラス(258行目)である。本プログラムでは3層(入力2つ,中間層2ニューロン,出力層1ニューロン)の多層パーセプトロンを作成している(99行目~116行目)。記述は長いが、やっていることは上記の数式モデルの作成と数式の実装だけである。
多層パーセプトロンの学習はMultiLayerPerceptron::learn関数(123行目~226行目)で行っている。処理の流れは単純パーセプトロンと同様で、まず入力値を与えて出力値を計算する(141行目~154行目)。教師データの出力との誤差が許容範囲内か確認して、誤差が許容範囲を超えた場合は誤差逆伝播学習則による学習を実施している(161行目~206行目)。終了条件はすべての教師データに正解する(180行目~188行目)か、試行回数の上限に達するかである。
実行結果を見ていただくと分かるが、収束までに2万回かかっており収束がわるい実装になっている。高速化するためにはηバラメータの調整が必要なのかなという感じ。もしかすると誤差逆伝播学習則の適用タイミングが悪いのかもしれないが、収束したのでとりあえず良しとする。
→(追記)閾値についても学習フェーズで更新するように実装を変えたところ、1500~3000回程度で収束する模様。収束までに2万回はかかりすぎると思った直観は間違ってなかったようだ。
■ 参照
- ニューラルネットワーク入門
改訂履歴・2016年 5月28日 一部改訂。『多層パーセプトロンの学習の挙動確認』を追記・2017年 5月6日 一部改訂。誤差逆伝番学習で閾値も更新するようにソースコードを修正・2018年 6月14日 一部改訂。『多層パーセプトロンの学習の挙動確認』の計算ミスを修正
1. 無題
コメントめっちゃでほんとわかりやすいプログラムです!
自分の書いたプログラムのようにスイスイ処理を脳内で試せます!
Re:無題