使用飞桨框架开发跌倒警报APP,Paddle Lite表现亮眼!
1.项目背景
据美国的一项调查报告显示,33%的老年人每年会摔倒。由于老人的身体状况不如年轻人健壮,一旦发生跌倒情况,情况可能会非常严重。如果老人摔倒在地且不能及时得到救助,每分每秒的流逝都会对老人的生命健康造成严重威胁。在美国,约68%的老人医护费用都与“跌倒”相关,因跌倒直接导致的医疗费用高达200亿美元。每年死亡的65岁以上老人中,有1000位是因为跌倒造成的。当老人发生跌倒事故时,危急情况下如何智能求救是一个亟待解决的问题。因此,老人单独活动时,提供便携智能的监护设备尤为重要。本文旨在介绍基于飞桨框架设计一种算法感知人体活动,并依托Paddle Lite和Android Studio开发跌倒监测报警手机客户端。
当前,感知人体活动的主流方法有两种:一种是基于传感器数据的方法,通过三轴加速度计、陀螺仪等传感器采集到的随人体运动而实时变化的运动数据,再以机器学习算法对数据进行分类,判断当前用户处于哪一种运动状态;另外一种则是基于摄像头采集人体图像,通过大量的训练样本做图像分类,该方法需要采集海量图片,以数据驱动训练分类模型,并且由于摄像头的固定性和经济价格,常用于公共场合如学校、车站,不适合单个用户的实时跟踪监护。本文在第一种方案的基础上设计一款APP,无需用户使用可穿戴设备,只需一部手机即可完成价低、实时、准确的运动状态感知,没有任何额外负担。
目前,常见的基于传感器计步的应用程序,缺乏智能监护的软件。人体活动类别多种多样,样式复杂,在设计设APP时,我们只考虑几种常见的活动状态,完成跌倒检测与报警求助任务。成品可以下载和安装到手机上,实现运动状态感知功能。
飞桨深度学习框架基于编程一致的深度学习计算抽象以及对应的前后端设计,拥有易学易用的前端编程界面和统一高效的内部核心架构,目前已经更新到2.0版本,支持静态图和动态图编程,且接口简单易用,方便用户设计和实现神经网络算法,并能让用户基于自己采集的数据完成模型训练。
Paddle Lite是飞桨轻量化推理引擎,经过对Paddle模型保存和量化后,可以转换成方便移动端部署的模型。基于Paddle Lite引擎加持,用户可以使用Java语言完成客户端模型推理,对采集到的加速度数据进行分类,得到运动状态,从而为用户提供服务。
本项目为2020年中国高校计算机大赛人工智能创意赛的参赛方案,并在总决赛中获得三等奖的成绩。代码和说明已在AI Studio公开,演示视频已经上传至B站,相关连接为:
https://aistudio.baidu.com/aistudio/projectdetail/1286602
https://www.bilibili.com/video/BV1hg4y1v7me/
https://github.com/tyb311/HAR
2.实现方案
本文依托飞桨框架2.0版本介绍分类算法。下面,我们将分别介绍数据预处理方法、模型设计、模型训练和量化保存。
图2展示了手机采集到的三轴加速度数据样本,图内所展示的9种运动状态下的数据形态各异。每种运动数据在频率、幅值方面都有着不小的差异,甚至有些动作的数据折线图可以通过肉眼分析,这也说明该任务是可行的,且极有可能用简单的神经网络获得高准确率。
2.1数据预处理方法
由于我们采集到的数据样本较少,不能对应人体在生活中复杂的运动形式。为了增加数据多样性,本文对训练集的每个数据样本都进行加噪处理,以训练出更具鲁棒性的分类模型。这里,我们采用了Noise-92数据库的噪声样本,对原始加速度数据进行相加运算。在预处理阶段,我们先将数据集和噪声数据保存为npy文件,方便随时读取。
详细程序如下,在自定义的MyDataset数据加载器中,首先从npy文件中读取训练样本和噪声数据,对数据集进行训练或测试划分。在测试状态下,直接输出原始数据。在训练状态下,则随机截取noise与训练样本相加。实验表明,加噪处理可以显著提升测试准确率。
from scipy.io import loadmat
from sklearn.model_selection import train_test_split
import numpy as np
import random
import paddle
print(paddle.__version__)#2.0.1
class MyDataset(paddle.io.Dataset):
__name__ = 'har'
# gpu = paddle.CUDAPlace(0)
cpu = paddle.CPUPlace()
def __init__(self, har5=True, seq='151', root='./npys/'):
super(MyDataset, self).__init__()
self.__name__ = 'har92'
self.device = self.cpu#paddle.CUDAPlace(2)#
# self.device = self.gpu if paddle.device.is_compiled_with_cuda() else self.cpu
print('Using Device:', self.device)
self.babble = loadmat(root+'babble.mat')['babble'].reshape(-1).astype(np.float32)
db='db18'
if seq=='151':
x = np.load(root+'{}_acc.npy'.format(db)).astype(np.float32)
x = x.reshape(-1, 3, 151)#[:32,:,:150]
y = np.load(root+'{}_lab.npy'.format(db)).astype(np.int64)
else:
x = np.load(root+'{}_acc51.npy'.format(db)).astype(np.float32)
x = x.reshape(-1, 3, 51)#[:32,:,:150]
y = np.load(root+'{}_lab51.npy'.format(db)).astype(np.int64)
x_train, x_test, y_train, y_test = train_test_split(x, y, stratify=y, test_size=0.3)
if har5:
tran = [0,0,1,2,1,2,1,0,0,3,3,3,3,3,3,3,3,4]
for i in range(18):
y_train[y_train==i] = tran[i]
y_test[y_test==i] = tran[i]
self.y_train = y_train.astype(np.int64)
self.y_test = y_test.astype(np.int64)
self.x_train = x_train
self.x_test = x_test
def __getitem__(self, index):
data = self.x_train[index]
label = self.y_train[index]
# Add Noise
len_noise = np.prod(data.shape)
idx = random.randint(0, self.babble.shape[0]-len_noise)
noise = self.babble[idx:idx+len_noise]
data = data+noise.reshape(3,-1)
return data, label
def __len__(self):
if self.isTrainMode:
return self.x_train.shape[0]
return self.x_test.shape[0]
2.2模型设计
近年来,深度学习大火,神经网络方法在各个领域大放光彩,被证实有着极强的应用特性。卷积神经网络在计算机视觉、信号处理等领域有着极为广泛的应用。在处理图像时,二维卷积网络经常会被用到。在处理三维医学图像数据或者视频时,三维卷积神经网络则大显身手。这里,我们要处理的是一维的加速度数据,每个样本为3秒钟的浮点数序列样本,共151个数字,三个通道。
笔者在参加比赛时,Paddle Lite对二维卷积的部署支持较好。因此,大家可以在这里把一维信号当做二维处理,卷积核设计为3*1大小即可。下面程序中Block类包含了基础的二维卷积、批标准化和激活函数层,三者构成卷积网络的基本单元。然而,当时Dropout层尚不支持移动端部署,所以我们并没有使用,丢失层可以通过正则化得到更为鲁棒的模型。
import numpy as npimport os
#加载飞桨和相关类库V2.0.1import paddlefrom paddle import nnprint(paddle.__version__)
class Block(nn.Layer): def __init__(self, in_channels=3, out_channels=3, kernel_size=3, *args): super(Block, self).__init__() self.nn = nn.Sequential( nn.Conv2D(in_channels=in_channels, out_channels=out_channels, kernel_size=(1,kernel_size), padding=0, bias_attr=False), nn.BatchNorm2D(num_features=out_channels), # nn.ReLU(), nn.LeakyReLU(.2), # nn.Dropout(p=.2) ) def forward(self, x): return self.nn(x)
下面,我们将介绍模型的整体架构,整个模型分为两大部分。
第一部分是全卷机网络,经过5*1、3*1和1*1的卷积核进行卷积,得到对应的三阶段中间张量。然后通过自适应平均池化获取空域特征。
第二部分是循环神经网络,这里采用的是长短期记忆模型。当然,大家也可以选用GRU等其他组件。由于循环神经网络在时序信息处理方面较为擅长,恰恰运动数据又是一段连续的样本,因此,我们可以断定RNN类的神经网络对动作分类是有增益加持的,实验也证明了这一点。
分类算法命名为FallNet,详细实现如下:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import os
#加载飞桨和相关类库V2.0.1
import paddle
from paddle import nn
from paddle.nn import Conv1D, BatchNorm1D, Dropout, Flatten
from paddle.nn import PReLU, Linear, LSTM, GRU
from paddle.static import InputSpec
import paddle.optimizer as opt
print(paddle.__version__)
class Block(nn.Layer):
def __init__(self, in_channels=3, out_channels=3, kernel_size=3, *args):
super(Block, self).__init__()
self.nn = nn.Sequential(
nn.Conv1D(in_channels=in_channels, out_channels=out_channels,
kernel_size=kernel_size, padding=0, bias_attr=False),
nn.BatchNorm1D(num_features=out_channels),
nn.LeakyReLU(.2),
nn.Dropout(p=.2)
)
def forward(self, x):
return self.nn(x)
class FallNet(nn.Layer):
def __init__(self, in_channels=3, out_classes=5, hid=64, num=64):
super(FallNet, self).__init__()
self.cnn0 = Block(in_channels,hid,7,0)
self.cnn1 = Block(hid,hid,5,0)
self.cnn2 = Block(hid,hid,3,0)
self.cnn3 = Block(hid,hid,1,0)
self.avg = nn.AdaptiveAvgPool1D(output_size=num)
# self.rnn0 = nn.LSTM(input_size=145, hidden_size=num, dropout=.2, num_layers=3)
self.rnn0 = nn.GRU(input_size=145, hidden_size=num, num_layers=1, dropout=0.2)
self.rnn1 = Block(hid,hid,1,0)
self.rnn2 = Block(hid,4,3,0)
self.cls = nn.Sequential(
nn.Linear(in_features=1016, out_features=128),#1016 with RNN
nn.Dropout(p=.2),
nn.Linear(in_features=128, out_features=out_classes),
nn.Softmax(axis=1)
)
@paddle.jit.to_static(input_spec=[InputSpec(shape=[None, 3, 151], dtype='float32')])
def forward(self, x):#
batch = x.shape[0]
# print(x.shape)
x = self.cnn0(x)
y = self.cnn1(x)
y1 = self.avg(y)
y = self.cnn2(y)
y2 = self.avg(y)
y = self.cnn3(y)
y3 = self.avg(y)
# print('CNN:', y1.shape, y2.shape, y3.shape)
r,t = self.rnn0(x)
x = paddle.concat([y1,y2,y3,r], axis=-1)
# print(x.shape)
x = self.rnn1(x)
x = self.rnn2(x)
x = paddle.flatten(x, start_axis=1)
x = self.cls(x)
return x
2.3模型训练
飞桨框架在1.x阶段已经支持动态图编程,用起来较为方便,在2.0版本则更好地支持了类keras的高级API。在编写训练程序时,大家可以很快上手,快速实现,进入训练阶段。这里,我们将展示以高级API训练的部分。
import numpy as npimport osos.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID'# os.environ['CUDA_VISIBLE_DEVICES'] = '1,2'os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
为了加快模型的训练,这里采用了GPU加速。飞桨应用GPU加速算法也极为方便,安装对应的GPU版本飞桨框架,即可设置os变量,在GPU上训练模型。
EPOCH_NUM = 1
def train(layer, loader):
loss_fn = nn.CrossEntropyLoss()
adam = opt.Adam(learning_rate=0.001, parameters=layer.parameters())
for epoch_id in range(EPOCH_NUM):
for batch_id, (image, label) in enumerate(loader()):
# print(image.shape, label.shape)
out = layer(image)
loss = loss_fn(out, label)
loss.backward()
adam.step()
adam.clear_grad()
print('Epoch {} batch {}: loss = {}'.format(epoch_id, batch_id, np.mean(loss.numpy())))
from pd_data import *
if __name__ == '__main__':
layer = FallNet()
# create data loader
dataset = MyDataset()
loader = paddle.io.DataLoader(dataset,
batch_size=8,
shuffle=True,
drop_last=True,
num_workers=2)
# train
train(layer, loader)
# save
path = 'infer/fallnet'
paddle.jit.save(layer, path)
在划分好的测试集上,该模型得到了0.9883的准确率,可以用于实际测试应用。
2.4量化保存
这里使用动态图进行模型实现,但是移动端部署要使用静态图,因此要先把动态图转为静态图,我们使用了TraceLayer完成转换任务。
import paddle
in_var = paddle.uniform(shape=[1, 3, 151], dtype='float32')out_dygraph, static_layer = paddle.jit.TracedLayer.trace(layer, inputs=[in_var])
# 内部使用Executor运行静态图模型out_static_graph = static_layer([in_var])print(len(out_static_graph)) # 1print(out_static_graph[0].shape) # (2, 10)
# 将静态图模型保存为预测模型static_layer.save_inference_model(dirname='./saved_infer_model')
然后,可以基于Paddle Lite把模型量化,转换成适合移动端快速推理的文件。
# 引用Paddlelite预测库
from paddlelite.lite import *
# 1. 创建opt实例
opt=Opt()
# 2. 指定输入模型地址
opt.set_model_dir('./saved_infer_model')
# 3. 指定转化类型:arm、x86、opencl、npu
opt.set_valid_places('arm')
# 4. 指定模型转化类型:naive_buffer、protobuf
opt.set_model_type('naive_buffer')
# 4. 输出模型地址
opt.set_optimize_out('android')
# 5. 执行模型优化
opt.run()
3.移动程序设计
在上面部分,我们已经介绍完人体活动分类算法的设计和训练,并完成模型的转化和量化。下面,我们可以就上述文件进行移动端应用开发。
3.1APP功能模块设计
下面展示关于APP三个层面的设想。
第一层在算法上,可以识别静止、行走、奔跑、跌倒和求救(自定义活动状态)5种基本类别,足够日常监护使用,前文也已介绍完毕。
第二层在应用上,需要连续识别运动状态序列,这里可以设计一个状态机。当正常运动时发生跌倒,并倒地不起,就可以视为跌倒序列,引发报警程序。
第三层在功能上,不仅要实现自动拨号求救,发送定位信息,后续还可加入远程检测、定期总结的功能,并且,每月能够对用户活动量做出报告、提出建议。
3.2APP运行流程图
图6展示了开发完毕的APP监测界面,最上方的曲线展示了最近1秒钟的三轴加速度数据,分别为AccX、AccY和AccZ;中间部分给出了由百度地图SDK提供的经纬度坐标和地理位置信息,方便信息求救;最下面的柱状图则实时更新当前运动类别,方便研究人员测试程序。
4.总结与展望
通过使用飞桨框架2.0版本,我们快速完成了人体活动感知模型的设计开发,用少量代码程序获得了高准确率的加速度数据分类模型。基于Paddle Lite部署工具,成功在安卓APP上运行起了我们的轻量级神经网络模型,完美完成了检测应用的UI设计和后台开发,实现移动应用的运行,并证实该APP能用于人体活动感知。
希望飞桨继续加速迭代,更好地支持最新技术,简化开发流程,受益大众。同时,也希望移动端推理引擎支持更多的算子,推理速度更快,让更多的模型可以在口袋里运行,造福百姓!