JavaScriptのみで構築するディープラーニング with Jupyter and TensorFlow.js

## はじめに

間も無く終了する2010年代はJavaScriptが大きく成長した十年でした。Reactなどの革新的なフレームワークが登場し、Node.js、React Naitive などブラウザ以外でのエコシステムも大きく成長しました。また言語自体もES2015以降日々進歩しており、JavaScriptは2010年代でもっとも多くの人に使われたプログラミング言語となりました。さらにJavaScriptに静的型付を導入した[TypeScript](https://www.typescriptlang.org/)とそのエコシステムもここ数年で大きく成長しており、JavaScriptは大規模なソフトウェアの開発にも耐えうるようになってきています。

<img width="744" alt="top-languages" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/75666cb5-df68-c0a8-4474-4e2fa8438550.png">
出典: https://octoverse.github.com/#top-languages

一方で2010年代にJavaScript以上に大きく人気を伸ばしたプログラミング言語がありました。Pythonです。2010年代に大きく伸びた分野であるデータサイエンスと機械学習(AI)の分野ではPythonはその利便性で他のメジャーなプログラミング言語を圧倒しています。[2010年代はPython2から下位互換性のないPython3への移行という混乱と困難](https://stackoverflow.blog/2019/11/14/why-is-the-migration-to-python-3-taking-so-long/)があったにも関わらず、Pythonはデータ分析・機械学習分野の成長と歩みをともにして大きく人気を伸ばしました。2019年のGitHubの報告ではJavaを抜いて遂にユーザー数が2位となっています。JavaScriptはまだPythonにユーザー数では優っているもののデータ分析・機械学習の分野では大きく遅れをとっており、この分野の今後の伸び次第では近いうちにランキングが逆転するかもしれません。逆にユーザー数で優位にあるJavaScriptがこの分野でも人気を獲得し、その地位を不動のものとするかもしれません。

2020年代がJavaScript, Pythonにとってどのような時代になるのか楽しみです。この記事では2020年代におけるJavaScriptの機械学習、特にディープラーニングにおける成長の可能性を探るべく、JavaScriptのみを使ってディープニューラルネットワークモデルを構築することに挑戦してみます。

## TensorFlow

[TensorFlow](https://www.tensorflow.org/)はGoogleが公開しているディープラーニングのフレームワークです。FacebookのPyTorchと人気を二分するディープラーニングの二大フレームワークの1つです。学術分野では最近PyTorchに追い上げられているとの[指摘](https://thegradient.pub/state-of-ml-frameworks-2019-pytorch-dominates-research-tensorflow-dominates-industry/)もありますが、産業分野では一日の長があり2019年現在もっとも広く使用されているディープラーニングのフレームワークです。

TensorFlowのメインのターゲットはPythonですが、Python以外にも[JavaScript, C++, Java, Go, Swift](https://www.tensorflow.org/api_docs/)などが(実験的に)サポートされています。

## TensorFlow.js

> TensorFlow.js is a JavaScript Library for training and deploying machine learning models in the browser and in Node.js.

[TensorFlow.jsはTensorFlowのJavaScript版のAPIです](https://www.tensorflow.org/js)。TensorFlow.jsのもっとも重要な用途は他の環境で作成したTensorFlowのトレーニング済みモデルをウェブブラウザ上のアプリケーションに組み込めるようにすることです。ただしTensorFlow.jsはブラウザ上でのトレーニング済みモデルの実行だけでなく、Node.js上での実行やJavaScriptを使ったディープラーニングモデルのトレーニングもサポートしています。そのためTensorFlow.jsを使えばPythonを使うことなく、JavaScriptのみでディープラーニングモデルの構築からトレーニング、実際のアプリケーションとしての利用までエンドツーエンドで行うことが可能になっています。

## Jupyter

Pythonをブラウザからインタラクティブに実行するための環境です。コードや実行結果が「ノート」として保存されるので非常に便利です。JavaScriptユーザーにはあまり知られてないツールかもしれませんが、Pythonを用いたデータ処理や機械学習、あるいは教育分野で非常に人気のあるツールであり、ここ数年で急激にユーザを増やしています。[2019年のGitHub OctoverseでもJupyterの急激な成長](https://octoverse.github.com/#industry-spotlight-data-science)が言及されています。Jupyter Notebook自体についてもっと知りたい人は[GoogleでJupyterを検索](https://www.google.co.jp/search?hl=ja&q=jupyter+project)して下さい。

Jupyterは「カーネル」を追加することでPython以外の言語もサポートすることが可能です。さまざまな言語のカーネルが存在しています。今回は僕が作っているJavaScript/TypeScript用のカーネルを使用してJupyter上で機械学習を行います。

<img width="1077" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/6eb57a3d-c3c0-9acc-cd9c-f67866993193.png">
出典: https://octoverse.github.com/#industry-spotlight-data-science

## tslab

Jupyter上でJavaScriptおよびTypeScriptをインタラクティブに実行するためのJupyterカーネルです。

[yunabe/tslab - Interactive JavaScript and TypeScript programming with Jupyter (GitHub)](https://github.com/yunabe/tslab)

(気に入ったらぜひStarもして下さい:star:)

tslabはTypeScriptをベースに作られており、以下のような特徴があります:

- Jupyter上でのJavaScriptおよびTypeScriptのインタラクティブな実行
- TypeScript由来の静的型情報の活用
  - JavaScriptでも静的型チェックが行われる。
  - 静的型情報を利用した、コード補完やtipsの表示
- TypeScript 3.7のサポート
- Top-level await のサポート

またtslabは[JavaScript/TypeScriptのリッチなREPL](https://github.com/yunabe/tslab#usage-repl-console)として使用することもできます。

この記事ではtslabを使って、Jupyter上でインタラクティブにディープラーニングモデルを構築していきます。

<img width="400" alt="complete.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/7690b2cb-9d10-eb97-2697-524f309e3c7b.jpeg">
<img width="400" alt="complete.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/b1ea1d60-4c0a-2889-bde9-fc0ef8a49fa6.jpeg">


## 環境構築

### Node.js

Node.jsがインストールされていない場合は[Node.js](https://www.google.co.jp/search?hl=ja&q=node.js+%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)を導入しましょう。現在の最新のLTSであるv12を前提に話を進めますが、v10, v13でも問題はないと思います。

### Jupyter

Jupyterを使ってインタラクティブにディープラーニングモデルの構築を行いたいので、[Jupyterもインストール](https://www.google.co.jp/search?hl=ja&q=jupyter+%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)しましょう。Pythonがよくわからない場合はAnacondaを使って導入するのが一番簡単だと思います。最新版のJupyterはPython2のサポートをすでに停止しています。特に理由がなければPython3版を使用して下さい。

### tslab

[tslabのREADME.mdに従ってインストール](https://github.com/yunabe/tslab/blob/master/README.md#installing-tslab)してください。

```shell
npm install -g tslab
```

インストールに成功したら、tslabをJupyterに登録します

```shell
tslab install
```

`tslab install`が成功したら、`jupyter kernelspec list`でカーネルがJupyterに登録されていることを確認しましょう。

```shell
$ jupyter kernelspec list
Available kernels:
  jslab      /usr/local/google/home/yunabe/.local/share/jupyter/kernels/jslab
  tslab      /usr/local/google/home/yunabe/.local/share/jupyter/kernels/tslab
```

これでJupyter上でJavaScript/TypeScriptを実行できる環境が整いました。

```shell
jupyter notebook [--port=8888]
```

を実行して、JavaScript, TypeScriptの「ノートブック」がJupyterで作成でき、JavaScriptをインタラクティブにノートブック上で実行できることを確かめてください。

<img width="536" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/220a482c-1c5a-337f-a956-f07f7f38ae8e.png">


## npmプロジェクトの作成

Node.js, Jupyter, tslab のセットアップができたら環境のセットアップは完了です。npmのプロジェクトを作ってTensorFlowをインストールし、ディープニューラルネットーワークをJavaScriptで行っていきましょう。今回は僕が作成した[npmプロジェクトとJupyterのノートがGitHubに置いてある](https://github.com/yunabe/qiita-20191202-jsml)のでそれを取得してください。

```shell
git clone https://github.com/yunabe/qiita-20191202-jsml.git
cd qiita-20191202-jsml
npm install # or yarn
```

一からプロジェクトを構築する場合は通常通り、`npm init`や`yarn init`でプロジェクトを作ってください。

## TensorFlow.js のインストール

チュートリアル[TensorFlow.js in Node](https://www.tensorflow.org/js/guide/nodejs)に従って、TensorFlow.jsをプロジェクトにインストールしてください。

```shell
npm install @tensorflow/tfjs-node
```

TensorFlow.jsはGPUによる高速化にも対応しています。
LinuxでGPU/CUDAが使用可能な環境であれば、`tsjs-node`ではなく、`@tensorflow/tfjs-node-gpu`をインストールしてください。

これでJupyter上でTensorFlow.jsを使ってディープラーニングを行う準備は完了です。Jupyterを起動してJavaScriptあるいはTypeScriptのノートブックを作成してください。ここから先のコードはJupyter上で実行していきます。

```shell
jupyter notebook
```

ここで使ったノートブックはGitHubにコミットしてあります。ここから先はGitHub上のノートブックを直接参照してもらっても構いません。この記事ではJavaScriptを使用していますが、ノートブックはTypeScript版も用意しています。TypeScriptが分かる人にはTypeScript版をお勧めします。

**GitHub**

- [JavaScript版ノートブック](https://github.com/yunabe/qiita-20191202-jsml/blob/master/notebooks/tensorflow.ipynb)
- [TypeScript版ノートブック](https://github.com/yunabe/qiita-20191202-jsml/blob/master/notebooks/tensorflow_typescript.ipynb)

**nbviewer (GitHubよりノートブックがみやすいです)**

- [JavaScript版ノートブック](https://nbviewer.jupyter.org/github/yunabe/qiita-20191202-jsml/blob/master/notebooks/tensorflow.ipynb)
- [TypeScript版ノートブック](https://nbviewer.jupyter.org/github/yunabe/qiita-20191202-jsml/blob/master/notebooks/tensorflow_typescript.ipynb)

## JavaScriptと静的型チェック

今回使用する[JavaScript/TypeScriptの実行環境 tslab](https://github.com/yunabe/tslab)はTypeScriptをベースに実装されており、JavaScriptに対してもある程度の静的型チェックを行います。

```javascript
let x = 123;
x += ' hello';
console.log(x);
```

例えば上のコードは1行目では`x`の型は`number`であることを意図しているようにみえるのに、2行目で`string`を代入しています。tslabはこのようなケースに対してエラーを表示します。

```
2:1 - Type 'string' is not assignable to type 'number'.
```

ただし、上記のコードは`string`の代入が本当に意図的なのであれば正当なJavaScriptのコードです。そのような場合には[JSDoc](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc)を使って`x`が任意の型(`any`)をとりうることを明示する必要があります。

```javascript
/** @type {any} */
let x = 123;
x += ' hello';
console.log(x);
```

このノートブックでは

- `@param`, `@return`による[関数の型情報の宣言](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#param-and-returns)
- 型チェックをパスするための[キャスト](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#casts)

を利用しているので留意して下さい。型チェックの詳細については[Type Checking JavaScript Files
](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html)を参照して下さい。

またtslabでは静的型情報を使っているので、強力なコードの補完(`Tab`)と変数・関数定義情報の表示(`Shift-Tab`)が使用可能です。活用しながらコーディングして下さい。

## TensorFlow.jsがインストールされていることの確認

まず初めに、TensorFlow.jsが正しくimportでき実行できるかバージョンを表示して確かめてみましょう。ちなみにtslabでHTMLや画像を表示するには`tslab.display`を使用します。無事バージョン番号が表示されたでしょうか。

ちなみにJupyter上では`Tab`でコード補完、`Shift-Tab`で関数定義などの表示が行えます。活用しながらコードを書いてください。


```typescript
// const tf = require('@tensorflow/tfjs-node') も可能。
import * as tf from '@tensorflow/tfjs-node'
import * as tslab from "tslab";

tslab.display.html('<h2>tf.version</h2>')
console.log(tf.version);
tslab.display.html('<h2>tslab.versions</h2>')
console.log('tslab.versions:', tslab.versions);
```

## MNIST をダウンロードする

TensorFlowが無事にNodeにインストール出来たので、ニューラルネットワークモデルを実際に構築してJavaScriptでトレーニングを行ってみましょう。ここでは機械学習のチュートリアルで常に使わる手書き文字認識のデータセット[MNIST database](https://en.wikipedia.org/wiki/MNIST_database)を使って、数字の文字認識の機械学習を行ってみましょう。

TensorFlow.jsのサンプルコードの中に[MNISTをダウンロードしてTensorFlowの内部表現に変換するコードの例](https://github.com/tensorflow/tfjs-examples/blob/master/mnist-node/)が存在するので今回はそれを利用します。このコードはすでに[例のレポジトリにコピーしてある](https://github.com/yunabe/qiita-20191202-jsml/blob/master/lib/mnist.js)のでここではそれを`import`します。興味があればどのような実装になっているかのぞいてみてください。

`loadData()`は`Promise`を返すのでタスクの終了まで待機するのを忘れないでください。tslabはtop-level awaitをサポートしているので`await`をつけるだけでOKです。

```javascript
import mnist from '../lib/mnist';
await mnist.loadData();
```

## データの確認と可視化

[WikipediaのMNISTの記事](https://en.wikipedia.org/wiki/MNIST_database)にも書かれているように、MNISTのデータは60,000の訓練用データ(training data)と10,000の評価用データ(test data)に事前に分けられています。実際にダウンロードされたデータの大きさを確認してみましょう。

訓練データに60,000個、評価データに10,000個の数字の画像(`images`)と文字認識の正解データ(`labels`)が存在することが確認できたと思います。数字の画像データは28x28の白黒1チャンネル（グレースケール）であることも分かります。

ちなみにデータの内部表現として使われている[`Tensor`のAPIはここにドキュメントがあります](https://js.tensorflow.org/api/latest/#class:Tensor)。

```javascript
tslab.display.html('<h2>訓練データのサイズ</h2>')
console.log(mnist.getTrainData());
tslab.display.html('<h2>評価データのサイズ</h2>')
console.log(mnist.getTestData());
```

```javascript
// 訓練データのサイズ
{
  images: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 60000, 28, 28, 1 ],
    dtype: 'float32',
    size: 47040000,
    strides: [ 784, 28, 1 ],
    dataId: {},
    id: 0,
    rankType: '4'
  },
  labels: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 60000, 10 ],
    dtype: 'float32',
    size: 600000,
    strides: [ 10 ],
    dataId: {},
    id: 10,
    rankType: '2',
    scopeId: 6
  }
}
// 評価データのサイズ
{
  images: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 10000, 28, 28, 1 ],
    dtype: 'float32',
    size: 7840000,
    strides: [ 784, 28, 1 ],
    dataId: {},
    id: 11,
    rankType: '4'
  },
  labels: Tensor {
    kept: false,
    isDisposedInternal: false,
    shape: [ 10000, 10 ],
    dtype: 'float32',
    size: 100000,
    strides: [ 10 ],
    dataId: {},
    id: 21,
    rankType: '2',
    scopeId: 14
  }
}
```

次にMNISTのデータの可視化もしてみましょう。MNISTの画像データは28x28の`0`から`1.0`のグレースケールのデータの配列です。画像ライブラリ`jimp`を使用してPNGに変換して可視化してみます。

```javascript
import Jimp from 'jimp';
import {promisify} from 'util';

/**
 * @param {tf.Tensor4D} images
 * @param {number} start
 * @param {number} size
 * @return {Promise<Buffer[]>}
 */
async function toPng(images, start, size) {
    // Note: mnist.getTrainData().images.slice([index], [1]) is slow.
    let arry = images.slice([start], [size]).flatten().arraySync();
    let ret = [];
    for (let i = 0; i < size; i++) {
        let raw = [];
        for (const v of arry.slice(i * 28 * 28, (i+1)*28*28)) {
            raw.push(...[v*255, v*255, v*255, 255])
        }
        let img = await promisify(cb => {
            new Jimp({ data: Buffer.from(raw), width: 28, height: 28 }, cb);
        })();
        ret.push(await img.getBufferAsync(Jimp.MIME_PNG));
    }
    return ret;
}

{
    const size = 8;
    const labels = await mnist.getTestData().labels.slice([0], [size]).argMax(1).array();
    const pngs = await toPng(mnist.getTestData().images, 0, size);
    for (let i = 0; i < size; i++) {
        tslab.display.html(`<h3>label: ${labels[i]}</h3>`)
        tslab.display.png(pngs[i]);
    }
}
```
<img width="96" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/897ff8e7-1fb4-e6bc-6bda-0b9dfef326ba.png">

## ディープラーニングで文字認識を行う

MNISTのデータの素性が一通り分かったので、TensorFlow.jsを使って「ディープニューラルネットワーク」機械学習のモデルを設計、訓練し文字認識を行ってみます。
初めは[PythonのTensorFlowチュートリアル](https://www.tensorflow.org/tutorials/quickstart/beginner)でも利用されている、128ノードの中間層を一つ持つ単純なニューラルネットワークモデルを使って文字認識を行います。

[TensorFlow.jsは、PythonのTensorFlowでも使われているkerasをベースにしたAPIを提供しています](https://www.tensorflow.org/js/guide/layers_for_keras_users)。そのためこの程度のシンプルなディープニューラルネットワークはTensorFlow.jsでも非常に簡単に実装できます。[Layer APIの詳細はTensorFlow.jsのドキュメント](https://www.tensorflow.org/js/guide/models_and_layers)を参照してください。

```javascript
const model = tf.sequential();
model.add(tf.layers.flatten({inputShape: [28, 28, 1]}));
model.add(tf.layers.dense({units: 128, activation: 'relu'}));
model.add(tf.layers.dropout({rate: 0.2}));
model.add(tf.layers.dense({units: 10, activation: 'softmax'}));

model.compile({
  optimizer: 'adam',
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

/**
 * @param {tf.Sequential} model
 * @param {number} epochs
 * @param {number} batchSize
 * @param {string} modelSavePath
 */
async function train(model, epochs, batchSize, modelSavePath) {
  // Hack to suppress the progress bar by TensorFlow.js
  process.stderr.isTTY = false;
  const {images: trainImages, labels: trainLabels} = mnist.getTrainData();
  model.summary();

  let epochBeginTime;
  let millisPerStep;
  const validationSplit = 0.15;
  const numTrainExamplesPerEpoch =
      trainImages.shape[0] * (1 - validationSplit);
  const numTrainBatchesPerEpoch =
      Math.ceil(numTrainExamplesPerEpoch / batchSize);
  const batchesPerEpoch = Math.floor(trainImages.shape[0]*(1-validationSplit)/batchSize);
  /** @type {tslab.Display} */
  let display = null;
  await model.fit(trainImages, trainLabels, {
    callbacks: {
      onEpochBegin: (epoch) => {
        display = tslab.newDisplay();
      },
      onBatchBegin: (batch) => {
        display.text(`Progress: ${(100*batch/batchesPerEpoch).toFixed(1)}%`)
      },
    },
    epochs,
    batchSize,
    validationSplit,
  });

  const {images: testImages, labels: testLabels} = mnist.getTestData();
  const evalOutput = model.evaluate(testImages, testLabels);

  console.log(
      `\nEvaluation result:\n` +
      `  Loss = ${evalOutput[0].dataSync()[0].toFixed(3)}; `+
      `Accuracy = ${evalOutput[1].dataSync()[0].toFixed(3)}`);

  if (modelSavePath != null) {
    await model.save(`file://${modelSavePath}`);
    console.log(`Saved model to path: ${modelSavePath}`);
  }
}

const epochs = 5;
const batchSize = 32;
const modelSavePath = 'mnist'
await train(model, epochs, batchSize, modelSavePath);
```

```text
_________________________________________________________________
Layer (type)                 Output shape              Param #   
=================================================================
flatten_Flatten1 (Flatten)   [null,784]                0         
_________________________________________________________________
dense_Dense1 (Dense)         [null,128]                100480    
_________________________________________________________________
dropout_Dropout1 (Dropout)   [null,128]                0         
_________________________________________________________________
dense_Dense2 (Dense)         [null,10]                 1290      
=================================================================
Total params: 101770
Trainable params: 101770
Non-trainable params: 0
_________________________________________________________________
Epoch 1 / 5
Epoch 2 / 5
Epoch 3 / 5
Epoch 4 / 5
Epoch 5 / 5
14906ms 292us/step - acc=0.974 loss=0.0845 val_acc=0.975 val_loss=0.0796 

Evaluation result:
  Loss = 0.083; Accuracy = 0.980
Saved model to path: mnist
```

## トレーニングしたモデルを実行する

98%とそこそこ精度のよい文字認識のモデルができたので実際にテストデータを使って文字認識を行ってみましょう。

```javascript
const predicted = /** @type {number[]} */(tf.argMax(/** @type {tf.Tensor} */ (model.predict(mnist.getTestData().images)), 1).arraySync());
const labels = /** @type {number[]} */(tf.argMax(mnist.getTestData().labels, 1).arraySync());
console.log('predictions:', predicted.slice(0, 10));
console.log('labels:', labels.slice(0, 10));
```

```text
predictions: [
  7, 2, 1, 0, 4,
  1, 4, 9, 5, 9
]
labels: [
  7, 2, 1, 0, 4,
  1, 4, 9, 5, 9
]
```

正しく文字認識ができていますね。せっかくなので文字認識に失敗する例を可視化してみましょう。精度が98%程度あるとはいえ、人間であれば間違えないようなものが多いですね。

```javascript
const predicted = /** @type {number[]} */(tf.argMax(/** @type {tf.Tensor} */ (model.predict(mnist.getTestData().images)), 1).arraySync());
const labels = /** @type {number[]} */(tf.argMax(mnist.getTestData().labels, 1).arraySync());

const numSamples = 32;
let count = 0;
for (let i = 0; i < predicted.length && labels.length; i++) {
    const pred = predicted[i];
    const label = labels[i];
    if (pred === label) {
        continue;
    }
    tslab.display.html(`<h3>予測: ${pred}, 正解: ${label}</h3>`)
    const pngs = await toPng(mnist.getTestData().images, i, 1);
    tslab.display.png(pngs[0]);
    count++;
    if (count >= numSamples) {
        break;
    }
}
```

<img width="140" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/a0cc5cab-2396-10bb-f5e5-0d732ed76617.png">

# CNN (convolutional neural network) による画像認識

MNISTの文字列認識は典型的な画像を対象としたディープラーニングなので、[CNNによるディープラーニング](https://developers.google.com/machine-learning/glossary/#convolutional_neural_network)も試してみましょう。モデルの構造は[TensorFlow.jsの例から拝借してきます](https://github.com/tensorflow/tfjs-examples/tree/master/mnist-node)。

```javascript
const cnnModel = tf.sequential();
cnnModel.add(tf.layers.conv2d({
  inputShape: [28, 28, 1],
  filters: 32,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.conv2d({
  filters: 32,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
cnnModel.add(tf.layers.conv2d({
  filters: 64,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.conv2d({
  filters: 64,
  kernelSize: 3,
  activation: 'relu',
}));
cnnModel.add(tf.layers.maxPooling2d({poolSize: [2, 2]}));
cnnModel.add(tf.layers.flatten());
cnnModel.add(tf.layers.dropout({rate: 0.25}));
cnnModel.add(tf.layers.dense({units: 512, activation: 'relu'}));
cnnModel.add(tf.layers.dropout({rate: 0.5}));
cnnModel.add(tf.layers.dense({units: 10, activation: 'softmax'}));

const optimizer = 'rmsprop';
cnnModel.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});
```

最初のモデルに比べると構造が複雑なのでトレーニングには時間がかかります。GPUが使える場合はGPUによる高速化の威力がよく実感できると思います。

```javascript
const epochs = 20;
const batchSize = 128;
const modelSavePath = 'cnn_mnist'
await train(cnnModel, epochs, batchSize, modelSavePath);
```

```text
Layer (type)                 Output shape              Param #   
=================================================================
conv2d_Conv2D5 (Conv2D)      [null,26,26,32]           320       
_________________________________________________________________
conv2d_Conv2D6 (Conv2D)      [null,24,24,32]           9248      
_________________________________________________________________
max_pooling2d_MaxPooling2D3  [null,12,12,32]           0         
_________________________________________________________________
conv2d_Conv2D7 (Conv2D)      [null,10,10,64]           18496     
_________________________________________________________________
conv2d_Conv2D8 (Conv2D)      [null,8,8,64]             36928     
_________________________________________________________________
max_pooling2d_MaxPooling2D4  [null,4,4,64]             0         
_________________________________________________________________
flatten_Flatten3 (Flatten)   [null,1024]               0         
_________________________________________________________________
dropout_Dropout4 (Dropout)   [null,1024]               0         
_________________________________________________________________
dense_Dense5 (Dense)         [null,512]                524800    
_________________________________________________________________
dropout_Dropout5 (Dropout)   [null,512]                0         
_________________________________________________________________
dense_Dense6 (Dense)         [null,10]                 5130      
=================================================================
Total params: 594922
Trainable params: 594922
Non-trainable params: 0
_________________________________________________________________
Epoch 1 / 20
  ...
Epoch 20 / 20

Evaluation result:
  Loss = 0.022; Accuracy = 0.994
Saved model to path: cnn_mnist
```

テストデータに対する精度が99.4%まで上昇しました。先の精度98%の単純なモデルでやったように、CNNでも認識に失敗している画像を表示してみましょう。
最初の単純なモデルの失敗例に比べると、人間でも認識に失敗しそうな、あるいはラベルが間違っていると言いたくなるような例が多くなっており、文字認識の精度が大きく向上していることが体感できると思います。

```javascript
const predicted = /** @type {number[]} */(tf.argMax(/** @type {tf.Tensor} */ (cnnModel.predict(mnist.getTestData().images)), 1).arraySync());
const labels = /** @type {number[]} */(tf.argMax(mnist.getTestData().labels, 1).arraySync());

const numSamples = 32;
let count = 0;
for (let i = 0; i < predicted.length && labels.length; i++) {
    const pred = predicted[i];
    const label = labels[i];
    if (pred === label) {
        continue;
    }
    tslab.display.html(`<h3>予測: ${pred}, 正解: ${label}</h3>`)
    const pngs = await toPng(mnist.getTestData().images, i, 1);
    tslab.display.png(pngs[0]);
    count++;
    if (count >= numSamples) {
        break;
    }
}
```

<img width="132" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218033/f5e47285-ff51-d7e1-3f46-36bc0fd98d69.png">

以上でJavaScriptのみで複雑なディープラーニングモデルを構築・訓練から実際にアプリケーション上で推論を行うところまでエンドツーエンドで行うことができました。TensorFlow.jsを使っているので、完成したモデルをブラウザ上で実行することも最小限の追加コードで実現できます。詳しくは[TensorFlow.js](https://www.tensorflow.org/js/guide)のドキュメントや他の人による解説記事を参照してください。

## 最後に

JavaScript (TypeScript) でもTensorFlowとJupyterを使用してディープラーニングのトレーニングが行えることを示しました。TensorFlow.jsを使えばPythonを使用しなくてもJavaScriptだけで最新のディープラーニングをモデルのトレーニングから実際のプロダクションでの利用までエンドツーエンドで行うことが可能です。

もちろん現状では機械学習の分野でのPythonの地位は圧倒的なものであり、TensorFlowがJavaScriptをサポートしているとはいえ、APIやドキュメントの充実度には同じTensorFlowの中でも天地ほどの差があります。また周辺のデータ分析関係のライブラリの充実度やエコシステムの規模も考えると機械学習のプロジェクトにJavaScript（というよりPython以外の言語）をメインで採用する合理的な理由は現時点ではほとんど存在しないと思います。

一方でPythonは2010年代に大きく成長したもう一つの分野であるモバイルアプリ開発やWebの分野では[それほど成功していない](http://pyfound.blogspot.com/2019/05/russell-keith-magee-python-on-other.html)現状や、Webフロントエンド開発でのJavaScriptの地位は当分の間は揺らぎそうにないこと、TypeScriptとそのエコシステムが大きく成長しているおり大規模開発にも耐えうるようになってきていることなどを踏まえるとデータ分析分野でのPythonの不動の地位と、それによるPythonの人気の上昇も必ずしも向こう10年間安泰ではないかもしれません。いずれにしてもプログラミング言語のある分野での人気と人気を裏付ける利便性は[鶏と卵の関係](https://www.google.co.jp/search?hl=ja&q=chicken-and-egg)にあるので誰かが初期投資をする必要があります。2019年末現在、この分野におけるJavaScriptのライブラリ、フレームワークは非常に貧弱であり、Pythonにおけるnumpy, pandas, sklearnなどのようなデファクトスタンダードは存在しません。JavaScriptユーザのみなさん、ブルー・オーシャンであるJavaScriptの機械学習分野での成長に賭けてみるのも面白いかもしれませんよ。