大约一年前,
英特尔Movidius发布了一种用于高效推理卷积神经网络的设备-Movidius神经计算棒(NCS)。 该设备允许使用神经网络在能耗有限的情况下(包括机器人任务)识别或检测物体。 NCS具有USB接口,功耗不超过1瓦。 在本文中,我将讨论将NCS与Raspberry Pi一起用于检测视频中人脸的经验,包括训练Mobilenet-SSD检测器并在Raspberry上启动它。
所有代码都可以在我的两个存储库中找到:
探测器培训和人
脸检测演示 。
在我的第一篇文章中,我已经写过有关使用NCS进行面部检测的
文章 :然后我们谈论的是
YOLOv2检测器,我将其从
Darknet格式转换为
Caffe格式,然后在NCS上启动了它。 转换过程非常简单:由于这两种格式以不同的方式定义了检测器的最后一层,因此必须使用Darknet中的一段代码在CPU上分别解析神经网络的输出。 此外,该检测器在速度(笔记本电脑上高达5.1 FPS)和准确性上都不能令人满意-后来我确信,由于对图像质量敏感,因此很难从Raspberry Pi上获得好的结果。
最后,我决定只训练我的探测器。 选择权在于带有
Mobilenet编码器的
SSD检测器:
Mobilenet的轻巧卷积使您能够在不损失质量的情况下实现高速传输,并且SSD检测器并不逊色于YOLO,并且可以开箱即用地用于NCS。
Mobilenet-SSD检测器如何工作让我们从Mobilenet开始。 在此架构中完成
3 \乘3 (在所有通道上)卷积被两个轻量级卷积代替:第一个
3 \乘3 分别为每个频道,然后完成
1 \乘以 1 卷积。 每次卷积后,将使用
BatchNorm和非线性(ReLU)。 通常将接收图像作为输入的第一个网络卷积完成。 由于预测质量的略微下降,该体系结构可以显着降低计算的复杂性。 有一个
更高级的选项 ,但是我还没有尝试过。
SSD(单发检测器)的工作方式如下:两个输出挂在几个卷积编码器的输出上
1 \乘以 1 卷积层:一个预测类的概率,另一个-边界框的坐标。 第三层提供当前级别的默认帧的坐标和位置。 意思是:任何一层的输出自然地分为单元; 靠近神经网络的末端,它们变得越来越小(在这种情况下,由于
stride=2
的卷积),每个细胞的视野都增加了。 对于几个选定层中的每个层上的每个像元,我们设置了几个不同大小和长宽比的默认帧,并使用其他卷积层来校正坐标并预测每个此类帧的类概率。 因此,SSD检测器(如YOLO)始终考虑相同数量的帧。 可以在不同的层上检测到同一对象:在训练期间,信号被发送到与该对象非常相交的所有帧,并且在应用过程中,使用非最大抑制(NMS)组合检测。 最后一层结合了来自所有层的检测结果,考虑了它们的完整坐标,切断了概率阈值并产生了NMS。
侦探训练
建筑学
训练探测器的代码位于
此处 。
我决定使用在
PASCAL VOC0712上训练有素的
现成Mobilenet-SSD检测器 ,并训练它来检测人脸。 首先,它有助于更快地训练网络,其次,您无需重新发明轮子。
原始项目包括
gen.py
脚本,该脚本从字面上收集了
.prototxt
模型文件,并替换了输入参数。 我将其转移到我的项目中,从而扩展了功能。 该脚本允许您生成四种类型的配置文件:
- 火车 :在入口-一个训练的LMDB基础,在输出-一个具有损失函数及其梯度的计算层,其中有BatchNorm
- test :在输入处-测试LMDB基础,在输出处-具有质量计算(平均平均精度)的层,存在BatchNorm
- deploy :在输入-图像,在输出-带有预测的层,BatchNorm丢失
- deploy_bn :在输入-图像,在输出-具有预测的层,存在BatchNorm
稍后我添加了后一个选项,以便您可以在脚本中从BatchNorm加载并转换网格,而无需接触LMDB数据库-否则,在没有数据库的情况下,将无法进行任何操作。 (通常,在Caffe中,数据源是在网络体系结构中设置的,这对我来说似乎很奇怪-至少这不是很实际)。
网络架构是什么样的(简短)- 登录名: 300 \乘300 \乘3
- 完全转换转换 :32个通道,
stride=2
- Mobilenet卷积conv1-conv11 : 64、128、128、256、256、512 ... 512通道,其中一些
stride=2
- 检测层: 19 \乘以 19 \乘以 512
- Mobilenet卷积conv12,conv13 :1024个通道, conv12的
stride=2
- 检测层: 10 \乘以 10 \乘以 1024
- 全卷积conv14_1,conv14_2 : 256,512通道,第一个
kernel_size=1
,第二个stride=2
- 检测层: 5 \乘5 \乘512
- 全卷积conv15_1,conv15_2 : 128,256通道,第一个
kernel_size=1
,第二个stride=2
- 检测层: 3 \乘3 \乘256
- 完成conv16_1,conv16_2卷积 : 128、256个通道,第一个
kernel_size=1
,第二个stride=2
- 检测层: 2 \乘以 2 \乘以 256
- 全卷积conv17_1,conv17_2 :64,128通道,第一个
kernel_size=1
,第二个stride=2
- 检测层: 1 \乘以 1 \乘以 128
- 最后一层检测输出
我稍微纠正了网络体系结构。 变更清单:
- 显然,类的数量已更改为1(不计算背景)。
- 训练过程中切块的长宽比限制:从 [0.5,2.0] 在 [0.7,1.4] (我决定稍微简化一下任务,而不是从拉伸的图片中学习)。
- 在默认帧中,仅剩下方形帧,每个单元格两个。 我大大减小了它们的尺寸,因为在经典的物体检测问题中,人脸明显小于物体。
Caffe计算默认框架尺寸如下:具有最小框架尺寸
s 和最大
L ,它会创建一个尺寸较大的框架
s 和
sqrtsL 。 由于我想检测尽可能小的脸部,因此我为每个检测层计算了全
stride
,并将最小帧大小与其等效。 使用这些参数,小的默认框架将彼此靠近放置并且不会相交。 因此,我们至少可以保证对于某种框架,将存在与对象的交集。 我将最大尺寸设置为两倍。 对于
conv16_2,conv17_2层
,我将眼睛的尺寸设置为相同。 这样
s,L 对于所有层是:
(16.32),(32.64),(64,128),(128,214),(214,300),(214,300)资料
我使用了两个
数据集 :
WIDER Face和
FDDB 。 WIDER包含许多带有非常小且模糊的面部的图片,并且FDDB更倾向于较大的面部图像(并且比WIDER小一个数量级)。 它们中的注释格式略有不同,但是这些已经是详细信息。
我并没有使用所有数据进行训练:我丢出了太小的面孔(少于6个像素或少于图像宽度的2%),丢出了所有纵横比小于0.5或大于2的图片,丢掉了WIDER数据集中所有标记为“模糊”的图片,因为它们大部分对应于非常小的人,所以我至少必须以某种方式调整大小脸的比例。 在那之后,我使所有框架都变成正方形,并扩大了最小的一面:我决定对面部比例不是很感兴趣,并且神经网络的任务也得到了简化。 我还丢掉了所有的黑白图片,这些图片很少,数据库构建脚本也因此崩溃了。
要使用它们进行培训和测试,您需要从它们组装一个LMDB基础。 怎么做:
- 对于每个图像,将以
.xml
格式创建标记。 train.txt
的train.txt
文件的格式为"path/to/image.png path/to/labels.xml"
,并创建了相同的测试文件。- 使用格式为
"test_image_name height width"
行test_name_size.txt
一个test_name_size.txt
文件。 - 创建具有数字
labelmap.prototxt
匹配项的labelmap.prototxt
文件
ssd-caffe/scripts/create_annoset.py
(来自Makefile的示例):
python3 /opt/movidius/ssd-caffe/scripts/create_annoset.py --anno-type=detection \ --label-map-file=$(wider_dir)/labelmap.prototxt --min-dim=0 --max-dim=0 \ --resize-width=0 --resize-height=0 --check-label --encode-type=jpg --encoded \ --redo $(wider_dir) \ $(wider_dir)/trainval.txt $(wider_dir)/WIDER_train/lmdb/wider_train_lmdb ./data
labelmap.prototxt item { name: "none_of_the_above" label: 0 display_name: "background" } item { name: "face" label: 1 display_name: "face" }
示例.xml标记 <?xml version="1.0" ?> <annotation> <size> <width>348</width> <height>450</height> <depth>3</depth> </size> <object> <name>face</name> <bndbox> <xmin>161</xmin> <ymin>43</ymin> <xmax>241</xmax> <ymax>123</ymax> </bndbox> </object> </annotation>
同时使用两个数据集仅意味着您需要成对地仔细合并相应的文件,而不要忘记正确地注册路径以及将文件改组以进行训练。
之后,您可以开始训练。
培训课程
在我的
Colab Notebook中可以找到模型训练的代码。
我在Google Colaboratory进行了培训,因为我的笔记本电脑几乎没有进行网格测试,并且通常都挂断了培训。 合作使我能够足够快速且免费地训练网络。 唯一要注意的是,我必须为Colaboratory编写一个SSD-Caffe编译脚本(包括诸如重新编译boost和编辑源代码之类的奇怪事情),这大约需要40分钟。 更多细节可以
在我以前的出版物中找到。
合作实验室还有一个功能:12小时后,汽车死亡,永久删除所有数据。 避免数据丢失的最好方法是每500-1000次训练迭代将Google磁盘安装到系统中并在其中节省网络权重。
至于我的检测器,在Colaboratory的一个会议中他设法取消了4,500次迭代,并在两个会议中接受了全面的培训。
对于最佳模型,我强调的测试数据集的预测质量(平均平均精度)(具有上述限制的WIDER和FDDB合并)约为0.87。 为了按照训练期间保存的比例测量mAP,有一个脚本
scripts/plot_map.py
。
检测器处理数据集中的一个(非常奇怪的)示例:
在NCS上启动
人脸检测演示在
这里 。
要为Neural Compute Stick编译神经网络,您需要
Movidius NCSDK :它包含用于编译和分析神经网络的实用程序,以及C ++和Python API。 值得注意的是,第二个版本是最近发布的,它与第一个版本不兼容:由于某些原因,所有API函数都被重命名,神经网络的内部格式已更改,添加了FIFO以与NCS交互,并且(最终)自动从float 32位转换为float 16位,这在C ++中是非常缺乏的。 我将所有项目都更新为第二个版本,但保留了一些拐杖以确保与第一个版本兼容。
训练完检测器后,值得将BatchNorm层与相邻的卷积合并以加速神经网络。
merge_bn.py
脚本
从这里开始 ,我也是从Mobilenet-SSD项目借来的。
然后,您需要调用
mvNCCompile
实用程序,例如:
mvNCCompile -s 12 -o graph_ssd -w ssd-face.caffemodel ssd-face.prototxt
项目的Makefile中有一个
graph_ssd
目标。 生成的
graph_ssd
文件是NCS可以理解的格式的神经网络描述。
现在介绍如何与设备本身进行交互。 该过程不是很复杂,但是需要相当大量的代码。 操作顺序大致如下:
- 通过序列号获取设备描述符
- 开启装置
- 将编译后的神经网络文件读取到缓冲区(作为二进制文件)
- 为NCS创建一个空的计算图
- 使用文件中的数据将图形放置在设备上,并在输入/输出上为其选择FIFO; 现在可以释放带有文件内容的缓冲区
- 检测器启动:
- 从相机(或任何其他来源)获取图像
- 处理:将其缩放为所需的大小,转换为float32并转换为[-1,1]范围
- 将图像上传到设备并请求推理
- 请求结果(程序将被阻止,直到收到结果为止)
- 解析结果,选择对象的框架(关于格式-进一步)
- 显示预测
- 释放所有资源:删除FIFO和计算图,关闭设备并删除其句柄
几乎所有使用NCS的操作都有其自己的独立功能,在C ++中,它看起来非常繁琐,您必须仔细监视所有资源的释放。 为了不重载代码,我创建
了一个包装器类以与NCS一起使用 。 在其中,所有初始化工作都隐藏在构造函数和
load_file
函数中,并且在释放资源时(在析构函数中)隐藏,使用NCS的工作减少为调用2-3个类方法。 另外,还有一个方便的功能可以解释发生的错误。
通过将输入大小和输出大小(元素数)传递给构造函数来创建包装器:
NCSWrapper NCS(NETWORK_INPUT_SIZE*NETWORK_INPUT_SIZE*3, NETWORK_OUTPUT_SIZE);
我们用神经网络加载编译后的文件,同时初始化我们需要的所有内容:
if (!NCS.load_file("./models/face/graph_ssd")) { NCS.print_error_code(); return 0; }
我们将图像转换为float32(
image
为
cv::Mat
,格式为
CV_32FC3
)并将其下载到设备:
if(!NCS.load_tensor_nowait((float*)image.data)) { NCS.print_error_code(); break; }
我们得到结果(
result
是一个自由
float
指针,结果缓冲区由包装器支持); 直到计算完成,程序才被阻止:
if(!NCS.get_result(result)) { NCS.print_error_code(); break; }
实际上,包装器还具有一种允许您同时加载数据和获取结果的方法:
load_tensor((float*)image.data, result)
。 我拒绝使用它是有原因的:使用单独的方法,可以稍微加快代码执行速度。 加载映像后,CPU将一直处于空闲状态,直到使用NCS执行结果为止(在这种情况下,约为100毫秒),此时您可以做一些有用的工作:读取一个新帧并将其转换,并显示以前的检测结果。 这就是演示程序的实现方式,在我看来,它会稍微提高FPS。 您可以走得更远,并在两个不同的流中异步启动图像处理和面部检测器-这确实有效,并且可以加快速度,但是在演示程序中未实现。
结果,检测器返回大小为
7*(keep_top_k+1)
的float数组。 在这里,
keep_top_k
是在模型的
.prototxt
文件中指定的参数,并显示应返回多少次检测(以置信度递减的顺序)。 可以在最后一层的模型
.prototxt
文件中配置此参数以及负责按最小置信度值过滤检测的参数和非最大抑制参数。 值得注意的是,如果Caffe返回的检测数量与图像中找到的检测数量相同,则NCS始终返回检测的
keep_top_k
以便数组大小恒定。
结果数组本身的组织方式如下:如果我们将其视为具有
keep_top_k+1
行和7列的矩阵,则在第一行中,第一个元素中将有检测次数,从第二行开始,将以
"garbage, class_index, class_probability, x_min, y_min, x_max, y_max"
的格式进行检测
"garbage, class_index, class_probability, x_min, y_min, x_max, y_max"
。 坐标在[0,1]范围内指定,因此需要将其乘以图片的高度/宽度。 数组的其余元素将是垃圾。 在这种情况下,即使在获得结果之前(似乎恰好在NCS上),也会自动执行非最大抑制。
检测器解析 void get_detection_boxes(float* predictions, int w, int h, float thresh, std::vector<float>& probs, std::vector<cv::Rect>& boxes) { int num = predictions[0]; float score = 0; float cls = 0; for (int i=1; i<num+1; i++) { score = predictions[i*7+2]; cls = predictions[i*7+1]; if (score>thresh && cls<=1) { probs.push_back(score); boxes.push_back(Rect(predictions[i*7+3]*w, predictions[i*7+4]*h, (predictions[i*7+5]-predictions[i*7+3])*w, (predictions[i*7+6]-predictions[i*7+4])*h)); } } }
Raspberry Pi启动功能
该演示程序本身可以在具有Ubuntu的常规计算机或便携式计算机上运行,或在具有Raspbian Stretch的Raspberry Pi上运行。 我正在使用Raspberry Pi 2 B型,但该演示也应在其他型号上运行。 该项目的makefile包含两个用于切换模式的目标:
make switch_desk
用于计算机/笔记本电脑,以及
make switch_rpi
用于Raspberry Pi。 程序代码的根本区别在于,在第一种情况下,OpenCV用于从相机读取数据,在第二种情况下,使用
RaspiCam库。 要在Raspberry上运行演示,您必须编译并安装它。
现在很重要的一点:安装NCSDK。 如果您遵循Raspberry Pi上的标准安装说明,它将不会有任何好处:安装程序将尝试拖动并编译SSD-Caffe和Tensorflow。 相反,NCSDK需要
在仅API模式下进行
编译 。 在这种模式下,只有C ++和Python API可用(也就是说,将无法编译和分析神经网络图)。 这意味着必须首先在常规计算机上编译神经网络图,然后将其复制到Raspberry。 为了方便起见,我向存储库添加了两个编译文件,分别是YOLO和SSD。
另一个有趣的地方是NCS与Raspberry的纯粹物理连接。 将它连接到USB连接器似乎并不难,但是您需要记住,它的外壳会阻塞其他三个连接器(它非常健康,因为它可以充当散热器)。 最简单的方法是通过USB电缆连接。
还需要牢记的是,不同版本的USB的执行速度会有所不同(对于该特定的神经网络:USB 3.0为102毫秒,USB 2.0为92毫秒)。
现在介绍NCS的功能。 根据文档,它消耗的功率高达1瓦(在USB连接器上为5伏时,它将高达200毫安;相比之下:Raspberry相机的最大消耗为250毫安)。 当使用2安培的常规5伏充电器供电时,一切正常。 但是,尝试将两个或多个NCS连接到Raspberry可能会导致问题。 在这种情况下,建议使用带有外部电源的USB分配器。
在Raspberry上,演示速度比在计算机/笔记本电脑上慢:7.2 FPS对10.4 FPS。 这是由于几个因素造成的:首先,无法摆脱CPU上的计算,但执行速度要慢得多。 其次,数据传输速度会影响(对于USB 2.0)。
另外,为了进行比较,我在第一篇文章中尝试在Raspberry YOLOv2上运行人脸检测器,但是效果很差:以3.6 FPS的速度,即使在简单的框架上,它也遗漏了很多人脸。 显然,它对输入图像的参数非常敏感,在Raspberry相机的情况下其质量远非理想。 尽管我不得不稍微调整RapiCam设置中的视频设置,但SSD的工作稳定得多。 他有时还会错过框架中的面孔,但是很少这样做。 为了增加实际应用中的稳定性,您可以添加一个简单的
质心跟踪器 。
顺便说一句:可以在Python中复制相同的内容,有一个
关于PyImageSearch的
教程 (Mobilenet-SSD用于对象检测任务)。
其他想法
我还测试了一些想法来加速神经网络本身:
第一个想法:您只能保留对
conv11
和
conv13
层的检测,并删除所有多余的层。 您将获得一个检测器,该检测器只能检测小脸并且工作更快。 总而言之,不值得。
第二个想法很有趣,但是没有用:我试图从权重接近零的神经网络中抛出卷积,希望它会变得更快。 但是,这种卷积很少,除去它们只会使神经网络稍慢一点(唯一的预感:这是由于通道数不再是2的幂的事实)。
结论
作为机器人项目的子任务,我想了很长时间在Raspberry上检测人脸。 我在速度和质量方面不喜欢经典的检测器,因此决定尝试神经网络方法,同时测试Neural Compute Stick,其结果是在GitHub上有两个项目,在Habré上有三篇文章(包括当前的一篇)。 通常,结果适合我-最有可能的是,我将在机器人中使用此检测器(也许还会有另一篇文章)。 值得注意的是,我的解决方案可能不是最佳的-尽管如此,这是一个培训项目,部分原因是出于对NCS的好奇心。 尽管如此,我希望本文对某人有用。