向手机端神经网络进发:MobileNet压缩指南
选自Machine Think
作者:Matthijs Hollemans
机器之心编译
参与:机器之心编辑部
随着 MobileNet 等面向移动设备的模型不断出现,机器学习正在走向实用化。不过,由于深度学习等方法需要消耗大量计算资源的因素,目前我们距离真正的移动端人工智能应用还有一段距离。在硬件之外,我们也需要对模型本身进行压缩,最近,荷兰工程师 Matthijs Hollemans 向我们展示了他压缩 MobileNet 的方法:通过删除卷积层的部分滤波器,他在保证准确性不变的情况下,让模型体量缩小了 25%,让我们看看他是怎么做的。
随着机器学习技术向移动设备渗透的趋势,人们正在越来越注重于寻找让深度神经网络更快、更简洁的方式。
一种方法是提出更智能化的神经网络设计。例如:MobileNet 可以在获得相同结果的情况下比 VGG-16 小 32 倍,速度快上 10 倍。
另一个方法是采用现有的神经网络,并用删除与结果无关的神经元的方法来压缩它。本文会着重介绍这种方法。
我们将着手改进 MobileNet-224,让它的体量减小 25%,换句话说,我们要把它的参数从 400 万个减少到 300 万个——同时不损失模型的准确性(好吧…只有一点点)。
如何做到更好
鉴于 MobileNet 比 VGG16 要小 32 倍,而准确性相同,前者捕获知识的效率显然更高。
的确,VGG 模型中的神经网络连接比我们所需要的多很多。斯坦福大学韩松等人 2015 年在论文《Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding》中提出的压缩网络方法展示了通过剪枝不必要的神经网络连接让 VGG16 缩小 49 倍,并保持准确性的方法。
现在问题来了:MobileNet 里还有不必要的连接接吗?即使这个模型已足够小,但我们能不能让它变得更小且不损失准确性呢?
当你试图压缩一个神经网络,需要权衡的问题是模型尺寸与准确性。通常,网络越小,速度就越快(当然也耗电更少),但预测出来的结果也会越差。例如,MobileNet 的性能要好过 SqueezeNet,同时前者也比后者大上 3.4 倍。
在理想情况下,我们总是希望找到尽可能小的神经网络——不过它们必须为我们传递准确的结果。这在机器学习中是一个开放问题,在正确的理论出现之前,让我们先试着从大模型开始剪枝吧。
在这个项目中,我使用了 Keras 2.0.7 版中预训练的 MobileNet,并运行在 TensorFlow1.0.3 上。如果使用 ImageNet ILSVRC2012 验证集来测试,它的得分是:
Top-1 accuracy over 50000 images =68.4%
Top-5 accuracy over 50000 images =88.3%
这意味着它有 68.4% 的几率一次性给出正确结果,如果范围扩大到排名前五的结果,则准确率则为 88.3%。我们希望压缩这个模型,并让它保持以上分数。
注意:MobileNet 论文(MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications)声称它在 ImageNet 上的准确率是 70.6%,VGG16 是 71.5%,GoogleNet 是 69.8%。我们还不知道这个数据是来自 ImageNet 还是验证集。无论如何,Keras 版的 MobileNet 在验证集上的得分是 68.4%,我们将会使用这个数字作为基准。
如何压缩一个卷积神经网络
就像多数的现代神经网络一样,MobileNet 有着许多卷积层。将每层的权重由小至大排列并剔除带有最小权重的连接就是一种压缩卷积层的方法。
这种方法是由 Han 等人在将 VGG 压缩 49 倍时提出的。听起来是个好方法但也存在着大缺陷:它造成了稀疏连接。不幸的是,GPUs 并不擅长处理稀疏矩阵,并且在计算上花费了更多的时间,尽管网络得到了压缩,你并未由此而省时。在这种情况下,压缩得更小并不意味着会更快。
但这对于追求魔鬼速度的我们并不奏效:小而快才是我们所追求的。我们将移除复杂的卷积滤波器,而不是修剪掉单个连接。这让我们保持连接紧密的同时也不会给 GPU 带来麻烦。回想一下一个卷积层产出一个带有一组特定数量的输出通道的图像。每一个输出通道都包含了单一卷积滤波器带来的影响。这样一个滤波器接管了所有来自于输入通道的加权和,并将这一加权和写入单一的输出通道。
我们要找出那种最不重要的卷积滤波器,并且将其输出通道从层中移除。
例如,MobileNet 中的层 conv_pw_12 有 1024 个输出通道。我们会舍弃这其中的 256 个通道使得被压缩版的 conv_pw_12 只有 768 个输出通道。
注意:为了迎合 Metal,我们应该以每次移除四个输出通道的速度进行。因为 Metal 实际上是一个图形 API,它用纹理(texture)来描述神经网络的图像数据,而每一个纹理为四个连续的通道保存数据。所以,如果我们只移除一个输出通道,Metal 仍然需要处理其他三个通道的纹理。考虑到 Metal 的这一特点,我们只有以四的倍数来移除通道才会使得压缩层有意义。
现在问题是:我们可以移除哪些滤波器和输出通道?我们仅希望在不影响太多性能的情况下移除一些输出通道。
我们可以使用不同的度量方案来估计滤波器的相关性,但是我们也可以选择非常简单的方法:滤波器权重的 L1 范数,即所有滤波器权重的绝对值之和。
例如以下是 MobileNet 前面几个卷积层(32 个滤波器)的 L1 范数:
如图所示,L1 范数非常小、接近于零的第一个层有 10 个滤波器。我们或许可以去除这些滤波器。但是由于我们的目标是使用带有 Metal 的网络,因此去除 10 个滤波器没有意义。我们必须去除 8 个或 12 个滤波器。
我首先尝试去除 8 个最小的滤波器。效果很好,准确率完全没有损失,因此我决定去除 12 个。稍后你可以看到,它的效果依然很好。这意味着我们实际上能够去除该网络第一个卷积层中 37.5% 的滤波器,而网络性能不会变差!
这里有 MobileNet 所有卷积层的 L1 范数示图。你可以看到很多层的滤波器对网络几乎没有贡献(低 L1 范数)。
注:由于并非所有层都具备相同数量的输出通道,上图中所有数据都按同样的标准进行归一化处理。横轴代表通道(按 L1 范数的值从低到高),纵轴代表实际的 L1 范数(同样是归一化处理后的)。
你可以在 Li et al 所写的论文《Pruning Filters For Efficient Convnets》中获取该方法的更多详情。
从一个层中去除滤波器意味着该层输出通道的数量变少。自然而然,这对该网络下一层也有影响,因为下一层现在的输入通道变少了。
因此,我们还必须从那一层去除对应输入通道。当卷积之后是批量归一化(BN)时,我们还必须从批量归一化参数中去除这些通道。
MobileNet 事实上有三种卷积层:
一个常规的 3×3 卷积(第一层)
Depth-wise 卷积
1×1 卷积(又名 pointwise convolution)
我们仅从 3×3 和 1×1 卷积中去除滤波器,而非 Depth-wise 卷积。一个 Depth-wise 卷积必须具备相同数量的输出通道和输入通道。压缩可能没有什么收获,而且 Depth-wise 卷积很快(因为它们的工作比常规卷积要少)。因此我们主要将注意力集中在带有 3×3 和 1×1 卷积的层。
再训练
因为从层中删除滤波器会让网络的准确性变差——毕竟,你在丢弃神经网络学习到的东西,即使它可能不是非常重要——你需要做一点重新训练,这样才能弥补丢弃造成的损失。
在训练意味着你需要再次调用 model.fit()。一点小小的试错后,我们就会把学习率定在 0.00001——一个非常小的数字,任何稍大的训练参数都会让结果超出控制。学习率如此之小的原因是,在这里,大部分网络已经被训练过了,我们只想进行小改进来提升结果。
过程是这样:
1. 在层中 4 倍并行地移除滤波器(即输出通道)
2. 再训练该神经网络几个 epoch
3. 对验证集进行评估,以检查该神经网络的准确性是否恢复至之前水准
4. 移动到下一层并重复这些步骤
正如你所看到的,这个过程费时费力,因为我们每次只能改动一层,而修改之后每次又要重新训练神经网络。每个层中,可以丢弃的滤波器都是不太一样的。
使用训练子集
MobileNet 已在 ILSVRC 比赛数据集(也就是 ImageNet)中进行过预训练了。这是一个巨大的数据集,其中包含超过 120 万张训练图片。
依靠最近装配的深度学习机器(只有一块英伟达 GTX 1080Ti 的 Linux 系统),每个 epoch 需要训练两小时之久。即使用 5 万张图像验证集来做这件事也需要 3 分钟。
毫无疑问,这样的硬件让快速迭代变得难以实现。我可不想每天盯着屏幕两个小时,只为看到模型出现一点点小变化。所以我们得在样本上找办法,而不是用完整的数据集,我在数据集 1000 个类别中每类随机找出五张图片(这多少有点代表性),形成了 5000 张图片的训练子集。现在每个 epoch 只需要 30 秒钟了。这要比两个小时方便多了!
为了进行验证,我从完整验证集中随机抽取了 1000 张图片作为验证集,用它来评估网络性能只需要画上 3 秒钟。
看来,使用样本的方法很有效。
压缩第一个卷积层
如你所见,第一个卷积层有 10 个非常小的 L1 规范滤波器。因为对于 Metal,我们需要以 4 的倍数来去除滤波器,所以我删除了具有最小 L1 规范的 12 个滤波器。
最初,我还没有从神经网络中删除任何滤波器,只是将他们的连接权重设置为 0。理论上,这样的事情可以让 Top-1 准确率从 69.4% 降到 68.7%——有一点损失,不过没有什么不是在训练不能解决的。
接下来,我创建了一个与原始层相同的新模型,并在这里删除了滤波器,所以在第一个卷积层中,实际上只有 24 个输出通道(而不是原来的 36 个)。但是现在准确率评分变得很低了:29.9%,发生了什么?
理论上,将连接权重设置为 0 和删除连接应该可以获得相同的结果,但实践中却出了差错:我忘了将下一层相应输入通道的权重设置为 0。而更糟的是,因为下一层是深度卷积,我还得设置相应的参数,让该层的批量归一化为 0。
教训:从一层中去除滤波器也会对其它层产生影响。而这些变化会影响评分。
所以删除第一层中的滤波器损失 37.5% 的准确率不太值得?在检查整个模型后,我发现问题在于第二个批量范数(batch norm)层上 12 个偏置值:当它们变成其他任何数字后,其他的东西都归零了。
看起来,是这 12 个数字让 68.7% 的识别率变成了 29.9%,真的吗?这个神经网络有 400 万个参数,12 个数字肯定不能决定一切,在这样的思路下,我觉得我可以做点什么了。
我用 5000 张图片的子集重训练了神经网络 10 个 epoch(只用了五分钟),现在准确率重新回到了 68.4%,这虽然不及原模型(69.4%),但已经很接近了。
一点样本图像就让准确率回到了 65% 以上,这太简单了,请记住:我们用于重训练的样本大小只有整个数据集的 0.4%。我认为,如果这样一点图像就可以让分数大体回复,那么整个数据集的训练应该可以让准确率完全回归原水平。
注解:使用相同的样本进行长时间训练可不是什么好主意。当你用同样的子集训练 10 个 epoch 之后,神经网络就会严重过拟合。所以在使用了 10 个 epoch 之后,你得换一个新的训练子集。
现在,第一卷积层减少了 37.5% 的权重,这听起来不错,但是这只是小小的一层而已。它只有三个输入通道与 32 个输出通道(削减后为 24 个)。总共节省的参数是:3×3×3×12=324,效果太不明显了,不过这是一个好的开始。
最后的 PW 卷积层
在压缩第一层后,我们可以考虑压缩分类层之前的最后一个卷积层。
在 MobileNet 的 Keras 版中,分类层也正好是一个卷积层,但是我们不能从这一卷积层中移除任何通道。因为这个网络是在 ImageNet 中训练的,该数据集有 1000 个种类,因此分类层也必须有 1000 个输出通道。如果我们删除了任何通道,那么模型就不能再对这些类别做预测。
conv_pw_13 层有 1024 个输出通道,虽然并没有理论依据,但我们可以先尝试移除 256 个。conv_pw_13 层有 104.8576 万个参数,它在整个网络中都是最大的层,因此我们可以对它执行多一点的压缩。
这一层的 L1 范数如下所示:
从上图来看,移除 256 个通道可能有点多,留待观察。
再一次,我们移除了网络层的输出通道,然后是批规范化层,然后调整下一层,因为它们也有一些输入通道。
移除 256 个通道,不止为 conv_pw_13 节省了 1024×1×1×256 = 262,144 个参数,也为分类层移除了 256,000 个参数。
在压缩完 conv_pw_13 层之后,验证得分掉到了 60.7%(top1)和 82.9%(top5)。也没多糟糕,特别是考虑到还没再训练。
在样本上再训练 10 个 epochs 之后,准确率提升到了 63.6%。在新的训练样本上再多训练 10 个 epochs 之后,提升到了 65.0%(top1)和 86.1%(top5)。对 10 分钟的训练来说,不错了。
得到 0.65 的得分我已经很开心了,能够继续修剪其他的层。虽然还没得到最初的得分,但已经证明网络成功地补偿了修剪的连接。
更多层 & 真实再训练
接下来,我使用同样的方法修剪 conv_pw_10(移除了 512 个滤波器中的 32 个)和 conv_pw_12(移除了 1024 个滤波器中的 256 个)。
在每个新的网络层上,我发现要得到之前的标准准确率越来越难。在 conv_pw_10 之后准确率是 64.2%,conv_pw_12 之后只有 63.4%。
每次我使用不同的训练样本,只是为了确保结果仍旧具有代表性,模型不会过拟合。
在 pw_10 与 pw_12 之后,我做了 conv_pw_11。其实在选择压缩层上我没有总体规划,是随机选择的。
在 conv_pw_11 上,我修剪了 512 个滤波器中的 96 个。在其他层上,我最多修剪掉滤波器个数的 25%,部分是基于 L1-norms 所获得的信息进行修剪,但主要是因为它是很好的约整数。但是,移除 128 个滤波器导致准确率下降太多,再训练也无法提升到 60% 以上。只剪除 96 个滤波器有更好的结果,但再训练之后得分也只是 61.5%。
在验证得分如此令人失望的情况下,无疑是我过于激动,移除了太多的滤波器,导致神经网络不再能够学习 ImageNet。
所有的再训练都是在 5000 张图像的小样本上进行的,所以修剪过的网络只是在所有训练集上的一部分再训练的。时间限制了网络只能在全部训练集上运行几轮。
在 1 个 epoch 之后,准确率达到 66.4 (top 1)、0.87(top 5)。我并未使用数据增强,只使用了原始的训练图像。
既然已经开始,我决定多训练几个 epoch,看看会有什么不同结果。在第二个 epoch 之后,网络得分是 67.2(top1)、87.7(top 5)。
之后,我又在完整训练集上训练了更多的 epochs,且加入了数据增强,用了更小的学习率。不幸的是,这对结果改善毫无益处。
于是,我停止了实验,时间不够。在多训练几轮,我保证有很大可能把网络再次压缩。而且,还有 9 个 pw 卷积层我们还没处理,我保证这些层也能修剪掉一些滤波器。
结论
Original network size:4,253,864 parameters
Compressed network size:3,210,232 parameters
Compressed to:75.5% of original size
Top-1 accuracy over 50000 images =67.2%
Top-5 accuracy over 50000 images =87.7%
以上结果未能达到我的预期目标:网络确实压缩了 25%,但准确率有点差,虽然准确率没有损失 25%。
因为我顾及的是整个流程,所以该工作流中有些地方可以改进。在选择需要移除的滤波器上,或者压缩网络层的顺序上,我做的也不科学。但对该项目而言,足够了,我只是想要获取可能压缩神经网络的思路。
可以明显看到,我未能做到对网络的最优修剪。使用 L1-norms 可能不是确定滤波器重要度的最佳方式。也可能一次性只移除一些滤波器,而非砍掉网络层输出通道的 1/4,这样会更好一些。我很开心使用的样本在再训练中表现非常好,不需要几个小时进行再训练,这意味着我能快速的进行新实验。
这件事是否值得做呢?我觉得是。假设你有一个神经网络在手机端运行需要 25FPS,意味着每帧需要 0.04 秒的处理时间。如果网络压缩了 25%,假设这意味着处理速度快了 25%,也就是每帧的处理时间为 0.03 秒,节约了很大的时间。在运行流畅与运行不畅的 APP 上,可能会有细微差别。