用循环神经网络在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 的原理图可以看我之前的博文,除了给出原理图外,我也简要介绍了软件流程和页面应用切换的实现方式,可酌情参考。

类似文章

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注