寝かしつけについて

Kaz Sato
9 min readDec 20, 2018

--

この記事は TensorFlow Advent Calendar 2018 の 20 日目の記事です。

最近、会社の同僚に子供が生まれた。深夜の夜泣きに目が覚めてしまうそうで、昼間も眠そうだ。苦労をお察しする。俺も息子が赤ん坊のころは夜中の 2 時に起こされて寝かしつけしてたのを思い出した。

赤ん坊は生まれた時点で洗練された認知能力をすでに備えており、親のズルを許さない。ソファに座ったままだっこして揺らす、という行為は手抜きとして認識され、いつまでも泣き止まない。そこで、強い眠気でふらふらになりながらも赤ん坊を抱えて立ち、母親の胎内にいるかのようにゆ〜らゆ〜らと優しくゆりうごかしてあげると、こちらが意識を失うあたりで向こうも寝付くのだ。

しかしここでゆっくりソファに座ると、たちまち赤ん坊の加速度センサーが手抜きを検知し、ふたたび泣き始めるのである。

本稿では、この寝かしつけの苦労を obniz と TensorFlow.js で再現する手順を解説する。

obniz はよくできた IoT デバイスだ。アマゾンで 6000 円ほど。箱を開けて USB 電源につないだら WiFi のパスワードを入れ、ブラウザ上で JavaScript コードを書いて helloworld するまで 5 分もかからない。

書いたコードはブラウザで動いてる。要するにこれはいろいろな電子部品にブラウザ JS から WiFi 経由で読み書きするためのデバイスだ。

これに、秋月で買った加速度センサーをつなぐ。

セロテープで留めた。雑でいいのだ。

obniz のドキュメントにあるコードをコピペして数行書けば、このセンサーが検知した x、y、z 方向の加速度を JS で読み出せる。

// init sensor
var sensor = obniz.wired("KXR94-2050", { vcc:5, enable:6, gnd:7, z:8, y:9, x:10, self_test:11 });
// get sensor values
while (true) {
let values = sensor.get();
console.log("x:" + values.x);
console.log("y:" + values.y);
console.log("z:" + values.z);
await obniz.wait(30);
}

生の数字を眺めても意味がわからない。three.js を使ってセンサーの値を 3D 表示してみた。下の黒い板はモバイルバッテリーである。

毎秒 10 回くらいで読める。時間をかけて 3D にしてみたが、かえって見づらいので後悔した。D3.js にしておけばよかった。

昨晩、俺はこのデバイスを腕に抱え、12 年前の辛い時期のことを思い出しながら、こんなふうにしたっけな、と、寝かしつけをしていた。まあ今回は、そんなに長い時間をかける必要はない。1 分間で十分だ。ついでに、赤ちゃん大好き「高い高い」、いちばん嫌いな「床に放置」、そして「はげしく揺さぶる」も 1 分間ずつ記録した。CSV にして localStorage に入れておいた。

TensorFlow.js は、JavaScript で動く ML ライブラリである。加速度センサーやら、なんちゃらセンサーやら、たくさんの数字を集めたって、人が書く if 文でできることはたかが知れているのである。だから、obniz を使い始めた当初から、TF.js と使うことを考えていた。

x、y、z の 3 つの数字が時系列で流れてくるとき、そのパターンをどのようにして知ればよいか。x、y、z を画素の色、時間軸を画素の並びと捉えれば、この直線の模様を画像認識できればよい。画像認識といえば CNN である。直線だから 1 次元の CNN でよい。RNN や LSTM を使ってもいいけど、なんかめんどくさそうなのでやらない。

ここに 1 次元 CNN のいい図があった。

この赤い枠がフィルタというもので、太いシマシマや細いシマシマの模様のついた色眼鏡のようなものだ。この色眼鏡を少しずつずらしながら数字を見ていくと、模様にぴったり合うところで数字がばっちり見える。ここには太いシマシマがあるぞ、とわかる。そして、いろんな模様のフィルタをたくさん作り、それら全部使って数字を眺めていけば、どんな模様がどこにあるかがわかる。

TensorFlow.js では、こういう 1 次元の CNN を作るための conv1d 関数があるので使ってみる。

const model = tf.sequential();model.add(
tf.layers.conv1d({
inputShape: [20, 3],
kernelSize: 10,
filters: 50,
strides: 1,
activation: “relu”,
kernelInitializer: “randomNormal”
})
);

ここでは、センサーの値を 20 個(つまりおよそ 2 秒分)ごとに区切り、x、y、z の値を持つ [20, 3] の tensor として入力する。この直線の模様を見分ける長さ 10 のフィルタを 50 個用意する。この 10 とか 50 とかの設定の調整を本気でやると面倒だが、仕事じゃないので適当に済ませておく。学習がうまく行けば、TF.js のオプティマイザが、 x、y、z の変化のパターンをうまく見分けられるいい塩梅のフィルタを 50 種類作ってくれる。

この conv1d 層から出てくる数字をざっくりまとめる maxPooling1d 層をつなげる。さらに、数字をわざとランダムに消すことで融通の利くモデルに育てるdropout 層、そして最後に、こんな模様の数字の並びはいったいどんな寝かしつけなのかを判断する dense 層をつなげる。

conv1d_Conv1D1 (Conv1D) [null,11,50]
max_pooling1d_MaxPooling1D1 [null,5,50]
flatten_Flatten1 (Flatten) [null,250]
dropout_Dropout1 (Dropout) [null,250]
dense_Dense1 (Dense) [null,4]

ちなみに、1 次元 CNN をもっときちんと理解してがっちり作るには、yuta さんが教えてくれたこのページが大変参考になる。

あと、学習をはじめたところ、どうやっても loss が NaN にしかならず、頭を抱えた。

そこで TF.js のオーソリティ、飯塚先生に見てもらったら、環境依存問題だったことがすぐに分かり助かった。ありがとうございました。

4 種類の動作をそれぞれ 1 分間記録すると、 x、y、z が 20 個ずつ入った tensor が 128 個できる。このうち 100 個を学習用、28 個をテスト用に分け、学習を始めると、おおよそ 1 分くらいで 96% 精度を超える。

07:40:57 Start training…
07:41:05 loss: 1.24, acc: 0.54
07:41:11 loss: 1.03, acc: 0.54
07:41:18 loss: 0.88, acc: 0.54
07:41:24 loss: 0.76, acc: 0.61
07:41:30 loss: 0.67, acc: 0.64
07:41:36 loss: 0.59, acc: 0.75
07:41:43 loss: 0.52, acc: 0.82
07:41:49 loss: 0.46, acc: 0.86
07:41:55 loss: 0.41, acc: 0.93
07:42:01 loss: 0.37, acc: 0.96
07:42:08 loss: 0.34, acc: 0.96
07:42:14 loss: 0.30, acc: 1.00
07:42:14 Training finished.

データが少ないから過学習してるかもだが、試してみた分にはそれっぽく動いてるのでよしとする。TF.js ではモデルの save 関数で local storage にモデルを入れておけて便利である。

ここまでのコードはここに置いてある。

ではこれを寝かしつけてみる。obniz そのままでは無骨なので、もふもふ動物ポーチに入れた。

obniz の画面があるところを切り抜き、動きに応じた表情を見せることで、より寝かしつけ感を出してみた。

こんな感じになった。

動物は、机に放置されてるときは悲しいので、泣き顔である。

しかし人が抱えてくれると泣き止む。好奇心まんまんである。

はげしく揺さぶって見る。目を回した。

高い高いすると喜ぶ。

ゆっくりと、やさしく寝かしつける。やがてすやすやと眠りに落ちる。

寝たかな、と思って、机にそっと戻すと、すぐ起きてしまうのだ。

Disclaimer: この記事は個人的なものです。ここで述べられていることは私の個人的な意見に基づくものであり、私の雇用者には関係はありません。

--

--