ゼロから作る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 ]); }
実行結果