アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

自作DPUをultra96v2で動かすまでの備忘録【Vitis-AI, hardware】

FPGA上でディープラーニングを動かすとき背後でDPUというものが動いていて、DPUないとFPGA上でディープラーニングが動かせない。

今回はDPU IPコアから自作DPUを作って、ultra96v2上で動かすまでの備忘録をまとめた。




目次
1.DPUのデザインblockをvivadoで作成
2.Petalinuxでbuildしてimageを作成するまで
3.image.ubとBOOT.binのコピーで動作させる




1.DPUのデザインblockをvivadoで作成

作業環境


Mac OS

VirtualBox

ubuntu 14.02.03LTS

・Vivado、Vitis 2020.1






メモリの増設



VirtualBoxで作業してるので、DPUは死ぬほどメモリを食うので


プロセッサーでCPUを6枚

・メインメモリを11800くらいに変更

して計算リソースを増やした。

f:id:trafalbad:20200808225619p:plain

f:id:trafalbad:20200808225623p:plain


計算リソースが足りないとBitStream時に「強制終了エラー」が出る。





DPUのversion


xilinxgithubで公開してるDPU IP(DPU-TRDフォルダ)はブランチとマスターでDPUのversionが違うので注意。

・branch v1.0 =>target version 1.4.0(3.1)

・branch v1.1 => target version 1.4.1(3.2)

・master => target version 1.4.1 (3.3)


masterのDPU IPはv1.0とv1.1とかなり違うし、vivado2019.2だと使えない気がした。(2020.1なら使える)。




DPU IPからXSAファイルの作成



このサイトを参考に作成して、DPUはこの変わった変わったDPUドライバを使った。



まず DPU IPを IP catalogにadd。
f:id:trafalbad:20200808225645p:plain


設計するときは、パラメータに加えて、

・Clock wizardでdspはdpuの2倍の周波数がないとダメ
・各Processor system resetと各DPUピンはシンクロしないとタイミングエラーになる

などなど、制約が多いのでXilinxのDPUの英語の最新文献を読み込まないとできなかった。


f:id:trafalbad:20200808230806p:plain



無事BitStreamが完了したので、XSAファイルを生成。

最後に右のダッシュボードからパラメータsummaryを載っけてみる。



f:id:trafalbad:20200808225704p:plain

f:id:trafalbad:20200808225700p:plain






2.Petalinuxでbuildするまで

1.必要ファイルのdownloadと環境設定



・Ultra96v2用のbspファイル
Avenetのultra96V2専用ページからdownload


これはPetalinuxでultra96v2用のprojectを作るとき必要。

# wget -P <path> <URL>
$ wget -P ultra96_oob_2019.2.bsp.zip http://downloads.element14.com/downloads/zedboard/ultra96/ultra96v2_oob_2019_2.zip
$ unzip ultra96_oob_2019.2.bsp.zip


・一応ultra96v2のplartform用ファイルも環境設定用にdownload

$ wget -P ULTRA96V2_1029_2.tar.xz http://avnet.me/ultra96v2-vitis-2019.2
$ tar -xf ULTRA96V2_1029_2.tar.xz
# 環境設定
$ export SDX_PLATFORM=/home/user/ULTRA96V2/ULTRA96V2.xpfm


・DNNDKとかのファイルに必要

$ git clone https://github.com/xelalin/Ultra96v2-DPU




2.projectの作成



$ petalinux-create -t project -s ultra96v2_oob_2019_2.bsp --name dpuprj
$ git clone https://github.com/xelalin/Ultra96v2-DPU
$ cd dpuprj


3.DNNDK用のファイルをコピー



$ cp -rp ../Ultra96v2-DPU/files/recipes-apps/dnndk/ project-spec/meta-user/recipes-apps/
$ cp -rp ../Ultra96v2-DPU/files/recipes-modules/ project-spec/meta-user/
$ cp -rp ../Ultra96v2-DPU/files/recipes-apps/autostart/ project-spec/meta-user/recipes-apps/
$ cp -rp ../Ultra96v2-DPU/files/recipes-core/base-files/ project-spec/meta-user/recipes-core/

*保留
dnndk.bbファイルをここのファイルに書き換える
https://gist.github.com/mmitti/664a99743e4b491ee47d1f1353995265





4.ファイルに項目を書き足す


$ sudo vi project-spec/meta-user/conf/user-rootfsconfig
CONFIG_dpu
CONFIG_autostart
CONFIG_dnndk



5.Projectの初期化



「XSA」ファルダにxsaファイルを入れておく。

$ petalinux-config --get-hw-description=../XSA

=> 「Image Packaging Configuration」>「RootFilesystem formats」にext4を追加してsave&exit

f:id:trafalbad:20200729174155p:plain

[INFO] sourcing bitbake
[INFO] generating plnxtool conf
~~~~~
[INFO] generating bbappends for project . This may take time ! 
[INFO] generating u-boot configuration files
[INFO] generating kernel configuration files
[INFO] generating kconfig for Rootfs
[INFO] silentconfig rootfs
[INFO] generating petalinux-user-image.bb



6. rootfsの設定


$ petalinux-config -c rootfs

=> 以下の項目にチェックを入れてファイルシステムに含める(全部の箇所にチェック)

<Filesystem Packages>
libs=>libmali-xlnx

<Petalinux Package Groups>
-matchbox
-opencv
-python-modules
-v4lutils
-x11

<apps>
-autostart

<mdules>
-dpu

<user packages>
-dnndk

チェックを入れたらsave&exit






7.カーネルの設定


$ petalinux-config -c kernel

=> 「Device Drivers」>「Generic Driver Options」>「Size in MegaBytes(DMA)で256を1024に変更。

[INFO] generating Kconfig for project
[INFO] sourcing bitbake
[INFO] generating plnxtool conf
~~~
0: linux-xlnx-4.19-xilinx-v2019.2+git999-r0 do_menuconfig - 0s (pid 18512)
Trying to run: screen -r devshell_18512
There is no screen to be resumed matching devshell_18512.
NOTE: Tasks Summary: Attempted 373 tasks of which 366 didn't need to be rerun and all succeeded.
NOTE: Updating config fragment /home/hagi/dpuprj/components/plnx_workspace/sources/linux-xlnx/oe-local-files/devtool-fragment.cfg
[INFO] successfully configured kernel



8.デバイスツリー



=> DPUのドライバーから回路を扱えるように回路側の情報を登録

$ sudo vi project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
&amba {
	axi_intc_0: axi-interrupt-ctrl {
		#interrupt-cells = <2>;
		compatible = "xlnx,xps-intc-1.00.a";
		interrupt-controller;
		reg = <0x0 0xA0090000 0x0 0x1000>;
		xlnx,kind-of-intr = <0x0>;
		xlnx,num-intr-inputs = <0x20>;
		interrupt-parent = <&gic>;
		// connected at PS interrupt offset [6] (89+6=95) 
		interrupts = <0 95 4>;
	};

        dpu {
                compatible = "xilinx,dpu";
                base-addr = <0x8f000000>;//CHANGE THIS ACCORDING TO YOURDESIGN
                dpucore {
                   compatible = "xilinx,dpucore"; 
                   interrupt-parent = <&intc>;
                   interrupts = <0x0 106 0x1 0x0 107 0x1>; 
                   core-num = <0x2>;
                }; 
        };

	zyxclmm_drm {
		compatible = "xlnx,zocl";
		status = "okay";
		interrupt-parent = <&axi_intc_0>;
		interrupts = <0  4>, <1  4>, <2  4>, <3  4>,
			     <4  4>, <5  4>, <6  4>, <7  4>,
			     <8  4>, <9  4>, <10 4>, <11 4>,
			     <12 4>, <13 4>, <14 4>, <15 4>,
			     <16 4>, <17 4>, <18 4>, <19 4>,
			     <20 4>, <21 4>, <22 4>, <23 4>,
			     <24 4>, <25 4>, <26 4>, <27 4>,
			     <28 4>, <29 4>, <30 4>, <31 4>;
	};
};


9.ビルド


1, 2時間かかるのでひたすら待つだけ。

$ petalinux-build




4.image.ubとBOOT.binのコピーで動作させる

参考サイト



Vitis-AIのgithub

Zynq DPU v3.2 Product Guide PG338 (v3.2) July 7, 2020

DNNDK on Vitis AI on Ultra96v2(qiita)

Vitis-AI 1.1 Flow for Avnet VITIS Platforms - Part1(hackster)

ultara96v2用DPU用ファイル

Ultra96-v2でCNN推論エンジン(DPU)を動かすまで

Vitis-AIを使ってultra96v2上で学習済みモデルを動かすまで【hardware, FPGA, AI】

今回はultra96v2上で学習済みモデルを動かしてみる。

この作業がすんなりできれば、論理回路の高位合成とか組み込み部分を除けば、学習済みモデルを作れればドローン制御、自動運転の制御とかいろんなことの礎になる。

本家サイト「Ultra96V2向けVitis AI(2019.2)の組み立て方」通りに進めたけど情報が古かったのでかなり苦労した。


f:id:trafalbad:20200429092854p:plain


目次
1.動作環境「vitis AI」の構築
2.Deep Learning Processor Unit (DPU) IP の作成
3.学習済みデータと画像の用意
4.学習済みデータから、FPGA用データへの量子化(quantization)
5.量子化したファイルをultra96v2上で動かすアプリケーションの作成
6.Ultra96v2ボードでの動作確認




1.動作環境「vitis AI」の構築

dockerのインストール



dockerが必要なのでdockerをインストール。

$ sudo apt-get update && sudo apt-get upgrade
$ sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
>>>
OK

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# Uninstall Old Versions of Docker
$ sudo apt-get remove docker docker-engine docker.io
# Install Docker
$ sudo apt install docker.io

# Start and Automate Docker
$ sudo systemctl start docker
$ sudo systemctl enable docker
# コンテナ確認
$ sudo docker ps
>>>
CONTAINER ID        IMAGE               COMMAND   CREATED             STATUS              PORTS     NAMES

参考:Ubuntu 18.04にDockerをインストールする(+docker-composeも)


vitis-Aiのインストール


「tesnsorflow_q_val」コマンドがvirtualbocxだとCPUの関係で使えないので、EC2インスタンスで作業した。

次にvitis AIのインストール。
XilinxのVitis-AIがupgradeされてた。ブランチのv1.1をgit clone。

$ git clone -b v1.1 https://github.com/Xilinx/Vitis-AI
# Dockerの設定
$ cd Vitis-AI/docker
$ ./docker_build_cpu.sh
$ cd Vitis-AI
$ ./docker_run.sh xilinx/vitis-ai-cpu:latest


今回はCPU環境で試してみて、動いた!
f:id:trafalbad:20200429092932p:plain

# 抜ける
$ exit
# GPUの時はこちら
$ sudo ./docker_build_gpu.sh
$ sudo ./docker_run.sh xilinx/vitis-ai-gpu:latest




2.Deep Learning Processor Unit (DPU) IP の作成

DPU IPは「DPU for Convolutional Neural Network v1.1」によるとFPGAでCNNとかを動かすリソース(レジスタ設定、データ コントローラー、たたみ込み演算の各モジュールとか)が組み込まれてる。

DPU IPはvivadoで作成しなきゃならないけど、ここは本家サイトからresnet50用のDPUをdownloadした。


DPU(AI)IP作成パート
f:id:trafalbad:20200429092954p:plain

download後にultra96v2ボードにetcherでコピー。


コピー後のボード内ファイル一覧

$ ls ultra96_oob
BOOT.BIN        image.ub        system.dtb
README.txt        init.sh            ultra96v2_oob.hwh
dpu.xclbin        platform_desc.tx

これはSDCARDのboot領域(/media/user/${SDCARD}/boot)にコピーされる



3.学習済みネットワークと画像の用意

学習済みネットワークと画像の用意のパート
f:id:trafalbad:20200429093029p:plain



学習済みネットワークの用意



まず、作業環境のDocker起動(これ以降はほとんどdocker上で動かす)

$ cd Vitis-AI
$ sudo ./docker_run.sh xilinx/vitis-ai-cpu:latest
# 作業ディレクトリ作成
$ mkdir workspace
$ cd workspace


“vitis-ai-tensorflow”(または”vitis-ai-caffe”)をcondaで実行。

$ conda activate vitis-ai-caffe
# tensorflowの場合
$ conda activate vitis-ai-tensorflow

モデルdownload用スクリプトを実行して、学習済みResnet50をdownload。

$ cp -r Vitis-AI/Tool-Example/* .
# モデルのdownload
$ cd AI-Model-Zoo && ./get_model.sh


この中にcaffe、tensorflow用のモデルが入ってるので、「tf_resnetv1_50_imagenet_224_224_6.97G」または「cf_resnet50_imagenet_224_224_7.7G」を選択。

今回はtensorflowの「tf_resnet50_imagenet_224_224_7.7G」そ使う。



評価用画像(Imagenet)の用意


ILSVRC2012_val_00000001.JPEG~ILSVRC2012_val_00001000.JPEGの1000枚を用意。

評価用としてILSVRC2012_valの画像を1000枚用意してディレクトリを作った。

# 評価用画像ファルダの作成
$ mkdir images/ILSVRC2012_val

「exzample_files/input.py」を次のように書き換えた。

import glob
calib_image_dir = "/workspace/workspace/images/ILSVRC2012_val/ILSVRC2012/*"
calib_image_list = "/workspace/workspace/images/tf_calib.txt"
calib_batch_size = 10
def calib_input(iter):
    images = []
    
    for img_path in sorted(glob.glob(calib_image_dir)):
        image = cv2.imread(img_path)
        image = resize_shortest_edge(image, 256)
        image = mean_image_subtraction(image, MEANS)
        image = central_crop(image, 224, 224)
        images.append(image)
    return {"input": images}


ここまでの"workspace"内の全体のディレクトリ構造。

$ tree workspace 

>>>
workspace
|-- 0_download_model.sh
|-- 3_tf_eval_frozen_graph.sh
|-- 4_tf_quantize.sh
|-- 5_tf_eval_quantize_graph.sh
|-- 6_tf_compile_for_v2.sh
|-- 6_tf_compile_for_v3.sh
|-- README.md
|-- example_file
|   |   
|   |-- input_fn.py
|   |-- resnet_eval.py
|   `-- trainval.prototxt
|-- images
|   |-- ILSVRC2012_val
|   |   `-- ILSVRC2012
|   |       |—-|—ILSVRC2012_val_00000000.JPEG
|   |          |〜
|  |      |-ILSVRC2012_val_00001000.JPEG
|   |-- caffe_calib.txt
|   |-- list.txt
|   `-- tf_calib.txt
`-- tf_resnetv1_50_imagenet_224_224_6.97G
    |-- code
    |   |-- conda_config
    |   |   `-- enveriment.yaml
    |   |-- gen_data
    |   |   |-- gen_data.py
    |   |   |-- get_dataset.sh
    |   |   `-- imagenet_class_index.json
    |   `-- test
    |       |-- eval_tf_classification_models_alone.py
    |       `-- run_eval_pb.sh
    |-- data
    |-- float
    |   `-- resnet_v1_50_inference.pb
    |-- quantized
    |   |-- deploy_model.pb
    |   `-- quantize_eval_model.pb
    `-- readme.md

ここまで用意したもの

・操作環境(docker)

・DPU(AI) IP

・tensorflow用の学習済みNN

・画像






4.学習済みデータから、FPGA用データへの量子化(quantization)

量子化(quantization)のパートf:id:trafalbad:20200429093057p:plain


1.dcfファイルの作成



次に、etcherでイメージをSDカードにコピーしたときできた、”ultra96v2_oob.hwh”のhwhファイル(ハードウェア情報ファイル)を使って、dcfファイルを作る。

$ dlet -f ultra96v2_oob.hwh
>>>
[DLet]Generate DPU DCF file dpu-11-18-2019-18-45.dcf successfully.
# rename
$ mv dpu-11-18-2019-18-45.dcf resnet50.dcf

dletコマンドは“vitis-ai-caffe”(“vitis-ai-tensorflow”)内でのみ使える。
dcfファイルは後で使う。




2.量子化するために環境設定ファイルを実行


ファルダ内の環境設定ファイル(4_tf_quantize.sh)を下のように書き換えて実行。
TF_NETWORK_PATHを「tf_resnetv1_50_imagenet_224_224_6.97G」にした。

4_tf_quantize.sh

#!/bin/sh
TF_NETWORK_PATH=tf_resnetv1_50_imagenet_224_224_6.97G
vai_q_tensorflow quantize --input_frozen_graph ${TF_NETWORK_PATH}/float/resnet_v1_50_inference.pb \
                          --input_fn example_file.input_fn.calib_input \
                          --output_dir ${TF_NETWORK_PATH}/vai_q_output \
                          --input_nodes input \
                          --output_nodes resnet_v1_50/predictions/Reshape_1 \
                          --input_shapes ?,224,224,3 \
                          --calib_iter 100 \

4_tf_quantize.shを実行。

$ sh 4_tf_quantize.sh

終わったら、「tf_resnetv1_50_imagenet_224_224_6.97G/vai_q_output」にdeploy_model.pb, quantize_eval_model.pbができてる。

$ tree tf_resnetv1_50_imagenet_224_224_6.97G

>>>
|-- code
〜〜〜
|-- float
|   `-- resnet_v1_50_inference.pb
|-- quantized
|   |-- deploy_model.pb
|   `-- quantize_eval_model.pb
|-- readme.md
`-- vai_q_output
    |-- deploy_model.pb
    `-- quantize_eval_model.pb

3.FPGA用データの作成



さっき作ったdcfファイルを使う。
dcfファイルを指定したjsonファイル(custom.json)を作る。

{"target": "dpuv2", "dcf": "resnet50.dcf", "cpu_arch": "arm64"}

ZCU102用のshellスクリプト(6_tf_compile_for_v2.sh)を作って量子化を実行。
ちなみに「6_tf_compile_for_v3.sh」はALVEO用(ALVEO_TARGET=dpuv3e)なので今回は使わない。

6_tf_compile_for_v2.sh

#!/bin/sh
TARGET=custom
NET_NAME=resnet50
DEPLOY_MODEL_PATH=vai_q_output
TF_NETWORK_PATH=tf_resnetv1_50_imagenet_224_224_6.97G
ARCH=${TARGET}.json

vai_c_tensorflow --frozen_pb ${TF_NETWORK_PATH}/${DEPLOY_MODEL_PATH}/deploy_model.pb \
                 --arch ${ARCH} \
                 --output_dir ${TF_NETWORK_PATH}/vai_c_output_${TARGET}/ \
                 --net_name ${NET_NAME}


終わったら「tf_resnetv1_50_imagenet_224_224_6.97G/vai_c_output_ultra96v2_obb」ファルダに

・dpu_resnet50_0.elf
・resnet50_kernel_graph.gv

ができてる。

$ tree tf_resnetv1_50_imagenet_224_224_6.97G
>>>
~~~
|-- quantized
|   |-- deploy_model.pb
|   `-- quantize_eval_model.pb
|-- readme.md
|-- vai_c_output_ultra96v2_obb
|   |-- dpu_resnet50_0.elf
|   `-- resnet50_kernel_graph.gv
`-- vai_q_output
    |-- deploy_model.pb
    `-- quantize_eval_model.pb


これでtensorflowの学習済みモデルをFPGA用へ圧縮する量子化は完了。




5.量子化したファイルをultra96v2上で動かすアプリケーションの作成

アプリケーションの作成パートf:id:trafalbad:20200429093128p:plain


1.開発環境構築・ライブラリをSDカードへコピー


Xilinx提供のVitis AI runtimeを実行。

$ ./docker_run.sh xilinx/vitis-ai:runtime-1.0.0-cpu

Vitis AIをUltra96上で動かすには、一部ライブラリーが必要なので、runtime パッケージをSDカード(rootのsdcardフォルダ)にコピー

# パッケージのinstall(開発環境構築)
$ sudo cp -r /opt/vitis_ai/xilinx_vai_board_package Vitis-AI/workspace/
$ cd Vitis-AI/workspace/xilinx_vai_board_package/
$ sudo ./install.sh

# パッケージをSDカードにコピー
$ sudo cp -r /opt/vitis_ai/xilinx_vai_board_package /media/user/${SDCARD}/root/sdcard


2.FPGA用アプリケーションの作成


Vitis-AIの「Vitis-AI/mpsoc/vitis_ai_dnndk_samples/resnet50」ファルダの中身の
Makefile
・src/main.cc

をそのままコピー。

さっき作った「dpu_resnet50_0.elf」を「Vitis-AI/mpsoc/vitis_ai_dnndk_samples/resnet50」のmodelファルダの中に移動。

$ mkdir -p workspace/resnet50/model
$ cp -r mpsoc/vitis_ai_dnndk_samples/resnet50/* workspace/resnet50/
$ mv tf_resnetv1_50_imagenet_224_224_6.97G/vai_c_output_ultra96v2_obb/dpu_resnet50_0.elf workspace/resnet50/model/

ディレクトリ構造

$ tree resnet50
>>>
resnet50
 +Makefile
 +src
  +main.cc
 +model
  +dpu_resnet50_0.elf

src/main.ccの一部を変更。

void CPUCalcSoftmax(const float *data, size_t size, float *result) {
    assert(data && result);
    double sum = 0.0f;

    for (size_t i = 0; i < size; i++) {
        result[i] = exp(data[i]);
        sum += result[i];
    }

    for (size_t i = 0; i < size; i++) {
        result[i] /= sum;
    }
}


void runResnet50(DPUTask *taskResnet50) {
    assert(taskResnet50);

    /* Mean value for ResNet50 specified in Caffe prototxt */
    vector<string> kinds, images;

    /* Load all image names.*/
    ListImages(baseImagePath, images);
    if (images.size() == 0) {
        cerr << "\nError: No images existing under " << baseImagePath << endl;
        return;
    }

    /* Load all kinds words.*/
    LoadWords(baseImagePath + "words.txt", kinds);
    if (kinds.size() == 0) {
        cerr << "\nError: No words exist in file words.txt." << endl;
        return;
    }

    /* Get the output Tensor for Resnet50 Task  */
//    int8_t *outAddr = (int8_t *)dpuGetOutputTensorAddress(taskResnet50, OUTPUT_NODE);
    /* Get size of the output Tensor for Resnet50 Task  */
//    int size = dpuGetOutputTensorSize(taskResnet50, OUTPUT_NODE);
    /* Get channel count of the output Tensor for ResNet50 Task  */
    int channel = dpuGetOutputTensorChannel(taskResnet50, OUTPUT_NODE);
    /* Get scale of the output Tensor for Resnet50 Task  */
//    float out_scale = dpuGetOutputTensorScale(taskResnet50, OUTPUT_NODE);
//    float *softmax = new float[size];
    float *softmax = new float[channel];
    float *FCResult = new float[channel];

    for (auto &imageName : images) {
        cout << "\nLoad image : " << imageName << endl;
        /* Load image and Set image into DPU Task for ResNet50 */
        Mat image = imread(baseImagePath + imageName);
        dpuSetInputImage2(taskResnet50, INPUT_NODE, image);

        /* Launch RetNet50 Task */
        cout << "\nRun DPU Task for ResNet50 ..." << endl;
        dpuRunTask(taskResnet50);

        /* Get DPU execution time (in us) of DPU Task */
        long long timeProf = dpuGetTaskProfile(taskResnet50);
        cout << "  DPU Task Execution time: " << (timeProf * 1.0f) << "us\n";
        float prof = (RESNET50_WORKLOAD / timeProf) * 1000000.0f;
        cout << "  DPU Task Performance: " << prof << "GOPS\n";

        /* Calculate softmax on DPU and display TOP-5 classification results */
//        dpuRunSoftmax(outAddr, softmax, channel, size/channel, out_scale);
        /* Get FC result and convert from INT8 to FP32 format */
        dpuGetOutputTensorInHWCFP32(taskResnet50, OUTPUT_NODE, FCResult, channel);

        /* Calculate softmax on CPU and display TOP-5 classification results */
        CPUCalcSoftmax(FCResult, channel, softmax);

        TopK(softmax, channel, 5, kinds);

        /* Display the impage */
        cv::imshow("Classification of ResNet50", image);
        cv::waitKey(1);
    }

    delete[] softmax;
    delete[] FCResult;
}

あとはmakeを実行

$ make 


アプリケーションとして「resnet50」ができるので、それをSDカード(rootのsdcardフォルダ)にコピー。

$ cp resnet50 /media/user/${SDCARD}/root/sdcard



5.Ultra96v2ボードでの動作確認

いつもみたいにultra96v2の実機を起動後に、wifiに接続してログイン

$ ssh -X root@192.168.2.1

# ライブラリのinstall
$ cd xilinx_vai_board_package
$ ./install.sh
>>>
Begin to install Xilinx DNNDK ...
Requirement already satisfied: Edge-Vitis-AI==1.0 from file:///sdcard/pkgs/python/Edge_Vitis_AI-1.0-py2.py3-none-any.whl in /usr/lib/python3.5/site-packages (1.0)
Complete installation successfully.

判別用画像をrootの「/dataset/image500_640_480」の中に付属されてるラベルのword.txtとファイルと一緒に入れる。
そのままアプリケーションを実行

$ cd /sdcard
$ sudo chmod 755 resnet50
$ ./resnet50

チュートリアル通りなので、うまくいくと、本家と同じ画像が表示される。
f:id:trafalbad:20200429093221p:plain



ここでこの部分の作業が終わって、AIを動かす過程が一通り終了。
f:id:trafalbad:20200429093241p:plain




ここまででようやくultra96v2上で学習済みモデルを動かせた。

f:id:trafalbad:20200429094131j:plain


参考サイト



Ultra96V2向けVitis AI(2019.2)の組み立て方

Ubuntu 18.04にDockerをインストールする(+docker-composeも)

Vitis AI User Guide

ザイリンクス社「Vitis AI開発環境」を評価キット ZCU102 で動かしてみた

GitHub - Xilinx/Vitis-AI at v1.1

Vitis-AI 1.1 Flow for Avnet VITIS Platforms - Part 1

UbuntuでSDカードのEXT4とFAT32のパーテションの作り方

ubuntuでSDカードのパーティション作成のメモ

目次
1.ubuntumacにインストールする
2. ubuntuを起動、SDカードの確認
3.パーティションの作成
4.GUIでパーテイションの内訳を確認してみる


1.ubuntumacにインストールする

1.USBカードを差し込む。

$ diskutil list
$ diskutil unMountDisk /dev/disk2 (USB= /dev/disk2)
$ sudo dd if=ubuntu-18.4.2.iso of=/dev/disk2 bs=1m

終わったら、「無視」をクリック。


2.macをoption押しながら起動してUSBの選択肢を選択する。


3.あとは[ubuntu〜 install]をクリックしてwifi設定も含めてubuntuをinstall
ほとんどvirtualBoxと同じ方法でinstall。




2. ubuntuを起動、SDカードの確認

macの「diskutil list」と同じコマンドを打って、SDカードの内訳確認

$ dmesg | tail
>>>>
...
[ 6854.215650] sd 7:0:0:0: [sdc] Mode Sense: 0b 00 00 08
[ 6854.215653] sd 7:0:0:0: [sdc] Assuming drive cache: write through
[ 6854.215659]  sdc: sdc1


この例では/dev/sdcとして認識されてる。





3.パーティションの作成

fdiskコマンドの確認
・p:確認
・d:パーティション削除
・n:パーティション新規作成
・a:起動フラグの有効化

$ sudo fdisk /dev/sdc

fdisk (util-linux 2.31.1) へようこそ。
ここで設定した内容は、書き込みコマンドを実行するまでメモリのみに保持されます。
書き込みコマンドを使用する際は、注意して実行してください。


・pコマンドで確認

コマンド (m でヘルプ): p
ディスク /dev/sdc: 14.9 GiB, 16022241280 バイト, 31293440 セクタ
単位: セクタ (1 * 512 = 512 バイト)
セクタサイズ (論理 / 物理): 512 バイト / 512 バイト
I/O サイズ (最小 / 推奨): 512 バイト / 512 バイト
ディスクラベルのタイプ: dos
ディスク識別子: 0xf20f0c70

デバイス   起動 開始位置 最後から   セクタ サイズ Id タイプ
/dev/sdc1           2048  2936831  2934784   1.4G  c W95 FAT32 (LBA)
/dev/sdc2        2936832 14680063 11743232   5.6G 83 Linux


・dコマンドでパーティション削除

コマンド (m でヘルプ): d
パーティション番号 (1,2, 既定値 2): 2

パーティション 2 を削除しました。

コマンド (m でヘルプ): d
パーティション 1 を選択
パーティション 1 を削除しました。


・pで確認

コマンド (m でヘルプ): p
ディスク /dev/sdc: 14.9 GiB, 16022241280 バイト, 31293440 セクタ
単位: セクタ (1 * 512 = 512 バイト)
セクタサイズ (論理 / 物理): 512 バイト / 512 バイト
I/O サイズ (最小 / 推奨): 512 バイト / 512 バイト
ディスクラベルのタイプ: dos
ディスク識別子: 0xf20f0c70


・nで新規作成(n => p => 1 => 2048)

コマンド (m でヘルプ): n
パーティションタイプ
   p   基本パーティション (0 プライマリ, 0 拡張, 4 空き)
   e   拡張領域 (論理パーティションが入ります)
選択 (既定値 p): p
パーティション番号 (1-4, 既定値 1): 1
最初のセクタ (2048-31293439, 既定値 2048): 2048
最終セクタ, +セクタ番号 または +サイズ{K,M,G,T,P} (2048-31293439, 既定値 31293439): 2936831

新しいパーティション 1 をタイプ Linux、サイズ 1.4 GiB で作成しました。
パーティション #1 には vfat 署名が書き込まれています。

署名を削除しますか? [Y]es/[N]o: Yes

署名は write (書き込み)コマンドを実行すると消えてしまいます。


・aで起動フラグを有効にする

コマンド (m でヘルプ): a
パーティション 1 を選択
パーティション 1 の起動フラグを有効にしました。


・pで確認

コマンド (m でヘルプ): p
ディスク /dev/sdc: 14.9 GiB, 16022241280 バイト, 31293440 セクタ
単位: セクタ (1 * 512 = 512 バイト)
セクタサイズ (論理 / 物理): 512 バイト / 512 バイト
I/O サイズ (最小 / 推奨): 512 バイト / 512 バイト
ディスクラベルのタイプ: dos
ディスク識別子: 0xf20f0c70

デバイス   起動 開始位置 最後から  セクタ サイズ Id タイプ
/dev/sdc1  *        2048  2936831 2934784   1.4G 83 Linux

パーティション 1 にあるファイルシステム/RAIDの署名が完全に消去されます。


・nで2個めのパーティション作成(n => p => 2 => 2936832 => 31293439)

コマンド (m でヘルプ): n
パーティションタイプ
   p   基本パーティション (1 プライマリ, 0 拡張, 3 空き)
   e   拡張領域 (論理パーティションが入ります)
選択 (既定値 p): p
パーティション番号 (2-4, 既定値 2): 2
最初のセクタ (2936832-31293439, 既定値 2936832): 2936832
最終セクタ, +セクタ番号 または +サイズ{K,M,G,T,P} (2936832-31293439, 既定値 31293439): 31293439

新しいパーティション 2 をタイプ Linux、サイズ 13.5 GiB で作成しました。
パーティション #2 には ext4 署名が書き込まれています。

署名を削除しますか? [Y]es/[N]o: Yes

署名は write (書き込み)コマンドを実行すると消えてしまいます。


・pで最終確認 & 終了

コマンド (m でヘルプ): p
ディスク /dev/sdc: 14.9 GiB, 16022241280 バイト, 31293440 セクタ
単位: セクタ (1 * 512 = 512 バイト)
セクタサイズ (論理 / 物理): 512 バイト / 512 バイト
I/O サイズ (最小 / 推奨): 512 バイト / 512 バイト
ディスクラベルのタイプ: dos
ディスク識別子: 0xf20f0c70

デバイス   起動 開始位置 最後から   セクタ サイズ Id タイプ
/dev/sdc1  *        2048  2936831  2934784   1.4G 83 Linux
/dev/sdc2        2936832 31293439 28356608  13.5G 83 Linux

パーティション 1 にあるファイルシステム/RAIDの署名が完全に消去されます。
パーティション 2 にあるファイルシステム/RAIDの署名が完全に消去されます。

コマンド (m でヘルプ): ^C
終了してよろしいですか? y




4.GUIパーティションの内訳を確認してみる

deskopから、「desk」を選択。

SDカードを差し込むと「boot」と「root」の両方があるのが見える。
f:id:trafalbad:20200703212618j:plain

FAT32パーティション(boot)f:id:trafalbad:20200703212646j:plain


EXT4(linux用)パーティション(root)f:id:trafalbad:20200703212652j:plain



FAT32パーティション(boot)の中身f:id:trafalbad:20200703212700j:plain



EXT4パーティション(root)の中身f:id:trafalbad:20200703212708j:plain


sdカードをFAT32EXT4に分割できた。

ちなみにmacでは無理で、ubuntuをOSで入れないとできない



参考サイト



How to format SD card for SD boot

fdiskでフォーマットする

PytorchのTransformerでテキストからの音声生成(TextToSpeech)をやってみた【機械学習】

今回はTransformerを改造して、文章から音声を生成してみた。

俗に言う、エンコーダ・デコーダ形式のモデル。
プロセスが長くなりそうなので、要点だけ備忘録もかねてまとめようと思う。


目次
1.Transformer-TextToSpeechとは?
2.テキスト前処理
3.TransformerとPostNetの学習
4.テキストから音声を作成してみる



1.transformer-TextToSpeechとは?

今回作ったtransformerでの音声生成は、Google が発表したTactron2を改造した。

tactron2のEncoderとDecoderをTransformerに置き換えて、waveGlowをpostnetに置き換えたモデル。

Tactron2はそもそもGoogleのこれまでの音声生成プロジェクトで作られた、WaveNetと初代Tacotronのネットワークを組み合わせたもので、詳しくはサイトを見て欲しい。

tacotron2での音声生成処理の流れ
f:id:trafalbad:20200630215005p:plain




今回作ったTransformerの音声生成処理の流れ
f:id:trafalbad:20200630215350j:plain


つまり、

・tacotron2 => Transformer
・waveGlow => PostNet
に置き換えた。

単純なAttentionモデルのseq2seqを使った場合より、4倍くらい速く、精度もbetter。

特にtransformerのattentionの部分が正確な音声の作成にかなり有効っぽい。




2.テキスト前処理

今回データセットは「The LJ Speech Dataset」を使った。

英語での音声を想定したデータセット


前処理では、日本語でもローマ字(英語のスペル)に変換する。
今回は英語対応なので、すべての文字を英語のスペルに変換。


試しにデータセットの一部分の、LJ050-0278のデータをのぞいてみる。

textデータ(csv)

LJ050-0278|the recommendations we have here suggested would greatly advance the security of the office without any impairment of our fundamental liberties.|the recommendations we have here suggested would greatly advance the security of the office without any impairment of our fundamental liberties.


音声データ

LJ050-0278.mag.npy
LJ050-0278.pt.npy
LJ050-0278.wav


まず訓練前にこのテキストデータを前処理した。







3.TransformerとPostNetの学習

まず、Transformerを学習させてから、最後にPostNetを学習して、音声合成する。

Transformerはスペクトログラム(人間の音高知覚に調整した特徴量:メル周波数)を出力。

PostNetは音声の波形データを出力する。

Pytorchなのでmodelをprint()してみた

Transformer

model = Transformer_model()
print(model)

>>>>>
Model(
(encoder): Encoder(
(pos_emb): Embedding(1024, 256)
(pos_dropout): Dropout(p=0.1, inplace=False)
(encoder_prenet): EncoderPrenet(
  (embed): Embedding(149, 512, padding_idx=0)
  (conv1): Conv(
    (conv): Conv1d(512, 256, kernel_size=(5,), stride=(1,), padding=(2,))

 ~略~

  (1): Attention(
    (key): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=False)
    )
    (value): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=False)
    )
    (query): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=False)

 ~略~

(decoder): MelDecoder(
(pos_emb): Embedding(1024, 256)
(pos_dropout): Dropout(p=0.1, inplace=False)
(decoder_prenet): Prenet(
  (layer): Sequential(
    (fc1): Linear(
      (linear_layer): Linear(in_features=80, out_features=512, bias=True)
    )

 ~略~

  (pre_batchnorm): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (dropout1): Dropout(p=0.1, inplace=False)
  (dropout_list): ModuleList(
    (0): Dropout(p=0.1, inplace=False)
    (1): Dropout(p=0.1, inplace=False)
    (2): Dropout(p=0.1, inplace=False)
  )
)
)
)

PostNet

model = PostNet_model() 
print(model)
>>>>>>

ModelPostNet(
(pre_projection): Conv(
(conv): Conv1d(80, 256, kernel_size=(1,), stride=(1,))
)
(cbhg): CBHG(
(convbank_list): ModuleList(
  (0): Conv1d(256, 256, kernel_size=(1,), stride=(1,))
  (1): Conv1d(256, 256, kernel_size=(2,), stride=(1,), padding=(1,))
  (2): Conv1d(256, 256, kernel_size=(3,), stride=(1,), padding=(1,))
  (3): Conv1d(256, 256, kernel_size=(4,), stride=(1,), padding=(2,))
  (4): Conv1d(256, 256, kernel_size=(5,), stride=(1,), padding=(2,))
  (5): Conv1d(256, 256, kernel_size=(6,), stride=(1,), padding=(3,))
  (6): Conv1d(256, 256, kernel_size=(7,), stride=(1,), padding=(3,))
  (7): Conv1d(256, 256, kernel_size=(8,), stride=(1,), padding=(4,))

  ~略~

  (linears): ModuleList(
    (0): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
    (1): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
    (2): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
    (3): Linear(
      (linear_layer): Linear(in_features=256, out_features=256, bias=True)
    )
  )
)
(gru): GRU(256, 128, num_layers=2, batch_first=True, bidirectional=True)
)
(post_projection): Conv(
(conv): Conv1d(256, 1025, kernel_size=(1,), stride=(1,))
)
)

コードだと分かりにくいのでモデルの全体像。
f:id:trafalbad:20200630215037p:plain



学習はfine-tuneで行った。
学習率のlearning rateは0.001がベストプラクティス。
あと学習が進むにつれてlearning rateも下がる調整もかなり重要っぽい

def adjust_learning_rate(optimizer, step_num, warmup_step=4000):
    lr = hp.lr * warmup_step**0.5 * min(step_num * warmup_step**-1.5, step_num**-0.5)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr


loss曲線はtctron2からの引用だけど、同等もさくはそれ以上に、かなりいい具合に減少。
f:id:trafalbad:20200630215049p:plain




でかいデータセットでやるとメモリエラーも起こるし、時間もとんでもなくかかるからfine-tuneがおすすめ。

最近はもうfine-tuneの方がかなり効率いいので、end-to-endの学習は余程のことがないとしないんじゃないかな?





4.テキストから音声を作成してみる

映画ホームアローンのハリーのセリフを作成してみる。

f:id:trafalbad:20200630215151j:plain



text1 = "I never made it to sixth grade"
text2 = "it dose not look like you are gonna"

周波数(fs)は低くするとドスのきいた声になり、高いと早口言葉みたいになる。

max_lenはtextの長さに比例するのでそれぞれちょうどいい具合に調整した。

def calculate_melsp(x, n_fft=1024, hop_length=128, n_mels=128):
    stft = np.abs(librosa.stft(x, n_fft=n_fft, hop_length=hop_length))**2
    log_stft = librosa.power_to_db(stft)
    melsp = librosa.feature.melspectrogram(S=log_stft, n_mels=n_mels)
    return melsp

# display wave in plots
def show_wave(x):
    plt.plot(x)
    plt.show()
    
    
# display wave in heatmap
def show_melsp(melsp, fs):
    librosa.display.specshow(melsp, sr=fs)
    plt.colorbar()
    plt.show()

text1 = "I never made it to sixth grade, kid."

max_len = 500
fs = 25000

text1 = "I never made it to sixth grade, kid."
wav = create_audio_wave(text1, max_len)

print(wav.shape)  # (137225,)
show_wave(wav)

melsp = calculate_melsp(wav, n_fft=fs, hop_length=max_len, n_mels=max_len)
print(melsp.shape) # (500, 275)

show_melsp(melsp, fs)

# 実際にjupyter上で音声が聞ける
ipd.Audio(wav, rate=fs)

この投稿をInstagramで見る

開発用 "I never made it to sixth grade"

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -





text2 = "it dose not look like you are gonna"

text2 = "it dose not look like you are gonna"
wav = create_audio_wave(text2, max_len)

ipd.Audio(wav, rate=fs)

この投稿をInstagramで見る

開発用2 "it dose not look like you are gonna"

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -




AttentionはNLPだけじゃなく、いろんな精度向上に役立つっぽい。

ガチの音声生成をしたのははじめてだった。分類系より、生成系は面白い。



参考サイト



GitHub - soobinseo/Transformer-TTS: A Pytorch Implementation of "Neural Speech Synthesis with Transformer Network"

強化学習で「手押し井戸ポンプ」で水をくむ動作をArduino Unoに学習させる【AI・Hardware】

「手押し井戸ポンプ」は、昔から田舎で使われてる「井戸から手動で水をくむポンプ」のこと(Amazonでも売ってる)。

「手押し井戸ポンプ」の水をくむまでの大まかな動作は

押す(down)=>水をくむ(push pump)=>あげる(up)

の行動パターンを、シーソーのごとく繰り返すことで、井戸から水をくめる。

手押し井戸ポンプの動作原理f:id:trafalbad:20200620111544g:plain



今回はArduinoを使って、実際に近い状況を再現(シミュレーション)して、強化学習で水をくむ動作を学習させた。

深層学習の強化学習アルゴリズムはCartPoleでお馴染みの「DQN」を使う。特にkeras-rlとか強化学習ライブラリを使わずに、普通にDQNの学習コード書いた。

その要点をまとめて行こうと思う。

目次
1.Arduinoで「手押し井戸ポンプ」の状況の再現
2.Arduinoの構成
3.DQNで学習
4.学習結果
5.最後に




1.Arduinoで「手押し井戸ポンプ」の状況の再現


まず、Arduinoで実際の「手押し井戸ポンプ」の状況を実際に再現してシミュレーションした。

はじめにwebカメラでデフォルトの画像を読み込ませる。

それをニューラルネットワーク(NN)に読み込ませて、DQNの学習システムに沿って水をくむ行動パターンを学習させていく。

1.webカメラで画像を読み込み、NNに入れる

2.PC側でNNから行動を出力して、Arduinoに送る

3.Arduinoで行動を読み取り動作させる。ポンプで水をくめたら、PCに「1」の信号送る

4.Arduinoから「1」を受け取りPC側で報酬(Reward)として受け取る


f:id:trafalbad:20200620111631j:plain

#include <Servo.h>

Servo mServo;

int state[2];
int pin_number = 10;

void setup() {
  mServo.attach(9);
  mServo.write(10);
  delay(500);
  Serial.begin(9600);
  pinMode(pin_number, OUTPUT);
  state[0] = 0;
  state[1] = 1;
  digitalWrite(pin_number, LOW);
}

void loop() {
  if (Serial.available() > 0) {
    char c = Serial.read();
    if (c == 'p') {
      if (state[0] == 0) {
        Serial.print("0");
        mServo.write(90);
        delay(500);
        state[0] = 1;
      }
      else {
        Serial.print("0");
        mServo.write(10);
        delay(500);
        state[0] = 0;
        state[1] = 1;
        digitalWrite(10, LOW);
      }
    }
    else if (c == 'i') {
      if (state[0] == 1 && state[1] == 1) {
        Serial.print("1");
        delay(500);
        digitalWrite(10, HIGH);
        state[1] = 0;
      }
      else {
        Serial.print("0");
      }
    }
    else if (c == 'c') {
      state[0] = 0;
      state[1] = 1;
      digitalWrite(10, LOW);
      mServo.write(10);
      delay(500);
    }
  }
}



Arduinoとリアルの「手押し井戸ポンプ」の行動の対応関係


「手押し井戸ポンプ」から水をくむのに必要な行動パターンは3つあって、順番に行動する(行動パターンを覚える)必要がある。



水をくむまで行動パターン

Action1(a1) : 押す(down)
Action2(a2) : 水をくむ(Push pump)
Action(a3) : あげる(up)



リアルの「手押し井戸ポンプ」の行動パターンf:id:trafalbad:20200620112256j:plain



Arduinoのシミュレーションの行動パターンf:id:trafalbad:20200620111714j:plain







2.Arduinoの構成

Arduino(Uno)で用意したのは付属パーツは

サーボモータ

・USBカメラ(Mac用)

・抵抗

・LED



Arduino回路図f:id:trafalbad:20200620111744j:plain




実際にUSBカメラで読み込むArduinoの映像

この投稿をInstagramで見る

#arduino for development

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -








3.DQNで学習

アルゴリズムはDeep-QNetwork(DQN)。
DQNはモデルフリーなので報酬とか戦略の設定が大事だと思う。


このDQNのメカニズムを使ってNNに「手押し井戸ポンプ」から水を汲む行動パターンを覚えさせる。



DQNでの学習サイクル(Episode cycle)
f:id:trafalbad:20200620111911j:plain








Model



DQNではlossにHuber lossを使うのが一般的らしい。「Squeeze-and-Excitation Networks」を使って少し凝ったCNNを作った。

ef QFunction(inputs_):
    o = Conv2D(32, (3, 3), padding='same', kernel_initializer='random_uniform')(inputs_)
    o = channel_spatial_squeeze_excite(o)
    o = MaxPooling2D((2, 2))(o)
    o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
    o = channel_spatial_squeeze_excite(o)
    o = MaxPooling2D((2, 2))(o)
    o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
    o = channel_spatial_squeeze_excite(o)
    o = Flatten()(o)
    o = Dense(2, activation='linear')(o)
    model = Model(inputs=inputs_, outputs=o)
    model.compile(optimizer=Adam(lr=0.001), loss=huber_loss_mean, metrics=['acc'])
    return model

USBカメラから画像を読み込む部分。

cap = cv2.VideoCapture(0)

def capture(ndim=3):
    ret, frame = cap.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    xp = int(frame.shape[1]/2)
    yp = int(frame.shape[0]/2)
    d = 400
    resize = 128
    cv2.rectangle(gray, (xp-d, yp-d), (xp+d, yp+d), color=0, thickness=10)
    cv2.imshow('gray', gray)
    gray = cv2.resize(gray[yp-d:yp + d, xp-d:xp + d],(resize, resize))
    env = np.asarray(gray, dtype=np.float32)
    if ndim == 3:
        return env[np.newaxis, :, :] 
    else:
        return env[np.newaxis, np.newaxis, :, :] 

camera_image = capture(ndim=3)
cap.release()



Environment


1回のepisodeでStepを30回回して、水汲みに8回成功したらsuccessに+1する。それを全episode中に5回(success=5)繰り返せたら学習終了。

NUM_EPISODES = 50 # エピソード数
GAMMA = 1.0 

# 探索パラメータ
E_START = 1.0 # εの初期値
E_STOP = 0.01 # εの最終値
E_DECAY_RATE = 0.001 # εの減衰率

SUCCESS_REWARD = 8

# メモリパラメータ
MEMORY_SIZE = 10000 # 経験メモリのサイズ
MAX_STEPS = 30 # 最大ステップ数
BATCH_SIZE = MAX_STEPS

H = 128
W = 128
C = 1
inputs = Input([H, W, C])

class Environment:
    def __init__(self, inputs):
        # main-networkの作成
        self.main_qn = QFunction(inputs)

        # target-networkの作成
        self.target_qn = QFunction(inputs)
        # 経験メモリの作成
        self.memory = Memory(MEMORY_SIZE)

 # 行動価値関数
    def action_value_function(self, action_b, step_reward_b):
        target = 0
        if action_b == 1:
            target = GAMMA + step_reward_b/MAX_STEPS
        else:
            target = GAMMA + step_reward_b/MAX_STEPS
        return target

    def default(self):
       # Memoryの初期化
        self.memory = Memory(MEMORY_SIZE)

    def run(self):
        # エピソード数分のエピソードを繰り返す
        total_step = 0 # 総ステップ数
        success_count = 0 # 成功数
        for episode in range(1, NUM_EPISODES+1):
            step = 0 # ステップ数
            R = 0
            batch_size = 0
            ser.write(b"c") # Arduino側を初期状態に戻す

            # target-networkの更新
            self.target_qn.set_weights(self.main_qn.get_weights())
            
            # 1エピソードのループ
            for _ in range(1, MAX_STEPS+1):
                step += 1
                total_step += 1
                camera_state = capture(ndim=3)
                camera_state = camera_state.reshape(1, H, W, C)
                # εを減らす
                epsilon = E_STOP + (E_START - E_STOP)*np.exp(-E_DECAY_RATE*total_step)
          
                # ランダムな行動を選択
                if epsilon > np.random.rand():
                    action = int(np.random.randint(0, 2, 1))
                # 行動価値関数で行動を選択
                else:
                    action = np.argmax(self.main_qn.predict(camera_state))
                    pred = self.main_qn.predict(camera_state)
   
                # 行動に応じて状態と報酬を得る
                reward = action_step(action)
                R += reward
                self.memory.add((camera_state, action, reward, R)) 
                print('step', step, 'action', action, "R", R, 'reward', reward)
            if R >=SUCCESS_REWARD:
                success_count += 1
                
            # ニューラルネットワークの入力と出力の準備
            inputs = np.zeros((BATCH_SIZE, H, W, C)) # 入力(状態)
            targets = np.zeros((BATCH_SIZE, 2)) # 出力(行動ごとの価値)
            # バッチサイズ分の経験を取得
            minibatch = self.memory.sample(BATCH_SIZE)
            
            # ニューラルネットワークの入力と出力の生成
            for i, (state_b, action_b, reward_b, step_reward_b) in enumerate(minibatch):
                
                # 入力に状態を指定
                inputs[i] = state_b
                
                # 採った行動の価値を計算
                target = self.action_value_function(action_b, step_reward_b)
                # 出力に行動ごとの価値を指定
                targets[i] = self.main_qn.predict(state_b)
                targets[i][action_b] = target # 行動の価値

            # 行動価値関数の更新
            print('training....')
            self.main_qn.fit(inputs, targets, epochs=30, verbose=0)
            
            # エピソード完了時のログ表示
            print('エピソード: {}, ステップ数: {}, epsilon: {:.4f}'.format(episode, step, epsilon))
            self.default()
            # 5回成功で学習終了
            if success_count >= 5:
                break

WEbカメラから画像をNNに入れる => 行動価値関数を出力 => 行動に変換 => 報酬get


の簡単な流れを図にしてみた。
f:id:trafalbad:20200620111700j:plain




4.学習結果


epochの1~50までのパターン行動の成功回数の推移
f:id:trafalbad:20200620112004p:plain

順調にパターンを学習して、水をくむ回数が増えていってるのがわかる。


行動パターンを覚えさせるために、行動価値関数を次のように設計した。

# 行動価値関数
 def action_value_function(action_b, step_reward_b):
        target = 0
        if action_b == 1:
            target = GAMMA + step_reward_b/MAX_STEPS
        else:
            target = GAMMA + step_reward_b/MAX_STEPS
        return target

# 行動を返す関数
def action_step(actions):
    r = 0
    if actions==0:
        ser.write(b"p")
    else:
        ser.write(b"i")
    time.sleep(1.0)
    r = ser.read() 
    return int(r)

この設定だとepisodeが進むにつれて、それぞれの行動(0と1)の行動価値が最後は均衡してくる。

NNからoutputされる行動価値の推移f:id:trafalbad:20200620112018j:plain

もっと上手い設定方法はあるけど、とりあえずうまく学習できてたので自分としては上出来。



5.最後に

50回目までにStep1回で水汲み16回以上を達成できるようになって、無事クリア。DQN

・報酬の設定

・行動価値をどう決めるか 

・戦略

・経験の学習



とかが一番学習結果に影響した。逆にネットワークは別にそんな複雑なものである必要はなかった。

現に今回は白黒画像でやってもこれだけの成果が出たし。強化学習は戦略とか「どう学ばせるか」が肝だと思う。


ハードウェアで強化学習ができるのはいろいろと厨二病的な愉悦。

f:id:trafalbad:20200620112502j:plain

webカメラ+Arduino Uno+CNN画像分類(AI)でカギの開錠・施錠システムを作ってみた【hardware,機械学習】

今回はArduino機械学習(AI)を埋め込んで、webカメラから読み込んだストリーミング画像の画像分類結果からカギを解錠・施錠できるシステムを作ってみた。

機械学習(AI)をハードウェアで実際に取り込んで作ったのはVitis AI以来、今回がはじめて。

Arduino系の記事はたくさんあるけど、機械学習を使った例はあんまなかったので、今回はその作成過程をまとめてく。


目次
1.CNN+Arduinoの鍵の開錠・施錠システムの構造
2.データセットの作成・CNNで学習
3.CNN+Arduinoで金庫の鍵の開錠・施錠


1.CNN+Arduinoの鍵の開錠・施錠システムの構造

用意したもの

Arduino Uno
・ジャックDIP化キット
サーボモータ
・ACアダプター(5V)
・USBカメラ
・スイッチ


Arduinoの回路図の概略f:id:trafalbad:20200601112800j:plain



作業手順

1.webカメラで学習用画像集めてデータセットを作成する

2.CNNで学習

3.key.txtに開錠番号を記載

4.Webカメラで取り込んだストリーミング画像を推論
=> 推論結果とkey.txtと同じ番号ならArduinoでカギの解錠

今回工夫したのは、
Webカメラからデータセットを作れるようにした

Webカメラのストリーミング画像を推論してkey.txtの番号と一致したら解錠する仕組み

の2点。


実物f:id:trafalbad:20200601113319j:plain



2.データセットの作成・CNNで学習

学習から推論までは下のフローで進めてく。

Webカメラからデータセットを作成=>訓練=>Webカメラのストリーミング画像を推論

解錠システムのデータセット作成から推論までの流れは図にするとこんな感じ
f:id:trafalbad:20200601112958j:plain





Webカメラから学習データ収集


今回はWebカメラから直接データセットを作れるようにした。

訓練画像にしたい部分をwebカメラバウンディングボックス内に写す。
そのとき、PCキーボードの番号(0~5)を押せばその番号のimgファルダに画像が保存されるので、大量の画像を短時間で作れる。

今回は訓練画像は6種類。何も写ってない画像を0として含めた。

訓練画像1~5f:id:trafalbad:20200601113108j:plain

Webカメラで写しながら、キーボードの数字を押せばimgファルダの該当番号のファルダに保存されてく仕組み。

import cv2

n0 = 0
n1 = 0
n2 = 0
n3 = 0
n4 = 0
n5 = 0
cap = cv2.VideoCapture(0)
while True:
    ret, frame = cap.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    xp = int(frame.shape[1]/2)
    yp = int(frame.shape[0]/2)
    d = 400 # bbox size
    resize = 256
    cv2.rectangle(gray, (xp-d, yp-d), (xp+d, yp+d), color=0, thickness=10)
    cv2.imshow('gray', gray)
    gray = cv2.resize(gray[yp-d:yp + d, xp-d:xp + d],(resize, resize))
    c =cv2.waitKey(10) 
    if c == 48:#0
        cv2.imwrite('img/0/{0}.png'.format(n0), gray)
        n0 = n0 + 1
    elif c == 49:#1
        cv2.imwrite('img/1/{0}.png'.format(n1), gray)
        n1 = n1 + 1
    elif c == 50:#2
        cv2.imwrite('img/2/{0}.png'.format(n2), gray)
        n2 = n2 + 1
    elif c == 51:#3
        cv2.imwrite('img/3/{0}.png'.format(n3), gray)
        n3 = n3 + 1
    elif c == 52:#4
        cv2.imwrite('img/4/{0}.png'.format(n4), gray)
        n4 = n4 + 1
    elif c == 53:#5
        cv2.imwrite('img/5/{0}.png'.format(n5), gray)
        n5 = n5 + 1
    elif c == 27:#Esc
        break
cap.release()
$ tree img
img
├── 0
│   └── 0.png
├── 1
│   └── 1.png
├── 2
│   └── 2.png
├── 3
│   └── 3.png
├── 4
│   └── 4.png
└── 5
  └── 5.png




CNNで学習



MNIST並みに単純な画像なので、普通のCNNで訓練した。
Google colab上でGPUで訓練してから、学習済みモデルをローカルに落として使うことで、簡単に再訓練ができる構造。

def create_model(inputs_):
  o = Conv2D(32, (3, 3), padding='same', kernel_initializer='random_uniform')(inputs_)
  o = channel_spatial_squeeze_excite(o)
  o = MaxPooling2D((2, 2))(o)
  o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
  o = channel_spatial_squeeze_excite(o)
  o = MaxPooling2D((2, 2))(o)
  o = Conv2D(64, (3, 3), padding='same', kernel_initializer='random_uniform')(o)
  o = channel_spatial_squeeze_excite(o)
  #x = Dense(64, activation='relu')(x)
  o = Flatten()(o)
  o = Dense(6, activation='softmax')(o)
  model = Model(inputs=inputs_, outputs=o)
  model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
  return model


def make_train_dataset(img_dir):
    x = []
    y =[]
    label = 0
    for c in os.listdir(img_dir):
        print('class: {}, class id: {}'.format(c, label))
        d = os.path.join(img_dir, c) 
        try:       
            imgs = os.listdir(d)
        except:
            continue
        for i in [f for f in imgs if ('png' in f)]:
            x.append(cv2.imread(os.path.join(d, i), 0))
            y.append(label)            
        label += 1
    return np.array(x)/255, to_categorical(y)


X, Y = make_train_dataset('img')
H = X.shape[1]
W = X.shape[2]
X = np.reshape(X, (len(X), H, W, 1))
model = create_model(inputs)
model.summary()

try:
    model.fit(X, Y, batch_size=batch_size, epochs=num_ep, callbacks=callback,
                          validation_data=(valid_X, valid_y), shuffle=True)
finally:
    model.save('CNN_model.h5')




3.CNN+Arduinoで金庫の鍵の開錠・施錠

Webカメラで深層学習の画像分類できるかテスト



バウンディングボックスに写った画像を推論。

推論結果がkey.txtの番号と一致していたら開錠の指令をシリアル通信でArduinoに送る仕組み。
f:id:trafalbad:20200601112840j:plain




Webカメラで写したPC画面はこんな感んじ。
f:id:trafalbad:20200601113042p:plain

Security.py

import numpy as np
import os
import cv2
import keras
from keras.models import Model
from keras.layers import *
from train_modules.scse import channel_spatial_squeeze_excite
from CNN_model import create_model
import serial
import time

H = 256
W = 256
inputs = Input((H, W, 1))
model = create_model(inputs)
model.load_weights('CNN_model.h5')
key_file = 'key.txt'
cap = cv2.VideoCapture(0)

with serial.Serial('/dev/cu.usbmodem14301', timeout=0.1) as ser:

    while True:
        ret, frame = cap.read()            
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        xp = int(frame.shape[1]/2)
        yp = int(frame.shape[0]/2)
        d = 400 # bbox size
        resize = 256
        cv2.rectangle(gray, (xp-d, yp-d), (xp+d, yp+d), color=0, thickness=10)
        cv2.imshow('gray', gray)
        if cv2.waitKey(10) == 27:
            break
        a = ser.read()
        print(a)
        if a == b'1':
            print('pass')
            gray = cv2.resize(gray[yp-d:yp + d, xp-d:xp + d],(resize, resize))
            img = np.asarray(gray,dtype=np.float32)  
            img = np.reshape(img, (1, 256, 256, 1))
            img = img/255
            y_pred = model.predict(img)
            c = int(np.argmax(y_pred, axis=1))
            print(c)
            with open('key.txt', 'r') as f:
                b = int(f.read())
            if b==0:
                if c!=0:
                    time.sleep(5.0) # wait 5 second
                    ser.write(b"o")
                    with open(key_file, 'w') as f:
                        f.write(str(c))
                    print('close')
            else:
                if b==c:
                    time.sleep(5.0) # wait 5 second
                    ser.write(b"c")
                    with open(key_file, 'w') as f:
                        f.write('0')
                    print('open')
cap.release()




Aruduinoで画像分類して、サーボモータで鍵を開錠・施錠


シリアル通信で開錠の指令がきたらサーボモータを180度回転させて解錠。

番号が間違ってたら施錠したまま。

Security.ino

#include <Servo.h>

Servo myservo; // Servoオブジェクト宣言

void setup() {
  Serial.begin(9600);
  myservo.attach(9); //9番ピンでサーボモータを動かす
  pinMode(2, INPUT_PULLUP); // Inputモードでプルアップ抵抗を有効
  pinMode(LED_BUILTIN, OUTPUT);
  myservo.write(120); // angle
  digitalWrite(LED_BUILTIN, HIGH);
}
 
void loop(){
  static int flag=0;
  if(digitalRead(2)==LOW){
    flag=1;
  }
  else{
    if(flag==1){
      flag=0;
      Serial.write('1');
      delay(500);
    }
  }
  if(Serial.available()>0){
    char a = Serial.read();
    if(a=='o'){
      myservo.write(120); // angle
      digitalWrite(LED_BUILTIN, HIGH);
    }
    else if(a=='c'){
      myservo.write(20);  // angle
      digitalWrite(LED_BUILTIN, LOW);
    }
  }
}

PythonコードとArduinoコードが用意できたら、ラストはPC側でPythonコードを実行。

Arduino回路のスイッチを押せば推論が行われるので、推論結果がkey.txtと合ってれば解錠。

# PC側で実行
$ python3 Security.py
>>>
Using TensorFlow backend.
StreamExecutor device (0): Host, Default Version
b''
b''
b’1' # 推論実行
pass
5
close # 不一致なので施錠

b''
b''
b’1' # 推論実行
pass
3
open # key.txtの番号と推論結果が一致したので解錠
b''


下の画像は解錠・施錠を交互に行った動画。

この投稿をInstagramで見る

保存用動画

Tatsuya Hagiwara(@gosei_creater)がシェアした投稿 -

金庫がないのでわかりにくいけど、USBカメラに画像を写せば「ピピっ」と漫画みたいに反応した後、キーの解錠・施錠ができる。

セキュリティのために金庫の鍵の解錠施錠とかいろんな用途に使えそう。

f:id:trafalbad:20200601113617j:plain



Vitis AIでチュートリアルをやった以来、はじめてハードウェアで機械学習(AI)を組み混んでモノを作ってみた。
Arduinoは速度は置いといて、仕組みさえわかれば、かなり簡単に機械学習システムを組み込めた。


参考サイト


Arduinoを用いてサーボモータを制御する

Arduino UnoとPCをシリアル通信させる方法と参考コード集まとめ【hardware】

ArduinoとPCとの情報のやりとりはシリアル通信でする。このサイトを参考にした。

深層学習(DNN)にはシリアル通信は必須なので、やり方とデモもかねて動かしたコードをまとめていく。

f:id:trafalbad:20200526161410j:plain

目次
1.Arduino Unoのシリアル通信の設定
2.Arduino UnoからPCへのデータ送信
3.PCからArduino Unoへのデータ送信



1.Arduinoのシリアル通信の設定

Arduino for Macを起動して、[ツール]-[シリアルボード~]で表示されてるパスを取得して確認。
f:id:trafalbad:20200526161555p:plain

# シリアル通信できるか確認
$ sudo pip install pyserial
$ python3
>>>import serial
>>>ser = serial.Serial("/dev/cu.usbmodem14301")
>>>print(ser)
Serial<id=0x106a8f710, open=True>(port='/dev/cu.usbmodem14301', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)

>>>ser.close()
>>>exit()

シリアル通信ができてる。





2.Arduino UnoからPCへのデータ送信

f:id:trafalbad:20200526161623j:plain

0~9までの数字を一秒おきに送信


Arduinoから1秒毎にカウントされる値がPCに送られる。
通信中はArduinoのTXと書かれた通信用LEDが一秒毎に点滅する。

serial_send.ino

void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
}

void loop() {
 static int count=0;
 Serial.print(count);
 count ++;
 if(count == 10)
   count = 0;
 delay(1000);
}


serial_receive1.py

# -*- coding: utf-8 -*-
import serial
import time
wait_second = 5.0
total_number = 10
ser = serial.Serial("/dev/cu.usbmodem14301", timeout=0.5)
time.sleep(wait_second)
for i in range(total_number):
   line = ser.read()
   print(line)
ser.close()
# 実行
$ python3 serial_receive1.py
>>>
b'0'
b'1'
b'2'
~
b'5'

f:id:trafalbad:20200526161707g:plain



1~10までを読みこんで表示


ser.read()関数で一文字ごとに読み込んで表示。

serial_receive2.py

# -*- coding: utf-8 -*-
import serial
import time

wait_second = 5.0
total_number = 10 
with serial.Serial("/dev/cu.usbmodem14301") as ser:
   time.sleep(wait_second)
   for i in range(total_number):
       line = ser.read()
       print(line)
# 実行
$ python3 serial_receive2.py
>>>
b'0'
b'1'
b'2'
~
b'8'
b'9'
# ファルダ構造
$ tree Serial_receive
>>>
Serial_receive
├── serial_send
│   └── serial_send.ino
├── serial_receive1.py
└── serial_receive2.py




データロガー



データロガーは「Arduinoで計測したデータをPCに送って、PCでそのデータを保存する」こと。

試しに一秒おきに

0, 0, 0.00
1, 1, 2.00

と0, 1, 2, 3と増えるデータをArduinoからPCに送信して、テキストファイル(data.txt)に保存してみる。

serial_datalogger.ino

void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
}

void loop() {
 static int count=0;
 Serial.print(count);
 Serial.print(',');
 Serial.print(count);
 Serial.print(',');
 Serial.println(count*2.0);
 count ++;
 if(count == 10)
   count = 0;
 delay(1000);
}


serial_datalogger.py

# -*- coding: utf-8 -*-
import serial
import time
wait_second = 12.0

with serial.Serial('/dev/cu.usbmodem14301', timeout=0.5) as ser:
   time.sleep(wait_second)
   with open('data.txt', 'w') as f:
       for i in range(10):
           line = ser.readline()
           line = line.rstrip().decode('utf-8')
           print(line)
           f.write((line)+'\n')
# 実行
$ python3 serial_datalogger.py
>>>
0,0,0.00
1,1,2.00
2,2,4.008,8,16.00
9,9,18.00


データがうまく保存できないならtime.sleep()の時間を増やすのがコツ

# ファルダ構造
$ tree Serial_datalogger

Serial_datalogger
├── data.txt
├── serial_datalogger
│   └── serial_datalogger.ino
└── serial_datalogger.py




3.PCからArduino Unoへのデータ送信

今度は逆にPCからArduinoへ文字や数値を送信する。
これを使えばDNNで得た学習モデルを使って、Arduinoを含めたラズパイとかの電子工作を動かせる。

f:id:trafalbad:20200526161758j:plain

バイト文字の送信



void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
 pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
 if(Serial.available()>0){
   char c = Serial.read();
   if(c=='a')
         digitalWrite(LED_BUILTIN,HIGH);
   else if(c=='b')
         digitalWrite(LED_BUILTIN,LOW);
 }
}
# -*- coding: utf-8 -*-
import serial
import time

with serial.Serial('/dev/cu.usbmodem14301') as ser:
   time.sleep(5.0)
   for i in range(5):
       ser.write(b'a')
       time.sleep(1.0)
       ser.write(b'b')
       time.sleep(1.0)


受信した変数が’a’なら確認用LEDを点灯、’b’なら確認用LED消灯。

‘a’を送信し、1秒待ち、’b’を送信して1秒待つを5回繰り返す。

Arduinoの確認用LEDが5回点滅する。
f:id:trafalbad:20200526161831g:plain





数値の送信



0~255の数値を送ってLEDの明るさを変化させる(もっと大きい数値(500とか1500)も送信可能)。

デジタル9番ピンを使ってLEDの明るさを変える。
serial_receive_value.ino

int pin_number = 9;

void setup() {
 int signal_speed = 9600;
 Serial.begin(signal_speed);
 pinMode(pin_number, OUTPUT);
}

void loop() {
 if(Serial.available()>0){
   long int v = Serial.parseInt();
   analogWrite(pin_number, v);
 }
}


Serial.available()関数は浮動小数点値を受け取る。

LEDがだんだん明るくなる=>消える

を5回繰り返す(0~255までの値を送る)。
serial_receive_value.py

# -*- coding: utf-8 -*-
import serial
import time

wait_second = 0.01
with serial.Serial('/dev/cu.usbmodem14301') as ser:
   time.sleep(5.0)
   for i in range(5):
       for j in range(255):
           ser.write((str(j)+'\n').encode('utf-8'))
           time.sleep(wait_second)  # 0.01秒待つ
# 実行
$ python3 serial_send_value.py

f:id:trafalbad:20200526161855g:plain

$ tree serial_receive_value
>>>
serial_receive_value
├── serial_receive_value.ino
└── serial_receive_value.py

これでArduinoを使って深層学習ができる。


速度とかとはultra96v2と比較して劣るものの、Arduino Unoは動かすのはかなり簡単だった。


f:id:trafalbad:20200526163409j:plain