まず初めに、TensorFlow.jsが正しくimportでき実行できるかバージョンを表示して確かめてみましょう。ちなみにtslabでHTMLや画像を表示するにはtslab.display
を使用します。無事バージョン番号が表示されたでしょうか。
ちなみにJupyter上ではTab
でコード補完、Shift-Tab
で関数定義などの表示が行えます。活用しながらコードを書いてください。
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);
{ 'tfjs-core': '1.3.2', 'tfjs-data': '1.3.2', 'tfjs-layers': '1.3.2', 'tfjs-converter': '1.3.2', tfjs: '1.3.2', 'tfjs-node': '1.3.2' }
tslab.versions: { tslab: '1.0.5', typescript: '3.7.2', node: 'v12.13.0' }
TensorFlowが無事にNodeにインストール出来たので、ニューラルネットワークモデルを実際に構築してJavaScriptでトレーニングを行ってみましょう。ここでは機械学習のチュートリアルで常に使わる手書き文字認識のデータセットMNIST databaseを使って、数字の文字認識の機械学習を行ってみましょう。
TensorFlow.jsのサンプルコードの中にMNISTをダウンロードしてTensorFlowの内部表現に変換するコードの例が存在するので今回はそれを利用します。このコードはすでに例のレポジトリにコピーしてあるのでここではそれをimport
します。興味があればどのような実装になっているかのぞいてみてください。
loadData()
はPromise
を返すのでタスクの終了まで待機するのを忘れないでください。tslabはtop-level awaitをサポートしているのでawait
をつけるだけでOKです。
import mnist from '../lib/mnist';
await mnist.loadData();
WikipediaのMNISTの記事にも書かれているように、MNISTのデータは60,000の訓練用データ(training data)と10,000の評価用データ(test data)に事前に分けられています。実際にダウンロードされたデータの大きさを確認してみましょう。
訓練データに60,000個、評価データに10,000個の数字の画像(images
)と文字認識の正解データ(labels
)が存在することが確認できたと思います。数字の画像データは28x28の白黒1チャンネル(グレースケール)であることも分かります。
ちなみにデータの内部表現として使われているTensor
のAPIはここにドキュメントがあります。
tslab.display.html('<h2>訓練データのサイズ</h2>')
console.log(mnist.getTrainData());
tslab.display.html('<h2>評価データのサイズ</h2>')
console.log(mnist.getTestData());
{ 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に変換して可視化してみます。
import Jimp from 'jimp';
import {promisify} from 'util';
async function toPng(images: tf.Tensor4D, start: number, size: number): Promise<Buffer[]> {
// Note: mnist.getTrainData().images.slice([index], [1]) is slow.
let arry = images.slice([start], [size]).flatten().arraySync();
let ret: Buffer[] = [];
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: (err, v: Jimp)=>any) => {
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]);
}
}
MNISTのデータの素性が一通り分かったので、TensorFlow.jsを使って「ディープニューラルネットワーク」機械学習のモデルを設計、訓練し文字認識を行ってみます。 初めはPythonのTensorFlowチュートリアルでも利用されている、128ノードの中間層を一つ持つ単純な"ディープ"ニューラルネットワークモデルを使って文字認識を行います。
TensorFlow.jsは、PythonのTensorFlowでも使われているkerasをベースにしたAPIを提供しています。そのためこの程度のシンプルなディープニューラルネットワークはTensorFlow.jsでも非常に簡単に実装できます。Layer APIの詳細はTensorFlow.jsのドキュメントを参照してください。
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'],
});
async function train(model: tf.Sequential, epochs: number, batchSize: number, modelSavePath: string) {
// 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);
let display: tslab.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);
_________________________________________________________________ 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
Progress: 100.0%
16931ms 332us/step - acc=0.906 loss=0.328 val_acc=0.958 val_loss=0.145 Epoch 2 / 5
Progress: 100.0%
14936ms 293us/step - acc=0.953 loss=0.160 val_acc=0.971 val_loss=0.103 Epoch 3 / 5
Progress: 100.0%
14905ms 292us/step - acc=0.964 loss=0.121 val_acc=0.973 val_loss=0.0910 Epoch 4 / 5
Progress: 100.0%
14268ms 280us/step - acc=0.971 loss=0.0960 val_acc=0.975 val_loss=0.0811 Epoch 5 / 5
Progress: 100.0%
14906ms 292us/step - acc=0.974 loss=0.0845 val_acc=0.975 val_loss=0.0796 Evaluation result: Loss = 0.077; Accuracy = 0.976 Saved model to path: mnist
98%とそこそこ精度のよい文字認識のモデルができたので実際にテストデータを使って文字認識を行ってみましょう。
const predicted = tf.argMax(model.predict(mnist.getTestData().images) as tf.Tensor, 1).arraySync() as number[];
const labels = tf.argMax(mnist.getTestData().labels, 1).arraySync() as number[];
console.log('predictions:', predicted.slice(0, 10));
console.log('labels:', labels.slice(0, 10));
predictions: [ 7, 2, 1, 0, 4, 1, 4, 9, 5, 9 ] labels: [ 7, 2, 1, 0, 4, 1, 4, 9, 5, 9 ]
正しく文字認識ができていますね。せっかくなので文字認識に失敗する例を可視化してみましょう。精度が98%程度あるとはいえ、人間であれば間違えないようなものが多いですね。
const predicted = tf.argMax(model.predict(mnist.getTestData().images) as tf.Tensor, 1).arraySync() as number[];
const labels = tf.argMax(mnist.getTestData().labels, 1).arraySync() as number[];
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;
}
}
MNISTの文字列認識は典型的な画像を対象としたディープラーニングなので、CNNによるディープラーニングも試してみましょう。モデルの構造はTensorFlow.jsの例から拝借してきます。
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による高速化の威力がよく実感できると思います。
const epochs = 20;
const batchSize = 128;
const modelSavePath = 'cnn_mnist'
await train(cnnModel, epochs, batchSize, modelSavePath);
_________________________________________________________________ Layer (type) Output shape Param # ================================================================= conv2d_Conv2D1 (Conv2D) [null,26,26,32] 320 _________________________________________________________________ conv2d_Conv2D2 (Conv2D) [null,24,24,32] 9248 _________________________________________________________________ max_pooling2d_MaxPooling2D1 [null,12,12,32] 0 _________________________________________________________________ conv2d_Conv2D3 (Conv2D) [null,10,10,64] 18496 _________________________________________________________________ conv2d_Conv2D4 (Conv2D) [null,8,8,64] 36928 _________________________________________________________________ max_pooling2d_MaxPooling2D2 [null,4,4,64] 0 _________________________________________________________________ flatten_Flatten2 (Flatten) [null,1024] 0 _________________________________________________________________ dropout_Dropout2 (Dropout) [null,1024] 0 _________________________________________________________________ dense_Dense3 (Dense) [null,512] 524800 _________________________________________________________________ dropout_Dropout3 (Dropout) [null,512] 0 _________________________________________________________________ dense_Dense4 (Dense) [null,10] 5130 ================================================================= Total params: 594922 Trainable params: 594922 Non-trainable params: 0 _________________________________________________________________ Epoch 1 / 20
Progress: 100.0%
40096ms 786us/step - acc=0.922 loss=0.250 val_acc=0.982 val_loss=0.0641 Epoch 2 / 20
Progress: 100.0%
38724ms 759us/step - acc=0.978 loss=0.0700 val_acc=0.984 val_loss=0.0571 Epoch 3 / 20
Progress: 100.0%
38442ms 754us/step - acc=0.985 loss=0.0487 val_acc=0.983 val_loss=0.0554 Epoch 4 / 20
Progress: 100.0%
41739ms 818us/step - acc=0.988 loss=0.0390 val_acc=0.987 val_loss=0.0487 Epoch 5 / 20
Progress: 100.0%
42047ms 824us/step - acc=0.990 loss=0.0333 val_acc=0.992 val_loss=0.0279 Epoch 6 / 20
Progress: 100.0%
42048ms 824us/step - acc=0.991 loss=0.0284 val_acc=0.993 val_loss=0.0288 Epoch 7 / 20
Progress: 100.0%
42536ms 834us/step - acc=0.992 loss=0.0257 val_acc=0.993 val_loss=0.0290 Epoch 8 / 20
Progress: 100.0%
42460ms 833us/step - acc=0.993 loss=0.0227 val_acc=0.994 val_loss=0.0248 Epoch 9 / 20
Progress: 100.0%
42492ms 833us/step - acc=0.994 loss=0.0194 val_acc=0.994 val_loss=0.0233 Epoch 10 / 20
Progress: 100.0%
42412ms 832us/step - acc=0.994 loss=0.0177 val_acc=0.992 val_loss=0.0267 Epoch 11 / 20
Progress: 100.0%
42587ms 835us/step - acc=0.995 loss=0.0158 val_acc=0.994 val_loss=0.0275 Epoch 12 / 20
Progress: 100.0%
42507ms 833us/step - acc=0.995 loss=0.0146 val_acc=0.994 val_loss=0.0307 Epoch 13 / 20
Progress: 100.0%
42602ms 835us/step - acc=0.996 loss=0.0126 val_acc=0.994 val_loss=0.0292 Epoch 14 / 20
Progress: 100.0%
42281ms 829us/step - acc=0.996 loss=0.0122 val_acc=0.994 val_loss=0.0243 Epoch 15 / 20
Progress: 100.0%
41718ms 818us/step - acc=0.996 loss=0.0120 val_acc=0.995 val_loss=0.0236 Epoch 16 / 20
Progress: 100.0%
41984ms 823us/step - acc=0.997 loss=0.0109 val_acc=0.994 val_loss=0.0280 Epoch 17 / 20
Progress: 100.0%
42295ms 829us/step - acc=0.996 loss=0.0101 val_acc=0.994 val_loss=0.0321 Epoch 18 / 20
Progress: 100.0%
42540ms 834us/step - acc=0.997 loss=9.02e-3 val_acc=0.993 val_loss=0.0322 Epoch 19 / 20
Progress: 100.0%
42968ms 843us/step - acc=0.997 loss=9.14e-3 val_acc=0.995 val_loss=0.0305 Epoch 20 / 20
Progress: 100.0%
42547ms 834us/step - acc=0.997 loss=9.66e-3 val_acc=0.994 val_loss=0.0277 Evaluation result: Loss = 0.020; Accuracy = 0.995 Saved model to path: cnn_mnist
テストデータに対する精度が99.4%まで上昇しました。先の単純なモデルでやったように、CNNを使ってもなお認識に失敗している画像を表示してみましょう。 最初の単純なモデルの失敗例に比べると、人間でも認識に失敗しそうな、あるいはラベルが間違っていると言いたくなるような例が多くなっており、文字認識の精度が大きく向上していることが体感できると思います。
const predicted = tf.argMax(cnnModel.predict(mnist.getTestData().images) as tf.Tensor, 1).arraySync() as number[];
const labels = tf.argMax(mnist.getTestData().labels, 1).arraySync() as number[];
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;
}
}
以上でJavaScriptのみで複雑なディープラーニングモデルを構築・訓練から実際にアプリケーション上で推論を行うところまでエンドツーエンドで行うことができました。TensorFlow.jsを使っているので、完成したモデルをブラウザ上で実行することも最小限の追加コードで実現できます。詳しくはTensorFlow.jsのドキュメントや他の人による解説記事を参照してください。