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





目次
ご注意
本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

この記事のデモを無料でお試しいただけます »
毎月提供されるデベロッパー向けアップデート情報とともに、クレジットコードを受け取ることができます。
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 のプロジェクトを作成
ウィザードでは [Name] にプロジェクト名 "SampleProject" を入力し [Board] は "M5Stack Core2" をプルダウンから選択して [Finish] をクリックします。
つぎに、M5Stack Core2 を動かすためのライブラリをプロジェクトに追加します。PlatformIO の左メニューから [Libraries] を選択して "M5Core2" で検索します。検索結果のなかから "M5Core2" ライブラリを選択します。
ライブラリを追加したプロジェクト 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 インスタンスを作成します。
後述の 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'])
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 = µ_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. 最後の最後で動かない、なぜ
いつも何か作るときは、
- 技術要素を調査して使うものを決めて
- アーキテクチャを考えて
- プログラムを実装する、
みたいな流れで、今回もいい感じにブロッカーもなく進めることができたのですが、最後の最後で動かないという悲劇が待っていました。
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 に変更しました。以下が修正プログラムです。
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) してくださいね !!
筆者プロフィール

清水 崇之 (しみず たかゆき) (@shimy_net)
アマゾン ウェブ サービス ジャパン合同会社
技術統括本部 西日本ソリューション部 ソリューションアーキテクト / 部長
最先端技術を追いながら世界を爆笑の渦に巻き込みたい、そんな AWS 芸人になりたいと思って AWS に入社して早 6 年。今年からは AWS DJ として頑張ります。
AWS を無料でお試しいただけます