motoh's blog

主に趣味の電子工作やプログラミングについて書いていきます

M5Stack (ESP32) でエッジAIを試してみた

私のAI技術レベルは数年前ディープラーニングが流行りだした頃にMNISTを動かしてみたくらいのレベルなのですが、最近、「人気ブロガーからあげ先生のとにかく楽しいAI自作教室」 (Amazonリンク)という本を見つけて数年ぶりにAIについて学び直しています。この本の中で、ラズパイでディープラーニング(推論)を動かす方法が解説されており、推論だけならばESP32のようなもっと小型なデバイスでも動かせるのではないかと考え、たまたま持っていたM5StackシリーズのTimer Camera X(ESP32マイコンを搭載)でディープラーニングの推論を試してみました。

2023.6.5追記
本記事の内容を最新のM5Stack CoreS3に移植し、記事も刷新しました。 mzmlab.hatenablog.com

Timer Camera X

Timer Camera XはESP32ベースの高性能マイコン(CPU 240MHz、PSRAM 8MB)にカメラも搭載していますが、3000円前後というお手頃価格で購入できます。スペック的にはエッジAI向きなデバイスのように思いますが、ネット上では例が少なく、想像以上に試行錯誤が必要でした。今回、自分で用意したじゃんけんの画像データセットで学習させたモデルを使って推論させることができたので、その方法について記述したいと思います。Timer Camera Xの開発環境はArduinoを用いています(実は、Arduinoで凝った開発をしようとしたことが苦労の一因になっています。本当はESP-IDFを使ったほうがよいのかもしれませんが、使ったことがないため見送りました^^;)。試行錯誤しながらここまでたどり着くまで道のりは長かったのですが、ソースコードやデータセットGitHubで公開していますので、比較的容易に再現できるのではないかと思います。今回はTimer Camera Xを使いましたが、おそらく他のM5Stack(例えばM5Camera)でもほぼ同じようにできると思います。

[補足]
上記の本でも紹介されていますが、実はM5stickVというAI開発をターゲットとしたM5Stack製品があります。サイズ感はTimer Camera Xとあまり変わりませんが処理性能はさらに高くなっています(ESP32ではありません)。こちらを使えば余計な苦労をすることなくエッジAI開発を体験できるようです(ただ、その分価格も少しお高めです)。

開発環境

項目 名称 バージョン
M5Stack実機 Timer Camera X
開発用PC Windows 10 Home 21H2
IDE
(実機プログラム開発用)
Arduino
ライブラリ : Timer-CAM
1.8.19
0.0.2
AI開発環境
(学習モデル生成用)
Google Colaboratory
ライブラリ : Neural Network Library (nnabla)

1.33.1

【STEP0】 スケッチ例”web_cam”を動かす

本記事では、Arduinoのライブラリ”Timer-CAM”内のスケッチ例”web_cam”を動かせることを前提とし、このweb_camのソースコードを改造していきます。web_camを動かすまでの手順はこちらの記事等情報があるため、本記事での説明は割愛します。

lang-ship.com

【STEP1】 MNISTのサンプルを動かす

AI開発に慣れていればいきなり自分のデータセットで学習させてもよいと思いますが、私は初心者なのでまずはMNISTのサンプルプログラムを動かしてみるところから始めました。そのため、解説もその流れで進めたいと思います。

マイコンディープラーニングの推論を動かす方法はいくつかあるようですが、今回はSonyフレームワーク Neural Network Library (nnabla)を使う方法を選びました。nnablaは学習済みモデルをマイコンで実行できるCソースコードに変換する機能を備えています。

大まかな手順は次の通りです。以降、詳しく解説していきます。

No. 作業内容 作業環境
nnablaのMNISTサンプルプログラムを実行し、学習済みモデルのファイル(.nnp)を出力する。 Python
nnabla_cliコマンドでnnpファイルをCソースコードに変換する。 Python
変換で得られたCソースコードをスケッチweb_camに取り込む。 Arduino
Arduinoのライブラリとしてnnablaのランタイム(nnabla-c-runtime)を取り込む。 Arduino
カメラ画像から推論するように、スケッチのソースコードを改造する。 Arduino

STEP1 手順①〜②

Python環境でnnablaのMNISTサンプルプログラムを実行し、学習済みモデルのファイル(.nnp)を出力します。その後、nnabla_cliコマンドでnnpファイルをCソースコードに変換します。

この作業はPC上のPython環境で行ってもよいのですが、nnablaはGoogle Colaboratory (以下Colab)でも実行することができますので、今回はColabを利用しました。Colabなら環境構築に手間をかけることなく、GPUも簡単に使うことができます(ここではColabの使い方は解説しませんが、Googleのアカウントさえ持っていれば簡単に使い始めることができます)。

解説のコメントを付けたColabノートブックをGitHubで公開しますので、詳しい手順はそちらをご覧ください。このノートブックのコードを順番に実行していけば、MNISTデータセットにより学習済みモデルを生成した後、その学習済みモデルをCソースコードに変換することができます。

github.com

変換結果として以下のCソースコードが得られ、以降の手順で使用します。

  • Validation_inference.c
  • Validation_inference.h
  • Validation_parameters.c
  • Validation_parameters.h

[補足]
ColabでGPUを有効にしていないと、上記ノートブックを実行できません。もし有効になっていなければ、Colabのメニューの「ランタイムのタイプを変更」から設定してください。

STEP1 手順③

ここからはArduinoでの作業になります。変換で得られたCソースコードをスケッチweb_camに取り込みます。

スケッチ例web_camをコピーしてweb_cam_mnistにリネームしたフォルダを用意し、その中に下図のようにファイルを配置します。図中のstb_image_resize.hは、カメラ画像を前処理としてリサイズするときに使うライブラリで、こちらGitHubリポジトリで公開されています。

また、Validation_inference.cとValidation_parameters.cは次のように修正が必要です。

■Validation_inference.c 修正①
ランタイム(後述)のヘッダファイルをインクルードしている部分を修正します。nnabla-c-runtime.hはこの後手順④で自分で作ります。

//#include <nnablart/functions.h>
#include <nnabla-c-runtime.h>

■Validation_inference.c 修正②
130行目付近の関数宣言をコメントアウトします(そうしないと、ビルド時に二重定義だと言われる)。

//void *(*rt_variable_malloc_func)(size_t size) = malloc;
//void (*rt_variable_free_func)(void *ptr) = free;

//void *(*rt_malloc_func)(size_t size) = malloc;
//void (*rt_free_func)(void *ptr) = free;

■Validation_parameters.c 修正
全てのfloat型配列にconstを付けます。ビルド時にRAMがオーバーフローしたというエラーが出たため、配置先をFlashに変更するためにconstを付けました。

const float Validation_parameter11[] = {      /* その他全てのfloat型配列にconstを付ける */

STEP1 手順④

Cソースコードに変換した学習済みモデルをマイコンで動かすには、nnablaのランタイムが必要です。ランタイムもCソースコードなので、一緒にビルドすればよいのですが、ランタイムの各ソースコードは"nnabla-c-runtime/include/nnablart"内のヘッダファイルを#include <nnablart/network.h>のような形でインクルードしているため、"nnabla-c-runtime/include"をインクルードパスとして設定しておかないとコンパイル時に「network.hが見つかりません」と怒られてしまいます。普通、IDEにインクルードパスを設定するのは当たり前の作業ですが、Arduinoの場合、どこで設定すればよいのかいくら調べてもわかりませんでした。悩んだ末たどり着いた方法が、nnabla-c-runtimeをライブラリとして取り込むという方法です。以下、その方法について説明します。ちょっと面倒ですが、このライブラリ化の作業は一回行えばその後は不要です。(インクルードパスの設定方法さえ分かればライブラリ化は不要ですので、知っている方がいたら教えてくださいm(__)m )

(1) GitHubからnnabla-c-runtimeを入手する
適当なフォルダでgit cloneしてnnabla-c-runtimeを取得します。

> git clone https://github.com/sony/nnabla-c-runtime.git

(2) 取得したnnabla-c-runtimeをArduinoのlibrariesフォルダに配置する
Arduinoではlibraries/(ライブラリ名)/srcの場所にインクルードパスが通るようなので、下図のように配置することで元のnnabla-c-runtime/includeにパスが通るようにします。

以下、自分で作成するファイルの内容です。

■library.properties
Arduinoに独自ライブラリを認識させるためにlibrary.propertiesファイルを作成します。内容はこのような感じです。

name=nnabla-c-runtime
version=1.0.0
author=*** (適当な名前)
maintainer=*** (適当な名前)
sentence=Runtime of nnabla
paragraph=See more on ...
category=Other
url=
architectures=esp32

■nnabla-c-runtime.h

#ifndef H_NNABLA_C_RUNTIME_H__
#define H_NNABLA_C_RUNTIME_H__

#include <nnablart/functions.h>

#endif

うまくいけば、このようにメニューの「スケッチ」→「ライブラリをインクルード」に独自ライブラリが出てきます。

ここまで出来たら、一旦Arduinoでビルドしてみることをお勧めします。まだweb_camのソースコードは改造していないため、ライブラリはリンクされませんが、ライブラリのソースコードコンパイルされますので、ここまでの作業に問題があればコンパイルエラーが出ます。

STEP1 手順⑤

カメラ画像から推論するように、スケッチのソースコードを改造します。この部分に関しては、ほとんどこちらの記事を参考にさせていただきました。

qiita.com

変更した部分について解説します。全体はGitHubArduino/web_cam_mnistをご覧ください。

■web_cam_mnist.ino

カメラ画像をグレースケールのQQVGA (160x120)に変更します。

  //config.pixel_format = PIXFORMAT_JPEG;
  config.pixel_format = PIXFORMAT_GRAYSCALE;
  //config.frame_size = FRAMESIZE_UXGA;
  config.frame_size = FRAMESIZE_QQVGA;
  config.jpeg_quality = 10;
  //config.fb_count = 2;
  config.fb_count = 1;

■app_httpd.cpp

stream_handler()に推論のコードを追加します。

#include "Validation_inference.h"
#include "Validation_parameters.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize.h"

static esp_err_t stream_handler(httpd_req_t *req){
    ~省略~
    uint8_t resized_img[NNABLART_VALIDATION_INPUT0_SIZE];

    ~省略~
    _context = nnablart_validation_allocate_context(Validation_parameters);
    float *nn_input_buffer = nnablart_validation_input_buffer(_context, 0);
    
    while(true){    

        fb = esp_camera_fb_get();
        if (!fb) {
            Serial.println("Camera capture failed");
            res = ESP_FAIL;
        } else {

              // 28x28にリサイズ
              stbir_resize_uint8(fb->buf, 160, 120, 0, resized_img, 28, 28, 0, 1);
          
              // 白黒反転 (MNISTデータセットが黒背景に白文字であるため)
              for (int i = 0; i < NNABLART_VALIDATION_INPUT0_SIZE; i++) {
                  uint8_t p = ~(resized_img[i]);    
                  nn_input_buffer[i] = p;
              }

              // 推論
              int64_t infer_time = esp_timer_get_time();
              nnablart_validation_inference(_context);
              infer_time = (esp_timer_get_time() - infer_time) / 1000;

              // 推論結果をフェッチ
              float *probs = nnablart_validation_output_buffer(_context, 0);

              int top_class = 0;
              float top_probability = 0.0f;
              for (int classNo = 0; classNo < NNABLART_VALIDATION_OUTPUT0_SIZE; classNo++) {
                  if (top_probability < probs[classNo]) {
                      top_probability = probs[classNo];
                      top_class = classNo;
                  }
              }
                
              Serial.printf("Result %d   Inferrence-time %ums", top_class, (uint32_t)infer_time);  
            } 

    ~省略~

プログラムをコンパイルして書き込んだら、元のスケッチ(web_cam)を動かすときと同じ要領でWebブラウザで実機にアクセスし、Start Streamボタンでストリーミングを開始すると推論が動きます。推論の結果はシリアルモニタに表示されます。次の通り、若干正答率が悪い気がしますが、推論できている様子がわかります。

【STEP2】 自分で用意した画像データセットで学習したモデルで推論する

いよいよ、自分で用意したデータセットを使えるようにしていきます。手順は次の通りです。青文字の部分がSTEP1に追加となる手順です。

No. 作業内容 作業環境
CSVファイルでデータセットのリストを作成する(nnablaでデータセットを読み込むときに必要)。 Python
nnablaのMNISTサンプルプログラムを、MNISTデータセットではなく自分のデータセットを読み込めるように改造する。そのプログラムを実行し学習済みモデルのファイル(.nnp)を出力する。 Python
nnabla_cliコマンドでnnpファイルをCソースコードに変換する。 Python
変換で得られたCソースコードをスケッチweb_camに取り込む。 Arduino
Arduinoのライブラリとしてnnablaのランタイム(nnabla-c-runtime)を取り込む(STEP1ですでにライブラリ化していれば作業不要)。 Arduino
カメラ画像から推論するように、スケッチのソースコードを改造する。 Arduino

手順⓪~②はColabでの作業です。ColabノートブックをGitHubで公開します。ノートブックにコメントも記載していますが、より詳細な解説を以降に記載しますので合わせてご覧ください。

github.com

STEP2 手順⓪

nnablaにデータセットを読み込ませるためには、データセットのリストをCSVファイルで渡す必要があります。 CSVファイルは次のようなフォーマットで、学習用とテスト用に分けて作成します。

x:image,y
画像データのファイルパス, 分類(0,1,...,n)
画像データのファイルパス, 分類(0,1,...,n)
画像データのファイルパス, 分類(0,1,...,n)
・
・
・

今回、分類は次のようにしました。

0: 手が写っていない画像
1: グーの画像
2: チョキの画像
3: パーの画像

データセットGitHubのmy_datasetフォルダに、このような感じで分類毎のフォルダに分けて入っています。

今回、このようにフォルダ分けしたデータセットから、自動的にCSVファイルを作成するPythonスクリプトも作ってGitHubに入れました(make_dataset_csv.py)。データセットを8:2の割合で分けて、train.csvとtest.csvを作成してくれます。さらに、データセットの画像を28x28のグレースケールに変換してくれます(変換後の画像データはconverted_datasetsフォルダに保存されます)。

Colabノートブックでは、このデータセットスクリプトを利用してCSVファイルを作成しています。

STEP2 手順①

STEP1のColabノートブック上で使用したnnablaのMNISTサンプルプログラム(classification.py)を、MNISTデータセットの代わりにCSVファイル(train.csv,、test.csv)を読み込むように改造します。

改造の仕方はこちらの記事がとても参考になりますのでここでの解説は割愛しますが、改造済みのプログラム(classification_mydata.py)をGitHubに置いています。Colabノートブックでもこのプログラムをダウンロードして使用しています。

cedro3.com

STEP2 手順②~⑤

手順②以降はSTEP1とまったく同じです。STEP2の最終的なArduinoスケッチはGitHubArduino/web_cam_mydataに入れてあります。

以下、Timer Camera Xにスケッチを書き込んで実行した様子です。うまく推論できているのがわかりますが、影の位置も学習してしまったようで、影が違うところにくると正解率が著しく悪くなりました^^;

まとめ

M5Stack Timer Camera XというESP32ベースのデバイスを使ってディープラーニングの推論を動かしました。すぐできるだろうと軽い気持ちで始めましたが、意外と情報が少なく、まずnnablaが良さそうだという情報にたどり着くまでに相当時間がかかりました。その後もいろいろなところでハマり、やっぱりラズパイでやるべきなのか...と思ったりもしましたが、このブログに書くことをモチベーションにして何とか最後までがんばることができました...汗

今後の展望としては、こちらの記事でやったことを組み合わせて簡単なAIロボットを作れたら楽しそうだなと妄想しています。

mzmlab.hatenablog.com

参考書籍・サイト

文中で紹介したものですが、改めて参考書籍・サイトを記載します。

人気ブロガーからあげ先生のとにかく楽しいAI自作教室 (Amazon)

M5Stackは扱われていませんが、ディープラーニングを学び直したり今回の挑戦をしたりするきっかけになった本です。また、じゃんけんの判定をしたりGoogle Colaboratoryを使ったりしているのはこの本の影響を強く受けています^^;

マイコンでディープラーニングした話 on ESP32 - Qiita

M5StackでNeural Network Library (nnabla)のMNISTのサンプルを動かす方法について参考にさせていただきました。

SONY Neural Network Libraries データセットの読ませ方 | cedro-blog

nnablaに独自のデータセットを読み込ませる方法について参考にさせていただきました。