「変身ベルト」を機械学習で作ってみた

2021-08-03
日常で楽しむクラウドテクノロジー

清水 崇之

ご無沙汰しております、ソリューションアーキテクト / AWS 芸人しみず (@shimy_net) です。前回の記事から 1 年、時が経つのは早いですねぇ。今回の記事は夏休みの工作ということで「変身ベルト」を作ります。ちょうど手元に M5Stack Core2 for AWS がありまして、小さなボディのなかにディスプレイ、加速度センサー、温度センサー、LED など初めから盛りだくさんの機能がビルトインされていて便利そうなので使ってみようと思います。まずは出来上がった「変身ベルト」を見てください。

この記事のデモを無料でお試しいただけます »

毎月提供されるデベロッパー向けアップデート情報とともに、クレジットコードを受け取ることができます。 


1. アーキテクチャ

M5Stack Core2 には加速度センサーがついており、この加速度データを元に「変身ポーズ」のモーションを判定してベルトの LED をキラキラ光らせます。モーション判定の機械学習には TensorFlow を利用して、Amazon SageMaker の Notebook で開発します。作成されたモデルは M5Stack Core2 上の TensorFlow Lite for Microcontroller にデプロイします。これで M5Stack Core2 はスタンドアローンでモーションを判定から LED 点灯まで実行できます。この「変身ベルト」を身につけていれば、いつでもヒーローになれますね !


2. M5Stack Core2 の開発環境をセットアップ

まず、M5Stack Core2 のプログラムを開発するための環境を整えます。当初は Arduino IDE で開発していたのですが、途中で Visual Studio Code と PlatformIO を試してみたらとても便利だったので、この方法をご紹介します。Visual Studio Code と PlatformIO のセットアップは AWS IoT EduKit にまとまっていますのでドキュメントの手順にそって準備してください。

クリックすると拡大します


3. M5Stack Core2 のプロジェクトを作成

続いて M5Stack Core2 の雛形プロジェクトを作成します。PlatformIO ホーム画面を表示し [+ New Project] をクリックします。

クリックすると拡大します

ウィザードでは [Name] にプロジェクト名 "SampleProject" を入力し [Board]"M5Stack Core2" をプルダウンから選択して [Finish] をクリックします。

クリックすると拡大します

つぎに、M5Stack Core2 を動かすためのライブラリをプロジェクトに追加します。PlatformIO の左メニューから [Libraries] を選択して "M5Core2" で検索します。検索結果のなかから "M5Core2" ライブラリを選択します。

クリックすると拡大します

"M5Core2" ライブラリの詳細画面で [Add to Project] をクリックします。

クリックすると拡大します

ウィザードではライブラリの追加先のプロジェクト "Projects/SampleProject" をプルダウンから選択して [Add] をクリックします。

クリックすると拡大します

ライブラリを追加したプロジェクト SampleProject > src > main.cpp を表示し、 #include <M5Core2.h> と修正してライブラリをインクルードして完了です。

クリックすると拡大します


4. 加速度センサーのデータを収集する M5Stack Core2 プログラムを実装

それではプログラムを作っていきましょう。M5Stack Core2 は 6 軸慣性測定ユニット (IMU) を内蔵しておりデバイスに発生した加速度を計測できます。加速度データは I2C で接続された MPU6886 から取得できます。ここでは M5Stack Core2 に発生した加速度データを計測して SD カードに保存するアプリを実装します。

Timer を使って 20msごと (50Hz) に MPU6886 から加速度 (accX, accY, accZ), ジャイロ (gyroX, gyroY, gyroZ) の 計 6 種類のデータを取得して CSV ファイルに保存します。ここで、UCI Machine Learning Repository が提供する機械学習用のデータセットの構成を参考にしました。このデータセットは計 9 軸 (加速度: 6 軸 + ジャイロ: 3 軸) で構成されています。センサーから得られる加速度 (3 軸) には重力成分と体動成分が混ざっていますが、重力加速度は低周波であると仮定することでローバスフィルタ (カットオフ周波数 0.3Hz) で分離して 6 軸にしているようです。今回のプログラムでは、この方式を採用して 6 軸から 9 軸に軸を増やしています。

また M5Stack のボタンをクリックして 4 種類のモーション (0:Walking、1:Sitting、2:Standing、3:Henshing) を切り替えて保存できるようにしました。実際は、収集したデータは手作業でクレンジングしたので、この機能にあまり意味はなかったのですが・・・。

※参考 : UCI Human Activity Recognition Using Smartphones Data Set

main.cpp

#include <Arduino.h>
#include <M5Core2.h>
#include "filter.h"
#include <map>
#include <string>
#include <cstdio>
#include <ctime>
using namespace std;

// MPU
float accX, accY, accZ;
float filtered_accX, filtered_accY, filtered_accZ;
float gyroX, gyroY, gyroZ;
float pitch, roll, yaw;
float temp;

// Timer
volatile int interruptCounter;
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

// Low-pass Filter
float cutoff   = 0.3;  // Hz
float sampling = 0.02; // Sec
Filter fx(cutoff, sampling);
Filter fy(cutoff, sampling);
Filter fz(cutoff, sampling);

// File
File output_file;
bool isLogging = true;

// Activities
int activity = 0; // 0: 'WALKING', 1: 'SITTING', 2: 'STANDING', 3: 'HENSHING'
std::map<int, std::string> activityName;

// RTC
RTC_DateTypeDef RTC_DateStruct; // Data
RTC_TimeTypeDef RTC_TimeStruct; // Time

String zeroPadding(int num,int cnt){
  char tmp[256];
  char prm[5] = {'%','0',(char)(cnt+48),'d','\0'};
  sprintf(tmp,prm,num);
  return tmp;
}

// Setup
void setup(){
  // Activity Name
  activityName[0] = " WALKING ";
  activityName[1] = " SITTING ";
  activityName[2] = " STANDING";
  activityName[3] = " HENSHING";
  
  // M5 Init
  M5.begin();
  M5.IMU.Init();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(GREEN , BLACK);
  M5.Lcd.setTextSize(2);

//  時計合わせが必要なときに利用
//  RTC_TimeStruct.Hours   = 23;
//  RTC_TimeStruct.Minutes = 59;
//  RTC_TimeStruct.Seconds = 59;
//  M5.Rtc.SetTime(&RTC_TimeStruct);
//  RTC_DateStruct.WeekDay = 6;
//  RTC_DateStruct.Month = 8;
//  RTC_DateStruct.Date = 1;
//  RTC_DateStruct.Year = 2021;
//  M5.Rtc.SetDate(&RTC_DateStruct);

  // Timer
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 20000, true); // 20ms  
  timerAlarmEnable(timer);

  // File
  if (!SD.begin()) {
    M5.Lcd.println("ERROR: SD CARD");
    while (1) ;
  }
  M5.Rtc.GetDate(&RTC_DateStruct);
  M5.Rtc.GetTime(&RTC_TimeStruct);
  String datetime = zeroPadding(RTC_DateStruct.Year, 4) 
                  + zeroPadding(RTC_DateStruct.Month, 2) 
                  + zeroPadding(RTC_DateStruct.Date, 2) 
                  + "-"
                  + zeroPadding(RTC_TimeStruct.Hours, 2)
                  + zeroPadding(RTC_TimeStruct.Minutes, 2)
                  + zeroPadding(RTC_TimeStruct.Seconds, 2);
  String filepath = "/imu_data_"  + datetime + ".csv";
  output_file = SD.open(filepath.c_str(), FILE_WRITE);
  if (!output_file) {
      M5.Lcd.println("ERROR: OPEN FILE");
      while (1) ;
  }
  output_file.println("activity,accX,accY,accZ,filtered_accX,filtered_accY,filtered_accZ,gyroX,gyroY,gyroZ"); // Header
  M5.Lcd.setCursor(0,221);
  M5.Lcd.printf("%s",activityName[activity].c_str());
}

// Loop
void loop(){
  M5.update();
  if (interruptCounter > 0) {
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux); 

    M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);
    M5.IMU.getAccelData(&accX,&accY,&accZ);
    M5.IMU.getAhrsData(&pitch,&roll,&yaw);
    M5.IMU.getTempData(&temp);
    M5.Lcd.setCursor(0, 0);    
    M5.Rtc.GetDate(&RTC_DateStruct);
    M5.Rtc.GetTime(&RTC_TimeStruct);
    M5.Lcd.printf("%04d.%02d.%02d  %02d:%02d:%02d", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date, RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
    M5.Lcd.setCursor(0, 20);
    M5.Lcd.printf("%6.2f  %6.2f  %6.2f      ", gyroX, gyroY, gyroZ);
    M5.Lcd.setCursor(220, 42);
    M5.Lcd.print(" o/s");
    M5.Lcd.setCursor(0, 65);
    M5.Lcd.printf(" %5.2f   %5.2f   %5.2f   ", accX, accY, accZ);
    M5.Lcd.setCursor(0, 87);
    M5.Lcd.printf(" %5.2f   %5.2f   %5.2f   ", filtered_accX=fx.filter(accX), filtered_accY=fy.filter(accY), filtered_accZ=fz.filter(accZ));
    M5.Lcd.setCursor(220, 109);
    M5.Lcd.print(" G");
    M5.Lcd.setCursor(0, 132);
    M5.Lcd.printf(" %5.2f   %5.2f   %5.2f   ", pitch, roll, yaw);
    M5.Lcd.setCursor(220, 154);
    M5.Lcd.print(" degree");
    M5.Lcd.setCursor(0, 177);
    M5.Lcd.printf("Temperature : %.2f C", temp);

    // Logging
    if(isLogging){
      output_file.printf("%d,%.7e,%.7e,%.7e,%.7e,%.7e,%.7e,%.7e,%.7e,%.7e", activity, accX, accY, accZ, filtered_accX, filtered_accY, filtered_accZ, gyroX, gyroY, gyroZ);
      output_file.println();
    }
  }

  // Button A, change activities
  if (M5.BtnA.wasPressed()) {
    if(activity < activityName.size() - 1){
      activity++;
    } else {
      activity=0;
    }
    M5.Lcd.setCursor(0,221);
    M5.Lcd.printf("%s",activityName[activity].c_str());
   }

  // Button B
  if (M5.BtnB.wasPressed()) {
  }

  // Button C, stop logging
  if (M5.BtnC.wasPressed()) {
    if (isLogging) {
      isLogging=false;
      output_file.close();
      M5.Lcd.setCursor(180,221);
      M5.Lcd.printf("LOG STOPPED");
    }
  }
}

5. モーションデータを収集

上記プログラムを実行する M5Stack Core2 をヘソの前あたりにベルトで固定して、4 種類のモーション (0: Walking、1: Sitting、2: Standing、3: Henshing) の加速度データを収集します。それぞれのモーションごとに 10 分間ほど動きを継続して収集します。「変身ポーズ」のモーションを 10 分間もやり続けるのは肉体的にも精神的にもキツいです。ヒーローへの道は厳しいですね。

SD カードに出力された CSV ファイルの中身を見てみましょう。書き出されたデータの開始部分や終了部分はベルトを固定したりボタンを操作したりと、計測したいモーション以外のモーションが記録されますので手作業でトリミングしました。数値の羅列だけをみてトリミングするのは難しい ので、エクセルなどでグラフ化してトリミング範囲を検討すると良いでしょう。

imu_data_yyyyMMddhhmmss.csv

activity,accX,accY,accZ,filtered_accX,filtered_accY,filtered_accZ,gyroX,gyroY,gyroZ
0,-3.3105469e-01,4.9902344e-01,1.2788086e+00,-1.7048791e-05,2.5698915e-05,6.5856613e-05,1.1315918e+02,-2.0324707e+02,1.3262939e+02
0,-9.7656250e-04,5.8593750e-01,1.0732422e+00,-4.9911367e-05,1.0533417e-04,2.4787523e-04,-8.7707520e+01,-6.9335938e+01,4.9377441e+01
0,-5.5908203e-02,4.8608398e-01,1.0976562e+00,-1.0022672e-04,2.5979974e-04,5.9364061e-04,-9.8937988e+01,1.1218262e+02,2.2094727e+01
0,-3.7939453e-01,4.1894531e-01,7.3022461e-01,-1.8612391e-04,5.0480373e-04,1.1278975e-03,-3.5522461e+01,1.2005615e+02,2.2216797e+01
0,-3.5351562e-01,1.8188477e-01,7.6586914e-01,-3.2298174e-04,8.4244699e-04,1.8748904e-03,-4.2724609e+00,-1.7761230e+01,7.5683594e+00
0,-1.4648438e-01,1.4428711e-01,1.0473633e+00,-5.1425502e-04,1.2724729e-03,2.8709837e-03,-8.4960938e+01,-6.2866211e+01,2.9785156e+01
....

各モーションのデータ (トリミング済み) を表示したグラフはこちらの通りです。それぞれのグラフにはモーションの特徴が表れており機械学習で判定できそうな予感がします。

クリックすると拡大します


6. モーションデータの構成を変換

いま表示した CSV ファイル (生データ) は 20 ms ごとに (50Hz で) サンプリングしたセンサー値を 1 行ごとに記録しているだけです。このままでは機械学習のインプットデータとしては使いにくいため、構成を変換します。

先に紹介した UCI Human Activity Recognition Using Smartphones Data Set のデータセットの構成が使いやすそうですので参考にしました。

まず、元の CSV ファイルには 9 軸分のデータが含まれているので、軸ごとに 9 つのファイルに分割してファイルに書き出します。

つぎに、128 サンプル (2.56 秒) 分のセンサー値を 1 つのモーションとして扱います。この 128 サンプル (2.56 秒) をウィンドウサイズとして、ウィンドウを 64 サンプル (1.28 秒) ずつシフトしながらモーションデータを切り出し、この切り出した 1 つのモーションデータ(128 サンプル分)を 1 行分として書き出します。

この変換プログラムを適当に作りました。(壊滅的に汚くてゴメンナサイ)

クリックすると拡大します

converter.py

import csv

input = open('imu_data_yyyyMMddhhmmss.csv', 'r')
reader = csv.reader(input)
header = next(reader) # skip

output_acc_x = open('test/body_acc_x.txt', 'w')
writer_acc_x = csv.writer(output_acc_x, lineterminator='\n', delimiter=' ')
output_acc_y = open('test/body_acc_y.txt', 'w')
writer_acc_y = csv.writer(output_acc_y, lineterminator='\n', delimiter=' ')
output_acc_z = open('test/body_acc_z.txt', 'w')
writer_acc_z = csv.writer(output_acc_z, lineterminator='\n', delimiter=' ')
output_total_acc_x = open('test/total_acc_x.txt', 'w')
writer_total_acc_x = csv.writer(output_total_acc_x, lineterminator='\n', delimiter=' ')
output_total_acc_y = open('test/total_acc_y.txt', 'w')
writer_total_acc_y = csv.writer(output_total_acc_y, lineterminator='\n', delimiter=' ')
output_total_acc_z = open('test/total_acc_z.txt', 'w')
writer_total_acc_z = csv.writer(output_total_acc_z, lineterminator='\n', delimiter=' ')
output_gyro_x = open('test/body_gyro_x.txt', 'w')
writer_gyro_x = csv.writer(output_gyro_x, lineterminator='\n', delimiter=' ')
output_gyro_y = open('test/body_gyro_y.txt', 'w')
writer_gyro_y = csv.writer(output_gyro_y, lineterminator='\n', delimiter=' ')
output_gyro_z = open('test/body_gyro_z.txt', 'w')
writer_gyro_z = csv.writer(output_gyro_z, lineterminator='\n', delimiter=' ')
output_activity = open('test/activity.txt', 'w')
writer_activity = csv.writer(output_activity, lineterminator='\n', delimiter=' ')

accX1 = []
accX2 = []
accY1 = []
accY2 = []
accZ1 = []
accZ2 = []
totalAccX1 = []
totalAccX2 = []
totalAccY1 = []
totalAccY2 = []
totalAccZ1 = []
totalAccZ2 = []
gyroX1 = []
gyroX2 = []
gyroY1 = []
gyroY2 = []
gyroZ1 = []
gyroZ2 = []
activity1 = []
activity2 = []

def is_all_same(es):
    return all([e == es[0] for e in es[1:]]) if es else False

counter = 0
for row in reader:
    accX1.append(row[1])
    accY1.append(row[2])
    accZ1.append(row[3])
    totalAccX1.append(row[4])
    totalAccY1.append(row[5])
    totalAccZ1.append(row[6])
    gyroX1.append(row[7])
    gyroY1.append(row[8])
    gyroZ1.append(row[9])
    activity1.append(row[0])

    if counter >= 64:
        accX2.append(row[1])
        accY2.append(row[2])
        accZ2.append(row[3])
        totalAccX2.append(row[4])
        totalAccY2.append(row[5])
        totalAccZ2.append(row[6])
        gyroX2.append(row[7])
        gyroY2.append(row[8])
        gyroZ2.append(row[9])
        activity2.append(row[0])

    if len(accX1) == 128:
        if is_all_same(activity1):
            writer_acc_x.writerow(accX1)
            writer_acc_y.writerow(accY1)
            writer_acc_z.writerow(accZ1)
            writer_total_acc_x.writerow(totalAccX1)
            writer_total_acc_y.writerow(totalAccY1)
            writer_total_acc_z.writerow(totalAccZ1)
            writer_gyro_x.writerow(gyroX1)
            writer_gyro_y.writerow(gyroY1)
            writer_gyro_z.writerow(gyroZ1)
            writer_activity.writerow(activity1[0])
        accX1 = []
        accY1 = []
        accZ1 = []
        totalAccX1 = []
        totalAccY1 = []
        totalAccZ1 = []
        gyroX1 = []
        gyroY1 = []
        gyroZ1 = []
        activity1 = []

    if len(accX2) == 128:
        if is_all_same(activity2): 
            writer_acc_x.writerow(accX2)
            writer_acc_y.writerow(accY2)
            writer_acc_z.writerow(accZ2)
            writer_total_acc_x.writerow(totalAccX2)
            writer_total_acc_y.writerow(totalAccY2)
            writer_total_acc_z.writerow(totalAccZ2)
            writer_gyro_x.writerow(gyroX2)
            writer_gyro_y.writerow(gyroY2)
            writer_gyro_z.writerow(gyroZ2)
            writer_activity.writerow(activity2[0])
        accX2 = []
        accY2 = []
        accZ2 = []
        totalAccX2 = []
        totalAccY2 = []
        totalAccZ2 = []
        gyroX2 = []
        gyroY2 = []
        gyroZ2 = []

    counter = counter + 1

input.close()
output_acc_x.close()
...

変換プログラムを実行すると、128 サンプル分を1行として出力した 9 ファイル (9 軸分) と各行のモーションのラベルを出力した 1 ファイルの計 10 ファイルが生成されます。出力ファイル名も UCI のデータセットを参考にしました。

たとえば、 body_acc_x.txt (加速度 X 軸) の中身を見ると、1 行ごとにモーションの X 軸の加速度が 128 サンプル (2.56 秒分) で構成されています。そして 64 サンプル (1.28 秒) ずつシフトして切り出していったモーションが 2 行目、3 行目と続きます。

body_acc_x.txt (学習時には body_acc_x_train.txt にリネームして利用)

1.8505859e-01 1.8310547e-02 -1.6430664e-01 -4.5654297e-02 6.1523438e-02 -2.4707031e-01 -4.1894531e-01 3.3691406e-02 -2.3071289e-01 -9.3017578e-02 -2.8808594e-02 2.5512695e-01 -8.1298828e-02 -4.5654297e-02 5.7617188e-02 1.0278320e-01 -1.8505859e-01 1.9067383e-01 -1.2768555e-01 -2.7001953e-01 -6.7138672e-02 1.0961914e-01 -5.4370117e-01 -1.6552734e-01 -8.1054688e-02 -2.4487305e-01 -3.0273438e-02 1.1010742e-01 1.2500000e-01 8.9843750e-02 -4.3701172e-02 -1.6845703e-02 1.3134766e-01 -3.1884766e-01 1.6381836e-01 4.2968750e-02 -6.3964844e-02 -1.0864258e-01 -4.8583984e-02 -5.3808594e-01 -1.7407227e-01 -3.4423828e-01 6.5429688e-02 -2.2485352e-01 1.1962891e-01 5.6884766e-02 1.7822266e-01 -1.2329102e-01 1.4038086e-01 1.5185547e-01 -2.9174805e-01 1.4550781e-01 1.3012695e-01 -1.0083008e-01 -4.4677734e-02 1.3598633e-01 -1.0742188e-01 4.4189453e-02 -4.8779297e-01 1.1230469e-01 -1.7065430e-01 1.3549805e-01 -5.3466797e-02 -1.2719727e-01 9.3261719e-02 3.2910156e-01 -1.6577148e-01 1.0717773e-01 -1.0498047e-02 5.0048828e-02 -1.7016602e-01 -7.9345703e-02 -1.6992188e-01 -1.1279297e-01 -3.0175781e-01 2.6196289e-01 -4.3383789e-01 -3.7109375e-02 -2.1484375e-02 1.4843750e-01 5.2490234e-02 1.9042969e-02 2.5634766e-02 3.6621094e-01 -5.3466797e-02 1.8945312e-01 -4.1503906e-03 1.4648438e-03 -7.4218750e-02 -2.4926758e-01 -1.4477539e-01 -9.9121094e-02 -2.3510742e-01 -1.8261719e-01 -2.8173828e-01 -1.0595703e-01 -4.5898438e-02 1.4062500e-01 1.4038086e-01 -1.0498047e-02 -2.1679688e-01 1.1767578e-01 8.7646484e-02 -2.1411133e-01 3.2592773e-01 -5.0292969e-02 -1.7846680e-01 -2.0996094e-02 -1.3330078e-01 -4.7680664e-01 1.4941406e-01 -2.7172852e-01 -1.2207031e-03 -7.7636719e-02 1.5502930e-01 1.0522461e-01 -1.4404297e-02 -1.8139648e-01 1.7749023e-01 1.6210938e-01 -2.7197266e-01 9.5214844e-02 5.5908203e-02 -2.1801758e-01 -5.0048828e-02 -1.1938477e-01 -2.3730469e-01 3.9111328e-01
9.3261719e-02 3.2910156e-01 -1.6577148e-01 1.0717773e-01 -1.0498047e-02 5.0048828e-02 -1.7016602e-01 -7.9345703e-02 -1.6992188e-01 -1.1279297e-01 -3.0175781e-01 2.6196289e-01 -4.3383789e-01 -3.7109375e-02 -2.1484375e-02 1.4843750e-01 5.2490234e-02 1.9042969e-02 2.5634766e-02 3.6621094e-01 -5.3466797e-02 1.8945312e-01 -4.1503906e-03 1.4648438e-03 -7.4218750e-02 -2.4926758e-01 -1.4477539e-01 -9.9121094e-02 -2.3510742e-01 -1.8261719e-01 -2.8173828e-01 -1.0595703e-01 -4.5898438e-02 1.4062500e-01 1.4038086e-01 -1.0498047e-02 -2.1679688e-01 1.1767578e-01 8.7646484e-02 -2.1411133e-01 3.2592773e-01 -5.0292969e-02 -1.7846680e-01 -2.0996094e-02 -1.3330078e-01 -4.7680664e-01 1.4941406e-01 -2.7172852e-01 -1.2207031e-03 -7.7636719e-02 1.5502930e-01 1.0522461e-01 -1.4404297e-02 -1.8139648e-01 1.7749023e-01 1.6210938e-01 -2.7197266e-01 9.5214844e-02 5.5908203e-02 -2.1801758e-01 -5.0048828e-02 -1.1938477e-01 -2.3730469e-01 3.9111328e-01 -3.4790039e-01 1.1743164e-01 -2.3315430e-01 2.1728516e-02 1.4819336e-01 -1.4501953e-01 -6.2744141e-02 4.2309570e-01 -2.0507812e-02 -5.2490234e-02 -7.6904297e-02 1.1376953e-01 -2.7636719e-01 -1.7578125e-02 5.6152344e-02 1.8066406e-02 4.3945312e-02 -3.1567383e-01 9.0820312e-02 -2.8515625e-01 -4.2480469e-02 1.0913086e-01 -8.8623047e-02 2.3437500e-02 3.2568359e-01 -1.7431641e-01 1.9873047e-01 -1.7651367e-01 1.9921875e-01 -2.6147461e-01 -7.2265625e-02 -8.2763672e-02 -2.0727539e-01 -1.1474609e-01 -2.8027344e-01 9.8144531e-02 -1.7114258e-01 -1.5380859e-02 1.6870117e-01 1.4160156e-02 6.8359375e-03 5.7373047e-02 -1.8530273e-01 2.8857422e-01 -1.2402344e-01 1.7822266e-01 -2.9980469e-01 6.6894531e-02 -1.7260742e-01 1.1230469e-02 -5.8764648e-01 -2.4243164e-01 -3.8085938e-02 -2.0507812e-02 -1.0864258e-01 1.5478516e-01 6.8603516e-02 1.4135742e-01 -1.9287109e-02 2.3876953e-01 1.4501953e-01 8.3007812e-02 9.5458984e-02 -2.2778320e-01
-3.4790039e-01 1.1743164e-01 -2.3315430e-01 2.1728516e-02 1.4819336e-01 -1.4501953e-01 -6.2744141e-02 4.2309570e-01 -2.0507812e-02 -5.2490234e-02 -7.6904297e-02 1.1376953e-01 -2.7636719e-01 -1.7578125e-02 5.6152344e-02 1.8066406e-02 4.3945312e-02 -3.1567383e-01 9.0820312e-02 -2.8515625e-01 -4.2480469e-02 1.0913086e-01 -8.8623047e-02 2.3437500e-02 3.2568359e-01 -1.7431641e-01 1.9873047e-01 -1.7651367e-01 1.9921875e-01 -2.6147461e-01 -7.2265625e-02 -8.2763672e-02 -2.0727539e-01 -1.1474609e-01 -2.8027344e-01 9.8144531e-02 -1.7114258e-01 -1.5380859e-02 1.6870117e-01 1.4160156e-02 6.8359375e-03 5.7373047e-02 -1.8530273e-01 2.8857422e-01 -1.2402344e-01 1.7822266e-01 -2.9980469e-01 6.6894531e-02 -1.7260742e-01 1.1230469e-02 -5.8764648e-01 -2.4243164e-01 -3.8085938e-02 -2.0507812e-02 -1.0864258e-01 1.5478516e-01 6.8603516e-02 1.4135742e-01 -1.9287109e-02 2.3876953e-01 1.4501953e-01 8.3007812e-02 9.5458984e-02 -2.2778320e-01 -6.1279297e-02 -4.5410156e-02 1.4086914e-01 6.8359375e-03 -2.0849609e-01 -4.5849609e-01 2.1166992e-01 -2.0385742e-01 1.4160156e-02 6.9091797e-02 -8.2031250e-02 -4.8583984e-02 1.2451172e-01 -1.3818359e-01 4.9072266e-02 -4.7119141e-02 7.4707031e-02 -9.4482422e-02 -1.4648438e-01 -1.7065430e-01 3.9550781e-02 -5.5688477e-01 -1.9677734e-01 3.1738281e-02 -1.7846680e-01 -1.2744141e-01 2.0703125e-01 3.5888672e-02 5.1757812e-02 -9.2529297e-02 -6.0058594e-02 3.0175781e-01 -2.1337891e-01 1.7041016e-01 -8.4472656e-02 -2.1728516e-02 -7.4462891e-02 -5.5664062e-02 -6.3305664e-01 6.5185547e-02 -1.2451172e-01 1.2695312e-02 -1.4575195e-01 1.1035156e-01 1.3110352e-01 -1.3159180e-01 -2.5195312e-01 4.0917969e-01 7.9345703e-02 -9.4482422e-02 1.0302734e-01 9.7167969e-02 -2.4682617e-01 -1.7016602e-01 1.4160156e-02 1.3183594e-02 -8.1054688e-02 -5.0903320e-01 2.1899414e-01 -2.0776367e-01 4.8095703e-02 1.1889648e-01 4.2968750e-02 4.0039062e-02
-6.1279297e-02 -4.5410156e-02 1.4086914e-01 6.8359375e-03 -2.0849609e-01 -4.5849609e-01 2.1166992e-01 -2.0385742e-01 1.4160156e-02 6.9091797e-02 -8.2031250e-02 -4.8583984e-02 1.2451172e-01 -1.3818359e-01 4.9072266e-02 -4.7119141e-02 7.4707031e-02 -9.4482422e-02 -1.4648438e-01 -1.7065430e-01 3.9550781e-02 -5.5688477e-01 -1.9677734e-01 3.1738281e-02 -1.7846680e-01 -1.2744141e-01 2.0703125e-01 3.5888672e-02 5.1757812e-02 -9.2529297e-02 -6.0058594e-02 3.0175781e-01 -2.1337891e-01 1.7041016e-01 -8.4472656e-02 -2.1728516e-02 -7.4462891e-02 -5.5664062e-02 -6.3305664e-01 6.5185547e-02 -1.2451172e-01 1.2695312e-02 -1.4575195e-01 1.1035156e-01 1.3110352e-01 -1.3159180e-01 -2.5195312e-01 4.0917969e-01 7.9345703e-02 -9.4482422e-02 1.0302734e-01 9.7167969e-02 -2.4682617e-01 -1.7016602e-01 1.4160156e-02 1.3183594e-02 -8.1054688e-02 -5.0903320e-01 2.1899414e-01 -2.0776367e-01 4.8095703e-02 1.1889648e-01 4.2968750e-02 4.0039062e-02 9.4238281e-02 -1.8798828e-01 1.4892578e-01 -2.4365234e-01 1.5063477e-01 8.7890625e-03 -2.2851562e-01 -1.7163086e-01 3.1494141e-02 -5.4199219e-01 1.8896484e-01 -1.6357422e-01 4.5654297e-02 -1.0986328e-01 9.0576172e-02 2.0263672e-02 -8.3007812e-03 -1.5429688e-01 4.6679688e-01 1.1865234e-01 -1.5136719e-01 -1.4404297e-02 1.3452148e-01 -2.1582031e-01 -7.2509766e-02 -1.3427734e-02 -2.1801758e-01 3.6718750e-01 -4.6313477e-01 1.6284180e-01 -1.7285156e-01 1.4038086e-01 6.9580078e-02 2.3437500e-02 -9.5458984e-02 4.2065430e-01 -2.1728516e-02 5.1269531e-03 -9.1796875e-02 -1.5869141e-02 -1.1303711e-01 -9.6679688e-02 -9.2529297e-02 -1.4648438e-02 9.5214844e-03 -4.2675781e-01 6.7382812e-02 -1.4916992e-01 -5.2978516e-02 2.4658203e-02 -1.3671875e-01 7.8857422e-02 -3.2470703e-02 -1.9189453e-01 2.2949219e-01 1.5893555e-01 5.6884766e-02 -1.4477539e-01 1.4843750e-01 -4.8095703e-02 1.1181641e-01 -5.6762695e-01 -1.8188477e-01 -7.8125000e-03
9.4238281e-02 -1.8798828e-01 1.4892578e-01 -2.4365234e-01 1.5063477e-01 8.7890625e-03 -2.2851562e-01 -1.7163086e-01 3.1494141e-02 -5.4199219e-01 1.8896484e-01 -1.6357422e-01 4.5654297e-02 -1.0986328e-01 9.0576172e-02 2.0263672e-02 -8.3007812e-03 -1.5429688e-01 4.6679688e-01 1.1865234e-01 -1.5136719e-01 -1.4404297e-02 1.3452148e-01 -2.1582031e-01 -7.2509766e-02 -1.3427734e-02 -2.1801758e-01 3.6718750e-01 -4.6313477e-01 1.6284180e-01 -1.7285156e-01 1.4038086e-01 6.9580078e-02 2.3437500e-02 -9.5458984e-02 4.2065430e-01 -2.1728516e-02 5.1269531e-03 -9.1796875e-02 -1.5869141e-02 -1.1303711e-01 -9.6679688e-02 -9.2529297e-02 -1.4648438e-02 9.5214844e-03 -4.2675781e-01 6.7382812e-02 -1.4916992e-01 -5.2978516e-02 2.4658203e-02 -1.3671875e-01 7.8857422e-02 -3.2470703e-02 -1.9189453e-01 2.2949219e-01 1.5893555e-01 5.6884766e-02 -1.4477539e-01 1.4843750e-01 -4.8095703e-02 1.1181641e-01 -5.6762695e-01 -1.8188477e-01 -7.8125000e-03 -1.9433594e-01 -1.8310547e-01 1.8774414e-01 6.0546875e-02 6.4453125e-02 8.3740234e-02 -8.4960938e-02 2.0117188e-01 -1.2426758e-01 1.0644531e-01 -1.2158203e-01 -8.1054688e-02 -6.6894531e-02 5.9326172e-02 -3.3325195e-01 -2.3852539e-01 -1.1889648e-01 -9.8388672e-02 2.5146484e-02 -1.5869141e-02 5.3222656e-02 5.6396484e-02 -3.2226562e-02 4.6142578e-02 2.8466797e-01 -1.8823242e-01 1.2646484e-01 -2.8076172e-02 -3.0712891e-01 -1.3916016e-01 1.8017578e-01 -6.4355469e-01 -1.9799805e-01 -1.5380859e-02 -1.5454102e-01 -7.8125000e-02 1.7187500e-01 -2.4658203e-02 2.3681641e-02 -8.0322266e-02 -8.0566406e-03 2.0336914e-01 -8.8623047e-02 2.8295898e-01 -2.0190430e-01 -1.9970703e-01 -1.6674805e-01 1.4062500e-01 -4.7119141e-01 -2.7270508e-01 1.7407227e-01 -2.6464844e-01 -9.3750000e-02 1.1840820e-01 6.7382812e-02 2.9296875e-02 -6.6894531e-02 6.8359375e-03 2.9760742e-01 -1.5771484e-01 7.6904297e-02 -9.9121094e-02 -2.7685547e-01 -1.2500000e-01
...

activity.txt は他 9 ファイルの各行が何のモーションであるかを識別するためのラベル ( 0: Walking、1: Sitting、2: Standing、3: Henshing) となっています。

activity.txt (学習時には y_train.txt にリネームして利用)

0
0
0
...
1
1
...
2
2
...
3
3
...

7. Amazon SageMaker の開発環境を準備

ここまでの手順でデータセットが集まりましたので、これを学習データとして機械学習モデルを作成してモーションを判定します。

ここでは TensorFlow の環境として Amazon SageMaker で JupyterLab を利用します。マネージメントコンソールのガイドにそって Notebook インスタンスを作成します。

クリックすると拡大します

Notebook から "conda_amazonei_tensorflow2_p36" を選択します。

クリックすると拡大します

後述の TensorFlow Lite コンバータがデフォルトインストールの TensorFlow バージョン 2.1.3 では動作しなかったため 2.5.0 にバージョンアップしています。ちなみに、この記事では TensorFlow 2.5.0, Python 3.6.13 を利用しています。


8. 収集したデータを学習して TensorFlow モデルを作成

さきほど変換したデータセットを使って機械学習モデルを学習させます。人の動きのように時系列データの判定には LSTM (Long Short-Term Memory) が有効であろうと考えて TensorFlow Keras LSTM でモデルを組みました。TensorFlow の学習プログラムは以下のとおりです。

henshing_train.py

import numpy as np 
import pandas as pd
import tensorflow as tf
from keras.models import Sequential
from keras.models import load_model
from keras.layers import LSTM
from keras.layers.core import Dense, Dropout
from util import load_data
from util import confusion_matrix

# Load Dataset
records_train, records_test, labels_train, labels_test = load_data()
timesteps = len(records_train[0])
input_dim = len(records_train[0][0])
n_classes = len(set([tuple(category) for category in labels_train]))
print(timesteps, input_dim, n_classes)

# Create Model / LSTM
model = tf.keras.Sequential([
    tf.keras.layers.Bidirectional(
        tf.keras.layers.LSTM(32),
        input_shape=(timesteps, input_dim)),
    tf.keras.layers.Dense(n_classes, activation="sigmoid")
])
model.compile(
    loss='categorical_crossentropy', 
    optimizer='rmsprop', 
    metrics=['accuracy'])

# Train
model.fit(
    records_train,
    labels_train,
    batch_size=16,
    validation_data=(records_test, labels_test),
    epochs=200
)

# Save Model
model.save('henshing_model.h5')

# Result
print(confusion_matrix(labels_test, model.predict(records_test)))

util.py ではデータセットの読み込みをしています。先述のとおり UCI HAR Dataset も読み込みすいように構成を揃えているので、必要に応じて適宜修正してください。

util.py

import pandas as pd
import numpy as np

#DATADIR = 'UCI HAR Dataset'
DATADIR = 'M5Stack Dataset'

AXES = [
    "body_acc_x",
    "body_acc_y",
    "body_acc_z",
    "body_gyro_x",
    "body_gyro_y",
    "body_gyro_z",
    "total_acc_x",
    "total_acc_y",
    "total_acc_z"
]

# UCI HAR Dataset を読み込むときは適宜修正
LABELS = {
    0: 'WALKING',
    1: 'SITTING',
    2: 'STANDING',
    3: 'HENSHING',
}

def get_records(mode):
    records = []
    for ax in AXES:
        records.append(
            pd.read_csv(f'{DATADIR}/{mode}/Inertial Signals/{ax}_{mode}.txt', delim_whitespace=True, header=None).values
        ) 
    return np.transpose(records, (1, 2, 0))

def get_labels(mode):
    label = pd.read_csv(f'{DATADIR}/{mode}/y_{mode}.txt', delim_whitespace=True, header=None)[0]
    return pd.get_dummies(label).values

def load_data():
    return get_records('train'), get_records('test'), get_labels('train'), get_labels('test')

def confusion_matrix(true, pred):
    true = pd.Series([LABELS[i] for i in np.argmax(true, axis=1)])
    pred = pd.Series([LABELS[i] for i in np.argmax(pred, axis=1)])
    return pd.crosstab(true, pred, rownames=['True'], colnames=['Pred'])

作業中のため大量のファイルで溢れていてお恥ずかしいですが、Notebook はだいたいこんな感じで開発しています。

クリックすると拡大します


9. テストデータを使ってモーションを判定

収集したデータを使って学習・テストした結果は以下の通りです。変身ポーズのモーション (3: Henshing) のテスト 6 件のうち 5 件が正しく判定されています。それ以外のモーション (0: Walking、1: Sitting、2: Standing) についても判定できていることがわかります。

変身ベルトまでは作らなくていいよ (モーション判定の実験をするだけ) ということであれば、先に紹介した UCI Human Activity Recognition Using Smartphones Data Set を使うと良いでしょう。こちらの UCI HAR Dataset のツリー構成を参考にしていますのでデータセットのルートディレクトリを切り替えればどちらも読み込めます。

クリックすると拡大します


10. TensorFlow モデルを TensorFlow lite for Microcontrollers モデルに変換

ここまでの手順で TensorFlow モデル "henshing_model.h5" を生成することができました。しかし、このままのフォーマットでは M5Stack Core2 にデプロイできないため、変換しなくてはなりません。

そこで、まずは TensorFlow Lite コンバータを使って TensorFlow Lite モデル "henshing_model.tflite" に変換します。

tf2tflite.py

model = load_model("henshing_model.h5")
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
open("henshing_model.tflite", "wb").write(tflite_model)

さらに xxd コマンドを使って TensorFlow Lite モデル "henshing_model.tflite" を C バイト配列 "henshing_model.cc" に変換します。

$ xxd -i henshing_model.tflite > henshing_model.cc 

出力された "henshing_model.cc" をエディタで開いて以下のように修正します。ついでにヘッダー "henshing_model.h" も作成しておきましょう。

github にある TensorFlow Lite for Microcontroller のサンプル「Hello World」に含まれる “model.h” と“model.cc” を参考にして修正すると良いでしょう。これでマイクロコントローラである M5Stack Core2 で使えるようになります。

henshing_model.cc

#include "henshing_model.h"
alignas(8) const unsigned char g_model[] = {
  0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x14, 0x00, 0x20, 0x00,
  0x04, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x14, 0x00, 0x00, 0x00,
  0x18, 0x00, 0x1c, 0x00, 0x14, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
  ...
}
const int g_model_len = 1186508;

henshing_model.h

#ifndef TENSORFLOW_LITE_MICRO_HENSHING_MODEL_H_
#define TENSORFLOW_LITE_MICRO_HENSHING_MODEL_H_
extern const unsigned char g_model[];
extern const int g_model_len;
#endif

11. M5Stack Core2 で TensorFlow Lite for Microcontroller を動かすための準備

M5Stack Core2 の雛形プロジェクトに TensorFlow Lite for Microcontroller のライブラリを取り込みます。

まず、Github から TensorFlow のリポジトリをダウンロードして TensorFlow Lite フォルダから ESP32 のプロジェクトの何かひとつ (Hello World) を生成します。以下のコマンドを実行すると "tensorflow/lite/micro/tools/make/gen/esp_xtensa-esp32_default/prj/hello_world" にサンプルプロジェクトが生成されます。

$ sudo make -f tensorflow/lite/micro/tools/make/Makefile TARGET=esp generate_hello_world_esp_project

生成されたサンプルプロジェクトのなかに tfmicro ライブラリが内包されています。 tfmicro ライブラリのフォルダ "tensorflow/lite/micro/tools/make/gen/esp_xtensa-esp32_default/prj/hello_world/esp-idf/components/tfmicro" をコピーして M5Stack Core2 プロジェクトの lib に配置します。

ただし、そのままのディレクトリ構成だとうまくパスが通らないのでこちらのように配置しました。さらに数箇所でビルドエラーが発生するので適宜修正します。このあたりの修正は「TensorFlow, Meet The ESP32」や「M5Stack で TensorFlow Lite for MCU Hello world」の記事を参考にすると良いでしょう。

クリックすると拡大します


12. 変身モーションを判定する M5Stack Core2 プログラムを実装

やっと全ての準備が整いましたので (2 週間くらいかかった・・・)、いよいよ変身ベルトを作っていきましょう。

以下のプログラムは M5Stack Core2 の変身ベルトプログラムです。IMU から 20ms ごとのセンサーデータ 128 サンプル分 (2.56 秒分) を 1 つのモーションとして TensorFlow Lite for Microcontroller で変身モーション (3: HENSING) を判定しています。変身モーションを検知したら M5Stack Core2 に接続した LED テープ が点灯します。

main.cpp

#include <math.h>
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "henshing_model.h"
#include <M5Core2.h>
#include <cstdio>
#include <ctime>
#include "filter.h"
#include "FastLED.h"

#define LEDS_PIN 25
#define LEDS_NUM 10
#define LEDSTAPE_PIN 32
#define LEDSTAPE_NUM 72
CRGB ledsBuff[LEDS_NUM];
CRGB ledsTapeBuff[LEDSTAPE_NUM];

// MPU
float accX, accY, accZ;
float filtered_accX, filtered_accY, filtered_accZ;
float gyroX, gyroY, gyroZ;

// Filter
const float cutoff   = 0.3;
const float sampling = 0.02;
Filter fx(cutoff, sampling);
Filter fy(cutoff, sampling);
Filter fz(cutoff, sampling);

// Acc data
std::vector<std::vector<float>> input_tensor; 

// Timer
volatile int interruptCounter;
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
}
volatile int invoke_counter;

constexpr int tensor_pool_size = 60 * 1024;
uint8_t tensor_pool[tensor_pool_size];
const tflite::Model* model;
tflite::MicroInterpreter* interpreter;
TfLiteTensor* input;
TfLiteTensor* output;

// Setup
void setup() {
  M5.begin();
  M5.IMU.Init();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(GREEN , BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.println("Start Now");

  // Timer
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 20000, true); // 20ms
  timerAlarmEnable(timer);

  // LED
  FastLED.addLeds<SK6812, LEDSTAPE_PIN>(ledsTapeBuff, LEDSTAPE_NUM);
  for (int i = 0; i < LEDSTAPE_NUM; i++){
      ledsTapeBuff[i].setRGB(0, 0, 0);
  }
  FastLED.show();

  // Load Model
  model = tflite::GetModel(g_model);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    M5.Lcd.println("Model Error");
  }else{
    M5.Lcd.println("Model Loaded");
  }

  // MicroInterpreter
  static tflite::AllOpsResolver resolver;
  static tflite::ErrorReporter* error_reporter;
  static tflite::MicroErrorReporter micro_error;
  error_reporter = &micro_error;
  static tflite::MicroInterpreter static_interpreter(
      model, resolver, tensor_pool, tensor_pool_size, error_reporter
  );
  interpreter = &static_interpreter;
  TfLiteStatus allocState = interpreter->AllocateTensors();
  if (allocState != kTfLiteOk) {
      M5.Lcd.println("AllocateTensors Error");
      return;
  }
  input = interpreter->input(0);
  output = interpreter->output(0);
}

// Loop
void loop() {
  M5.update();
  if (interruptCounter > 0) {
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux); 

    M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);
    M5.IMU.getAccelData(&accX,&accY,&accZ);
    filtered_accX=fx.filter(accX);
    filtered_accY=fy.filter(accY);
    filtered_accZ=fz.filter(accZ);

    std::vector<float> temp_vectore{ accX, accY, accZ, filtered_accX, filtered_accY, filtered_accZ, gyroX, gyroY, gyroZ};
    input_tensor.push_back(temp_vectore);
    M5.Lcd.setCursor(0, 0);    
    M5.Lcd.println(accX);      

    if (input_tensor.size() == 128) {
      // Fill input
      for(int h = 0; h < 128; ++h) {
        for(int w = 0; w < 9; ++w) {
            auto i = h * 9  + w;
            input->data.f[i] = input_tensor[h][w];
        }
      }

      // Invoke
      if (kTfLiteOk != interpreter->Invoke()) {
        M5.Lcd.println("Invoke Error");
      }else{
        invoke_counter = invoke_counter + 1;
        M5.Lcd.setCursor(0, 60);    
        M5.Lcd.println("Invoke OK");
        M5.Lcd.println("++++++++++++");
        M5.Lcd.println(invoke_counter);      
        M5.Lcd.printf("WALKING:  %5.2f", output->data.f[0]); //WALKING
        M5.Lcd.println();
        M5.Lcd.printf("SITTING:  %5.2f", output->data.f[1]); //SITTING
        M5.Lcd.println();
        M5.Lcd.printf("STANDING: %5.2f", output->data.f[2]); //STANDING
        M5.Lcd.println();
        M5.Lcd.printf("HENSHING: %5.2f", output->data.f[3]); //HENSHIN

        // HENSHING モーション判定でLEDを点灯
        if(output->data.f[3] > output->data.f[0] 
        && output->data.f[3] > output->data.f[1] 
        && output->data.f[3] > output->data.f[2]){
          for (int k = 0; k < 30; k++){
            for (int i = 0; i < LEDSTAPE_NUM/4; i++){
              ledsTapeBuff[i*4+0].setRGB(0, 20, 0);
              ledsTapeBuff[i*4+1].setRGB(0, 20, 0);
              ledsTapeBuff[i*4+2].setRGB(0, 0, 0);
              ledsTapeBuff[i*4+3].setRGB(0, 0, 0);
            }
            FastLED.show(); 
            delay(100);
            for (int i = 0; i < LEDSTAPE_NUM/4; i++){
              ledsTapeBuff[i*4+0].setRGB(0, 0, 0);
              ledsTapeBuff[i*4+1].setRGB(0, 20, 0);
              ledsTapeBuff[i*4+2].setRGB(0, 20, 0);
              ledsTapeBuff[i*4+3].setRGB(0, 0, 0);
            }
            FastLED.show(); 
            delay(100);
            for (int i = 0; i < LEDSTAPE_NUM/4; i++){
              ledsTapeBuff[i*4+0].setRGB(0, 0, 0);
              ledsTapeBuff[i*4+1].setRGB(0, 0, 0);
              ledsTapeBuff[i*4+2].setRGB(0, 20, 0);
              ledsTapeBuff[i*4+3].setRGB(0, 20, 0);
            }
            FastLED.show(); 
            delay(100);
          } 
          for (int i = 0; i < LEDSTAPE_NUM; i++){
              ledsTapeBuff[i].setRGB(0, 0, 0);
          }
          FastLED.show();
        }
      }
      delay(1000);
      input_tensor.clear();
    }
  }
}

13. 最後の最後で動かない、なぜ

いつも何か作るときは、

  1. 技術要素を調査して使うものを決めて
  2. アーキテクチャを考えて
  3. プログラムを実装する、

みたいな流れで、今回もいい感じにブロッカーもなく進めることができたのですが、最後の最後で動かないという悲劇が待っていました。

Jupyter Lab 上で TensorFlow および TensorFlow Lite を使えば作成した LSTM モデルは正常に動いていました。「あとは M5Stack Core2 にデプロイするだけだ」と思って余裕でいたのですが、上記のプログラムを実行すると TfLiteStatus allocState = interpreter->AllocateTensors(); でエラーが発生して動きません。

当初、 tensor_pool_size の値を大きくするなど試してみたのですが、どうやら TensorFlow Lite for Microcontroller では RNN や LSTM が動かないっぽいということがわかりました。もともと、1) の段階で 公式の TensorFlow Lite for Microcontroller のサンプル のなかに LSTM の実装があることをチラッと観測していたので行けるものだと思っていました。しかし、よくよくソースコードを眺めるとその部分は実質的に CNN が動いてるっぽいんですよね。ということで、LSTM を一旦あきらめて CNN に変更しました。以下が修正プログラムです。

※参考 RNN support for Tensorflow Lite Micro #36688

henshing_train.py

import numpy as np 
import pandas as pd
import tensorflow as tf
from keras.models import Sequential
from keras.models import load_model
from keras.layers import LSTM
from keras.layers.core import Dense, Dropout
from util import load_data
from util import confusion_matrix

# Load Dataset
records_train, records_test, labels_train, labels_test = load_data()
timesteps = len(records_train[0])
input_dim = len(records_train[0][0])
n_classes = len(set([tuple(category) for category in labels_train]))
print(timesteps, input_dim, n_classes)

def reshape_function(data):
  reshaped_data = tf.reshape(data, [-1, 3, 1])
  return reshaped_data

# Create Model / CNN
model = tf.keras.Sequential([
      tf.keras.layers.Conv2D(8, (4, 3), padding="same", activation="relu", input_shape=(timesteps, input_dim, 1)),
      tf.keras.layers.MaxPool2D((3, 3)), 
      tf.keras.layers.Dropout(0.1),  
      tf.keras.layers.Conv2D(16, (4, 1), padding="same", activation="relu"),  
      tf.keras.layers.MaxPool2D((3, 1), padding="same"),  
      tf.keras.layers.Dropout(0.1), 
      tf.keras.layers.Flatten(),
      tf.keras.layers.Dense(16, activation="relu"),
      tf.keras.layers.Dropout(0.1),
      tf.keras.layers.Dense(n_classes, activation="softmax")
])
records_train = records_train.reshape(402,128, 9, 1)
records_test = records_test.reshape(152, 128, 9, 1)
model.compile(
    loss='categorical_crossentropy', 
    optimizer='rmsprop', 
    metrics=['accuracy'])

# Train
model.fit(
    records_train,
    labels_train,
    batch_size=16,
    validation_data=(records_test, labels_test),
    epochs=200
)

# Save
model.save('henshing_model.h5')

# Result
print(confusion_matrix(labels_test, model.predict(records_test.reshape(152, 128, 9, 1))))

14. 変身ベルトを百均グッズで工作して完成 !!

最後は、変身ベルトを「安心と伝統」の百均グッズで作ります。

表側はこんな感じ。

押しボタン式の LED ライトの中身を取り出して LED テープに置き換えました。

仮止めですが、裏側はこんな感じ。

LED テープのケーブルを M5Stack Core2 に接続しました。

こんな感じでクルクルと LED が発光します !


まとめ

というわけで、機械学習を使って「変身ベルト」を作ってみました。簡易的な作りですので精度は高くありませんが、ちゃんと変身ポーズを検出してくれました。

今回の記事を投稿するにあたり、技術的な課題や苦労は多かったですし、細かい話まで全てを書ききれていないのですが、この程度のアイディアであれば十分に実現性は高いです。みなさん、機械学習の活用には苦労されていると思いますが、ぜひ変身 (DX) してくださいね !!


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

清水 崇之 (しみず たかゆき) (@shimy_net)
アマゾン ウェブ サービス ジャパン合同会社
技術統括本部 西日本ソリューション部 ソリューションアーキテクト / 部長

最先端技術を追いながら世界を爆笑の渦に巻き込みたい、そんな AWS 芸人になりたいと思って AWS に入社して早 6 年。今年からは AWS DJ として頑張ります。

過去のプレゼンテーションはこちら »

さらに最新記事・デベロッパー向けイベントを検索

下記の項目で絞り込む
1

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する