カテゴリー
【tensorflowjs入門】Nexeでシェルスクリプトからデータ解析できるツールを作成する
※ 当ページには【広告/PR】を含む場合があります。
2022/08/22
かねてよりtensorflowjsをNexe化して、Linuxコマンドと連携してコンソールから動かしてみたいと考えておりました。
ということで、今回はtensorflowjsが手軽に動かせるNexeアプリの導入方法をじっくりと考えてみたいと思います。
TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
tensorflowjsをNexeを使ったバイナリビルド
以前の記事で説明したように、Nexeを使って「Hello World」的なバイナリアプリを作る方法を解説していました。
Nexeプロジェクトの作成はそちらの記事で確認いただくとして、以降の内容ではNexeアプリのリソースの実装に関してのみ解説します。
なお、今回のプロジェクト程度のレベルではjavascriptでも十分なのですが、Typescriptベースで話を進めています。
まずはtensorflowjsをインストールします。
$ yarn add @tensorflow/tfjs
ここで試しに、プロジェクトのエンドポイントのリソースファイルとして
index.ts
import * as tf from '@tensorflow/tfjs';
(() => {
const t = tf.tensor([-2, 1, 0, 5]);
const o = tf.layers.activation({activation: 'relu'}).apply(t);
(o as any).print();
})();
としてみます。
これを
tsc
dist/index.js
$ tsc
$ nexe dist/index.js --target=linux-x64-14.15.3 -o nexe-app
ここでのターゲットはLinuxOS x64向けです。
では早速ビルドできたNexeアプリを起動してみると、
$ ./nexe-app
============================
Hi there 👋. Looks like you are running TensorFlow.js in Node.js. To speed things up dramatically, install our node backend, which binds to TensorFlow C++, by running npm i @tensorflow/tfjs-node, or npm i @tensorflow/tfjs-node-gpu if you have CUDA. Then call require('@tensorflow/tfjs-node'); (-gpu suffix for CUDA) at the start of your program. Visit https://github.com/tensorflow/tfjs-node for more details.
============================
Tensor
[0, 1, 0, 5]
Nexeを使っても特に問題もなく、tensorflowjsがバイナリとして動作することが分かります。
TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
tensorflowjsで解析する入力データの準備
tensorflowが提供している解析法の種類はここでは挙げきれないほど多岐に渡ります。
以前の話で、tensorflowjsで
今回もそちらの内容に沿う形で、視覚化は抜きにしてコマンドラインから線形回帰解析の結果だけを得られるようなNexeアプリに仕上げることを目指します。
フィッテングデータのツールアプリへの入力ファイル
まずは、解析で使えるように入力データを準備していきます。
作業ルートにtmpフォルダを作り、そこに代表的なサンプルとして公開されているAuto MPG用のデータセットをダウンロード・データの整理を一括して行います。
$ mkdir ./tmp
$ cd tmp && wget https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data
$ cat auto-mpg.data | sed -r '/\?/d' | sed -r 's/\t/,/' | sed -r 's/ {2,}/,/g' > output.csv
なお、データの整理はデータ構造の特性を考慮してSedで一括して行いましたが、整理内容の詳しい中身は
今回も
「車の重量」
「燃費」
ということで、解析に必要なデータを重量でソート・抽出するには、
$ cat output.csv | sort -k5 -t, | awk -F, '{print $5","$1}'
1613.,35.0
1649.,31.0
1755.,39.1
#...中略
5140.,13.0
このCSV形式のデータ列をNexeアプリで扱えるようにしていくことを以降で考えていきます。
TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
自作のNexeアプリもパイプ処理できるようにシェルスクリプト化する
Nexeアプリへのデータの読ませ方・入力方法はいくつかあって、もっとも利用されるのは
fs.readFile(ファイル名)
他には、データサーバーのバックエンドにRestAPIなどの仕組みが提供されていれば、
fetch
ここではNexeでtensorflowjsをCLIとしてシェルスクリプト化できるという特色を最大限に活かし、catコマンドなどから読み込んだ入力データを標準出力でパイプし、後段のNexe化したコマンドでさらに標準入力として受け取るように工夫してみましょう。
利用イメージとしては、
$ cat input.csv | ./nexe-app
#解析結果が表示される
Nexeアプリでコマンド引数を受け取る
Nexeといえど、nodejsコマンドと考え方はほぼ同じです。
つまり、
process.argv[引数番号]
例えば、
index.ts
(() => {
console.log(process.argv[0]);
console.log(process.argv[1]);
console.log(process.argv[2]);
console.log(process.argv[3]);
})();
これで、Nexeアプリをビルドし、以下のコマンドで利用してみます。
$ ./nexe-app '太郎, 花子' '次郎, 良子'
/[現在の作業ディレクトリ]/nexe-app
/[現在の作業ディレクトリ]/dist/index.js
太郎, 花子
次郎, 良子
というように表示されるはずです。
nodejsコマンド同様に注意が必要なのは、
process.argv[0]
process.argv[1]
つまり、コマンド引数自体は、
process.argv[2]
Nexeをパイプ処理できるようにする
自作のシェルスクリプトでパイプするには、以前解説したテクニックを利用することになります。
以下のようにパイプを補助してくれるシェルスクリプトを作成してみましょう。
#!/bin/bash
if [ -p /dev/stdin ]; then
_str=$(cat -)
else
_str=$@
fi
./nexe-app "${_str}"
これで通常の使い方もパイプもできているか確認してみます。
$ chmod +x nexe-pipe.sh
$./nexe-pipe.sh '太郎, 花子'
/[現在の作業ディレクトリ]/nexe-app
/[現在の作業ディレクトリ]/dist/index.js
太郎, 花子 次郎, 良子
undefined
$ echo '太郎, 花子' | ./nexe-pipe.sh
/home/taconocat/tacochart/tacochart_viewer/tca/hurst_analysis/hurst-app
/home/taconocat/tacochart/tacochart_viewer/tca/hurst_analysis/dist/index.js
太郎, 花子
undefined
ちゃんと、コマンド引数が通常のやり方とパイプからでも処理されています。
なお、今回は引数が1つしか使いませんが、複数の引数を使いたい場合には、同じく解説した
TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
CLI版tensorflowjsで線形回帰する
では入力データと、Nexeアプリへの引数の渡し方も押さえていただいたところで、本題のtensorflowjsでの線型回帰のコードを作成していきます。
引数から渡した生データを配列化した数値に復元する
まずはコマンド引数で与える生のcsv文字列から、javascriptで使える2次元配列形式に変換する関数を作成します。
index.ts
function csv2Array(str: string): number[][] {
const csvData = [];
const lines = str.split('\n');
for (let i = 0; i < lines.length; ++i) {
if (lines[i] == '') { continue; }
const cells = lines[i].split(',');
csvData.push(cells.map(v => parseFloat(v)));
}
return csvData;
}
(() => {
const rawCSV = process.argv[2];
if (rawCSV) console.dir(csv2Array(rawCSV));
})();
これをビルドし、生成されたNexeアプリをこれまで解説してきたテクニックで統合して使ってみます。
$ cat tmp/output.csv | sort -k5 -t, | awk -F, '{print $5","$1}' | ./nexe-pipe.sh
[
[ 1613, 35 ], [ 1649, 31 ], [ 1755, 39.1 ], [ 1760, 35.1 ], [ 1773, 31 ],
[ 1795, 33 ], [ 1795, 33 ], [ 1800, 36.1 ], [ 1800, 36.1 ], [ 1825, 29.5 ],
#...中略
[ 2220, 23 ], [ 2220, 25 ], [ 2223, 25 ], [ 2226, 21 ], [ 2228, 25 ],
... 292 more items
]
ちゃんと入力データが数値の2次元配列となっていることが分かります。
線型回帰解析のフィッティングパラメータを得る
tensorflowjsのkerasモデルの活性関数「linear」一層で訓練することは、すなわち線形回帰で最適のパラメーター(比例項とバイアス項)を得ること、と同義です。
このことを念頭に、
index.ts
import * as tf from '@tensorflow/tfjs';
function csv2Array(str: string): number[][] {
const csvData = [];
const lines = str.split('\n');
for (let i = 0; i < lines.length; ++i) {
if (lines[i] == '') { continue; }
const cells = lines[i].split(',');
csvData.push(cells.map(v => parseFloat(v)));
}
return csvData;
}
async function linearRegression(rawData: number[][]) {
const model = tf.sequential();
//👇入力層兼フィッテング解析関数
model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true, activation: 'linear'}));
console.log('Training started.');
const originalData = [];
for (const row in rawData) {
originalData.push({x: rawData[row][0], y: rawData[row][1]});
};
//👇テンソルに変換・データの正規化
const {inputs, labels, inputMax, inputMin, labelMax, labelMin} = convertToTensor(originalData);
//👇モデルをトレーニング(=線型回帰解析)
await trainModel(model, inputs, labels);
//👇解析前に正規化・標準化していたデータを考慮して、元のXY空間に復元
const xmax = inputMax.dataSync()[0];
const xmin = inputMin.dataSync()[0];
const ymax = labelMax.dataSync()[0];
const ymin = labelMin.dataSync()[0];
let prop = model.layers[0].getWeights()[0].dataSync()[0];
let bias = model.layers[0].getWeights()[1].dataSync()[0];
bias = ymin + (ymax - ymin) * (bias - prop * xmin / (xmax - xmin));
prop = prop * (ymax - ymin) / (xmax - xmin);
console.log(`推定曲線: y = ${prop} x + ${bias}`);
console.log('Fitting has done.');
}
//👇学習データをTensor型に変換する関数
function convertToTensor(data: Array<{x:number, y:number}>) {
return tf.tidy(() => {
//👇学習データをシャッフル
tf.util.shuffle(data);
//👇xyデータ配列をNx1テンソルに変換
const inputs = data.map(d => d.x)
const labels = data.map(d => d.y);
const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
const labelTensor = tf.tensor2d(labels, [labels.length, 1]);
//👇入力データの正規化
const inputMax = inputTensor.max();
const inputMin = inputTensor.min();
const labelMax = labelTensor.max();
const labelMin = labelTensor.min();
const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));
return {
inputs: normalizedInputs,
labels: normalizedLabels,
inputMax,
inputMin,
labelMax,
labelMin
}
});
}
//👇モデルの学習を行う関数(=線型回帰解析本体)
async function trainModel(model: tf.Sequential, inputs: tf.Tensor, labels: tf.Tensor) {
//👇モデルをコンパイル => 学習方法を指定
model.compile({
optimizer: tf.train.adam(),
loss: tf.losses.meanSquaredError,
metrics: ['mse'],
});
//👇バッチサイズ(16~32くらいで良い)
const batchSize = 32;
//👇エポック数
const epochs = 500;
//👇エポック回数の学習を実行する
return await model.fit(inputs, labels, {
batchSize,
epochs,
shuffle: true,
callbacks: {
onEpochEnd: async(epoch, logs) => {
//👇繰り返し回数と損失をコンソール出力
console.log(`Epoch#${epoch} : Loss(${logs.loss}) : mse(${logs.mse})`);
}
}
});
}
(async () => {
const rawCSV = process.argv[2];
if (rawCSV) {
const csvData = csv2Array(rawCSV);
await linearRegression(csvData);
}
})();
これをビルドしてtensorflowjsを組み込んだNexeアプリで線型回帰解析ができるかを確認してみます。
$ cat tmp/output.csv | sort -k5 -t, | awk -F, '{print $5","$1}' | ./nexe-pipe.sh
============================
Hi there 👋. Looks like you are running TensorFlow.js in Node.js. To speed things up dramatically, install our node backend, which binds to TensorFlow C++, by running npm i @tensorflow/tfjs-node, or npm i @tensorflow/tfjs-node-gpu if you have CUDA. Then call require('@tensorflow/tfjs-node'); (-gpu suffix for CUDA) at the start of your program. Visit https://github.com/tensorflow/tfjs-node for more details.
============================
Training started.
Epoch#0 : Loss(0.4150969684123993) : mse(0.4150969684123993)
Epoch#1 : Loss(0.40245047211647034) : mse(0.40245047211647034)
Epoch#2 : Loss(0.3915448486804962) : mse(0.3915448486804962)
#...中略
Epoch#498 : Loss(0.013235836289823055) : mse(0.013235836289823055)
Epoch#499 : Loss(0.013220091350376606) : mse(0.013220091350376606)
推定曲線: y = -0.0076351825945764045 x + 46.188474321707034
Fitting has done.
今回はエポックを500回程度にしてますが、残渣(エラー)関数は十分収束していることも分かります。
肝心の結果は、
y = -0.0076x + 48.2
これは

TensorFlowの導入からKerasまでを実践的に解説 現場で使える!TensorFlow開発入門 Kerasによる深層学習モデル構築手法
まとめ
今回は以前紹介したtensorflowjsによる「線型回帰解析」をシェルコマンド化して、ツールとしてさらに使いやすくするためのガイダンスのような内容で説明していきました。
話の題材を簡単にするため、線型回帰でやりましたが、tensorflowjsで応用できるテーマはほとんどNexeツール化できると思います。
今後も面白い応用を思いついたらこのブログ記事で紹介してみようと思います。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー