ゼロから作るDeep Learning
順伝播型ニューラルネットワーク「FFNNクラス」の実装(JavaScript)
昨今注目を集めているAI(人工知能)を学びたいと思い立ち、ディープラーニング(Deep Learning、深層学習)と呼ばれるAIの数理モデルである多層構造のニューラルネットワークを書籍「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」を参考にを独習していきたいと思います。本書籍ではプログラミング言語としてPythonが利用されていますが、本項ではJavaScriptで実装していきます。
目次
- 準備1:行列の和と積を計算する関数の実装
- 準備2:ベクトルと行列の積を計算する関数の実装
- 準備3:多変数関数の数値微分と極小値の探索
- 1.1層ニューラルネットワークの実装(バイアスなし、活性化関数なし、学習なし)
- 2.1層ニューラルネットワークへのバイアスと活性化関数の追加
- 3.1n1型2層ニューラルネットワークの実装(学習なし)
- 4.1変数関数を学習させてみる1:勾配法による学習計算アルゴリズム
- 5.1変数関数を学習させてみる2:勾配法による学習計算アルゴリズムの実装
- 6.1変数関数を学習させてみる3:ニューロン数による学習効果の違い
- 7.誤差逆伝搬法(バックプロパゲーション)の導出
- 8.順伝播型ニューラルネットワーク「FFNNクラス」の実装(JavaScript)
- 9.三角関数のサンプリング学習(WebWorkersによる並列計算)
- 10.学習後の各層ニューロンの重みの可視化
- 11.層数とニューロン数による学習効果の違い
順伝播型ニューラルネットワーク「FFNNクラス」の実装
前項の誤差逆伝搬法(バックプロパゲーション)を組み込んだ順伝播型ニューラルネットワークを実現するクラスを示します。
FNNNクラス
第一引数:初期重み多重配列(必須)
第二引数:初期バイアス多重配列(必須)
第三引数:学習率
第四引数:活性化関数
第五引数:活性化関数の導関数
第六引数:最終層の活性化関数
第七引数:最終層の活性化関数の導関数
//順伝播型ニューラルネットワーク
var FNNN = function( W, B, eta, h, hd, sigma, sigmad ){
//重み
this.W = W;
//W.length : 層数
//W[].length : 列(後ニューロン数)
//W[][].length : 行(前ニューロン数)
//1ステップ前のWを保持する多重配列
this._W = [];
//バイアス
this.B = B;
this._B = [];
//偏微分値を格納する多重配列
this.dEdW = [];
this.dEdB = [];
//誤差逆伝搬法で計算した偏微分値を格納する多重配列
this._dEdW = [];
this._dEdB = [];
//活性化関数(ReLU関数)
this.h = h || function( x ){
if( x >= 0 ) return x;
else return 0;
}
//活性化関数の微分
this.hd = hd || function( x ){
if( x >= 0 ) return 1;
else return 0;
}
//出力層の活性化関数(恒等関数)
this.sigma = sigma || function( x ){
return x;
}
this.sigmad = sigmad || function( x ){
return 1;
}
//ニューロンの初期化
this.X = [];
//活性化関数を通す前のニューロン
this.x = [];
//誤差逆伝搬法のデルタ値
this.delta = [];
//学習効率
this.eta = eta || 0.1;
this.setup();
}
//各種プロパティの初期化
FNNN.prototype.setup = function( ){
////////////////////////////////////////////
// ニューロンの初期化
////////////////////////////////////////////
for( var i = 0; i < this.W.length; i++ ){
this.X[ i ] = [];
this.x[ i ] = [];
this.delta[ i ] = [];
for( var j = 0; j < this.W[ i ][ 0 ].length; j++ ){
this.X[ i ][ j ] = 0;
this.x[ i ][ j ] = 0;
this.delta[ i ][ j ] = 0;
}
}
//出力層
this.X[ this.W.length ] = [];
this.x[ this.W.length ] = [];
this.delta[ this.W.length ] = [];
for( var j = 0; j < this.W[ this.W.length-1 ].length; j++ ){
this.X[ this.W.length ][ j ] = 0;
this.x[ this.W.length ][ j ] = 0;
this.delta[ this.W.length ][ j ] = 0;
}
////////////////////////////////////////////
// 重み格納用多重配列と偏微分値格納多重配列の初期化
////////////////////////////////////////////
for( var i = 0; i < this.W.length; i++ ){
this._W[ i ] = [];
this.dEdW[ i ] = [];
this._dEdW[ i ] = [];
for( var j = 0; j < this.W[ i ].length; j++ ){
this._W[ i ][ j ] = [];
this.dEdW[ i ][ j ] = [];
this._dEdW[ i ][ j ] = [];
for( var k = 0; k < this.W[ i ][ j ].length; k++ ){
this._W[ i ][ j ][ k ] = this.W[ i ][ j ][ k ] ;
this.dEdW[ i ][ j ][ k ] = 0;
this._dEdW[ i ][ j ][ k ] = 0;
}
}
}
////////////////////////////////////////////
// バイアス格納用多重配列
////////////////////////////////////////////
for( var i = 0; i < this.B.length; i++ ){
this._B[ i ] = [];
this.dEdB[ i ] = [];
this._dEdB[ i ] = [];
for( var j = 0; j < this.B[ i ].length; j++ ){
this._B[ i ][ j ] = this.B[ i ][ j ];
this.dEdB[ i ][ j ] = 0;
this._dEdB[ i ][ j ] = 0;
}
}
}
//重みによる勾配
FNNN.prototype.resetDEdW = function( ){
for( var l = 0; l < this.W.length; l++ ){
for( var i = 0; i < this.W[ l ].length; i++ ){
for( var j = 0; j < this.W[ l ][ i ].length; j++ ){
this.dEdW[ l ][ i ][ j ] = 0;
this._dEdW[ l ][ i ][ j ] = 0;
}
}
}
}
FNNN.prototype.add_DEdW = function( factor ){
factor = factor || 1;
for( var l = 0; l < this.W.length; l++ ){
for( var i = 0; i < this.W[ l ].length; i++ ){
for( var j = 0; j < this.W[ l ][ i ].length; j++ ){
this._dEdW[ l ][ i ][ j ] += this.dEdW[ l ][ i ][ j ] * factor;
}
}
}
}
FNNN.prototype.restoreDEdW = function( ){
for( var l = 0; l < this.W.length; l++ ){
for( var i = 0; i < this.W[ l ].length; i++ ){
for( var j = 0; j < this.W[ l ][ i ].length; j++ ){
this.dEdW[ l ][ i ][ j ] = this._dEdW[ l ][ i ][ j ];
}
}
}
}
FNNN.prototype.resetDEdB = function( ){
for( var l = 0; l < this.B.length; l++ ){
for( var i = 0; i < this.B[ l ].length; i++ ){
this.dEdB[ l ][ i ] = 0;
this._dEdB[ l ][ i ] = 0;
}
}
}
FNNN.prototype.add_DEdB = function( factor ){
factor = factor || 1;
for( var l = 0; l < this.B.length; l++ ){
for( var i = 0; i < this.B[ l ].length; i++ ){
this._dEdB[ l ][ i ] += this.dEdB[ l ][ i ] * factor;
}
}
}
FNNN.prototype.restoreDEdB = function( ){
for( var l = 0; l < this.B.length; l++ ){
for( var i = 0; i < this.B[ l ].length; i++ ){
this.dEdB[ l ][ i ] = this._dEdB[ l ][ i ];
}
}
}
//重み多重配列を一時保持用多重配列へ格納
FNNN.prototype.storeW = function( ){
for( var l = 0; l < this.W.length; l++ ){
for( var i = 0; i < this.W[ l ].length; i++ ){
for( var j = 0; j < this.W[ l ][ i ].length; j++ ){
this._W[ l ][ i ][ j ] = this.W[ l ][ i ][ j ] ;
}
}
}
}
//バイアス多重配列を一時保持用多重配列へ格納
FNNN.prototype.storeB = function( ){
for( var l = 0; l < this.B.length; l++ ){
for( var i = 0; i < this.B[ l ].length; i++ ){
this._B[ l ][ i ] = this.B[ l ][ i ];
}
}
}
//重み多重配列を一時保持用多重配列へ格納
FNNN.prototype.updateW = function( ){
for( var i = 0; i < this.W.length; i++ ){
for( var j = 0; j < this.W[ i ].length; j++ ){
for( var k = 0; k < this.W[ i ][ j ].length; k++ ){
this.W[ i ][ j ][ k ] -= this.eta * this.dEdW[ i ][ j ][ k ] ;
}
}
}
}
//バイアス多重配列を一時保持用多重配列へ格納
FNNN.prototype.updateB = function( ){
for( var i = 0; i < this.B.length; i++ ){
for( var j = 0; j < this.B[ i ].length; j++ ){
this.B[ i ][ j ] -= this.eta * this.dEdB[ i ][ j ];
}
}
}
//入力層(0層目ニューロン値)へのインプット
FNNN.prototype.setInput = function( Input ){
for( var i = 0; i < Input.length; i++ ){
this.x[ 0 ][ i ] = Input[ i ];
}
this.adoptAFh( this.x[ 0 ], this.X[ 0 ] );
}
//出力層へのアウトプット
FNNN.prototype.getOutput = function( ){
//各層ニューロン値の計算
for( var i = 0; i < this.W.length; i++ ){
this.multiplayMatrixVector ( this.W[ i ], this.X[ i ], this.X[ i+1 ] );
this.addVectors ( this.X[ i+1 ], this.B[ i ], this.x[ i+1 ] );
//活性化関数の実行
if( i < this.W.length-1 ){
//隠れ層
this.adoptAFh( this.x[ i+1 ], this.X[ i+1 ] );
} else {
//出力層
this.adoptAFsigma( this.x[ i+1 ], this.X[ i+1 ] );
}
}
return this.X[ this.X.length -1 ];
}
//誤差逆伝搬法のデルタ値へのインプット
FNNN.prototype.setDelta = function( Input ){
for( var i = 0; i < Input.length; i++ ){
this.delta[ this.delta.length - 1 ][ i ] = Input[ i ];
}
}
//誤差逆伝搬法のデルタ値と勾配の計算
FNNN.prototype.computeBackPropagation = function( ){
//層番号
for( var l = this.delta.length - 2; l > 0; l-- ){
for( var i = 0; i < this.delta[ l ].length; i++ ){
this.delta[ l ][ i ] = 0;
for( var j = 0; j < this.delta[ l + 1 ].length; j++ ){
if( l + 2 == this.delta.length){
//最終層
this.delta[ l ][ i ] += this.delta[ l + 1 ][ j ] * this.W[ l ][ j ][ i ];
} else{
this.delta[ l ][ i ] += this.delta[ l + 1 ][ j ] * this.hd( this.x[ l + 1 ][ j ] ) * this.W[ l ][ j ][ i ];
}
}
}
}
for( var l = 0; l < this.W.length; l++ ){
for( var i = 0; i < this.W[ l ].length; i++ ){
for( var j = 0; j < this.W[ l ][ i ].length; j++ ){
if( l == this.W.length - 1){
//最終層
this.dEdW[ l ][ i ][ j ] = this.delta[ l + 1 ][ i ] * this.X[ l ][ j ];
} else{
this.dEdW[ l ][ i ][ j ] = this.delta[ l + 1 ][ i ] * this.X[ l ][ j ] * this.hd( this.x[ l + 1 ][ i ] );
}
}
}
}
for( var l = 0; l < this.B.length; l++ ){
for( var i = 0; i < this.B[ l ].length; i++ ){
if( l == this.B.length - 1){
//最終層
this.dEdB[ l ][ i ] = this.delta[ l + 1 ][ i ];
} else {
this.dEdB[ l ][ i ] = this.delta[ l + 1 ][ i ] * this.hd( this.x[ l + 1 ][ i ] );
}
}
}
}
//行列×ベクトルの計算
FNNN.prototype.multiplayMatrixVector = function( M, V, C ){
C = C || [];
var Mgyou = M.length;
var Mretu = M[ 0 ].length;
for( var i = 0; i < Mgyou; i++ ){
C[ i ] =0;
for( var j = 0; j < Mretu; j++ ){
C[ i ] += M[ i ][ j ] * V[ j ];
}
}
return C;
}
//ベクトルの和
FNNN.prototype.addVectors = function( V1, V2, V3 ){
V3 = V3 || [];
for( var i = 0; i < V1.length; i++ ){
V3[ i ] = V1[ i ] + V2[ i ];
}
return V3;
}
//活性化関数の実行
FNNN.prototype.adoptAFh = function( V_in, V_out ){
V_out = V_out || [];
for( var i = 0; i < V_in.length; i++ ){
V_out[ i ] = this.h( V_in[ i ] );
}
return V_out;
}
FNNN.prototype.adoptAFsigma = function( V_in, V_out ){
V_out = V_out || [];
for( var i = 0; i < V_in.length; i++ ){
V_out[ i ] = this.sigma( V_in[ i ] );
}
return V_out;
}
実行方法
FNNNクラスを用いて、入力数1,出力数1の順伝播型ニューラルネットワークで三角関数を表現するプログラムを示します。 重みとバイアスに与える初期多重配列の構造がニューラルネットワークの層数と各層のニューロン数に対応します。
//////////////////////////////////
//ニューラルネットワークの生成
//////////////////////////////////
//学習対象関数
function f( x ){
return Math.sin(2*Math.PI*x);
}
//重みとバイアスの初期値
var W = [];
var B = [];
for( var l = 0; l < N.length-1; l++ ){
W[ l ] = [];
for( var j = 0; j < N[l+1]; j++){
W[ l ][ j ] = [];
for( var i = 0; i < N[l]; i++){
var w = (Math.random() - 0.5);
W[ l ][ j ][ i ] = w; //W^{(1)}_0i
}
}
B[ l ] = [];
for( var i = 0; i < N[l+1]; i++){
var b = (Math.random() - 0.5);
B[ l ][ i ] = b; //b^{(0)}_i
}
}
//////////////////////////////////
//ニューラルネットワークの生成
//////////////////////////////////
var nn = new FNNN( W, B, eta);
//グラフ用データ
var data1 = [];
//T回の学習
for( var t=0; t<T; t++){
//勾配データの初期化
nn.resetDEdW();
nn.resetDEdB();
//ミニバッチによる学習
for( var xi = 0; xi<=M; xi++ ){
//入力値
var x = x_min + (x_max - x_min) * xi/M;
var X0 = [ x ];
//入力層へのインプット
nn.setInput( X0 );
//出力層へのアウトプット
var X2 = nn.getOutput();
var y = X2[ 0 ];
//逆誤差伝搬の初期値
nn.setDelta( [ y - f( x ) ] );
//逆誤差伝搬の計算
nn.computeBackPropagation();
//ミニバッチ平均勾配の計算(加算平均)
nn.add_DEdB( 1/(M+1) );
nn.add_DEdW( 1/(M+1) );
}
//勾配データの更新
nn.restoreDEdB();
nn.restoreDEdW();
//重みとバイアスデータの更新
nn.updateW();
nn.updateB();
//学習結果のチェック
var sumL = 0;
for( var xi = 0; xi<M; xi++ ){
//入力値
var x = x_min + (x_max - x_min) * xi/M;
var X0 = [ x ];
nn.setInput( X0 );
var X2 = nn.getOutput( );
var y = X2[0];
sumL += 1.0/2.0*( y-f( x ) )*( y-f( x ) );
}
data1.push([ t, sumL ]);
}
実行結果



