原文:How fast is my model?
译者注:译者接触深度学习的时间不长,文中部分词汇不知道如何翻译,可能有些地方翻译的也不是很通顺。总之,还是那句话,译者笔力有限,英文水平过得去的可以看英文原文。
在移动设备上运行深入学习时,模型预测的准确性并不是唯一要考虑的因素,你还需要注意以下问题:
- 在App发布包中占用的空间——单个模型有能会使你的App下载体积增加几百MB。
- 运行时占用的内存量——在iPhone和iPad上,GPU可以使用设备中的所有RAM,但是总共也只有几GB,当空闲内存用完时,应用程序会被操作系统终止。
- 模型运行速度——尤其是在处理实时视频或大图像时(如果模型需要几秒钟来处理单个图像,那么使用云服务可能更好)。
- 耗电速度——多久会把电池耗干,或使设备热得难以接受。
学术论文的作者通常不担心这些事情。他们可以在有多块GPU的服务器或计算集群上运行他们的模型。但如果您计划将这种模型转换到移动设备上运行,您将需要了解该模型在目标设备上的运行速度以及使用的电池电量。
测量模型速度的最好方法是连续运行多次,并取平均运行时间。单一测量结果可能有相当大的误差——CPU或GPU可能正忙于执行其他任务(例如绘制屏幕)—— 多次运行取平均值,将大大减少该误差。
当然,这假设您已经有了一个可以在设备上运行的模型。
在开始训练模型之前,对您的模型多做一些理论上的分析是非常有价值的,因为训练非常耗费资源。
案例研究:我的一个客户最近用MobileNetV2层替换了他们模型中的MobileNetV1层。V2使用的运算比V1少得多的,所以您可能会认为这种改变会使模型更快(模型中有许多额外的层,但是这些层没有改变)。
在V2中,他们设定depth multiplier值为1.4,这给每层增加了更多的滤波器,但是这仍然导致网络比以前具有更少的参数。即便如此,我还是预感到,V2层的特定配置不会比原来的V1层快很多。
结果证明我的预感是正确的——这个V2模型实际上更慢!在这篇文章中,我将展示为什么会这样,以及如何在理论层面用数学预估时间。
计算
获得模型的速度的一种方法是概括地统计它的计算量。我们通常统计计算量使用FLOPs(浮点运算数),但是这里我们使用MACCs或乘法累加数(multiply-accumulate operations )。
译者注:FLOPS和FLOPs的区别:
- FLOPS:注意全大写,是floating point operations per second的缩写,意指每秒浮点运算次数,理解为计算速度。是一个衡量硬件性能的指标。
- FLOPs:注意s小写,是floating point operations的缩写(s表复数),意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。
注意:在继续之前,我必须指出,单独统计计算数量并不能告诉您需要知道的所有信息。计算计算数量只是为了大致了解模型的计算成本是有用的,但是其他因素,例如内存带宽,通常更重要(稍后我们将进行讨论)。
点积
我们为什么要统计乘法累加(multiply-accumulate)呢?因为神经网络中的很多操作都是点乘,像下面这样:
y = w[0]*x[0] + w[1]*x[1] + w[2]*x[2] + ... + w[n-1]*x[n-1]
这里,w和x是两个向量,结果y是一个标量(单个数字)。
在卷积层或全连接层(现代神经网络中的两种主要层)中,W是层的学习权重,X是层的输入。
Y是层的输出之一。通常一个层会有多个输出,所以我们需要计算许多点积。
我们把 w[0]*x[0]+… 计作一次乘法累加(multiply-accumulate)或1 MACC。
这里的“累积”运算是加法,因为我们把所有乘法的结果加了起来。上面的公式中含有n个MACC。
因此,大小为n的两个向量之间的点积的运算量为n MACCs。
注:严格的所,上述公式中只有n-1个加法,比乘法数少一个。这里MACC的数量是一个近似值,就像Big-O符号是一个算法复杂性的近似值一样。
以FLOPS为统计单位的话,点积执行2n-1次FLOPS,因为有n个乘法和n-1加法。
所以一个MACC大约是两个FLOPS,乘法累加(multiply-accumulate)非常常见,以至于很多硬件可以把乘法加法运算融合到一条MACC指令的情中。
译者注:一条MACC指令执行以下操作
a <- a + (b * c)
接下来让我们来分析几个不同的层类型,以及如何计算这些层的MACC数量。
全连接层
在全连接的层中,所有输入都连接到所有输出。对于具有i输入值和j输出值的层,其权重w可以存储在i×j矩阵中。完全连接层执行的计算是:
y = matmul(x, W) + b
这里,x是包含i个输入值的向量,w是包含层权重的i×j矩阵,b是包含j个偏移值的向量。结果y是由层计算的输出值,也是大小为J的向量。
为了计算MACC的数量,我们需要研究点积在哪里发生。对于全连接层,点积发生在矩阵乘法matmul(x,w)之中。
矩阵乘法就是一组点积。每个点积都在输入x和矩阵w中的一列之间。两者都有i个元素,因此这算作i MACCs。我们需要计算j列数据,所以MACC的总数是i×j,和权重矩阵的大小一样。
偏移b并不真正影响MACC的数量。回想一下,一个点积的加法比乘法少一个,所以加上这个偏差值只会被最后的运算吸收。
示例:一个具有300个输入神经元和100个输出神经元的完全连接层执行300×100=30000 MACCs。
注:有时,完全连接层的公式编写时没有明确的偏差值。在这种情况下,偏差向量作为一行添加到权重矩阵中,使其大小为(i+1)×j,但这实际上是一种数学简化——我认为该操作从未在真正的软件中那样实现过。在任何情况下,它只会增加j个额外的乘法,所以MACC的数量不会受到很大的影响。记住这是一个近似值。
通常,将长度为i的向量与i×j矩阵相乘得到长度为j的向量,运算量为i×j MACCs或(2i-1)×j FLOPs。
如果完全连接层直接跟随卷积层,则其输入大小可能不是指定为单个矢量长度i,而是指定为具有形状(512、7、7)的特征图。一些包(如keras)要求您首先将这个输入“展平”到一个向量中,即 i=512×7×7。从数学角度看,二者相同。
注:在所有这些计算中,我假设批量大小为1。如果您想知道大批量B的macc数量,那么只需将结果乘以b。
激活函数
通常一个层后面跟着一个非线性的激活函数,如relu或sigmoid。当然,计算这些激活函数需要时间。我们不使用MACCs中测量它的计算量,因为它们不是点积运算,这里要使用FLOPs。
有些激活函数比其他函数更难计算。例如,relu的表达式为:
y = max(x, 0)
这是GPU上的单个操作。激活函数仅应用于层的输出。在一个与j输出神经元全连接的层上,relu使用这些计算中的j,所以我们称之为j FLOPs。
sigmoid激活函数的成本更高,因为它需要取一个指数:
y = 1 / (1 + exp(-x))
在计算FLOPs时,我们通常将加法、减法、乘法、除法、求幂、平方根等作为单个FLOP进行计数。由于在sigmoid函数中有四种不同的操作,因此每个输出将计为4个FLOP,总层输出为j×4个FLOPs。
实际上,我们经常不统计这些操作,因为它们只占总时间的一小部分。我们主要对(大)矩阵乘法和点积感兴趣,因为它们耗时较多,相比之下激活函数耗时可忽略。
总之:不用担心激活函数耗时。
卷积层
卷积层的输入和输出不是向量,而是尺寸为h×w×c的三维特征图,其中h是特征图的高度,w是宽度,c是每个像素的通道数。
今天使用的大多数卷积层都有卷积核。对于内核大小为k的conv层,MACC的数量为:
K × K × Cin × Hout × Wout × Cout
- Hout × Wout 对应输出特征图中的像素数目
- K x K 卷积核的宽度和长度
- Cin 输入通道数
- Cout 是卷积核的数目,即输出的维度
同样,我们忽略这里的偏移量和激活函数。
我们不应该忽略的是卷积的步长,以及任何膨胀因素、填充等。这就是为什么我们要查看层的输出特征图的尺寸Hout × Wout,而不是输入尺寸。
示例:对于具有128个滤波器的3×3卷积,在具有64个通道的112×112输入特征映射上,MACC的数量为:
3 × 3 × 64 × 112 × 112 × 128 = 924,844,032
这几乎是10亿次乘法运算!必须让GPU忙着…
注意:在这个例子中,我们使用了padding= same和strides =(1, 1),这样输出特征纬度的大小与输入特征纬度的大小相同。同样常见的是,卷积层使用stride=2,这会将输出特征图大小减半,我们在上面的计算中使用56×56而不是112×112。
Depthwise Separable Convolution深度可分离卷积
深度可分离卷积是将常规的卷积分解为两个较小的运算,Depthwise(DW)卷积与Pointwise(PW)卷积。它们一起占用了更少的内存(更少的权重),速度更快。当然,这只是一个“完整”的卷积层所能做的,因此您可能实际上需要更多的卷积层来在您的模型中获得相同的表达能力,但即使这样,您仍然可以脱颖而出。
这些类型的层在移动设备上运行良好,是MobiNeNET模型的基础,也是更大的模型(如Xception)的基础。
第一个运算Depthwise(DW)卷积是反方向卷积。它在许多方面类似于常规卷积,只是不组合输入通道。输出通道的数量始终与输入通道的数量相同。
Depthwise(DW)卷积层的MACC数为:
K × K × C × Hout × Wout
这做了一个系数C较小的工作,使这比一个常规的卷积层效率高很多。
示例:在具有64个输入通道的112×112功能图上进行3×3纵向卷积,MACC的数量为:
3 × 3 × 64 × 112 × 112 = 7,225,344
请注意,这种卷积总是具有与输入通道完全相同的滤波器,并且每个滤波器仅在单个通道上工作。这就是上面计算中没有系数×128的原因。
注:有一种叫做“纵向通道乘数”。如果这个乘数大于1,则每个输入通道都有D个输出通道。因此,现在每个通道都有D个过滤器,而不是每个通道只有一个过滤器。但我在实践中并没有看到这一点。
仅仅是深度可分离卷积是不够的,我们还需要添加“可分离”位。第二个操作是常规卷积,但始终使用内核大小1×1,所以被称为Pointwise(PW)卷积。
对于Pointwise(PW)卷积层的MACC数为:
Cin × Hout × Wout × Cout
这里K = 1
示例:我们以有112×112×64特征映射的深度卷积的输出为例,将其投影到128个维度中,以创建新的112×112×128特征映射。MACC的数量为:
64 × 112 × 112 × 128 = 102,760,448
正如您所看到的,Pointwise(PW)卷积比Depthwise(DW)卷积运算量多很多倍。但是,如果我们把它们放在一起,总的MACC数要比常规3×3卷积要少得多:
3×3 depthwise : 7,225,344
1×1 pointwise : 102,760,448
depthwise separable : 109,985,792 MACCs
regular 3×3 convolution: 924,844,032 MACCs
常规卷积的计算量大约是深度可分离卷积的8.4倍!
当然只是简单比较这两种层有点不公平,因为常规的3×3卷积更具表现力:它可以计算出更有趣的东西。但同样的成本,与常规卷积相比你可以使用8倍以上的深度可分离卷积层,或给他们更多的过滤器。
为了完整起见,深度可分离卷积层的总MACC为:
(K × K × Cin × Hout × Wout) + (Cin × Hout × Wout × Cout)
可简写为
Cin × Hout × Wout × (K × K + Cout)
如果将它与常规卷积层的公式进行比较,您会注意到唯一的区别是,常规卷积层我们用了×cout,而这里它是+cout。做加法而不是乘法有很大的不同…
根据经验,使用深度可分离卷积层的成本几乎比常规卷积层低一倍。在上面的例子中,它是一个系数8.4,实际上几乎与k×k=3×3=9相同。
注:系数准确值为: K × K × Cout / (K × K + Cout).
还应该指出,Depthwise(DW)卷积层有时的步幅大于1,这会减小其输出特征图的尺寸。但是,一个Pointwise(PW)卷积层的步幅通常为1,因此它的输出特征图将始终具有与Pointwise(PW)卷积层相同的尺寸。
深度可分离层是Mobilenet v1的主要组成部分。然而,Mobilenet v2稍微改变了一些,添加了三层组成的“扩展块”:
- 一个1×1的卷积,将更多的通道添加到特征图中(称为“扩展”层)
- 一个 Depthwise(DW)卷积用以过滤数据
- 一个1×1的卷积,它再次减少了通道的数量(作为瓶颈卷积的“投影”层)
下面是此类扩展块中MACC的数量公式:
Cexp = (Cin × expansion_factor)
expansion_layer = Cin × Hin × Win × Cexp
depthwise_layer = K × K × Cexp × Hout × Wout
projection_layer = Cexp × Hout × Wout × Cout
这些都是我之前给出的公式。expansion_factor用于为深度层创建额外的通道,使Cexp成为该块内使用的通道数。
注:扩展层的输出尺寸为Hin × Win,因为1×1的卷积不会改变特征图的宽度和高度。但如果Depthwise(DW)卷积层的步幅大于1,那么hout×wout将不同于hin×win。
把所有这些放在一起:
Cin × Hin × Win × Cexp + (K × K + Cout) × Cexp × Hout × Wout
如果步幅为1,则简化为:
(K × K + Cout + Cin) × Cexp × Hout × Wout
和v1使用的深度可分离层相比,如果我们使用112×112×64作为输入特征,扩展因子为6,3×3深度卷积,步幅为1,128个输出通道,那么MACC的总数是:
(3 × 3 + 128 + 64) × (64 × 6) × 112 × 112 = 968,196,096
这真的比以前好多了吗?是的,它甚至比原来的3×3卷积还要多。但是…请注意,由于扩展层的原因,在这个块中,我们实际上使用64×6=384个通道。因此,这组层要比原来的3×3卷积做工作的多很多(它“只”从64到128个通道),但它们计算成本大致相同。
批归一化(Batch Normalization)
在新式的网络中,通常在每个卷积层之后都包含一个batchnorm层,它的耗时如何?
batchnorm获取上一层的输出,并对每个输出值应用以下公式:
z = gamma * (y - mean) / sqrt(variance + epsilon) + beta
这里,Y是上一层的输出要素图中的一个元素。我们首先通过减去该输出通道的平均值 mean 并除以标准偏差(epsilon用于确保不除以0,通常是0.001之类的值)来规范化该值。然后我们通过一些因子gamma进行缩放,并添加一个偏差或偏移beta。
每个通道都有自己的gamma、beta、mean和variance值,因此如果卷积层的输出中有C通道,那么batchnorm层就学习了C×4个参数。
这似乎是相当多的FLOPS,因为上面的公式要应用于输出特征图中的每个元素。
然而…… batchnorm层常常应用于卷积层之后,非线性层(relu)之前。在这种情况下,我们可以做一些数学运算,使batchnorm层消失!
由于在完全连通层中进行的卷积或矩阵乘法只是一组点积,它们是线性变换,上面给出的batchnorm公式也是线性变换,因此我们可以将这两个公式合并为一个变换。
换句话说,我们可以将batchnorm层的学习参数“折叠”到前一个卷积/完全连接层的权重中。
将batchnorm参数折叠成前面层的权重的数学方法相当简单。在上面的公式中,Y表示上一层的单个输出值。让我们将y扩展到它的计算中:
z = gamma * ((x[0]*w[0] + x[1]*w[1] + ... + x[n-1]*w[n-1] + b) - mean)
/ sqrt(variance + epsilon) + beta
这是一个点积,要么来自卷积核,要么来自矩阵乘法。x表示输入数据,w是该层的权重,b是该层的偏差值。
为了将批处理规范参数折叠到前一层,我们要重写这个方程,使gamma、beta、mean和variation只适用于w和b,但不包含x。对公式做了一点修改:
w_new[i] = w[i] * gamma / sqrt(variance + epsilon)
b_new = (b - mean) * gamma / sqrt(variance + epsilon) + beta
这里,w_new[i]是第i个权重的新值,b_new是层偏差的新值。
从现在开始,我们可以使用这些值作为卷积层或完全连接层的权重。我们现在可以写为:
z = x[0]*w_new[0] + x[1]*w_new[1] + ... + x[n-1]*w_new[n-1] + b_new
这个结果与之前的结果完全相同,但不需要使用批处理规范层。
如果你不相信我,替换上述公式中的w_new和b_new的值,并简化。你应该重新得到原始的批量标准公式。
请注意,紧跟在batchnorm后面的层本身通常没有偏倚b,因为batchnorm层已经提供了一个偏倚b(beta)。在这种情况下,b_new的公式变得更简单(我们将b设置为0):
b_new = beta - mean * gamma / sqrt(variance + epsilon)
长话短说:我们完全可以忽略批处理规范层的影响,因为我们在进行推理时实际上将其从模型中移除。
注:此技巧仅在层的顺序为:卷积、批范数、relu-时有效,但不适用于:卷积、relu、批范数。RELU是一个非线性运算,它会扰乱数学。(尽管我认为如果一个批处理规范紧接着是一个新的卷积层,那么您可以反过来折叠参数。不管怎样,你的深度学习库已经为你做出了这些优化。)
其他卷积层
未完待续。。。