ローリングコンバットピッチなう!

AIとか仮想化とかペーパークラフトとか

ディープラーニングで自律走行するロボット(COZMO)にコースアウトからの復帰を学習させる(DONKEY COZMO Part.4)

[technology][AI][COZMO][Deep Learning][Chainer]DONKEY COZMOにコースアウトからの復帰や逆向きからの復帰を学習させてみた

rc30-popo.hatenablog.com
rc30-popo.hatenablog.com
弊ブログの上記記事で、COZMO SDKとChainerを使い、ディープラーニングで自律走行する様になったCOZMOですが、前回まではコース上を正常に周回する動作しか学習させていません。
例えばコースに対して逆向きに置いたり(テストに使ったコースは右回りで学習させた)、コースをはみ出したところに置いた時に自動でコースに復帰する様な動きのデータを取り、学習させてみました。

今回はpythonソースコード上のロジックは一切触らず、ひたすらCOZMOをダンボールで作成した箱庭コース上の様々な場所に、様々な向きで置き、そこから正常なコースに戻る操作を繰り返し学習させました。
まだデータ取り的に不十分な感じですが、それなりにコースに復帰する様になりました。

youtu.be

動画を見ると判るのですが、たまに左右どちらに動くべきか迷う様な挙動があります。
現状の実装では単純にカメラ画像を畳み込みニューラルネットワークに流し込んで、出てきた操作ラベルに従ってCOZMOを自動操縦しているのですが、左→右→左とか、右→左→右とかの迷う様な動きを検出したら、強制的に少し前進させるとか、左→右→左→左あるいは右→左→右→右等、視野を意図的に迷っている状態からずらす様に制御ロジックを入れた方が良いかもしれません。

別途試行してみたいと思っています。

普通にコースを周回する様子は下記の動画で。
youtu.be

AIロボットCOZMO自律走行用の畳み込みニューラルネットワークをちょっとチューニング

[technology][AI][COZMO][Deep Learning][Chainer]Donkey COZMOのCNNを少し小さくしてみる

rc30-popo.hatenablog.com
の続きです。

最初に作ったDONKEY COZMOの畳み込みニューラルネットワークの学習データをファイル保存すると180MBあったので、少しコンパクト化を試みました。
最初はチャンネル数16と32の畳み込みでしたが、これを8と16に変更。全結合層の中間層1024ノードを256ノードに減らしました。

class czCnn(Chain):
    def __init__(self):
        super(czCnn, self).__init__(
            conv1 = L.Convolution2D(in_channels=1, out_channels=8, ksize=4, stride=1, pad=1),
            conv2 = L.Convolution2D(in_channels=8, out_channels=16, ksize=4, stride=1, pad=1),
            l0 = L.Linear(None,256),
            l1 = L.Linear(None,3),
            bncv1 = L.BatchNormalization(8),
            bncv2 = L.BatchNormalization(16),
            bn0 = L.BatchNormalization(256),
        )
    def forward(self, x,ratio=0.5):
        h = F.reshape(x,(len(x),1,160,120))
        h = F.max_pooling_2d(F.relu(self.bncv1(self.conv1(h))),2)
        h = F.max_pooling_2d(F.relu(self.bncv2(self.conv2(h))),2)
        h = F.dropout(F.relu(self.bn0(self.l0(h))),ratio=ratio)
        h = self.l1(h)
        return h

これで前回の走行データを学習させましたが、20epochくらいで学習データに対するフィッティングは100%正解が出る様になりました。
10epochだとちょっとまだ足りない感じです。学習データのサイズは1/10くらいに。

実際に走らせてみると挙動不安定感が元のネットワークより増した様な気もします。

更に別のコースを作って、そちらもテスト。前回はコース10周分くらい学習させましたが、今回は6周くらい。
だいたい上手く行くのですが、うまく曲がらずに壁にぶつかるケースがありました。
このコースももう少し学習用データを増やして、更に最初のコース用データと合わせて学習してどちらのコースでも走行可能なネットワークを作ってみたいと思います。


youtu.be


7/6: 学習データのサイズに誤りがあったので訂正。

Donkey COZMOを作る Part.2/3(AIロボットCOZMOをChainerで自律走行させる)

[technology][deep learning][chainer]COZMOを内蔵カメラを利用してDeep Learning(Chainer+python)で自律走行させる。Part.2-学習編-

rc30-popo.hatenablog.com
の続きです。

前回、COZMOのカメラ画像と操作(前進、左旋回、右旋回)の紐付けデータが取れたので、これをDeep Learningで学習させます。
画像を畳み込みニューラルネットワークに読み込ませ、操作ラベル(前進、左旋回、右旋回)を教師データにします。

畳み込みニューラルネットワークの実装には国産Deep LearningフレームワークのChainerを利用しています。

ニューラルネットワークの設計

一応学習に使うマシンにはGeForceが載っているのですが、GPUに読ませるデータはGPU内蔵2GBとメインメモリから2GBの計4GBしか使えないので、なるべくメモリサイズが小さくなる様にあまり大規模なものは組まない様にしています。
ニューラルネットワークの入力は180x120とCOZMOのQVGA(320x240)画像を更に縦横1/2に縮小します。前回取得したカメラ画像を手動で何段階か縮小した感触として、このくらいのサイズなら画像から特徴抽出して操作ラベルとの紐付け学習ができるかな?と直感で決めたサイズです。

畳み込みとMaxプーリングを2段重ねて、全結合層へ。全結合層は中間層を1024ノードとしましたが、これもたいした根拠はなく適当です。
畳み込みのチャンネル数は一段目が16,二段目が32チャンネルを出力しています。中間層のノード数と合わせて、これも適当に決めましたが、結果としては上手く行っています。
もう少し各パラメータ小さい値でも良かったかもしれません。

学習

ミニバッチサイズ50,エポック数は10でやりました。
これでGPU内のメモリ使用量(nvidia-smiコマンドで確認)は1.6GBくらいだったので、バッチサイズは100でも良かったかもしれません。
通常は集めたデータの一部を検証用に残すのですが、今回は全データ学習させて、後から100個ほどランダムに抽出してフィッティングの具合だけ確認しました。
学習が終わったらモデルデータをファイルに保存します。


以下、pythonソースコード

畳み込みニューラルネットワークの定義: cozmo_dnn.py

# -*- Coding: utf-8 -*-
#  Donkey COZMO
#    Convolution network definition
#  Copyright (C) RC30-popo,2019

import chainer
import chainer.links as L
import chainer.functions as F
from chainer import Chain, optimizers, Variable

# chainer.config.train = False

class czCnn(Chain):
    def __init__(self):
        super(czCnn, self).__init__(
            conv1 = L.Convolution2D(in_channels=1, out_channels=16, ksize=4, stride=1, pad=1),
            conv2 = L.Convolution2D(in_channels=16, out_channels=32, ksize=4, stride=1, pad=1),
            l0 = L.Linear(None,1024),
            l1 = L.Linear(None,3),
            bncv1 = L.BatchNormalization(16),
            bncv2 = L.BatchNormalization(32),
            bn0 = L.BatchNormalization(1024),
        )
    def forward(self, x,ratio=0.5):
        h = F.reshape(x,(len(x),1,160,120))
        h = F.max_pooling_2d(F.relu(self.bncv1(self.conv1(h))),2)
        h = F.max_pooling_2d(F.relu(self.bncv2(self.conv2(h))),2)
        h = F.dropout(F.relu(self.bn0(self.l0(h))),ratio=ratio)
        h = self.l1(h)
        return h

学習走行Script: cozmo_dnn_train.py

# -*- Coding: utf-8 -*-
#  Donkey COZMO
#    Convolution network training
#  Copyright (C) RC30-popo,2019


# OS
import os
# OpenCV
import cv2
# Numpy
import numpy as np
# Chainer
import chainer
import chainer.links as L
import chainer.functions as F
from chainer import Chain,optimizers,Variable
from chainer import serializers
from chainer import cuda
# random
import random

xp = cuda.cupy

from cozmo_dnn import czCnn

# Files
DONKEY_COZMO_DATAFILE = 'data/donkey_cozmo.dat'
DONKEY_COZMO_MDLFILE = 'donkey_cozmo_mdl.npz'
DONKEY_COZMO_OPTFILE = 'donkey_cozmo_opt.npz'

img_x = 160
img_y = 120

x_train_data = []
t_train_data = []

# Read data and convert to vector
with open(DONKEY_COZMO_DATAFILE, mode='r', encoding='utf-8') as f:
    for line in f:
        imgfile,label = line[:-1].split(',')
        print('imgfile = '+imgfile+',label = '+label)
        img = cv2.imread(imgfile)
        img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
        img = cv2.resize(img,(img_x,img_y),interpolation=cv2.INTER_AREA)
#        print('img shape = ',img.shape)
        img_gs = img.flatten()

        x_train_data.append(img_gs)
        t_train_data.append(int(label))

x_train = xp.array(x_train_data,dtype=xp.float32)
x_train /= 255
t_train = xp.array(t_train_data,dtype=xp.int32)
total_datacount = len(x_train)
print('Total number of training data = ',total_datacount)

# Initialize Neural Network
model = czCnn()
chainer.config.train = True
optimizer = optimizers.Adam()
optimizer.setup(model)

# GPU setup
gpu_device = 0
cuda.get_device(gpu_device).use()
model.to_gpu(gpu_device)

# number of epochs
n_epoch = 10

# batch size
batch_size = 50

# Training main loop
for epoch in range(n_epoch):
    sum_loss = 0
    perm = np.random.permutation(total_datacount)

    for i in range(0,total_datacount,batch_size):
        if i + batch_size <= total_datacount:
            local_batch_size = batch_size
        else:
            local_batch_size = total_datacount - i
        x = Variable(x_train[perm[i:i+local_batch_size]])
        t = Variable(t_train[perm[i:i+local_batch_size]])

        y = model.forward(x,ratio=0.3)
        model.cleargrads()
        loss = F.softmax_cross_entropy(y, t)
        loss.backward()
        optimizer.update()
#        sum_loss += loss.data*local_batch_size
        sum_loss += float(cuda.to_cpu(loss.data)) * local_batch_size

    print("epoch: {0}, mean loss: {1}".format(epoch,sum_loss/total_datacount))

# Check accuracy
perm = np.random.permutation(total_datacount)
x = Variable(x_train[perm[0:100]])
t = t_train[perm[0:100]]
chainer.config.train = False
y = model.forward(x)
cnt = 0
for i in range(100):
    ti = t[i]
    yi = xp.argmax(y.data[i])
    print('[%d] t = %d, y = %d' % (i,ti,yi))
    if ti == yi:
        cnt += 1
# Display Result
# print(cnt)
print("accuracy: {}".format(cnt/(100)))



model.to_cpu()
serializers.save_npz(DONKEY_COZMO_MDLFILE, model)
# serializers.save_npz(DONKEY_COZMO_OPTFILE, optimizer)

[technology][deep learning][chainer]COZMOを内蔵カメラを利用してDeep Learningで自律走行させる。Part.3-自律走行編-

学習が終わったので、早速自律走行させてみます。
以下がその動画。スクリプトのバグ取りのみで、Deep Learningの学習部分は一発で上手くいきました。
カメラ画像だけで判断して自律走行ができています。

ちなみに原理上、新しいコースを走らせる場合は、新しいコースで再度、データ取りと学習をやり直す必要があります。

youtu.be


自律走行にはデータ取りに使ったスクリプトを改造します。
データ取りでは人間が操作をするために、キー入力を拾って前進、左旋回、右旋回のコマンドをCOZMOに送っていましたが、このキー入力を拾う部分をChainerで作成したCNNに置き換え、カメラ画像をCNNに渡して、出てきた操作ラベルに従ってCOZMOにコマンドを送るだけです。

以下、pythonソースコード

COZMO自律走行スクリプト: donky_cozmo_run.py

#!/usr/bin/env python3
# -*- Coding: utf-8 -*-
#  Donkey COZMO
#    autonomous run by deep learning result 
#  Copyright (C) RC30-popo,2019



import sys
import time
import termios
import os

import cv2
from PIL import Image
import numpy as np
import cozmo
from cozmo.util import degrees, distance_mm, speed_mmps

# Chainer
import chainer
import chainer.links as L
import chainer.functions as F
from chainer import Chain,optimizers,Variable
from chainer import serializers
from chainer import cuda
# random
import random

xp = cuda.cupy

from cozmo_dnn import czCnn

# Initial settings
DONKEY_COZMO_FACE_INIT_ANGLE = 0.0 # -25.0 degree to 44.5 degree
DONKEY_COZMO_TURN_ANGLE = 10.0
DONKEY_COZMO_FORWARD_DIST = 20.0
DONKEY_COZMO_SPEED = 50.0

# COZMO actions
DONKEY_COZMO_ACTION_FORWARD = 0
DONKEY_COZMO_ACTION_LEFT_TURN = 1
DONKEY_COZMO_ACTION_RIGHT_TURN = 2


# Files
DONKEY_COZMO_MDLFILE = 'donkey_cozmo_mdl.npz'

img_x = 160
img_y = 120

# Decide action by Deep Learning prediction from COZMO's camera image
def cozmo_decide_action(cur_img):
    global img_x
    global img_y
    global model

    img = cv2.resize(cur_img,(img_x,img_y),interpolation=cv2.INTER_AREA)
    img_gs = img.flatten()
    x_data = []
    x_data.append(img_gs)
    x = xp.array(x_data,dtype=np.float32)
    x /= 255
    x = Variable(x)
    y = model.forward(x)
#    print('y = ',y)
    res = xp.argmax(y.data[0])
#    print('res = ',res)
    return res


# Let COZMO go forward
def cozmo_go_forward(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_FORWARD_DIST
    global DONKEY_COZMO_SPEED
    robot.drive_straight(distance_mm(DONKEY_COZMO_FORWARD_DIST), speed_mmps(DONKEY_COZMO_SPEED)).wait_for_completed()

# Let COZMO turn left
def cozmo_left_turn(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_TURN_ANGLE
    robot.turn_in_place(degrees(DONKEY_COZMO_TURN_ANGLE)).wait_for_completed()

# Let COZMO turn right
def cozmo_rigth_turn(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_TURN_ANGLE
    robot.turn_in_place(degrees(-1.0 * DONKEY_COZMO_TURN_ANGLE)).wait_for_completed()

# Record COZMO camera image with action(go straight, left turn, right turn)
def cozmo_donkey_run(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_FACE_INIT_ANGLE

    robot.camera.image_stream_enabled = True # Enable COZMO camera capture
    lift_action = robot.set_lift_height(0.0, in_parallel=False)
    lift_action.wait_for_completed()
    head_action = robot.set_head_angle(cozmo.util.Angle(degrees=DONKEY_COZMO_FACE_INIT_ANGLE),in_parallel=False)
    head_action.wait_for_completed()
    firstTime = True
    try:
        while True:
            duration_s = 0.1
            latest_image = robot.world.latest_image
            if latest_image != None:
                gray_image = latest_image.raw_image.convert('L')

                cv_image = np.asarray(gray_image)
#                print('cv_image shape = ',cv_image.shape)
                if firstTime:
                    height, width = cv_image.shape[:2]
                    print('*** Start captureing COZMO camera')
                    print('image height   = ',height)
                    print('image width    = ',width)
                    firstTime = False

                cv2.imshow('frame',cv_image)
                cv2.waitKey(1)

                action = cozmo_decide_action(cv_image)
                if action == DONKEY_COZMO_ACTION_FORWARD:
                    print('*** Go FORWARD ***')
                    cozmo_go_forward(robot)
                elif action == DONKEY_COZMO_ACTION_LEFT_TURN:
                    print('*** Turn LEFT ***')
                    cozmo_left_turn(robot)
                elif action == DONKEY_COZMO_ACTION_RIGHT_TURN:
                    print('*** Turn RIGHT ***')
                    cozmo_rigth_turn(robot)
                
#            time.sleep(duration_s)
    except KeyboardInterrupt:
        print('Keyboard Interrupt!!')
        print('Exit Cozmo SDK')
        cv2.destroyAllWindows()
        pass


# Initialize Neural Network
model = czCnn()
optimizer = optimizers.Adam()
optimizer.setup(model)
chainer.config.train = False
serializers.load_npz(DONKEY_COZMO_MDLFILE, model)
# GPU setup
gpu_device = 0
cuda.get_device(gpu_device).use()
model.to_gpu(gpu_device)

# Run main code
cozmo.run_program(cozmo_donkey_run)


Chainer v2による実践深層学習
新納 浩幸
オーム社
売り上げランキング: 270,162

Donkey COZMOを作る Part.1 (AIロボットCOZMOをChainer+pythonで自律走行させる)

[technology][deep learning][chainer]COZMOを内蔵カメラを利用してDeep Learning(chainer)で自律走行させる。Part.1-データ取り編-

DONKEY CARと同じ原理でCOZMOを自動走行させる実験のPart.1
www.donkeycar.com

DONKEY CARは1/16のラジコンカーにraspberry piを搭載し、カメラ映像でコースを解析しながら走行するロボットカーです。
方式としては、一度人間がリモートでコースを走らせ、その際の操作(加減速、左右操舵角)をDONKEY CARに搭載したカメラの映像と紐付けてDeep Learningで学習させた後に自動走行させます。

COZMOはANKI社の手のひらサイズのAIロボットで、カメラと走行用キャタピラ、顔の表情を表現するためのOLEDディスプレイ等を搭載しています。日本ではタカラトミーから発売されています。
このCOZMO、スマフォをPCと接続することで、pythonからリモート制御できるSDKが公開されています。
このSDKを使って、DONKEY CARと同じ事が出来るんじゃないかと

まずはコースを走行させながら、COZMOのカメラ画像と走行状態(前進、左旋回、右旋回)のひも付けデータをディープラーニングの学習用に取得するスクリプトを書きました。
コースは家にあった通販のダンボールを加工して作成。

走行状態の取得ですが、走行しながら左右旋回等は難易度が高そうなので、

  • 前進2cm
  • (その場で)左旋回 10度
  • (その場で)右旋回 10度

の3種類の操作を定義。

1.カメラ画像を取得
2.上記のうちのどれかの操作を1つ選択
3.カメラ画像と操作ラベル(0,1,2)をファイルに保存

という単純なスクリプトにしました。
データ取りの段階では2.を人間がコマンドを入力して選択。
最終的にはデータを学習させたディープニューラルネットワークに2.を置き換えます。


作成したスクリプトやデータ取得中の写真等は下記。
背景画像のバリエーションを作るためにコースの向きを変えたりしながら10周程度COZMOを走行させ、1000枚程度の画像を集めました。
COZMOのカメラは320x240のいわゆるQVGAでグレースケールの画像が取得できます。

サンプル画像を見てもらうと判る通り、結構左右の視野角が狭いので、画像から操作のヒントが得られる様にコースにセンターラインを足したり、コースの壁面に方角毎に違う絵を描いたりしています。


データ取り用リモート走行Pythonスクリプト

#!/usr/bin/env python3
#  Donkey COZMO
#    Data recorder for learning course
#  Copyright (C) RC30-popo,2019



import sys
import time
import termios
import os

import cv2
from PIL import Image
import numpy as np
import cozmo
from cozmo.util import degrees, distance_mm, speed_mmps

# Initial settings
DONKEY_COZMO_FACE_INIT_ANGLE = 0.0 # -25.0 degree to 44.5 degree
DONKEY_COZMO_TURN_ANGLE = 10.0
DONKEY_COZMO_FORWARD_DIST = 20.0
DONKEY_COZMO_SPEED = 50.0

# COZMO actions
DONKEY_COZMO_ACTION_FORWARD = 0
DONKEY_COZMO_ACTION_LEFT_TURN = 1
DONKEY_COZMO_ACTION_RIGHT_TURN = 2

# COZMO Key bindings
DONKEY_COZMO_KEY_FORWARD = 'l'
DONKEY_COZMO_KEY_LEFT_TURN = ','
DONKEY_COZMO_KEY_RIGTH_TURN = '.'

# Files
DONKEY_COZMO_DATADIR = 'data/'
DONKEY_COZMO_IMAGEPREFIX = 'img_'
DONKEY_COZMO_DATAFILE = 'donkey_cozmo.dat'
DONKEY_COZMO_SEQNOFILE = 'donkey_cozmo.seq'

# Data Sequense number management
def getSeqNo(filepath):
    if os.path.exists(filepath):
        with open(filepath, mode='r') as f:
            for line in f:
                seqno = int(line)
                break
    else:
        seqno = 0
    return seqno

def saveSeqNo(filepath,seqno):
    with open(filepath, mode='w', encoding='utf-8') as f:
        f.write(str(seqno))


# Get key input from stdin without Enter key
#   This code comes from
#   https://qiita.com/tortuepin/items/9ede6ca603ddc74f91ba
def get_keyinput():
    #標準入力のファイルディスクリプタを取得
    fd = sys.stdin.fileno()

    #fdの端末属性をゲットする
    #oldとnewには同じものが入る。
    #newに変更を加えて、適応する
    #oldは、後で元に戻すため
    old = termios.tcgetattr(fd)
    new = termios.tcgetattr(fd)

    #new[3]はlflags
    #ICANON(カノニカルモードのフラグ)を外す
    new[3] &= ~termios.ICANON
    #ECHO(入力された文字を表示するか否かのフラグ)を外す
    new[3] &= ~termios.ECHO

    try:
        # 書き換えたnewをfdに適応する
        termios.tcsetattr(fd, termios.TCSANOW, new)
        # キーボードから入力を受ける。
        # lfalgsが書き換えられているので、エンターを押さなくても次に進む。echoもしない
        ch = sys.stdin.read(1)
        termios.tcflush(fd,termios.TCIFLUSH)
        return ch

    finally:
        # fdの属性を元に戻す
        # 具体的にはICANONとECHOが元に戻る
        termios.tcsetattr(fd, termios.TCSANOW, old)

# Let COZMO go forward
def cozmo_go_forward(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_FORWARD_DIST
    global DONKEY_COZMO_SPEED
    robot.drive_straight(distance_mm(DONKEY_COZMO_FORWARD_DIST), speed_mmps(DONKEY_COZMO_SPEED)).wait_for_completed()

# Let COZMO turn left
def cozmo_left_turn(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_TURN_ANGLE
    robot.turn_in_place(degrees(DONKEY_COZMO_TURN_ANGLE)).wait_for_completed()

# Let COZMO turn right
def cozmo_rigth_turn(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_TURN_ANGLE
    robot.turn_in_place(degrees(-1.0 * DONKEY_COZMO_TURN_ANGLE)).wait_for_completed()

# Record COZMO camera image with action(go straight, left turn, right turn)
def cozmo_donkey_recorder(robot: cozmo.robot.Robot):
    global DONKEY_COZMO_FACE_INIT_ANGLE
    global DONKEY_COZMO_KEY_FORWARD
    global DONKEY_COZMO_KEY_LEFT_TURN
    global DONKEY_COZMO_KEY_RIGTH_TURN
    global DONKEY_COZMO_ACTION_FORWARD
    global DONKEY_COZMO_ACTION_LEFT_TURN
    global DONKEY_COZMO_ACTION_RIGHT_TURN
    global DONKEY_COZMO_DATADIR
    global DONKEY_COZMO_IMAGEPREFIX
    global DONKEY_COZMO_DATAFILE
    global DONKEY_COZMO_SEQNOFILE

    robot.camera.image_stream_enabled = True # Enable COZMO camera capture
    lift_action = robot.set_lift_height(0.0, in_parallel=False)
    lift_action.wait_for_completed()
    head_action = robot.set_head_angle(cozmo.util.Angle(degrees=DONKEY_COZMO_FACE_INIT_ANGLE),in_parallel=False)
    head_action.wait_for_completed()
    firstTime = True
    try:
        seqno = getSeqNo(DONKEY_COZMO_DATADIR + DONKEY_COZMO_SEQNOFILE)
        datf = open(DONKEY_COZMO_DATADIR + DONKEY_COZMO_DATAFILE,mode='a',encoding='utf-8')
        while True:
            duration_s = 0.1
            latest_image = robot.world.latest_image
            if latest_image != None:
                gray_image = latest_image.raw_image.convert('L')

                cv_image = np.asarray(gray_image)
                if firstTime:
                    height, width = cv_image.shape[:2]
                    print('*** Start captureing COZMO camera')
                    print('image height   = ',height)
                    print('image width    = ',width)
                    firstTime = False

                cv2.imshow('frame',cv_image)
                cv2.waitKey(1)

                ch = get_keyinput()
                current_action = None
                if ch == DONKEY_COZMO_KEY_FORWARD:
                    current_action = DONKEY_COZMO_ACTION_FORWARD
                    print('*** Go FORWARD ***')
                    cozmo_go_forward(robot)
                elif ch == DONKEY_COZMO_KEY_LEFT_TURN:
                    current_action = DONKEY_COZMO_ACTION_LEFT_TURN
                    print('*** Turn LEFT ***')
                    cozmo_left_turn(robot)
                elif ch == DONKEY_COZMO_KEY_RIGTH_TURN:
                    current_action = DONKEY_COZMO_ACTION_RIGHT_TURN
                    print('*** Turn RIGHT ***')
                    cozmo_rigth_turn(robot)
                else:
                    current_action = None
                
                # save image and action data
                if current_action != None:
                    img_filename = DONKEY_COZMO_DATADIR + DONKEY_COZMO_IMAGEPREFIX + '_' + format(seqno,'05d') + '.png'
                    cv2.imwrite(img_filename,cv_image)
                    datf.write(img_filename + ',' + str(current_action) + '\n')
                    seqno = seqno + 1
#            time.sleep(duration_s)
    except KeyboardInterrupt:
        print('Keyboard Interrupt!!')
        print('Exit Cozmo SDK')
        saveSeqNo(DONKEY_COZMO_DATADIR + DONKEY_COZMO_SEQNOFILE,seqno)
        datf.close()
        cv2.destroyAllWindows()
        pass

cozmo.run_program(cozmo_donkey_recorder)


Donkey COZMO用のコース

COZMOを走行させているところ(コースにセンターラインを書き足した)

データ取りスクリプトで取得したCOZMOカメラ画像


カメラ画像のファイル名とタグの例。タグは0が前進,1が左10度旋回,2が右10度旋回

data/img__00037.png,2
data/img__00038.png,2
data/img__00039.png,2
data/img__00040.png,0
data/img__00041.png,2
data/img__00042.png,0
data/img__00043.png,0
data/img__00044.png,0
data/img__00045.png,1
data/img__00046.png,0

追記: 環境はUbuntu16.04LTS+Anaconda3(python3.6)にOpenCV及び次のエントリで紹介するDeep Learningの学習にはChainerが必要です。
Windowsの場合、キー入力の部分を変更必要と思います。

COZMOのカメラ画像をOpenCVに渡す

[technology]COZMO SDKを使って、COZMOのカメラ映像をOpenCVに読み込ませる

COZMOは日本ではタカラトミーから発売されているAIロボットトイです。
www.takaratomy.co.jp

残念ながら開発元の米ANKI社は事業を畳むらしいのですが、我が家では小学生の息子の良い遊び相手として現役で活躍しています。

japanese.engadget.com

さて、このCOZMO、python経由でリモート制御するためのSDKが公開されており、またCOZMOの顔部分にはカメラも付いているのでそのうちのディープラーニングとか使って自律走行させたいと思っていました。
で、手始めにCOZMO君のカメラ画像をpythonスクリプトで読み出し、OpenCVで扱えるnumpyのベクトルに変換する方法を調査しました。

APIリファレンスは公開されているので、これを調べれば良いですが、APIリファレンスを読んでもなかなか全体像を把握しずらく、SDKについているサンプルソースから調査します。
cozmosdk.anki.com

SDKサンプルソースにtutorials/02_cozmo_face/02_cozmo_face_mirror.pyに参考になる実装があります。

    while True:
        duration_s = 0.1  # time to display each camera frame on Cozmo's face

        latest_image = robot.world.latest_image
〜中略〜
        time.sleep(duration_s)

抜粋すると上記の様な感じで、COZMOの制御用クラスcozmo.robot.Robotのインスタンスのworld.latest_imageというオブジェクトを定期的に参照することで、COZMOのカメラ画像を取得出来ます。
実際のカメラ画像はPillowのイメージフォーマットでworld.latest_imageのraw_imageをいうメンバー変数に格納されています。
COZMOのカメラ自体はモノクロですが、このraw_imageはRGBのカラーデータを可能する形式になっています。

このraw_imageをnumpyのarrayに変換すればOpenCVで扱えます。
下記はとりあえずOpenCVにカメラ画像を渡して表示するだけのサンプルです。

最終的にはOpenCVの物体検出器とかと組み合わせてCOZMOの制御を出来る様にしたいと思っています。


#!/usr/bin/env python3
#  Example to pass COZMO camera image to OpenCV function
#  Copyright (C) RC30-popo,2019



import sys
import time
import cv2
from PIL import Image
import numpy as np
import cozmo

def cozmo_camera_test(robot: cozmo.robot.Robot):
    robot.camera.image_stream_enabled = True # Enable COZMO camera capture
    head_action = robot.set_head_angle(cozmo.util.Angle(degrees=0.0),in_parallel=False)
    head_action.wait_for_completed()
    firstTime = True
    try:
        while True:
            duration_s = 0.1
            latest_image = robot.world.latest_image
            if latest_image != None:
                gray_image = latest_image.raw_image.convert('L')

                cv_image = np.asarray(gray_image)
                if firstTime:
                    height, width = cv_image.shape[:2]
                    print('*** Start captureing COZMO camera')
                    print('image height   = ',height)
                    print('image width    = ',width)
                    firstTime = False

                cv2.imshow('frame',cv_image)
                cv2.waitKey(1)

            time.sleep(duration_s)
    except KeyboardInterrupt:
        print('Keyboard Interrupt!!')
        print('Exit Cozmo SDK')
        cv2.destroyAllWindows()
        pass

cozmo.run_program(cozmo_camera_test)


老後資金2千万の話

[news]そもそも2000万で足りるの?

なんだか騒ぎになっている年金だけでは老後暮らしていけない。自己資金で2,000万貯めろと金融庁の報告書に書かれていた話、弊ブログは技術系のつもりなので、エンジニアの待遇うんぬん話以外にはあまり触れるつもりは無いのですが、どうも違和感を感じたので。
news.livedoor.com

まあ、年金だけで平和に暮らせればそれに越したことは無いし、政府は年金システムを維持する努力はすべきだけど、それはさておき自分が違和感を感じるのは「2,000万円なんて今までさんざん言われてきたじゃない。そもそもみんな自分の年金定期便ちゃんと見てないの?そこに書いてある数字見れば年金(+退職金)で暮らせない事なんか明白なんだけどなあ」という事。

もうひとつは「2,000万じゃ足りないよね?」ということ。

うちの会社は何年か前から会社のシステムで自分の退職金の見込み金額とか簡単に見られる様になっていて、自分は年1回くらい、年金定期便の内容と付きあわせて老後資金の試算とか現在の自分の資産内容の整理とかしています。個人情報なので計算の根拠は書きませんが、自分の場合、65歳〜85歳までの間に年金+退職金以外に2,500万円くらいは無いとほどほどの生活するの辛いなあという感触。これに不意の出費や住んでいる家の修繕費とか、うっかり90歳以上まで生きちゃったりする事を考えれば3,000万円くらいは欲しい。
さらに、いわゆる「ゆとりある老後」まで目指すと、なんだかんだ4,000万くらいは欲しいな...という感じです。正直2,000万は最低ラインでこれじゃ全然不安。
# ゆとりあると言うのは何年かに1度くらいは海外旅行行ったりとか..そんな感じ..

もしマスコミが書き立てているとおり、みんな「2,000万なんて聞いてないよ」状態なんだとしたら、あまりにも自分の将来を他人任せにしすぎだと思う。

あと、マスコミも野党も「2,000万も貯められるか!!」みたいな事を言っているんだけど、これも違和感。
もちろん年収200〜300万とかでかつかつの生活をしている人たちが多く居るのは判ります。

一方で休日に街に出るとアルファードだのヴェルファイヤだのに乗った比較的若めのファミリーが回転寿司屋の駐車場にクルマ並べてるのを見ると、この人たちは自分の先の事を考えているんだろうか?と思っちゃうわけで。余計のお世話だとは思うけど、今時回転寿司なんて別に安くもなく、家族3,4人で食べいけば結構な出費です。アルファードを目の敵にするわけじゃないけど、身の丈にあったクルマ、せめてシエンタかフリードクラスにしておけば、その分100万円くらいお金が浮くわけで、給料安いと文句言いながらも結局商業的流行に流れされて浪費している人たちもたくさん居る様に見えてしまい、そういうのを見ると年金については政府の無策も問題だけど、結局自分達の将来を他人任せにしている人がたくさん居て、何もしないで文句言っている様にも見えてしまうのです。
ビミョーに炎上しそうな文章だなあ...

とにかく、自分的結論は「2,000万じゃ足りません。」なので、日々頑張ってます...

で、自分はずいぶん乱暴に書いてしまったけど、下記の方のエントリーがキレイにまとまってると思う。
全部自己責任とは言わないけど、ちょっとみなさん自分で自分の事を考え、勉強した方が良いです。

www.landerblue.co.jp

単一時系列データの異常検知をKMeansによるクラスタリングで実装してみる

[technology][機械学習][scikit-learn]python3 + scikit-learnで時系列データの異常検知を実験

概要

時系列データ(複数ではなく、単純に時間軸に沿って、1種類のデータが並んでいるもの、例えばNICの通信量だったり、CPUの動作率だったり)から異常な動きを検出するいわゆる異常値検出を調べていたのですが、種々手法がある中で、「クラスタリングを使う」「スライディングウィンドウで時系列データを切り出し、個々のウィンドウの中のデータをベクトル化」みたいな流れは判ったのですが、とりあえず外れ値検知とおぼしきコードをscikit-learnのKMeansで組んでみました。

環境

Ubuntu16.04LTS+Anaconda3(python3.6)

やったこと

ソースを細かく説明したいのですが、とりあえずざっくりと。

  • 0〜359度までのSIN関数の値を360点の時系列データとして生成
  • 適当に異常値を入れるポイントを選び、本来のSINの値の1/2に置き換える
  • 出来上がった時系列データの階差データを取る(359点の階差データが出来る)
  • ウィンドウサイズを適当に決める(下記ソースでは4)
  • ウィンドウサイズに合わせて、階差データを4点づつベクトル化する

階差データがd0,d1,d2,d3,d4,d5,d6,,.....とするとベクトル化したデータは(d0,d1,d2,d3),(d1,d2,d3,d4),(d2,d3,d4,d5)...という感じでベクトルの要素が1個づつ後ろにずれて入る

ソース
# -*- Coding: utf-8 -*-

# numpy
import numpy as np

# scikit-learnx
from sklearn.cluster import KMeans

# Matplotlib
import matplotlib as mpl
mpl.use('TkAgg')  # or whatever other backend that you want
import matplotlib.pyplot as plt
from matplotlib import cm

# Windows Size
WSZ = 4

# 異常値を生成する角度のリスト
a_angles = [60,90,120,270,320]

# SINカーブを作る(0〜359度)
x = np.arange(0,360)
y = np.sin(x * np.pi/180)

# 異常値を作る
for angle in a_angles:
    y[angle] = y[angle] * 0.5

# 階差行列を作る
yd = np.diff(y)


# 階差行列にスライディングウィンドウを適用してベクトル化
ydlist = []
for i in range(0,359 - WSZ):
    ydlist.append(yd[i:i+WSZ])

ydlist = np.array(ydlist)


# スライディングウィンドウでベクトル化した階差データ列をKMeansでクラスタリング
kmeans = KMeans(n_clusters=8)
kmeans_model = kmeans.fit(ydlist)

# 各スライディングウィンドウのラベル(グループ番号)リストを取得
labels = kmeans_model.labels_


# 時系列データ(SIN波+異常データ)とクラスタリング結果を並べてグラフを描く
nlabels = np.r_[np.zeros(1,dtype=np.int32),labels,np.zeros(WSZ,dtype=np.int32)]

fig = plt.figure()
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
ax1.plot(x,y,label='wave form')
ax2.plot(x,nlabels,label='check result')

ax1.legend()
ax2.legend()

plt.show()
結果を描画したグラフ

正常なところがグループ0,異常値を含むウィンドウが0以外のグループ番号になっているのが判ると思います。
もっと複雑なデータで実行する場合は、ウィンドウサイズの調整等が必要になると思います。

グラフ上、異常値の場所より少し前に非0のグループ番号がある様に見えるのは、グループ番号の表示位置を各ウィンドウの先頭の階差データの位置に合わせたからです。
(異常値がそのウィンドウの最後に現れる。グループ番号はそのウィンドウの先頭のプロットされている。)


次は同じ手法で作った階差ベクトルをJubatusのアノマリ検知に食わせてみようかと思っています。

追記: 参考にしたのは下記のブログです
が、コードが理解しきれなかったので、自分で簡単な波形で考えながら書いたのが上記のコードです。
非常に異常値が分かりやすい例なので、もっと複雑な波形で上手く行くかは判りません。
blog.brains-tech.co.jp

追記2:
階差ベクトルのクラスタリングというのが何をやっているのか判りやすい様に可視化してみました。

# Windows Size
WSZ = 2

上記のソースで階差ベクトルを作るためのウィンドウサイズを2に変更します。
こうすると各階差ベクトルは2次元のベクトルになります。

plt.scatter(ydlist[:,0],ydlist[:,1])
plt.show()

2次元のベクトルなので、そのままmatplotlibでプロットしてみます。
その結果が下記です。正常なSIN波の階差ベクトルが原点付近に固まっているのが判ります。(赤丸の中)
そして通常の値の変動率(要は微分値)から大きく外れたベクトルが原点から離れた位置にあるのが判ります。
複雑な波形の場合は、これを3,4,5次元と...どんどん次元拡張していけば、考え方は同じになります。