目录结构
1、背景介绍
2、数据采集
3、网络设计
4、网络训练
5、网络部署
6、总结
1、背景介绍
最近采购了一块新的树莓派,迫不及待的想要在树莓派上实现一个实时的手势识别。从算法的角度讲,并不是太难;但是从工程的角度来说,主要有两个难点,一是手势数据的采集。大家都知道,深度学习的高精度离不开大量的训练数据,网络设计的再好,没有足够的数据是不行的。因此要想实现一个好的手势识别,采集数据就成了一个比较重要的难点;另外一个难点是如何在树莓派上实现实时的识别。树莓派实际上是一个使用arm作为处理器的linux系统,但是由于芯片的性能不是很强,比我们使用的手机要弱很多,并且树莓派目前对vulkan的支持并不好,无法使用vulkan加速,因此对网络的优化也是一个难点。要保证网络优化后的精度不能下降太多,但计算量必须要下降很多。 这次就从这两个角度出发,实现一套实时的手势识别。关注公众号"DL工程实践",后台回复“手势识别”四个字,就会自动获取所有源码的下载地址。由于手势的类型非常多,有识别数字的,识别字母的,识别动作的,这里为了抛砖引玉,设计一个相对简单的识别"剪刀,石头,布"的手势识别系统,后续可以用来制作一个剪刀石头布的对战机器人。想要实现其他类型的手势识别,也完全可以按照这个流程来做。
2、数据采集
对于数据采集,首先看看有没有开源的手势识别数据集。很遗憾,除了收费的手势识别数据集,基本上都是一些不太完整的手势识别数据集。因此我们需要自己采集。工欲善其事必先利其器,自己采集就得有一些比较好的数据采集工具。这里我设计了一款数据采集工具(关注公众号"DL工程实践",后台回复“手势识别”四个字,可获取)。大家也可以根据自己的需要开发自己的数据采集工具。其实本质上并不难,使用pyqt+opencv很容易就能开发一个顺手的数据采集工具。由于基于python开发,所以移植性非常好,既可以在windows下使用,也可以在linux,树莓派上使用。我设计的这个界面非常简洁,如下图所示:
opencv会调用camera开始预览,然后设置一下保存路径,保存标签,点击保存图片,就可以按照设置的保存间隔进行采集数据。例如默认的保存间隔为30,即30帧保存一张图片,相当于1秒钟保存一张,如果想要频率快一些,就将保存间隔设置的小一点。下面的视频展示了数据采集工具的采集过程,为了展示效果,我把保存间隔设置为了60帧,大约2秒保存一张图片。
视频插不进来,有兴趣的关注“DL工程实践”查看。
我把剪刀的标签设置为0,石头的标签设置为1,布的标签设置为2,最终通过该数据收集工具就收集到了三个文件夹:
接下来需要为训练数据创建标签文本。这里我将所有图片的80%作为训练数据数据集,剩余的20%作为验证数据集。使用python脚本很容易实现自动创建标签文件的脚本,代码如下:
import os
import random
MAX_LABEL=3 #类别的种类数目
label_list=[]
for label in range(0,MAX_LABEL+1):
for file in os.listdir(str(label)):
label_list.append(str(label)+'/' + str(file) + ' ' + str(label))
random.shuffle(label_list) #对列表进行shuffle操作
count = len(label_list)
train_count = int(count * 0.8) # 80%作为训练数据集
train_list = label_list[0:train_count]
test_list = label_list[train_count:]
print('total count=%d train_count=%d test_count=%d'%(count, train_count, count-train_count))
# 写入train.txt标签文件
with open('train.txt', 'w') as f:
for line in train_list:
f.write(line + '\r')
# 写入test.txt标签文件
with open('test.txt', 'w') as f:
for line in test_list:
f.write(line + '\r')
3、网络设计
完成了数据收集,那么就可以开始为手势识别系统设计一个网络了。由于需要在树莓派这样的低性能硬件上面运行CNN,那么可以考虑从轻量级网络中选择一个来进行优化。例如google的mobilenet系列,efficient lite系列,旷世的shufflenet系列,华为的ghostnet等。那这些模型如何选择呢?我之前有一篇关于这些轻量级的模型的评测,有兴趣的可以去看看,《谁才是轻量级CNN的王者?7个维度全面评测mobilenet/shufflenet/ghostnet》,通过之前的评测,我发现shufflenetv2在精度和推理延时上面有一个很好的平衡,因此我选择了shufflenetv2作为手势识别系统的基础网络。直接使用shufflenetv2虽然能够在树莓派上较为流畅的运行,但是还达不到实时的效果,因此需要对shufflentv2进行一些优化,主要是为了降低计算量,并且能够尽量保持精度。降低计算量可以从如下几个方面考虑:
降低shufflenet的通道系数
shufflenetv1/v2在设计之初,本身就考虑了应用在不同的资源设备上,因此设置了一个通道系数,直接调整该通道系数,就可以获得更小计算量的模型。然而通过实际测试,直接将通道系数从1.0x降低为0.5x,在降低计算量的同时,也会对精度损失较大。因此不采用该方案。
降低输入分辨率
shufflenet的原始输入分辨率为224*224,如果将分辨率降低x,那么计算量将降低x^2,因此收益很大。但是通过测试发现,直接将分辨率降低,对精度的影响也会很大。所以也不采用降低分辨率的方案。
裁剪shufflenetv2不重要的1*1卷积
通过观察shufflenet的block,可以分为两种结构,一种是每个stage的第一个block,该block由于需要降采样,升维度,所以对输入直接复制成两份,经过branch1,和branch2之后再concat到一起,通道翻倍,如下图中的降采样block所示。另外一种普通的block将输入split成两部分,一部分经过branch2的卷积提取特征后直接与branch1的部分进行concat。如下图中的普通block所示:
一般在DW卷积(depthwise卷积)的前或后使用1*1的卷积处于两种目的,一种是融合通道间的信息,弥补dw卷积对通道间信息融合功能的缺失。另一种是为了降维升维,例如mobilenet v2中的inverted reddual模块。而shufflenet中的block,在branch2中用了2个1*1卷积,实际上有一些多余,因为此处不需要进行升维降维的需求,那么只是为了融合dw卷积的通道间信息。实际上有一个1*1卷积就够了。因此将上述红色虚线框中的1*1卷积核删除。经过测试,精度几乎不降低,计算量却下降了30%。因此裁剪1*1的卷积核将是一个不错的方法。
加入CSP模块
csp在大型网络上取得了很大的成功。它在每个stage,将输入split成两部分,一部分经过原来的路径,另一部分直接shortcut到stage的尾部,然后concat到一起。这既降低了计算量,又丰富了梯度信息,减少了梯度的重用,是一个非常不错的trip。在yolov4,yolov5的目标检测中,也引入了csp机制,使用了csp_darknet。此处将csp引入到shufflenet中。并且对csp做了一定的精简,最终使用csp stage精简版本作为最终的网络结构。
经过测试,网络虽然能大幅降低计算量,但是精度降低的也很明显。分析原因,主要有两个,一是shufflenetv2本身已经使用了在输入通道split,然后concat的blcok流程,与csp其实是一样的,只是csp是基于一个stage,shufflenetv2是基于一个block,另外csp本来就是在densenet这种密集连接的网络上使用有比较好的效果,在轻量级网络上不见得效果会好。
因此最终将网络设计为基于shufflenetv2 1.0x,并精简了多余的1*1卷积的版本,命名为:shufflenetv2_liteconv版本。(关注公众号"DL工程实践",后台回复“手势识别”四个字,可获取)
4、网络训练
收集好了数据,并且也设计好了网络,那么接下来就是训练了。基于pytroch,大家可以很方便的编写出一个简单的训练流程。这里我选择从0开始训练,没有使用shufflenet v2 1.0x的预训练模型,因为我们对shufflenet做了优化,删除了很多1*1的conv,直接使用预训练模型会不匹配,因此从0开始训练。学习率可以适当的放大一些,epoch数目可以适当大一些。我把我的训练超参贴出来,大家可以参考使用:
训练epoch:60
初始学习率:0.01
学习率策略:multistep(35,40)
优化器:moment sgd
weight decay:0.0001
最终在训练完50个epoch之后,loss大约为0.1,测试集上面的精度为0.98。
5、网络部署
网络部署可以采用很多开源的推理库。例如mnn,ncnn,tnn等。这里我选择使用ncnn,因为ncnn开源的早,使用的人多,网络支持,硬件支持都还不错,关键是很多问题都能搜索到别人的经验,可以少走很多弯路。但是遗憾的是ncnn并不支持直接将pytorch模型导入,需要先转换成onnx格式,然后再将onnx格式导入到ncnn中。另外注意一点,将pytroch的模型到onnx之后有许多胶水op,这在ncnn中是不支持的,需要使用另外一个开源工具:onnx-simplifier对onnx模型进行剪裁,然后再导入到ncnn中。因此整个过程还有些许繁琐,为了简单,我编写了从"pytorch模型->onnx模型->onnx模型精简->ncnn模型"的转换脚本,方便大家一键转换,减少中间过程出错。我把主要流程的代码贴出来(详细的代码请关注公众号"DL工程实践",后台回复“手势识别”四个字,可获取)
# 1、pytroch模型导出到onnx模型
torch.onnx.export(net,input,onnx_file,verbose=DETAIL_LOG)
# 2、调用onnx-simplifier工具对onnx模型进行精简
cmd = 'python -m onnxsim ' + str(onnx_file) + ' ' + str(onnx_sim_file)
ret = os.system(str(cmd))
# 3、调用ncnn的onnx2ncnn工具,将onnx模型准换为ncnn模型
cmd = onnx2ncnn_path + ' ' + str(new_onnx_file) + ' ' + str(ncnn_param_file) + ' ' + str(ncnn_bin_file)
ret = os.system(str(cmd))
# 4、对ncnn模型加密(可选步骤)
cmd = ncnn2mem_path + ' ' + str(ncnn_param_file) + ' ' + str(ncnn_bin_file) + ' ' + str(ncnn_id_file) + ' ' + str(ncnn_mem_file)
ret = os.system(str(cmd))
导出到ncnn模型之后,就可以在ncnn模型上运行训练好的手势识别库。ncnn是基于C++开发的,因此编写上层应用的时候使用C++是效率最高的。我为了简单,使用python来调用ncnn的C++库也是可以的,不过会损失一丢丢的性能,但这是值得的,人生苦短,我用python。下面这个视频是最终部署好的手势识别程序(PS:为了增加乐趣,我实现了一个剪刀石头布对战的功能,也开源了,还是老方法获取:关注公众号"DL工程实践",后台回复“手势识别”四个字)
视频插不进来,有兴趣的关注“DL工程实践”查看。
6、总结
本次实践完成了基于树莓派的实时手势识别,算法上并不复杂,主要是工程实践上的一些问题,例如数据的采集,网络的优化,以及后期的推理转换等。实际上还有一些工作可以优化,例如对模型的量化,对数据的增强。通过模型量化,可以进一步提升运算效率,通过数据增强可以弥补我们自己采集的数据分布单一,过拟合的风险,这些问题就留给读者朋友们自己去思考了。