近年、人工知能や機械学習の話題が盛り上がっており、特にDeep learningという機械学習の分野への注目が高い。2012年に画像認識の精度を競うコンテスト(ImageNet Large Scale Visual Recognition Challenge)でDeep learnigを利用したチームが2位以下を大きく引き離して優勝。特に新しい概念ではなかったようだがこの功績により再評価され、以降の利用拡大につながっているとのこと(
*1)。また、2015年には囲碁用人工知能が世界トップクラスの棋士に勝ったというニュースが多数のメディアで取り上げられたことは記憶に新しい(
*2)。
プログラマとして内部の動きが気になるところなので調べてみたが、Deep learningとは『4層以上のニューラルネットワークによる学習』を示すそうである。とりあえず今回は基礎知識としてニューラルネットワークという概念を調べ、その一種である単層パーセプトロンを実装してみて機械学習の実装イメージをつかもうと思う。ちなみに、本記事のように1から実装するのではなくライブラリを利用したいという方は『
Javaで機械学習 - Deeplearnig4j入門』をご覧頂きたい。
■ ニューラルネットワークとは?
ニューラルネットワークとは人間の脳機能を参考にした数学モデルのことで、ニューロンと呼ばれる計算式を接続したネットワークである。計算式を接続するというのは、ある計算式の計算結果を接続先の計算式の入力値とすることである。ニューラルネットワークを利用すると、未知の計算式の(入力値,出力値)の組から近似式を作成することができる。一例として入力層・中間層・出力層という階層構造を持つニューラルネットワークを以下に示す。
図:ニューラルネットワークの一例
階層構造を持つニューラルネットワークでは、ニューロンの集合が層をなしている。各層のニューロンは前の層のニューロンから入力値を受け取り、内部の線形関数(y=ax+bの形の関数)で計算を実行し、計算結果がある一定の値(閾値)を超えた場合は1、そうでない場合は0を出力値に決定する。そして、次の層のニューロンはこの出力値を入力として同様の計算を行い、出力値をもとにまた次の層のニューロンが計算を行い…という繰り替えしを行う。このため、入力層に値を与えると出力層になんらかの計算結果が出力されることになり、ニューロン内の線形関数のパラメータをうまく設定いれば希望する計算式が作成できるという訳である。これは数値計算に限ったことではなく、入力として画像のピクセル配列を渡して、出力として画像が表す物体の分類値を出力させるといったことも可能であり、ニューラルネットワークは機械学習で広く利用されている。
しかし、そこで問題となるのが、「どうやってニューロン内のパラメータを適切に設定するか」である。そこで登場するのが学習である。学習というと難しそうに聞こえるが、中学で勉強した幾何学の世界で考えると以下の問題でパラメータa,bを求めるのと同じようなことである。
【問題】
関数「\( y=ax+b\)」が点\((0,3)\)および点\((4,0)\)を通るとき、\(a\)と\(b\)の値を答えなさい。
【答え】
「\( 3=a \times 0+b \)」から\(b = 3\)、「\( 0=a \times 4+3 \) 」から\( a = -\frac{3}{4} \)
つまり、入力xに対する出力yが分かっているのであれば、計算式中に現れるパラメータ(=適切な関数)は計算可能である。上記の幾何学の問題を機械学習の観点で見てみると、入力に対応するデータの組「(x,y)=(0,3),(4,0)」は教師データと呼ばれ、線形関数「y=ax + b」のパラメータa,bを計算することが学習にあたる。もちろん、上記例のように関数が1次関数であることが分かっていれば瞬時に正確なパラメータが計算できるが、まったく未知の関数に対しては別の方法でパラメータを近似していくというアプローチになる。このパラメータの学習方法の違いやニューロンのつなぎ方により、ニューラルネットワークが様々に分類される。
■ パーセプトロンとは?
パーセプトロンはニューラルネットワークの一種であり、複数の入力に対して2進数(0か1の配列)を返すモデルである。パーセプトロンを大別すると、以下の2種類に分けられる。
- 単純パーセプトロン
- 多層パーセプトロン
単純パーセプトロンは最も単純なニューラルネットワークであり、入力層と出力層しかないモデルである。イメージは以下のようになる。単純パーセプトロンは2層しかないために線形分離可能な問題しか解くことができないが、線形分離可能であれば必ず問題を解くことができる。これは1次関数(線形関数)で曲線(2次元以上の関数=非線形関数)が表現できないが、直線であれば必ず表現できるということと同様の理屈である。
図:単純パーセプトロンのイメージ
計算式
単純パーセプトロンのニューロンが利用する線形関数を式に表すと以下のとおりである。入力\(x_i\)に対してパラメータ\(v_i\)で重みづけして総和をとり、総和が閾値を超えると1、そうでなければ0を出力している。
\begin{align}
y & = f( \sum_{i=1}^{n} v_ix_i ) \\
& = f( v_1x_1 + v_2x_2 + \cdots + v_nx_n )
\end{align}
変数 |
内容 |
\(n\) |
入力値の個数 |
\(x_i\) |
i番目の入力 |
\(y\) |
出力 |
\(v_i\) |
i番目の入力の加重パラメータ(結合荷重) |
\(f\) |
活性化関数
\begin{align}
f( a ) & =
\begin{cases}
1 & ( a > \theta )\\
0 & ( a <= \theta )
\end{cases}
\end{align} |
\(\theta\) |
閾値 |
学習方法
単純パーセプトロンの学習方法は標準デルタ則と呼ばれる。標準デルタ則を求めるにはちょっと難しい数式(誤差関数が最小となるように再急降下法を利用するため、誤差関数を定義して微分する)を解く必要があるが、プログラムを組むうえでは結果だけを利用すればよく、以下の式に従ってパラメータを更新する。標準デルタ則では出力が教師データと異なる場合に「t (教師データ) - o (推測した出力)」の方向にパラメータを繰り返し更新しており、いつかは正しいパラメータが得られる(ただし、再急降下法を利用しているため極所解に陥りやすいという欠点も存在する)。
\begin{align}
v_k & = v_k + \triangle v_k \\
\triangle v_k & = \varepsilon ( t - o ) x_k
\end{align}
変数 |
内容 |
\(v_i\) |
i番目の入力の加重パラメータ(結合荷重) |
\(x_i\) |
教師データのi番目の入力 |
\(\varepsilon\) |
定数 |
\(t\) |
教師データの出力値 |
\(o\) |
出力層の出力値 |
学習フェーズでは正しい出力値がでるまで標準デルタ則によるパラメータ更新を行う必要があるため、多数の教師データを用いた学習が必要になる。ただし、学習完了はパラメータの更新は必要なくなるため、高速で軽量な計算が可能となる。
他方、多層パーセプトロンは3層以上の階層を持つニューラルネットワークで、バックプロパゲーション学習と呼ばれる学習方法を用いて単純パーセプトロンではできない線形非分離な問題も解くことができるようになっている。
■ 単純パーセプトロンの学習の挙動確認
OR計算を行う単純パーセプトロンの学習時の挙動を手計算で確認していく。まず、単純パーセプトロンについて、入力の数\(n=2\)としてパラメータを以下のように初期化したとする。
\begin{align}
v_1 & = 0.35\\
v_2 & = 0.8 \\
\theta & = 0.5 \\
\varepsilon & = 0.01 \\
\end{align}
ここに教師データ(入力)\((x_1,x_2)=(1,0)\)を与えると、単純パーセプトロンの出力値は以下のように計算される。
\begin{align}
y & = f( \sum_{i=1}^{n} v_ix_i ) \\
& = f( v_1x_1 + v_2x_2 ) \\
& = f( 0.35 \times 1 + 0.8 \times 0 ) \\
& = f( 0.35 ) \\
& = 0
\end{align}
OR計算では\((x_1,x_2)=(1,0)\)に対して\(y=1\)を出力してほしいので、この結果は正しくなく学習フェーズに入る。学習フェーズでは標準デルタ則に従って、パラメータを以下のように更新する。
\begin{align}
v_1 & = v_1 + \triangle v_1 \\
& = v_1 + \varepsilon ( t - o ) x_1 \\
& = 0.35 + 0.01 \times ( 1 - 0 ) \times 1 \\
& = 0.36 \\
\\
v_2 & = v_2 + \triangle v_2 \\
& = v_2 + \varepsilon ( t - o ) x_2 \\
& = 0.8 + 0.01 \times ( 1 - 0 ) \times 0 \\
& = 0.8
\end{align}
更新後のパラメータをもとに再度出力値を求めると以下のようになる。相変わらず不正解ではあるが、以前より正解に少し近づいていることが確認できる。このようにして入力値を変えて学習を繰り返せば、いつかは正解を出力するようになるのである。
\begin{align}
y & = f( \sum_{i=1}^{n} v_ix_i ) \\
& = f( v_1x_1 + v_2x_2 ) \\
& = f( 0.36 \times 1 + 0.8 \times 0 ) \\
& = f( 0.36 ) \\
& = 0
\end{align}
■ 実装プログラム
以下に単純パーセプトロンの実装プログラムを示す。実装した単純パーセプトロンはOR計算を学習する。
◇コード
package application;
import java.util.Random;
public class TestPerceptron
{
/**
* 単純パーセプトロンの実装
* @author karura
* @param args
*/
public static void main(String[] args)
{
// 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 = { 1.0f ,
1.0f ,
1.0f ,
0.0f };
// パーセプトロン作成
// 初期状態の出力
Perceptron perceptron = new Perceptron( 2 );
System.out.println( "[init] " + perceptron );
// 学習
int succeed = 0; // 連続正解回数を初期化
for( int i=0 ; i<1000 ; i++ )
{
// 行間を空ける
System.out.println();
System.out.println( String.format( "Trial:%d" , i ) );
// 使用する教師データを選択
int k = i % answer.length;
// 出力値を推定
double outY = perceptron.output( x[k] );
System.out.println( String.format( "[input] %f , %f" , x[k][0] , x[k][1] ) );
System.out.println( String.format( "[output] %f" , outY ) );
// 評価・判定
if( answer[k] != outY )
{
// 連続正解回数を初期化
succeed = 0;
// 学習
System.out.println( "[learn] before :" + perceptron );
perceptron.learn( answer[k] , outY , x[k] );
System.out.println( "[learn] after :" + perceptron );
}else{
// 連続正解回数を更新
// すべての教師データで正解を出せたら終了
if( ++succeed >= answer.length ){ break; }
}
}
// すべての教師データで正解を出すか
// 収束限度回数(1000回)を超えた場合に終了
System.out.println( "[finish] " + perceptron );
}
}
/**
* パーセプトロンを表すクラス
*
* ■x1 → V1
* ■ → y1
* θ
* ■x2 → V2
*
* x:入力
* y:出力
* v:結合加重
* θ:閾値
* 標準デルタ則を利用
* @author karura
*
*/
class Perceptron
{
// 内部変数
private int inputNeuronNum = 0; // 入力の数
private double[] inputWeights = null; // 入力ごとの結合加重
private double threshold = 0; // 閾値θ
private double epsilon = 0.01f; // 学習用の定数ε
/**
* 初期化
* @param inputNeuronNum 入力ニューロン数
*/
public Perceptron( 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(); }
// 確認メッセージ
System.out.println( "Init Neuron!" );
}
/**
* 学習
* @param t 教師データ
* @param o 出力値
* @param inputValues 入力データ
*/
public void learn( double t , double o , double[] inputValues )
{
// 標準デルタ則にしたがって学習
for( int i=0 ; i<inputNeuronNum ; i++ )
{
inputWeights[i] += epsilon * ( t - o ) * inputValues[i];
//System.out.println( String.format( "%f, %f , %f , %f , %f" , epsilon , t , o , inputValues[i] , epsilon * ( t - o ) * inputValues[i] ) );
}
}
/**
* 計算
* @param inputValues 入力ニューロンからの入力値
* @return 推定値
*/
public double output( double[] inputValues )
{
// 入力値の総和を計算
double sum = 0;
for( int i=0 ; i<inputNeuronNum ; i++ ){ sum += inputValues[i] * inputWeights[i]; }
// 出力関数は階段関数
double out = ( sum > threshold )? 1 : 0;
return out;
}
/**
* クラス内部確認用の文字列出力
*/
@Override
public String toString()
{
// 出力文字列の作成
String output = "weight : ";
for( int i=0 ; i<inputNeuronNum ; i++ ){ output += inputWeights[i] + " , "; }
return output;
}
}
◇実行結果
Init Neuron!
[init] weight : 0.4747772014074474 , 0.3010043393040287 ,
Trial:0
[input] 1.000000 , 1.000000
[output] 1.000000
Trial:1
[input] 1.000000 , 0.000000
[output] 0.000000
[learn] before :weight : 0.4747772014074474 , 0.3010043393040287 ,
[learn] after :weight : 0.48477720118393 , 0.3010043393040287 ,
…中略…
Trial:127
[input] 0.000000 , 0.000000
[output] 0.000000
Trial:128
[input] 1.000000 , 1.000000
[output] 1.000000
Trial:129
[input] 1.000000 , 0.000000
[output] 1.000000
Trial:130
[input] 0.000000 , 1.000000
[output] 1.000000
[finish] weight : 0.6247771980546861 , 0.6210043321514713 ,
◇解説
パーセプトロン学習用のデータとして入力データx・出力データanswerの組を用意している(16行目~23行目)。この教師データを元にニューロンによる関数計算を行い、出力値が教師データと異なる場合には学習を行うことを繰り返している(32行目~62行目)。学習発生時には「[learn]○○」という標準出力が行われ、内部パラメータの変化が見えるようになっている(53行目~55行目)。学習の終了条件はすべての教師データで正解か一定回数(1000回)のループ実行である。上記実行では127回目の試行から「0 or 0 = 0」「1 or 1 = 1」「1 or 0 = 1」「1 or 1 = 1」とすべての教師データで正解している。このため試行130回目で学習を終了しているが、試行は4回で終わる場合もあれば1000回でも終わらないことがある。
パーセプトロンを実装するクラスPerceptronでは初期化関数(101行目~115行目)・学習関数(123行目~131行目)・計算関数(138行目~148行目)が定義されている。学習関数と計算関数は上記で説明したとおりの実装となっている。また、本プログラムでは実行毎に学習内容をさせるため、パラメータの初期値として乱数を与えている(110行目~111行目)。
■ 参照
- Think it 「そもそもディープラーニングとは何か?」
- Science Portal「人工知能碁が世界トップに4勝1敗 人間棋士も健闘」
- ニューラルネットワーク入門
改訂履歴・2016年 5月28日 一部改訂。文章を一部修正。『単純パーセプトロンの学習の挙動確認』を追記