前言
Github:本文代码放在该项目中:NLP相关Paper笔记和代码复现
说明:讲解时会对相关文章资料进行思想、结构、优缺点,内容进行提炼和记录,相关引用会标明出处,引用之处如有侵权,烦请告知删除。
转载请注明:DengBoCong
对于深度学习模型来说,拥有一个非常好的设计思路和体系架构非常重要,对模型性能的影响非常之大,所以对于模型的研究倾向于在模型性能上的表现。但是对于商业应用来说,算法模型落地的另一个重要考量就是在满足业务场景内存占用、计算量等需要的同时,保证算法的性能,这个研究对于移动端的模型部署更加明显,这有利于压缩应用的体积。最近这段时间正好在研究关于移动端模型部署(TensorFlow Lite用的不是很顺心呀),所以要仔细研究一下模型的参数量等,这不仅可以让我们对模型的大小进行了解,还能更好的调整结构使得模型响应的更快。
可能有时候觉得,模型的大小需要计算嘛,直接保存大小直接看不就完事儿了?运行速度就更直接了,多运行几次取平均速度不就行了嘛?so easy?这个想法也没啥错,但是大前提是你得有个整型的模型呀(训练成本多高心里没数嘛),因此很多时候我们想要在模型设计之初就估计一下模型的大小以及可能的运行速度(通过一些指标侧面反应速度),这个时候我们就需要更深入的理解模型的内部结构和原理,从而通过估算模型内部的参数量和计算量来对模型的大小和速度进行一个初步评估。
一个朴素的评估模型速度的想法是评估它的计算量。一般我们用FLOPS,即每秒浮点操作次数FLoating point OPerations per Second这个指标来衡量GPU的运算能力。这里我们用MACC,即乘加数Multiply-ACCumulate operation,或者叫MADD,来衡量模型的计算量。不过这里要说明一下,用MACC来估算模型的计算量只能大致地估算一下模型的速度。模型最终的的速度,不仅仅是和计算量多少有关系,还和诸如内存带宽、优化程度、CPU流水线、Cache之类的因素也有很大关系。
下面我们对计算量来进行介绍和定义,方便我们后续展开层的讲解:
神经网络中的许多计算都是点积,例如:
1 | y = w[0]*x[0] + w[1]*x[1] + w[2]*x[2] + ... + w[n-1]*x[n-1] |
此处,$w$ 和 $x$ 是两个向量,结果 $y$ 是标量(单个数字)。对于卷积层或完全连接的层(现代神经网络中两种主要类型的层),$w$ 是该层的学习权重,$x$ 是该层的输入。我们将$w[0]*x[0]+…$计数为一个乘法累加或1个MACC,这里的“累加”运算是加法运算,因为我们将所有乘法的结果相加,所以上式具有 $n$ 个这样的MACC(从技术上讲,上式中只有 $n-1$ 个加法,比乘法数少一个,所以这里知识认为MACC的数量是一个近似值)。就FLOPS而言,因为有 $n$ 个乘法和 $n-1$ 个加法,所以点积执行 $2n-1$ FLOPS,因此,MACC大约是两个FLOPS。现在,我们来看几种不同的层类型,以了解如何计算这些层的MACC数量。
注意了:下面的阐述如果没有特别说明,默认都是batch为1。
全连接层计算量和参数量估计
在完全连接的层中,所有输入都连接到所有输出。 对于具有 $I$ 输入值和 $J$ 输出值的图层,其权重 $W$ 可以存储在 $I×J$ 矩阵中。 全连接层执行的计算为:
$$y = matmul(x, W) + b$$
在这里,$x$ 是 $I$ 输入值的向量,$W$ 是包含图层权重的 $I×J$ 矩阵,$b$ 是 $J$ 偏差值的向量,这些值也被相加。 结果 $y$ 包含由图层计算的输出值,并且也是大小 $J$ 的向量。对于完全连接层来说,矩阵乘法为 $matmul(x,W)$ ,其中具有 $I\times J$ 个MACC(和权重矩阵 $W$ 大小一样),对于偏置 $b$,正好补齐了前面我们所说的点积中正好少一个加法操作。因此,比如一个具有300个输入神经元和100个输出神经元的全连接层执行 $300\times 100 = 30,000$ 个MACC。特别提示:上面我们讨论的批次大小 $Batch=1$ 需要具体计算需要乘上Batch。
也就是说,通常,将长度为 $I$ 的向量与 $I×J$ 矩阵相乘以获得长度为 $J$ 的向量,则需要 $I×J$ MACC或$(2I-1)\times J$ FLOPS。
上面我们讨论了全连接层的计算量,那么它的参数量是多少呢?这个应该很容易就算出来,对于全连接层而言,它的参数分别是权重 $W$ 和 偏置$b$,所以对于上面的例子中具有300个输入神经元和100个输出神经元的全连接层的参数量是: $300 \times 100 + 100=30100$ ,这个很容易进行验证,下图是使用TensorFlow进行验证参数量:
激活函数计算量估计
通常,一个层后面紧接着就是非线性激活函数,例如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\times 4$ FLOPS。所以实际上,通常不计这些操作,因为它们只占总时间的一小部分,更多时候我们主要对(大)矩阵乘法和点积感兴趣,所以其实我们通常都是忽略激活函数的计算量。
对于参数量?注意了它压根没有参数,请看它们的公式,用TensorFlow验证如下:
LSTM计算量和参数量估计
关于LSTM的原理可以参考这一篇文章:Understanding LSTM Networks,LSTM结构如下:
实际上LSTM里面有 4 个非线性变换(3 个 门 + 1 个 tanh),每一个非线性变换说白了就是一个全连接网络,形如:$W[h_{t-1},x_t] + b$。其中,第一层是 $x_i$ 和 $h_i$ 的结合,维度就是embedding_size + hidden_size,第二层就是输出层,维度为 hidden_size,则它的计算量按照上文我们对全连接层的阐述,易得MACC为:
1 | (embedding_size + hidden_size) * hidden_size * 4 |
四个非线性变换中,还会对全连接层的输出进行激活函数计算(三个sigmoid和一个tanh),由上面讨论的sigmoid我们知道,对于sigmoid的计算量为:(embedding_size + hidden_size) * hidden_size * 3 *4个FLOPS,而tanh的计算公式为:$\frac{exp(x)-exp(-x)}{exp(x)+exp(-x)}$,其中共有八个加,减,乘,除,求幂,平方根等计算,所以计算量为:(embedding_size + hidden_size) * hidden_size * 8个FLOPS。除此之外,LSTM除了在四个非线性变换中的计算,还有三个矩阵乘法(不是点积)、一个加法、一个tanh计算,其中三个矩阵乘法都是shape为(batch, hidden_size),则这四个运算的计算量为:batch * hidden_size + batch * hidden_size + batch * hidden_size + batch * hidden_size + batch * hidden_size * 8,综上所述,LSTM的计算量为:
1 | (embedding_size + hidden_size) * hidden_size * 4 个MACC |
而该网络的参数量就是:
1 | ((embedding_size + hidden_size) * hidden_size + hidden_size) * 4 |
对于特征维128的输入,LSTM单元数为64的网络来说,LSTM的参数量为:((128 + 64) * 64 + 64) * 4 = 49408,通过TensorFlow验证如下:
卷积层计算量和参数量估计
卷积层的输入和输出不是矢量,而是shape为 $H\times W\times C$的三维特征图,其中 $H$是特征图的高度,$W$ 是宽度,$C$ 是每个位置的通道数,正如我们所见今天使用的大多数卷积层都是二维正方内核,对于内核大小为 $K$ 的转换层,MACC的数量为:
$$K \times K \times Cin \times Hout \times Wout \times Cout$$
- 输出特征图中有Hout × Wout × Cout个像素;
- 每个像素对应一个立体卷积核K x K x Cin在输入特征图上做立体卷积卷积出来的;
- 而这个立体卷积操作,卷积核上每个点都对应一次MACC操作
同样,我们在这里为了方便忽略了偏置和激活。我们不应该忽略的是层的stride,以及任何dilation因子,padding等。这就是为什么我们需要参看层的输出特征图的尺寸Hout × Wout,因它考虑到了stride等因素。比如,对于 $3\times 3$,128个filter的卷积,在 $112\times 112$ 带有64个通道的输入特征图上,我们执行MACC的次数是:
$$3 \times 3\times 64\times 112\times 112\times 128 = 924,844,032$$
这几乎是10亿次累积运算!注意:在此示例中,我们使用“same”填充和$stride = 1$,以便输出特征图与输入特征图具有相同的大小。通常看到卷积层使用$stride = 2$,这会将输出特征图大小减少一半,在上面的计算中,我们将使用 $56 \times 56$ 而不是 $112 \times 112$。
那我们现在来计算一下参数量,如果了解卷积的原理,应该也不难算出它的参数量(可能有人会说卷积原理怎么理解,这里推荐一篇写得通俗易懂的文章:CNN基础知识——卷积(Convolution)、填充(Padding)、步长(Stride)),根据卷积的原理,对于上面的例子中, $3\times 3$,128个filter的卷积,在 $112\times 112$ 带有64个通道的输入特征图上的参数量为:$3\times 3 \times 64 \times 128 + 128 = 73856$,用TensorFlow验证结果如下图:
深度可分离卷积层
深度可分离卷积是将常规卷积因式分解为两个较小的运算,它们在一起占用的内存更少(权重更少),并且速度更快,这些层在移动设备上可以很好地工作,既是MobileNet的基础,也是Xception等大型模型的基础。深度可分离卷积中,第一个操作是深度卷积,它在很多方面与常规卷积相似,不同之处在于我们不合并输入通道,也就是说输出通道数始终与输入通道数相同,深度卷积的MACC总数为:
$$K × K × Cin × Hout × Wout$$
这减少了 $Cin$ 的工作量,比常规的卷积层效率更高。当然,仅深度卷积是不够的,我们还需要增加“可分离”,第二个操作是常规卷积,但始终使用内核大小 $1\times 1$,即 $K=1$,也称为“逐点”卷积,MACC的数量为:
$$Cin × Hout × Wout × Cout$$
深度可分离卷积分为两个操作,深度卷积和可分离,所以现在我们对两种操作分别就上面的例子计算,和并和常规 $3\times 3$ 卷积进行比较:
1 | 3×3 depthwise : 7,225,344 |
所以深度可分离卷积的计算量简化为:
$$Cin × Hout × Wout × (K × K + Cout)$$
我们来看看MobileNet V2中的对深度可分离卷积的拓展,MobileNet V2相比与V1,主要是由DW+PW两层变成了下面的三层PW+DW+PW:
- 一个 $1\times 1$ 卷积,为特征图添加更多通道(称为expansion layer)
- $3\times 3$ 深度卷积,用于过滤数据(depthwise convolution)
- $1\times 1$ 卷积,再次减少通道数(projection layer,bottleneck convolution)
这种扩展块中MACC数量的公式:Cexp = (Cin × expansion_factor),(expansion_factor用于创建深度层要处理的额外通道,使得Cexp在此块内使用的通道数量)
- $MACC\ expansion\ layer = Cin \times Hin \times Win \times Cexp$,(参照上面传统卷积,把卷积核设置为1x1即得)
- $MACC\ depthwise\ layer = K \times K \times Cexp \times Hout \times Wout$(参照MoblieNet V1分析)
- $MACC\ projection\ layer = Cexp \times Hout \times Wout \times Cout$(参照MoblieNet V1分析,或者传统卷积把卷积核设置为1x1即得)
把所有这些放在一起:
$$MACC_v2 = Cin \times Hin \times Win \times Cexp + (K \times K + Cout) \times Cexp \times Hout \times Wout$$
这与MobileNet V1使用的深度可分层相比如何?如果我们使用输入特征图 $112\times 112\times 64$ 扩展因子6,以及 $stride = 1$ 的 $3\times 3$ 深度卷积和128输出通道,那么MACC的总数是:
$$(3 \times 3 + 128 + 64) \times (64 \times 6) \times 112 \times 112 = 968,196,096$$
这不是比以前更多吗?是的,它甚至超过了最初的 $3\times 3$ 卷积。但是……请注意,由于扩展层,在这个块内,我们实际上使用了 $64 \times 6 = 384$ 通道。因此,这组层比原始的$3\times3$ 卷积做得更多(从64到128个通道),而计算成本大致相同。
Batch normalization
在现代网络中,通常在每个卷积层之后都包含一个batch norm层。对于想要详细了解Batch normalization的原理,可参考这篇文章:论文阅读笔记:看完也许能进一步了解Batch Normalization
Batch normalization在层的输出上进行计算,针对每个输出值,计算公式如下:
$$z = gamma * (y - mean) / sqrt(variance + epsilon) + beta$$
此处,$y$ 是上一层的输出图中的元素。 我们首先通过减去该输出通道的平均值并除以标准偏差来对该值进行归一化(epsilon 用于确保不除以0,通常为0.001),然后,我们将系数gamma缩放,然后添加一个偏差或偏移beta。每个通道都有自己的gamma,beta,均值和方差值,因此,如果卷积层的输出中有 $C$ 个通道,则Batch normalization层将学习 $C\times 4$ 参数,如下图所示:
通常将Batch normalization应用于卷积层的输出,而在ReLU之前,在这种情况下,我们可以做一些数学运算以使Batch normalization层消失!由于在全连接层中进行的卷积或矩阵乘法只是一堆点积,它们是线性变换,而上面给出的Batch normalization公式也是线性变换,因此我们可以将这两个公式组合为一个变换。我们只需要将Batch normalization参数合并到前面各层的权重中,其数学运算非常简单。 具体可以参见上面的那篇关于Batch Normalization的文章,也就是说我们可以完全忽略Batch Normalization层的影响,因为我们在进行推理时实际上将其从模型中删除了。
注意:此trick仅在层的顺序为:卷积->BN->ReLU时才有效;不适用于:卷积->ReLU->BN。ReLU是一个非线性操作,它会把数据弄乱。(但如果批量标准化后面紧跟一个新的卷积层,你可以反过来折叠参数)
其它层
池化层
到此我们研究了卷积层和全连接层,这两个是现代神经网络中最重要的组成部分。但是也有其他类型的层,例如池化层。这些其他层类型肯定需要时间,但它们不使用点积,因此不能用MACC测量。如果你对计算FLOPS感兴趣,只需获取特征图大小并将其乘以表示处理单个输入元素的难度的常量。
示例:在 $112\times 112$ 具有128通道的特征图上具有过滤器大小2和步幅2的最大池化层需要 $112 \times 112 \times 128 = 1,605,632$ FLOPS或1.6兆FLOPS。当然,如果步幅与滤波器尺寸不同(例如 $3\times 3$窗口,$2\times 2$步幅),则这些数字会稍微改变。
但是,在确定网络的复杂性时,通常会忽略这些附加层。毕竟,与具有100个MFLOPS的卷积/全连接层相比,1.6 MFLOPS非常小。因此,它成为网络总计算复杂度的舍入误差。
Concate层
某些类型的操作,例如结果的连接,通常甚至可以免费完成。不是将两个层分别写入自己的输出张量中,然后有一个将这两个张量复制到一个大张量的连接层。相反,第一层可以直接写入大张量的前半部分,第二层可以直接写入后半部分。不需要单独的复制步骤。
- 减少参数
- 降低精度
- 融合计算单元步骤
参考资料: