用循环神经网络在STM32上进行运动识别
在之前的硬件设计中,我自制了一个非常mini的小开发板,取名为 SensorT Mini
,这个名字是相对于 WatchT
而言的。因为这个小板上我设计了一个三轴重力加速度计,初衷就是能够通过这个小传感器完成计步功能,所以现在就来实现了一个循环神经网络,来识别一下运行状态,为实现计步器做一点准备,也算是一个先导项目。
功能
模型计划实现三种运动模式,运动识别本质上还是一个分类问题:
- 静坐 对应类索引:0
- 走路 对应类索引:1
- 跑步 对应类索引:2
运动类型的数据都由我亲自采集,然后导出到PC端,整理成模型需要数据并归一化,最后喂给模型进行训练,最后将训练好的权重导出,在单片机上重构模型,进行验证。
先看下效果图,运动识别大约会延迟30秒左右,这还是由于数据的时序性,不过延迟参数可以调,这里我使用的是30秒延迟,在携带 SensorT Mini
走了一段路之后,我查看当前的识别结果:
模型搭建
与之前一样,在PC端通过keras框架来实现模型的搭建和训练,将训练后的权重导出成 weight.h
即可。这个模型使用了一个RNN层,和两个Dense层,模型结构图如下:
这里选择使用循环神经网络,是因为运动识别的数据来源于三轴加速度计,这些数据需要连接起来看,才能知道属于那种类别的运动,属性时序性数据,循环神经网络可以很好的处理有时序结构的数据。
模型关键结构的重定义
因为使用到了RNN,所以需要在单片机上实现RNN。我依然选择使用 PlatformIO + Mbed 的组合,先来定于RNN层:
// rnn层输入结构体
typedef struct
{
uint16_t input_steps;
uint16_t input_units;
uint16_t hidden_units;
arm_matrix_instance_f32 *input_weight;
arm_matrix_instance_f32 *hidden_weight;
arm_matrix_instance_f32 *hidden_b;
void (*afun)(float32_t in[], uint16_t len);
} rnn_input_t;
然后定义全连接层:
// dense层输入结构体
typedef struct
{
uint16_t dense_units;
arm_matrix_instance_f32 *dense_weight;
arm_matrix_instance_f32 *dense_b;
void (*afun)(float32_t in[], uint16_t len);
} dense_input_t;
以及模型重构中需要实现的函数原型定义:
// 网络结构
void nn_rnn(rnn_input_t inp_struct, float32_t **input, float32_t result[]);
void nn_dense(dense_input_t inp_struct, float32_t input[], float32_t result[]);
由于模型使用了 relu
激活,所以需要实现一下 relu
激活函数:
// relu 激活
void active_relu(float32_t in[], uint16_t len)
{
for (uint16_t i = 0; i < len; i++)
{
in[i] = in[i] < 0 ? 0 : in[i];
}
}
以及最后的伪softmax实现:
// softmax激活
uint16_t active_softmax(float32_t res[], uint16_t len)
{
float32_t max = res[0];
uint16_t index = 0;
for (uint8_t i = 0; i < len; i++)
{
if (max < res[i])
{
max = res[i];
index = i;
}
}
return index;
}
STM32端重构RNN
有了上述的关键结构,就可以实现完整的RNN了。运动数据使用三轴加速度计来实时获取,并组装成RNN需要的shape。这里模型使用的是 (20*15) 的Shape作为输入,重构RNN代码如下:
// 运行运动监测模型获取监测结果
uint16_t run_rnn_model(float32_t **rnn_input)
{
float32_t *rnn_out = (float32_t *)malloc(sizeof(float32_t) * C_HIDDEN_UNITS);
rnn_input_t rnn_in_struct = {
C_INPUT_STEPS,
C_INPUT_UNITS,
C_HIDDEN_UNITS,
&mat_input_w,
&mat_hidden_w,
&mat_hidden_b,
&active_relu};
nn_rnn(rnn_in_struct, rnn_input, rnn_out);
// 计算dense1
float32_t *dense1_out = (float32_t *)malloc(sizeof(float32_t) * C_DENSE_1_UNITS);
dense_input_t dense_in_struct_1 = {
C_DENSE_1_UNITS,
&mat_dense1_w,
&mat_dense1_b,
&active_relu};
nn_dense(dense_in_struct_1, rnn_out, dense1_out);
// 计算out层
float32_t *dense2_out = (float32_t *)malloc(sizeof(float32_t) * C_OUTPUT_UNITS);
dense_input_t dense_in_struct_2 = {
C_OUTPUT_UNITS,
&mat_out_w,
&mat_out_b,
&active_none};
nn_dense(dense_in_struct_2, dense1_out, dense2_out);
// 类softmax结果
uint16_t res_index = active_softmax(dense2_out, C_OUTPUT_UNITS);
// 释放内存
free(rnn_out);
free(dense1_out);
free(dense2_out);
// 返回分类结果
return res_index;
}
运行结果
这个模型移植到STM32F401上可以实现一个数据帧小于2ms的效果,可以说识别特别快,可以很轻松的识别出运行类型,在携带 SensorT Mini
走了一段路由,我坐在下来,准备喝杯水,这时 SensorT Mini
也可以准确识别出我处于静坐状态。
有关 SensorT Mini 的原理图可以看我之前的博文,除了给出原理图外,我也简要介绍了软件流程和页面应用切换的实现方式,可酌情参考。