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

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

Donkey COZMOを作る Part.2/3: AIロボットCOZMOをディープラーニング(Chainer+python)で自律走行させる

[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)