简要
与简单的加法运算相比,乘法运算具有更高的计算复杂度。深度神经网络中广泛使用的卷积正好是来度量输入特征和卷积滤波器之间的相似性,这涉及浮点值之间的大量乘法。现在作者提出了加法网络(AdderNets)来交换深度神经网络中的这些大规模乘法,特别是卷积神经网络(CNNs),以获得更简易的加法以降低计算成本。
在加法器网中,作者以滤波器与输入特征之间的L1范数距离作为输出响应。分析了这种新的相似性度量对神经网络优化的影响。为了获得更好的性能,通过研究全精度梯度开发了一种特殊的反向传播方法。然后,作者还提出了一种自适应学习率策略,根据每个神经元梯度的大小来增强加法网络的训练过程。
上图就是加法网络的特征可视化结果。
研究背景
虽然深度神经网络的二值化滤波器大大降低了计算成本,但原始识别精度往往无法保持。此外,二进制网络的训练过程不稳定,通常要求较慢的收敛速度和较小的学习速率。经典CNN中的卷积实际上是测量两个输入的相似性。研究人员和开发人员习惯于将卷积作为默认操作,从视觉数据中提取特征,并引入各种方法来加速卷积,即使存在牺牲网络能力的风险。但几乎没有人试图用另一种更有效的相似性度量来取代卷积。事实上,加法的计算复杂度要比乘法低得多。因此,作者有动机研究用卷积神经网络中的加法代替乘法的可行性。
相关工作
Network Pruning
网络剪枝主要通过移除冗余的权重来实现网络的压缩和加速。比如用奇异值分解(SVD)全连接层的权重矩阵、去除预训练中的部分权重、将filter变换到频域避免浮点计算。还有的通过去除冗余的filter、或者对channel进行选择这样冗余filter/channel后续的计算就无需考虑了。
Knowledge Distillation
除了去除网络中的冗余连接,Hinton还提出了knowledge distillation的概念,借助teacher网络的学习能力来指导student网络完成复杂任务的学习,变种有多个teacher网络、对中间隐层的学习以及对不同teacher网络学到的特征整合成新的知识来帮助student网络的训练。
Adder Network
不同类别的CNN特征按其角度来划分。由于AdderNet使用L1范数来区分不同的类,因此AdderNet的特征倾向于聚集到不同的类中心。
对于CNN中的卷积运算,假定输入X,filter表示为F,卷积后输出的是二者的相似性度量,表述如下面公式:
\[Y(m,n,t)=\sum \limits_{i=0}^{d}\sum \limits_{j=0}^{d}\sum \limits_{k=0}^{c_{in}}S(X(m+i,n+j,k),F(i,j,k,t)) \]
这也可以意味着当d=1时计算一个全连接层。事实上,还有许多其他的指标来衡量过滤器和输入特性之间的距离。然而,这些指标大多涉及乘法,这带来的计算成本大于增加的成本。
实际上二者的相似性度量可以有多种途径,但都涉及到大量的乘法运算,这就增加了计算开销。因此作者通过计算L1距离完成输入和filter之间的相似性度量。而L1距离仅涉及到两个向量差的绝对值,这样输出就变成了如下:
\[Y(m,n,t)=-\sum \limits_{i=0}^{d}\sum \limits_{j=0}^{d}\sum \limits_{k=0}^{c_{in}}|X(m+i,n+j,k)-F(i,j,k,t)| \]
加法是\(l_1\)距离测量的主要操作,因为使用补码可以很容易地将减法减少为加法。借助\(l_1\)距离,可以有效地计算出滤波器和特征之间的相似性。
我们注意到使用互相关运算还是L1距离都可以完成相似性度量,但二者的输出结果还是有一些差别的。通过卷积核完成输入特征图谱的加权和计算,结果可正可负;但adder filter输出的结果恒为负,为此作者引入了batch normalization将结果归一化到一定范围区间内从而保证传统CNN使用的激活函数在此依旧可以正常使用。虽然BN的引入也有乘法操作但计算复杂度已远低于常规卷积层。conv和BN的计算复杂度分别如下:
\[\omicron(d^2c_{in}c_{out}HW)\ and\ \omicron(c_{out}H^{'}W^{'}) \]
在实践中,在ResNet中给定一个输入通道号\(c_{in}\)=512和一个内核大小d=3,我们有:
\[\frac{d^2c_{in}c_{out}HW}{c_{out}H^{'}W^{'}}\approx 4068 \]
Optimization
神经网络利用反向传播来计算滤波器的梯度和随机梯度下降来更新参数。在CNN中,输出特征Y相对于滤波器F的偏导数被计算为:
\[\frac{\partial Y(m,n,t)}{\partial F(i,j,k,t)}=X(m+i,n+j,k) \]
但是在AdderNets中,Y相对于滤波器F的偏导数是:
\[\frac{\partial Y(m,n,t)}{\partial F(i,j,k,t)}=sgn(X(m+i,n+j,k)-F(i,j,k,t)) \]
但signSGD优化方法几乎不会选择到最陡的方向,而且随着维度增加效果会更差,因此本文使用如下公式进行梯度更新:
\[\frac{\partial Y(m,n,t)}{\partial F(i,j,k,t)}=X(m+i,n+j,k)-F(i,j,k,t) \]
此外,如果使用full-precision gradient的更新方法,由于涉及到前层的梯度值很容易导致梯度爆炸,因此本文还通过使用HardTanh将输出限定在[-1,1]范围内。
\[\frac{\partial Y(m,n,t)}{\partial X(m+i,n+j,k)}=HT(F(i,j,k,t)-X(m+i,n+j,k)) \]
输出特征Y相对于输入特征X的偏导数计算为:
\[HT(x)=\left\{ \begin{array}{rcl} x & & {-1<x<1}\\ 1 & & {x>1}\\ -1 & & {x<-1}\\ \end{array} \right. \]
Adaptive Learning Rate Scaling
在传统的CNN中,假设权值和输入特征是独立的,服从正态分布,输出的方差大致可以估计为:
\[\begin{aligned} Var[Y_{CNN}]&=\sum \limits_{i=0}^{d}\sum \limits_{j=0}^{d}\sum \limits_{k=0}^{c_{in}}Var[X\times F] \\ &=d^2c_{in}Var[X]Var[F] \end{aligned} \]
相反,对于AdderNets,输出的方差可以近似为:
\[\begin{aligned} Var[Y_{Adder\ Net}]&=\sum \limits_{i=0}^{d}\sum \limits_{j=0}^{d}\sum \limits_{k=0}^{c_{in}}Var[|X-F|] \\ &=(1-\frac{2}{\pi}d^2c_{in}(Var[X]+Var[F])) \end{aligned} \]
AdderNets的输出具有较大方差,在更新时根据常规的链式法则会导致梯度比常规CNN更小,从而导致参数更新过慢。因此自然而然想到通过引入自适应学习率调整参数的更新learningrate组成:
\[\Delta F_l=\gamma \times \alpha_l\times \Delta L(F_l) \]
包括神经网络的全局学习率和本地学习率,其中本地学习率表示为:
\[\alpha_l=\frac{\eta \sqrt{k}}{||\Delta L(F_l)||_2} \]
接下来,我们继续展示输出的输出差异对addenets参数更新的影响。为了提高激活函数的有效性,我们在每个加法器层后引入批归一化。给定一个小批\(B={x_1、···、x_m}\)上的输入x,批归一化层可以表示为:
这样可以保证每层更新的幅度一致,最终AdderNet的训练过程表述为:
\[y=\gamma \frac{x-\mu_B}{\sigma_B}+\beta \]
最终,算法的描述为:
实验
在MNIST、CIFAR及ImageNet数据集山验证了AdderNet的有效性,随后进行了消融实验以及对提取的特征进行可视化。
AdderNet使用L1距离来度量输入与filter之间的关系,而不是使用卷积的互相关。因此需要探究一些AdderNet与CNN特征空间上的差异。因此就在MNIST数据集上搭建了LeNet++:6conv+1fc,每层神经元数目依次为:32,32,64,64,128,128,2。同样其中的conv层用add filter替换可视化结果如图1所示,CNN的可视化结果为右侧,相似度通过cosin计算得到的,因此分类通过角度进行的分类。左侧是AdderNet的可视化结果,可以看到不同种类的聚类中心不同,这也验证了AdderNet具有同CNN相似的辨别能力。
对filter的可视化结果如上图所示,虽然AdderNet和CNN用的度量矩阵不同,但都具有特征提取的能力。
Learning curve of AdderNets using different optimization schemes
权重分布的可视化
对LeNet-5-BN的第三层进行可视化,AdderNet权重更接近Laplace分布,CNN的权重近似高斯分布,分别对应L1-norm和L2-norm。
注:左边是AdderNet,右边是CNNs
部分代码
不过,反向传播还没哟完全看懂。
点击查看代码
import torch
import torch.nn as nn
import numpy as np
from torch.autograd import Function
import math
def adder2d_function(X, W, stride=1, padding=0):
# [output_channel,input_channel,kernel_size,kernel_size]
n_filters, d_filter, h_filter, w_filter = W.size()
n_x, d_x, h_x, w_x = X.size()
h_out = (h_x - h_filter + 2 * padding) / stride + 1
w_out = (w_x - w_filter + 2 * padding) / stride + 1
# 得到输出大小h_out,w_out
h_out, w_out = int(h_out), int(w_out)
# X_col: [n_x, input_channel*kernel_size*kernel_size, h_out*w_out]
X_col = torch.nn.functional.unfold(X.view(1, -1, h_x, w_x), h_filter, dilation=1, padding=padding, stride=stride).view(n_x, -1, h_out*w_out)
# X_col: [input_channel*kernel_size*kernel_size, h_out*w_out*n_x]
X_col = X_col.permute(1,2,0).contiguous().view(X_col.size(1),-1)
W_col = W.view(n_filters, -1) # [output_channel, input_channel*kernel_size*kernel_size] or [n_filtrs, d_filter*h_filter*w_filter]
out = adder.apply(W_col,X_col)
out = out.view(n_filters, h_out, w_out, n_x)
out = out.permute(3, 0, 1, 2).contiguous()
return out
class adder(Function):
@staticmethod
def forward(ctx, W_col, X_col):
ctx.save_for_backward(W_col,X_col)
# W_col:[output_channel, input_channel*kernel_size*kernel_size, ]
# X_col:[ , input_channel*kernel_size*kernel_size, h_out*w_out*n_x]
output = -(W_col.unsqueeze(2)-X_col.unsqueeze(0)).abs().sum(1)
return output
@staticmethod
def backward(ctx,grad_output):
# W_col:[output_channel, input_channel*kernel_size*kernel_size, ]
# X_col:[ , input_channel*kernel_size*kernel_size, h_out*w_out*n_x]
W_col,X_col = ctx.saved_tensors
# grad_W_col:[output_channel, input_channel*kernel_size*kernel_size, h_out*w_out*n_x] --> [output_channel, ,1, ]
grad_W_col = ((X_col.unsqueeze(0)-W_col.unsqueeze(2))*grad_output.unsqueeze(1)).sum(2)
# compute local learning rate
grad_W_col = grad_W_col/grad_W_col.norm(p=2).clamp(min=1e-12)*math.sqrt(W_col.size(1)*W_col.size(0))/5
grad_X_col = (-(X_col.unsqueeze(0)-W_col.unsqueeze(2)).clamp(-1,1)*grad_output.unsqueeze(1)).sum(0)
return grad_W_col, grad_X_col
class adder2d(nn.Module):
def __init__(self,input_channel,output_channel,kernel_size, stride=1, padding=0, bias = False):
super(adder2d, self).__init__()
self.stride = stride
self.padding = padding
self.input_channel = input_channel
self.output_channel = output_channel
self.kernel_size = kernel_size
self.adder = torch.nn.Parameter(nn.init.normal_(torch.randn(output_channel,input_channel,kernel_size,kernel_size)))
self.bias = bias
if bias:
self.b = torch.nn.Parameter(nn.init.uniform_(torch.zeros(output_channel)))
def forward(self, x):
# 得到Adder的输出
output = adder2d_function(x,self.adder, self.stride, self.padding)
if self.bias:
output += self.b.unsqueeze(0).unsqueeze(2).unsqueeze(3)
return output
本地电脑显存不够,调试只能到forward.....
\(W_{col}:\)
144=1633
\(X_{col}:\)
Reference:
[1] AdderNet: Do We Really Need Multiplications in Deep Learning?