alpine(Dockerコンテナ)上でTensorflow.jsを利用する 〜 ラズパイへの導入編


2020/08/13

今回はラズパイ上でTensorflow.jsを軽量なalpine linuxベースのdockerコンテナを動作させるまでの手順を解説します。


Dockerイメージを構築する

まずは何はともあれラズパイ上で動くnode.js環境を作成します。

例えば以下のような
Dockerfileであれば、イメージサイズは最低限でnode.jsが動作させることができます。bashcurlなどのユーティリティは、あとでインタラクティブモードで利用するときのためお好みで追加しておきますが、無くてもtensorflow.jsは動作します。

            FROM arm32v7/node:12.18-alpine3.11

RUN apk update && apk upgrade && apk add --no-cache \
    openssh curl bash

WORKDIR /usr/src/app
        
また、本記事ではdocker-composeを利用したDockerを操作する方法を解説します。Dockerコマンドを長いオプション付きワンライナーで叩いてもいいのですが、docker-compose.ymlで記述したほうがイメージの内容を確認するときに見通しの良いですし、保守性も高まります。

ラズパイへのdocker-compose導入に関しては、
こちらの記事などに手順を説明していらっしゃる方がおられますので、そちらを参照ください。

今回は大した
docker-compose.ymlではないですが、以下のようなものを使います。

            version: "3"

services:
  app:
    image: tfjs-arm-dkcr:3.11
    build: .
    user: "node:node"
    environment:
      NODE_ENV: development
    volumes:
      - ./:/usr/src/app
        
ちなみにdocker-compose内のサービスの名前をappとしておきます。

ということで、
Dockerfiledocker-compose.ymlを使って今回のイメージをビルドします。

            $ docker-compose build
Building app
Step 1/7 : FROM arm32v7/node:12.18-alpine3.11
12.18-alpine3.11: Pulling from arm32v7/node
3cfb62949d9d: Pull complete
9f88341ca4dc: Pull complete
5765f8fb68cd: Pull complete
ed6b46590934: Pull complete
Digest: sha256:ab58814a31480be66de2f73d233d74f9d9a2761c3ed30ea4445e01f034dd2f58
Status: Downloaded newer image for arm32v7/node:12.18-alpine3.11
 ---> 7d07d27963b2
Step 2/7 : ENV NODE_ENV development
 ---> Running in 7eaef5b163ef
Removing intermediate container 7eaef5b163ef
 ---> b890256ee944
Step 3/7 : RUN apk update && apk upgrade && apk add --no-cache openssh curl sh
 ---> Running in 4133f832fea6
#...中略
Successfully built e08f1549a457
Successfully tagged tfjs-arm-dkcr:3.12
        
イメージが構成されたら以下のようになっていることが確認できます。

            $ docker images
REPOSITORY                   TAG                     IMAGE ID            CREATED             SIZE
tfjs-arm-dkcr                3.11                    e08f1549a457        5 minutes ago       89.4MB
arm32v7/node                 12.18-alpine3.11        7d07d27963b2        2 weeks ago         80.4MB
#...
        
イメージのビルドが完了すると、tfjs-arm-dkcrという名前で登録されています。ちなみにタグ名の3.11はalpineのディストーションリビジョンに合わせましたが、設定しない場合はlatestというタグ名になります。

node.jsが使えて90MB程度のイメージサイズならばなかなかに軽量といえると思います。

余談 〜 node:alpineイメージのグループ名・ユーザー名

一般にDocker Hubなどからプルして使うdockerイメージをroot権限で以外で利用したい場合には、Dockerfileなどに独自にユーザー名とグループ名・パスワードの有無などを設定する必要があります。

ただし、公式の
nodeイメージに限っては、パスワード無しのデフォルトユーザー名/グループ名のnodeがそのまま利用できます。

少し設定ファイルを覗いてみると、

            $ cat /etc/group
root:x:0:root
bin:x:1:root,bin,daemon
daemon:x:2:root,bin,daemon
sys:x:3:root,bin,adm
adm:x:4:root,adm,daemon
tty:x:5:
disk:x:6:root,adm
lp:x:7:lp
mem:x:8:
kmem:x:9:
wheel:x:10:root
floppy:x:11:root
mail:x:12:mail
news:x:13:news
uucp:x:14:uucp
man:x:15:man
cron:x:16:cron
console:x:17:
audio:x:18:
cdrom:x:19:
dialout:x:20:root
ftp:x:21:
sshd:x:22:
input:x:23:
at:x:25:at
tape:x:26:root
video:x:27:root
netdev:x:28:
readproc:x:30:
squid:x:31:squid
xfs:x:33:xfs
kvm:x:34:kvm
games:x:35:
shadow:x:42:
cdrw:x:80:
usb:x:85:
vpopmail:x:89:
users:x:100:games
ntp:x:123:
nofiles:x:200:
smmsp:x:209:smmsp
locate:x:245:
abuild:x:300:
utmp:x:406:
ping:x:999:
nogroup:x:65533:
nobody:x:65534:
node:x:1000:node
        
みたいに、ベースのalpineイメージだけでも、結構ずらずらーっとユーザー名とグループ名が実行権限によって事細かく設定されていることが確認できます。見ていただくと分かるようにnodeはユーザー名・グループ名で、uidは1000で設定されているようです。

独自にグループ名・ユーザー名を設定される場合には、これらの既存の設定値以外の名前にする必要がありますので、留意して利用しますよう。


ラズパイ上でコンテナ起動

では先ほど構築したイメージからコンテナを起動し、インタラクティブモードで中に入り、npmパッケージがインストールできるかを検証します。

インタラクティブモードで動作テスト

では再びtfjs-arm-dckrコンテナのインタラクティブモードに入って作業してみます。

            $ docker-compose run --rm app bash
Creating network "tfjs-arm-dckr_default" with the default driver
$ node -v
v12.18.3
$ npm --version
6.14.6
$ yarn --version
1.22.4
        
それでは以下のpackage.jsonを作業ディレクトリのルートに追加してtensorflow.js等をインストールしていきます。

            {
    "name": "tfjs-dckr-raspi",
    "version": "0.0.1",
    "description": "It enables RaspberryPi to use tensorflow.js via alpine:node on Docker.",
    "main": "dist/index.js",
    "scripts": {
        "build": "tsc",
        "tap": "babel-node dist/index.js",
    },
    "dependencies": {
        "@babel/core": "^7.10.5",
        "@babel/node": "^7.10.5",
        "@tensorflow/tfjs": "^2.0.1",
        "argparse": "^1.0.10",
        "ts-node": "^8.10.2",
        "typescript": "3.5.3"
    },
    "devDependencies": {
        "@types/argparse": "^1.0.38",
        "@types/node": "^13.7.1",
        "@types/shelljs": "^0.8.8",
        "shelljs": "^0.8.4",
        "tmp": "^0.2.1"
    }
}
        
ただしパッケージマネージャはyarnを利用します。以下でパッケージインストールを行っています。

            $ yarn install
yarn install v1.22.4
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "linux" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "@tensorflow/tfjs > @tensorflow/tfjs-data@2.0.1" has unmet peer dependency "seedrandom@~2.4.3".
[4/4] Building fresh packages...
Done in 296.66s.
        
以上で、tensorflow.jsが簡単に利用可能になりました。


コンテナからプログラム実行

ここではtypescriptでプログラミングをしていきます。

まずは注意事項として、現在2020/8月時点で
tensorflow.jsのプロジェクトで利用されているtypescriptのバージョンが3.5.3のみに固定されています。なので、これより古かったり新しいバージョンを利用するとエラーが出てビルドがコケる可能性があります。

もし、node.jsが原因不明のビルドエラーで止まるようでしたら、tscのバーションを確認して手元の環境で
3.5.3になっているかをご確認ください。

            $ ./node_modules/.bin/tsc --version
Version 3.5.3
        
初回のビルド前にはtsconfig.jsonの初期化から行います。

            $ ./node_modules/.bin/tsc --init
        
生成されたtsconfig.jsonの中身を確認すると、

            {
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
      "declaration": true,
      "sourceMap": true,
      "outDir": "./dist",
      "moduleResolution": "node",
      "esModuleInterop": true
    },
    "include": [
      "./index.ts",
      "./src/*",
    ],
    "exclude": [
      "./test/*.spec.ts"
    ],
    "compileOnSave": false
}
        
のようになっていたらOKです。

ソースファイルとして
index.tsを作業ディレクトリのルートに新規作成し、以下のような内容で編集します。

            import * as tf from '@tensorflow/tfjs';

const primitiveOps = () => {
    const x = tf.tensor1d([1,2,3]);
    const a = tf.scalar(4);
    console.log('Four arithmetic operation with scalar:');
    x.add(a).print();
    x.sub(a).print();
    x.mul(a).print();
    x.div(a).print();
}

const arrayOps = () => {
    const x1 = tf.tensor1d([1,2,3])
    const x2 = tf.tensor1d([2,3,4])
    console.log('Four arithmetic operation with tensor1d:');

    console.log('add')
    x1.add(x2).print();

    console.log('sub')
    x1.sub(x2).print();

    console.log('mul')
    x1.mul(x2).print();

    console.log('div')
    x1.div(x2).print();
}

const tensor2array = () => {
    const b = tf.tensor1d([1,2,3])
    console.log('Convert tensor1d to Array:');

    console.log('tensor1d:')
    b.print()

    const b_value = b.dataSync();
    console.log('--> Array:')
    console.log(b_value)
}

primitiveOps();
arrayOps();
tensor2array();
        
これをtscビルドして、ではtensotflow.jsが動作しているのか簡単な関数を呼び出してみましょう。

            $ yarn build
$ babel-node dist/index.js
#もしくは yarn tap
Overriding the gradient for 'Max'
Overriding the gradient for 'OneHot'
Overriding the gradient for 'PadV2'
Overriding the gradient for 'SpaceToBatchND'
Overriding the gradient for 'SplitV'
============================
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.
============================
Four arithmetic operation with scalar:
Tensor
    [5, 6, 7]
Tensor
    [-3, -2, -1]
Tensor
    [4, 8, 12]
Tensor
    [0.25, 0.5, 0.75]
Four arithmetic operation with tensor1d:
add
Tensor
    [3, 5, 7]
sub
Tensor
    [-1, -1, -1]
mul
Tensor
    [2, 6, 12]
div
Tensor
    [0.5, 0.6666667, 0.75]
Convert tensor1d to Array:
tensor1d:
Tensor
    [1, 2, 3]
--> Array:
Float32Array(3) [ 1, 2, 3 ]
    ✓
        
実行するとtensorflow.jsでのテンソルの計算ができているようです。ひとまずはここでラズパイでtensorflow.jsが動いてめでたしめでたしとしておきます。


まとめ

以上、ラズパイ上でtensorflow.jsを動作することができることを確認しました。

これでブラウザベースのtensorflowを用いたプログラムをラズパイで動かすことができるのですが、node.jsのネイティブアプリとしてコマンドラインから高速で走らせたいときにはalpineベースのdockerイメージではアプリのビルドに必要なライブラリが足りない状況に陥ります。もし
tensorflow-nodeがラズパイ上で必要とされるのなら、ラズパイのホストOS自体にnode.jsを導入する方法と、ubuntuベースのdockerイメージからtensorflow-nodeを使う方法などがあります。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

電子工作を身近に知っていただけるように、材料調達からDIYのハウツーまで気になったところをできるだけ細かく記事にしてブログ配信してます。