转自: https://zhuanlan.zhihu.com/p/41296455
在移动设备上进行深度学习时,模型预测的好坏并不是唯一的考虑因素。你还需要担心:
· 模型在应用程序包中占用的空间量- 单个模型有可能为应用程序的下载大小增加100个MB
· 它在运行时占用的内存量- 在iPhone和iPad上,GPU可以使用设备中的所有RAM,但这仍然只有几GB,当你的可用内存耗尽时,应用程序会被操作系统终止
· 模型运行的速度有多快 - 特别是在使用实时视频或大图像时(如果模型需要几秒钟来处理单个图像,那么使用云服务可能会更好)
· 它有多快耗尽电池 - 或使设备太热而无法运行!
学术论文的作者通常不担心这些事情。他们可以在桌面GPU或计算群集上运行他们的模型。但是如果你有兴趣将这种模型转换为在移动设备上运行,你需要知道模型在目标设备上的速度以及它使用的电池功率。
测量模型速度的最佳方法是连续多次运行并获取平均经过的时间。任何单次运行测量的时间可能具有相当大的误差 - CPU或GPU可能正在忙于执行其他任务(例如,绘制屏幕) - 但是当您对多次运行进行平均时,这将显着缩小误差。
由于训练费用昂贵,因此在开始训练之前,对模型的完成程度有一定的理论洞察力是非常必要的。
以MobileNet V1 和 MobileNetV2为例:
MobileNetV2层替换了他们模型中的MobileNetV1层。V2使用的计算量比V1少得多,因此您认为此更改会使模型更快(模型中有许多其他层,但这些层没有更改)。
对于V2的层,使用深度乘数1.4,这为每一层增加了更多的过滤器,但这仍然导致网络参数比V1少。即便如此,我还是预感到,特定配置的V2层不会比原始V1的层快得多。
事实证明我的预感是正确的 - V2模型实际上更慢!在这篇博文中将展示为什么会这样,以及如何计算这些值。
计算量
了解模型速度的一种方法是简单计算它执行的计算量。我们通常将其视为FLOPS,每秒浮点运算。稍有不同的其他度量是MACC或乘法累加运算,也称为MADD。
注意:在继续之前,我必须指出,计算出自己的计算次数并不能告诉你需要知道的一切。计算计算次数只是为了 大致了解模型的计算成本,但其他因素(如内存带宽)通常更为重要(我们稍后会详细介绍)。
点乘
为什么要多累积?神经网络中的许多计算都是点乘,例如:
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] + ...当作一个乘法累加或1个MACC。这里的“累积”操作是加法,因为我们把所有乘法的结果相加。上述公式包含n个这样的MACC。因此,两个大小为n的矢量之间的点积需要n个MACC。
注意:从技术上讲,上述公式中只有n-1个MACC,比乘法数少一个。将MACC的数量视为近似值,就像Big-O表示法是算法复杂度的近似值一样。
就FLOPS而言,点积产生2n - 1个FLOPS,因为存在n次乘法和n - 1次加法,。因此一个MACC大约相当于两个FLOPS,尽管乘法累加是如此常见以至于许多硬件可以进行合成乘法 - 加法运算,这样MACC是单个指令。
现在让我们看一些不同的层类型,以了解如何计算这些层的MACC数量。
全连接层
在全连接层中,所有输入都连接到所有输出。对于具有I个输入值和J个输出值的层,其权重W可以存储在I × J矩阵中。全连接层执行的计算是:
y = matmul(x, W) + b
这里,x是I个输入值的矢量,W是包含层权重的IXJ矩阵,b是包含J个元素的偏置值矢量。结果y包含由层计算的输出值,也是大小为J的向量。
为了计算MACC的数量,我们来看看点乘的发生位置。全连接层是矩阵相乘matmul(x, W)。
矩阵乘法只包含一大堆的点积运算。每个点积都在输入x和矩阵W的一列间发生。两者都有个I元素,因此这算作I个MACC。我们必须计算J个这样的点积,因此MACC的总数I × J与权重矩阵的大小相同。
偏置b并不会真正影响MACC的数量。回想一下,无论如何,点积比加法少一个,所以添加这个偏置值只会在最后的乘法累加中被吸收。
示例:具有300个输入神经元和100个输出神经元的全连接层执行300 × 100 = 30,000个MACC。
注意:有时,全连接层的公式是在没有明确偏置值的情况下编写的。在这种情况下,偏置向量作为一行添加到权重矩阵中,所以是(I + 1) × J,但这实际上更像是一种数学简化 - 我认为这种操作并不像真实软件那样实现。在任何情况下,它只会增加J个额外的乘法,所以无论如何MACC的数量都不会受到太大影响。记住这是一个近似值。
通常,将长度I的向量与I × J矩阵相乘以得到长度为J的向量,需要I × J个MACC或(2I - 1) × J个FLOPS。
如果全连接层直接跟随卷积层,则其输入大小可能不会被指定为单个矢量长度I,但是可能被指定为具有诸如形状(512, 7, 7)的特征图。例如Keras要求你先将这个输入“压扁”成一个向量,这样就可以得到I = 512×7×7个输入。
注意:在所有这些计算中,我假设批量大小为1.如果您想知道批量大小为B的MACC,那么只需将结果乘以B。
激活函数
通常,全连接层之后是非线性激活函数,例如ReLU或sigmoid。当然,计算这些激活函数需要时间。我们不使用MACC测量它而是用FLOPS,因为它们不是点积。
某些激活函数比其他函数更难计算。例如,ReLU:
y = max(x, 0)
这是GPU上的单个操作。激活仅应用于层的输出。在具有J个输出神经元的全连接层上,ReLU使用J个这样的计算,所以有J个FLOPS。
Sigmoid激活成本更高,因为它涉及一个指数:
y = 1 / (1 + exp(-x))
在计算FLOPS时,我们通常将加法,减法,乘法,除法,取幂,平方根等计为单个FLOP。由于sigmoid函数中有四个不同的操作,因此每个输出计为4 FLOPS或总层输出计为J × 4个FLOPS 。
实际上通常不计算这些操作,因为它们只占总时间的一小部分。我们最感兴趣的是(大)矩阵乘法和点积,我们只是假设激活函数是免费的。
总结:不需要担忧激活函数。
卷积层
卷积层的输入和输出不是矢量,而是三维特征图H × W × C,其中H是特征图的高度,W宽度和C是通道数。
今天使用的大多数卷积层都是方形内核。对于具有内核大小K的卷积层,MACC的数量为:
K × K × Cin × Hout × Wout × Cout
以下是该公式的来源:
· 对于输出特征图中的每个像素,它的大小为Hout × Wout,
· 权重和K × K窗口的点乘
· 我们对所有输入通道 Cin都做这样的操作
· 并且因为该层具有Cout个不同的卷积内核,所以我们重复做Cout次以创建所有输出通道。
同样,我们在这里为了方便忽略了偏置和激活。
我们不应该忽略的是层的stride,以及任何dilation因子,padding等。这就是为什么我们需要参看层的输出特征图的尺寸Hout × Wout,因它考虑到了stride等因素。
示例:对于3×3,128个filter的卷积,在112×112带有64个通道的输入特征图上,我们执行多个MACC:
3 × 3 × 64 × 112 × 112 × 128 = 924,844,032
这几乎是10 亿次累积运算!使得GPU忙于计算......
注意:在此示例中,我们使用“same”填充和stride = 1,以便输出特征图与输入特征图具有相同的大小。通常看到卷积层使用stride = 2,这会将输出特征图大小减少一半,在上面的计算中,我们将使用56 × 56而不是112 × 112。
可深度分离的卷积(Depthwise-separable convolution)
一进深度可分离卷积是一个普通的卷积的分解成两个较小的操作。它们总共占用更少的内存(更少的权重)并且速度更快。当然,这只是近似于“完整”卷积层可以做的事情,因此您可能实际上需要更多这样的运算来获得相当于模型中原常规卷积层相同的表现力,但即便需要更多层,您这样做仍然更快。
这些层在移动设备上运行良好,是MobileNet的基础,也是Xception等大型模型的基础。
第一个运算是深度卷积depthwise convolution(深度卷积)。这在很多方面类似于常规卷积,除了我们没有组合输入通道。总是有相同数量的输入通道和输出通道。
深度卷积的MACC总数为:
K × K × C × Hout × Wout
这可以减少C倍工作量,使其比常规卷积层有效率。
示例:在112×112特征图上使用输入通道为64的3×3核进行深度卷积,MACC:
3 × 3 × 64 × 112 × 112 = 7,225,344
请注意,此卷积始终具有与输入通道一样多的滤波器,并且每个滤波器仅应用于单个通道。这就是上述计算中没有× 128的原因。
注意:有一个称为“深度通道乘数(depthwise channel multiplier)”的东西。如果此乘数大于1,则每个输入通道有D个输出通道。因此,不是每个通道只有一个过滤器,现在每个通道都有D个过滤器。但深度乘数(depthwise multiplier)在实践中应用很少。
仅仅深度卷积是不够的,我们还需要添加“可分离(separable)”。第二个运算是常规卷积,但始终使用内核大小1×1,也称为“逐点(pointwise)”卷积。
对于这个逐点卷积层,MACC的数量是:
Cin × Hout × Wout × Cout
因为K = 1。
示例:让我们从深度卷积中获取具有112×112×64特征图的输出,并将其投影到128维中以创建新的112×112×128特征图。那么MACC:
64 × 112 × 112 × 128 = 102,760,448
如您所见,逐点卷积比深度卷积贵很多倍。但是,如果我们将它们组合在一起,则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。做加法而不是乘法会产生很大的影响......
作为一个快速的经验法则,使用深度可分层几乎是比常规转换层的成本少K×K倍。在上面的例子中,它是8.4,实际上几乎相当于K × K = 3 × 3 = 9。
注意:确切倍数是:K × K × Cout / (K × K + Cout)。
我还应该指出,深度卷积有时会有一个> 1的步幅,这会减小输出特征图的尺寸。但是逐点层通常具有stride = 1,因此其输出特征图将始终具有与深度层有相同的尺寸。
深度可分层是MobileNet V1中的主要构建块。然而,MobileNet V2稍微做了一些修改并使用了由三层组成的“扩展块”:
1. 一个1×1卷积,为特征图添加更多通道(称为“扩展”层)
2. 3×3深度卷积,用于过滤数据
3. 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卷积不会更改特征图的宽度和高度。但如果深度层的步幅> 1,那么Hout × Wout将不同于Hin × Win。
把所有这些放在一起:
Cin × Hin × Win × Cexp + (K × K + Cout) × Cexp × Hout × Wout
如果stride = 1,则简化为:
(K × K + Cout + Cin) × Cexp × Hout × Wout
这与V1使用的深度可分层相比如何?如果我们使用输入特征图112×112×64扩展因子6,以及stride = 1的3×3深度卷积和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层。
批量标准化获取层的输出,并将以下公式应用于每个输出值:
z = gamma * (y - mean) / sqrt(variance + epsilon) + beta
这y是前一层输出特征图中的元素。我们首先通过减去输出通道的mean并除以标准偏差来标准化该值(epsilon用于确保我们不除以0,它通常是这样的0.001)。然后我们按比例缩放gamma并添加偏差或偏移量beta。
每个通道有它自己的gamma,beta,mean,和variance的值,因此,如果在卷积层有C个输出通道,则该批量标准化层学习到C×4个参数。看起来有相当多的FLOPS,因为上面的公式应用于输出特征图中的每个元素。
然而......通常批量标准化应用于卷积层的输出但在非线性(ReLU)之前。在这种情况下,我们可以做一些数学运算来使批量标准化层消失!
由于在全连接层中进行的卷积或矩阵乘法只是一组点积,这是一个线性变换,并且上面给出的批量标准化公式也是线性变换,我们可以将这两个公式组合成一个单一运算。换句话说,我们可以将批量标准化层的学习参数“折叠”到前一个卷积/全连接层的权重中。
将批量标准参数折叠成前面层的权重的数学计算是相当简单的。在上面的公式中,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,并且variance只应用于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层已经提供了一个(beta)。在这种情况下,公式b_new变得更简单(我们设置b为0):
b_new = beta - mean * gamma / sqrt(variance + epsilon)
因此,即使原始层没有偏置,无论如何都会得到一个折叠的批量标准化层。
简而言之:我们可以完全忽略批量标准化层的影响,因为我们在进行推理时实际上将其从模型中删除。
注意:此技巧仅在层的顺序为:卷积,批量标准化,ReLU时才有效 - 但不适用于:卷积,ReLU,批量标准化。ReLU是一个非线性操作,它会把数据弄乱。(虽然我认为如果批量标准化后面紧跟一个新的卷积层,你可以反过来折叠参数。无论如何,你的深度学习库已经为你做了这些优化。)
其他层类型
我们研究了卷积层和全连接层,这两个是现代神经网络中最重要的组成部分。但是也有其他类型的层,例如池化层。
这些其他层类型肯定需要时间,但它们不使用点积,因此不能用MACC测量。如果您对计算FLOPS感兴趣,只需获取特征图大小并将其乘以表示处理单个输入元素的难度的常量。
示例:在112×112具有128通道的特征图上具有过滤器大小2和步幅2的最大池化层需要112 × 112 × 128 = 1,605,632FLOPS或1.6兆FLOPS。当然,如果步幅与滤波器尺寸不同(例如3×3窗口,2×2步幅),则这些数字会稍微改变。
但是,在确定网络的复杂性时,通常会忽略这些附加层。毕竟,与具有100个MFLOPS的卷积/全连接层相比,1.6 MFLOPS非常小。因此,它成为网络总计算复杂度的舍入误差。
某些类型的操作,例如结果的连接,通常甚至可以免费完成。不是将两个层分别写入自己的输出张量中,然后有一个将这两个张量复制到一个大张量的连接层。相反,第一层可以直接写入大张量的前半部分,第二层可以直接写入后半部分。不需要单独的复制步骤。
注意:在这个讨论中,我目前忽略了递归神经网络(RNN),它通常由LSTM或GRU层组成。LSTM层涉及两个大的矩阵乘法,一些sigmoids,一个tanh,以及一些元素乘法。本质上它与2个全连接层相同,因此MACC的数量主要取决于输入和输出向量的大小,以及隐藏状态向量的大小。同样,矩阵乘法中的点积是关键因素。