TensorFlow.JSで手入力数字の認識を学習させてみた（初歩の初歩／その３）

#３、TensorFlow.JSで手入力数字の認識を学習
　定番中の定番であるMNISTを使用した学習をTensorFlow.JSで作成しました。　通常は[MNIST特有仕様のデータ](https://weblabo.oscasierra.net/python/ai-mnist-data-detail.html)を使用しますが、今回は[PNG画像ファイル](https://github.com/pjreddie/mnist-csv-png)を使用しましたので学習データの取り扱いは容易かと思います。　但し、データの読み込みには時間を要しますが！　実際の動作は、[ここ（TensorFlow.JSで手入力数字の認識を学習させてみた／２中間層）](http://hal322.html.xdomain.jp/TensorFlowJS/200_MNIST_with_TensorFlowJS.html)あるいは[ここ（TensorFlow.JSで手入力数字の認識を学習させてみた／３中間層)](http://hal322.html.xdomain.jp/TensorFlowJS/201_MNIST_with_TensorFlowJS.html)をクリックして下さい。　Windows 10の環境で「Firefox / Ver. 65.0.2 (64ビット)」及び 「Google Chrome / Ver. 73.0.3683.86 (64ビット)」で動作を確認しました。　また、Android 7.0でも動作しましたが、iOSでは上手く動作しませんでした。 
　本プログラムの作成手順は、次の通りです。

##３－１、準備
　学習データ、検証データ合わせて６万のPNG画像ファイルがありますので、次にようなファイルリスト（Ａ列）とラベルデータ（Ｂ列）をセットにしたCSVファイルを作成しました。　このCSVファイルを作成するにはEXCEL+VBAを使用すると簡単ですよね。　なお、このCSVファイル内容を変更あるいは追加することで、任意の画像ファイルを学習データとすることが出来ます。
![MNIST_File_List.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408707/c9a1c420-3f89-d86a-e5d9-077156c2cf24.png)

##３－２、学習モデルの作成
　MNISTの学習モデルについては、色々なサイトで解説されていますのでここでは割愛します。（「[Deep Learning 深層学習 - 東京大学](http://ibis.t.u-tokyo.ac.jp/suzuki/lecture/2018/ML_Gairon/ML_Gairon_02.pdf)」が良い参考になると思います）　入力データは単色のPNG画像ですので、２８(dw：横画素数)×２８(dh：縦画素数)×１（色数）で１画像分のデータとなり、教師データは０～９までの数字１０個のラベルに対応したコード（例：０は「1,0,0,0,0,0,0,0,0,0」、９は「0,0,0,0,0,0,0,0,0,1」の配列）にしています。　このため、評価結果は各配列に対応したそれぞれの確率で出力されます。　以下に２中間層と３中間層の２種類の学習モデルを異なるパラメータ表現で示します。　なお、ここでのパラメータは最適化したものではありません。

```Javascript:学習モデルの作成（２中間層）
let model = tf.sequential();
  model.add(
    tf.layers.conv2d({
      inputShape: [dw, dh, 1],
      kernelSize: 5,
      filters: 8,
      strides: 1,
      activation: "relu",
      kernelInitializer: "varianceScaling"
    })
  );
  model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] }));
  model.add(tf.layers.conv2d({ kernelSize: 5, filters: 16, activation: "relu", kernelInitializer: "varianceScaling" }));
  model.add(tf.layers.maxPooling2d({ poolSize: [3, 3], strides: [3, 3] }));
  model.add(tf.layers.flatten());
  model.add(tf.layers.dense({ units: 10, kernelInitializer: "varianceScaling", activation: "softmax" }));
```


```Javascript:学習モデルの作成（３中間層）
let model = tf.sequential();
  model.add(
    tf.layers.conv2d({
      inputShape: [dw, dh, 1],
      kernelSize: 3,
      filters: 16,
      activation: "relu"
    })
  );
  model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));
  model.add(tf.layers.conv2d({ kernelSize: 3, filters: 32, activation: "relu" }));
  model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));
  model.add(tf.layers.conv2d({ kernelSize: 3, filters: 32, activation: "relu" }));
  model.add(tf.layers.flatten({}));
  model.add(tf.layers.dense({ units: 64, activation: "relu" }));
  model.add(tf.layers.dense({ units: 10, activation: "softmax" }));

```

##３－３、学習用データ、検証用データの読込
　読込んだ画像データ１枚毎にCANVASに２８×２８画素で表示させ、各１画素づつ数値データ化しています。　この数値データは０～２５５で表現されますが、そのままの数値「int32」形式では上手く動作しませんでしたので２５５で割った「float32」形式にしてxTrainDataに格納しました。　教師データとしてのラベル・コードは、yTrainDataに格納しています。　検証用データ（xTestDataとyTestData）も同様に処理しました。

```Javascript:学習用データの読込
    var ctx_train = canvas_train.getContext('2d');
    var Train_img = new Image();
    Train_img.src = "./Data/mnist_train/" + Temp_file;
    Train_img.onload = function() {
	ctx_train.drawImage(Train_img, 0, 0, dw * ds, dh * ds);
    };
    for(y=0; y<dh; y++) {
	  for(x=0; x<dw; x++) {
	    Temp_img = ctx_train.getImageData((x * ds), (y * ds), 1, 1);
	    xTrainData.push( Temp_img.data[0] / 255 );
	  }
    }
	Temp_label = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
	if(Train_File_List[ Temp_num ][1] == Label[1]) {
	    Temp_label = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[2]) {
	    Temp_label = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[3]) {
	    Temp_label = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[4]) {
	    Temp_label = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[5]) {
	    Temp_label = [0, 0, 0, 0, 0, 1, 0, 0, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[6]) {
	    Temp_label = [0, 0, 0, 0, 0, 0, 1, 0, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[7]) {
	    Temp_label = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[8]) {
	    Temp_label = [0, 0, 0, 0, 0, 0, 0, 0, 1, 0];
	} else if (Train_File_List[ Temp_num ][1] == Label[9]) {
	    Temp_label = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
	} else {
	    Temp_label = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
	}
	yTrainData.push(Temp_label);
```

##３－４、学習データと検証データをTensorFlow.JS用に変換
　画像データであるxTrainDataとxTestDataは tf.tensor4d（データ数、画像の横画素数、縦画素数、色数の４個）で、ラベルデータであるyTrainDataとyTestDataは tf.tensor2d（データ数、ラベル数の２個）でTensorFlow.JS用に変換しました。　ここでの色数は、単色ですので「１」となります。

```Javascript:学習データと検証データをTensorFlow.JS用に変換
  const xTrain = tf.tensor4d( xTrainData , [ (xTrainData.length/dw/dh), dw, dh, 1 ], 'float32' );
  const yTrain = tf.tensor2d( yTrainData, [ yTrainData.length, Label.length ], 'int32' );
  const xTest = tf.tensor4d( xTestData , [ (xTestData.length/dw/dh), dw, dh, 1 ], 'float32' );
  const yTest = tf.tensor2d( yTestData, [ yTestData.length, Label.length ], 'int32' );
```

##３－５、学習の実行
　作成した学習データ xTrain と yTrain を使用して学習させ、検証データとして xTest と yTest を指定しています。　なお、callbacksの中身は、途中経過の表示です。

```Javascript:学習の実行
// 学習過程の設定
  model.compile({
    loss: "categoricalCrossentropy",
    optimizer: tf.train.sgd( optimize ),
    metrics: ['accuracy']
  });

// 学習と学習経過をdspに格納
  const history = await model.fit( xTrain, yTrain, {
	batchSize: BatchSize,
	epochs: epochData,
	validationData:[ xTest, yTest ],
	shuffle: true,
	callbacks: {
	    onEpochEnd: async (epoch, logs) => {
		sampleData_x01.push(Train_count * 1.0 + epoch * 1.0);
		sampleData_y01.push(logs.loss);
		sampleData_z01.push(logs.val_loss);
		sampleData_x02.push(Train_count * 1.0 + epoch * 1.0);
		sampleData_y02.push(logs.acc);
		sampleData_z02.push(logs.val_acc);
		Learn_Chart_01();
		Learn_Chart_02();
		if(((epoch + 1) % 10) == 0) {
		    dsp = dsp + ("      " + (Train_count + epoch)).slice(-7) + " : " + (logs.loss + "000000000000").slice(0,12) + " / " + (logs.acc + "000000000000").slice(0,12) + "  /  " + (logs.val_loss + "000000000000").slice(0,12) + " / " + (logs.val_acc + "000000000000").slice(0,12) + "\n";
		    document.getElementById("dump").value = dsp;
		}
	    },
	}
  });
```

##３－６、評価用データの読込
　評価用データの入力には [signature_pad](https://github.com/szimek/signature_pad) を利用しています。　入力した手書き数字を２８×２８の学習データと同一サイズに変換して数値データ化しました。

```Javascript:評価用データの読込
    var EvData = [];
    var ctx_Ev = canvas_Ev.getContext('2d');
    var ctx_Ev_28 = canvas_Ev_28.getContext('2d');
    for(y=0; y<dh; y++) {
      for(x=0; x<dw; x++) {
	    Temp_img = ctx_Ev.getImageData((x * ds_Ev), (y * ds_Ev), 1, 1);
	    ctx_Ev_28.putImageData(Temp_img, x, y);
	    EvData.push( Temp_img.data[0] / 255 );
      }
    }
```

##３－７、評価用データをTensorFlow.JS用に変換
　学習用データと同様に評価用データもTensorFlow.JS用に変換します。　評価用データ数は１個ですので、tf.tensor4dのパラメータもそれに合わせています。

```Javascript:評価用データをTensorFlow.JS用に変換
    const inputData = tf.tensor4d( EvData, [ 1, dw, dh, 1 ] );
```

##３－８、評価の実行
　ここでは、resultsに１０個のラベル（０～９）に対応した評価結果（それぞれの確率）が配列として格納されます。

```Javascript:評価の実行
    const outputData = model.predict(inputData);
// 評価結果の表示
    var results = outputData.dataSync();
    document.getElementById('result_00').value = (Label[0] + ": " + Math.floor(results[0] * 100000 ) / 1000 + " %");
    document.getElementById('result_01').value = (Label[1] + ": " + Math.floor(results[1] * 100000 ) / 1000 + " %");
    document.getElementById('result_02').value = (Label[2] + ": " + Math.floor(results[2] * 100000 ) / 1000 + " %");
    document.getElementById('result_03').value = (Label[3] + ": " + Math.floor(results[3] * 100000 ) / 1000 + " %");
    document.getElementById('result_04').value = (Label[4] + ": " + Math.floor(results[4] * 100000 ) / 1000 + " %");
    document.getElementById('result_05').value = (Label[5] + ": " + Math.floor(results[5] * 100000 ) / 1000 + " %");
    document.getElementById('result_06').value = (Label[6] + ": " + Math.floor(results[6] * 100000 ) / 1000 + " %");
    document.getElementById('result_07').value = (Label[7] + ": " + Math.floor(results[7] * 100000 ) / 1000 + " %");
    document.getElementById('result_08').value = (Label[8] + ": " + Math.floor(results[8] * 100000 ) / 1000 + " %");
    document.getElementById('result_09').value = (Label[9] + ": " + Math.floor(results[9] * 100000 ) / 1000 + " %");
```

　実際の動作は、[ここ（TensorFlow.JSで手入力数字の認識を学習させてみた／２中間層）](http://hal322.html.xdomain.jp/TensorFlowJS/200_MNIST_with_TensorFlowJS.html)あるいは[ここ（TensorFlow.JSで手入力数字の認識を学習させてみた／３中間層)](http://hal322.html.xdomain.jp/TensorFlowJS/201_MNIST_with_TensorFlowJS.html)をクリックして下さい。


***
###TensorFlow.JSを使用してみたの記事内容
１、[TensorFlow.JSで簡単な関数を学習させてみた](https://qiita.com/halboujp/items/34771a814b41e36245d4)
２、[TensorFlow.JSでアヤメの分類を学習させてみた](https://qiita.com/halboujp/items/e224f0ff0cb5b9784d95)
３、TensorFlow.JSで手入力数字の認識を学習させてみた
４、[TensorFlow.JSで画像の認識（分類）を学習させてみた](https://qiita.com/halboujp/items/64dda5ce482d4f84d6de)


ここに記載したHTML/JavaScriptの動作を確認したい方は、[「http://hal322.html.xdomain.jp/」](http://hal322.html.xdomain.jp/)を参照ください。