目录
【唠叨一些废话】
【论文阅读部分】
【摘要】
【引言】
【相关工作】
【二值卷积神经网络】
【3.1 Binary-Weight-Networks】
【Train BWN】
【3.2 XNOR-Networks】
【实验】
4.1有效性分析
4.2 图像分类
4.3 ablation study(控制变量,验证思路实验)
【结论】
【代码部分】
【代码复现debug】(CIFAR_10)
【代码复现结果】
【代码二值化部分解读】
前向传播的基础网络NIN:
激活输入的二值化:
权重的二值化:
反向传播:
参考链接:
我回归CSDN了,以后会持续更新自己的研究方向,原因竟然是:这位小姐姐的一个点赞,哈哈哈。。。
其实内因是想不断记录和总结自己学习的东西啦~(还是那句话:科研小白一枚,有误望提出,一定磕头认错,并虚心接纳。手动狗头)
论文地址:XNOR-Net: ImageNet Classification Using Binary Convolutional Neural Networks
代码地址(pytorch):https://github.com/jiecaoyu/XNOR-Net-PyTorch
提出两种量化的网络:Binary-Weight-Networks(只有权重被量化) 和 XNOR-Networks(权重和激活都进行量化,并且引入了本论文核心:scaling factor 来保证量化后的激活和权重与全精度的激活和权重相差损失降到最低)。
效果:Binary-Weight-Networks节省32倍的内存 ,而XNOR-Networks节省32倍的内存,并且二元运算比卷积运算快58倍。
Binary-Weight-Networks:由于实值权重进行sign()后filter值为{-1, 1},binary filter与激活输入进行矩阵运算,就相当于对激活输入进行相加或相减的操作,所以没有乘积运算,速度比原来快2倍。
XNOR-Networks:weight和activation都进行二值化近似,其中用XNOR(异或非)+bitcount(位运算)代替了传统卷积中的乘积运算,速度快了58倍。
其中XNOR+bitcount的操作可参考下图:
(图片出处:Binary neural networks: A survey)
对于XNOR的具体做法:见到一篇博文中写道:XNOR在实际计算时是采用XOR(异或)的操作,其中在程序中-1用0表示。
X = [1,-1,1,1,-1]
W=[-1,1,1,-1,-1]
则乘积为X * W = 1 * (-1) + (-1) * 1 + 1 * 1 + 1 * (-1) + (-1) * (-1) = -1
而程序中则为:
X=[1,0,1,1,0]
W=[0,1,1,0,0]
则进行XOR(异或操作)得到a=11010
再进行转换公式:-(2*popcount(a)- len(a))= -(2*3-5) = -1
其中popcount()是指1的个数,len()是 对应向量的长度。
对于bitcount的操作:我的理解是直接按位相加的操作。
这一部分是解释为什么开展本论文,以及与论文的相关方面。
(起因)为什么要进行模型量化:
深度神经网络中参数过多,导致参数冗余的情况,会造成无效的计算以及内存使用,所以要进行模型压缩、量化等方法去解决该问题。
相关方面包括:
1、shallow networks——浅层网络
特点:网络层少,效果能够媲美深层网络,对于小数据而言;但是对于大数据来说,无法进行训练足够的参数去表征,效果很差。
还有一种方法:先训练深层模型,然后再训练浅层网络模型去近似逼近深度模型的效果,这有点知识蒸馏的味~
2、Compressing pre-trained deep networks——压缩预训练深度模型
剪枝:weight decay、通过loss function去减少连接数、减少参数的数量级
减少激活的数目、量化权重、霍夫曼编码、哈希函数、矩阵因子分解等方法
3、Designing compact layers——设计更加紧凑的层,在网络结构进行压缩
用global average pooling代替fully connected layer,链接(我觉得这篇论文值得一看)
Residual-Net中的bottlenect structure(瓶颈结构)
Inception-v4中的将conv进行分解成3×3和两个1×1conv的做法,链接(我觉得这篇论文值得一看,改天)
SqueezeNet使用1×1conv代替3×3conv,链接(我觉得这篇论文值得一看,改天天)
其实这些卷积块都是在conv结构上实现压缩参数量,但效果能媲美普通卷积块。
4、Quantizing parameters——参数量化
矢量量化技术→{+1,0,-1}三元权重的稀疏网络→32bit压缩为8bit→{+1,0,-1}三元权重,3bit激活输入→反向传播阶段量化,前向传播测试阶段保持全精度→{+1,-1}二值化量化。
这里主要介绍该论文符号的表示:
每一层的卷积运算都有一个三元组
代表每层的激活输入,代表卷积or过滤器or权重,代表卷积运算,普通卷积对应的是乘加运算。
from L-layer:只对filter量化,激活输入不变
代表L层的激活输入
代表卷积or过滤器or权重
其中将该filter进行量化操作,这个量化包括二值filterB+缩放因子α
于是就得到
于是进行卷积运算
这里的*变成了加减运算,很好理解,B为{-1,+1},相乘的过程自然变成了加减运算。
from 整体来看:
描述二值权重用四元组表示
其中
因此
为了找到optimum parameter of ) B(二值权重)and a(缩放因子),进行了进一步的推导:
两者近似,才能减少丢失更多的精度,也就是要保证其l2范数最小,所以问题转化为:
展开得到
thanks to and
化简为
由于第一项和第三项都是常量,最小化J(B,a)的过程,由于第二项有负号,变成最大化的问题,其中a为固定值忽略不计先。
化简得到如下式子:
其中实值权重W与对应的二值化权重B{-1,1}相乘,求最大值的问题:按照每个矩阵中的像素点来看:同号相乘为正的原则
当Wi >0时,确保乘积最大,则Bi should be +1
当Wi <0时,确保乘积最大,则Bi should be -1
于是得到二值权重B的最优解为:
再找缩放因子的最优解
对求导,并令,即可得到
带入已知可以得到缩放因子 的最优解为:
可以看出的最优解就是实值权重像素点绝对值的平均。
训练三步走:前向传播,反向梯度下降,更新参数
其中反向传播过程中更新实值权重所需的梯度为:
推导过程~:
二值化权重为:
计算梯度如下所示:
update parameter 的过程中,是更新全精度的权重W,这是由于梯度下降对参数的改变是微小的,这对于二值化的权重而言是可以忽略其变化的,无法进行训练优化,二值化的权重只作为更新权重的中间过程,所以仍然使用的是全精度的权重。
算法步骤如下所示:
第一部分:对于缩放因子和二值激活H和二值权重B的最优解的来源做的公式推导
对激活输入进行量化:
对权重进行量化:
则求解对应的缩放因子和二值激活H和二值权重B的最优解的表达式为:
令
则简化为与Binary-Weight-Network相同的优化式子:
则C的最优解为:
由于实值权重和实值激活输入之间是相互独立的,所以期望如下:
则的最优解为:
第二部分:对于二值运算近似卷积运算具体细节的讲解
1、二值权重的具体实现方式
W 近似为两部分:缩放因子与二值权重的乘积,如下图所示:
2、二值激活输入的具体实现方式(低效的方法和高效的方法)
低效的方法:
由于激活输入的尺寸往往比卷积核大,进行卷积运算需要通过对子激活输入进行滑动窗口,进行乘积运算得到新的特征。所以激活输入X要按照权重W的尺寸大小分成若干个激活子输入(与W尺寸一致),再对其取缩放因子,并将对应的子缩放因子存储在矩阵K中。
存在的问题,由于子激活输入是overlaps,是有重叠区域的,所以会造成一些计算上的冗余。
高效的方法:
将激活输入在通道上进行绝对值相加,之后再与w×h的卷积(与卷积核长宽一致)进行乘积运算,其中w×h卷积中的每个元素为1/(w×h),进行卷积运算,也就求得了整体的缩放因子矩阵K。NB plus~
两部分的二值化近似得到之后,直接进行XNOR-Bitcount操作,也就是
(小小补充)分别指的是什么?
我的理解:
表示带有滑动窗口的乘积求和运算
表示带有滑动窗口的Xnor+bitcount运算,特指在二值矩阵之间的运算,这是加速的核心
表示按元素乘积运算,或是通过广播的形式与常数进行逐点乘积运算
第三部分:block structure的优化
如图所示,左边为一个常规CNN的block,是CBAP的顺序;而右边是XNOR-Net的block,执行顺序为BN→B(A)→B(C)→P。
(Pool放在最后)Pool绝对不可以用于二值激活之后,因为会造成信息丢失。试想,采用max-pooling之后,那么pool之后的大多数元素值都为+1,这是有问题的,因此Pool位于卷积运算之后。
(BN→B(A))激活输入二值化之前,进行BN的目的是为了将其保持在0附近分布,减少二值化造成的信息丢失。
此外,还有二值梯度和k-bit量化的内容,具体看paper,这里不记录~
Duble Precision计算操作数目为:
由于binary Precision比Duble Precision小64倍,还加上非二值化的操作,这里具体指的应该是bitcount,故计算操作数目为:
因此二值化比双精度的运算快:
可以从上述公式看出加速是依赖卷积核的尺寸以及通道数,而不是激活输入的尺寸。下图4-bc的实验验证了这种说法。
这里的启示其实用于,对于通道数c=3或是卷积尺寸1×1而言,加速的效果是几乎很小的,也就代表了输入(通常通道数为3)不进行二值化,以及最后一层(filter的尺寸为1×1)不进行二值化。
下图4-a 用不同的网络结构作为基础模型,进行double precision和binary precision的内存占用的对比。
图5证实BWN和Xnor-Net在训练(train)和推理(validation)阶段对于分类任务top-1和top-5的精度。
表1除了对比【BWN BC】【Xnor-Net BNN】的最终精度,还对比了全精度的Top-1和Top-5的精度。
图5和表1都共同说明了一件事情(NB plus~):
BWN>BC Xnor-Net>BNN
表1还隐藏说明了一点是scaling factor 的作用远大于scaling factor 。所以在代码实现时并没加入scaling factor 。
表2表明还在ResNet-18以及GoogleNet的变体上进行了全精度、BWN以及Xnor-Net的实验,都证明了BWN和Xnor-Net是有效的,精度不会特别差。
表3-(a)中的实验:
的选取有两种,一个是取filter绝对值均值的方式得到;另外一种是将其作为二值化之后连接的一层(标量层),通过训练去得到。
表3-(b)中的实验:
卷积块的顺序设定,从普通设定CBAP到适合二值卷积块的BACP做了对比实验,从而证明BACP的优越性。
这张图已经把结论很好地说明了,细品。没有表明的就是:①在二值化的同时,为了使得与全精度的损失减到最低,要保证其尽量接近,权重和激活输入采用了scaling factor②还设置了一种适合Xnor-Net新的block顺序。由于之前看过BNN的paper,发现其主要区别就体现在这两点的创新性。
1、下载CIFAR_10数据集(google 云盘需要科学上网),放在./data目录下;
2、改numpy.load(open(train_data_path, 'r'))→numpy.load(open(train_data_path, 'rb')),用'utf-8'的方式无法正常读入,因为data/目录下的文件类型为binary,故改用二进制的方式读入;
3、改for key in state['state_dict'].key()→for key in list(state['state_dict'].key()),因为在迭代的过程中OrderedDict被改变了,所以要用list去固定住,每次迭代时不会改变。
1、由于本人的gpu为nvidia 1060,算力有限,所以只迭代了200 epochs,Accuracy=86.07%,github作者训练为320 epochs,Accuracy=86.28%,基本可信;
2、对比floating-point网络,Xnor-Net的精度只下降了3.39%,二值神经网络还是灰常强大的,run-time of test并没有测试过,感兴趣的小伙伴可以做该实验。
(解读)BinActive类中定义了激活输入进行二值化以及求出scaling factor ,但是在前向传播过程中并没有使用到,因为其影响精度并不明显。
# define
class BinActive(torch.autograd.Function):
'''
Binarize the input activations and calculate the mean across channel dimension.
'''
def forward(self, input):
self.save_for_backward(input)
size = input.size()
mean = torch.mean(input.abs(), 1, keepdim=True)
input = input.sign()
return input, mean
def backward(self, grad_output, grad_output_mean):
input, = self.saved_tensors
grad_input = grad_output.clone()
grad_input[input.ge(1)] = 0
grad_input[input.le(-1)] = 0
return grad_input
# using
x, mean = BinActive()(x)
这个是在训练和测试过程中才进行实现的,并不是模型初始化阶段实现。
(解读)NIN第一层和最后一层的conv不进行binary operation,只有中间7层进行,__init__将存储待二值化的权重层的参数。
def __init__(self, model):
# count the number of Conv2d
count_Conv2d = 0
for m in model.modules():
if isinstance(m, nn.Conv2d):
count_Conv2d = count_Conv2d + 1
start_range = 1
end_range = count_Conv2d-2
self.bin_range = numpy.linspace(start_range,
end_range, end_range-start_range+1)\
.astype('int').tolist()
self.num_of_params = len(self.bin_range)
self.saved_params = []
self.target_params = []
self.target_modules = []
index = -1
for m in model.modules():
if isinstance(m, nn.Conv2d):
index = index + 1
if index in self.bin_range:
tmp = m.weight.data.clone()
self.saved_params.append(tmp)
self.target_modules.append(m.weight)
(解读)在每次前向传播推理之前,将需要二值化的权重进行处理:
def binarization(self):
self.meancenterConvParams()
self.clampConvParams()
self.save_params()
self.binarizeConvParams()
(解读)零均值化函数:实值权重(B,C,H,W)按照通道数进行求均值(B,1,H,W),再进行扩展回原维度(B,C,H,W),实值权重元素之间减去该均值,标准化后均值为0:
def meancenterConvParams(self):
for index in range(self.num_of_params):
s = self.target_modules[index].data.size()
negMean = self.target_modules[index].data.mean(1, keepdim=True).\
mul(-1).expand_as(self.target_modules[index].data)
self.target_modules[index].data = self.target_modules[index].data.add(negMean)
(解读)卷积参数裁剪函数:将参数大于1或小于-1的元素缩放为1和-1。
def clampConvParams(self):
for index in range(self.num_of_params):
self.target_modules[index].data = \
self.target_modules[index].data.clamp(-1.0, 1.0)
(解读)保存实值权重函数
def save_params(self):
for index in range(self.num_of_params):
self.saved_params[index].copy_(self.target_modules[index].data)
(解读)权重参数二值化函数:找到scaling factor m,再将其扩展为原维度,再与二值化后的权重进行乘积。
def binarizeConvParams(self):
for index in range(self.num_of_params):
n = self.target_modules[index].data[0].nelement()
s = self.target_modules[index].data.size()
m = self.target_modules[index].data.norm(1, 3, keepdim=True)\
.sum(2, keepdim=True).sum(1, keepdim=True).div(n)
self.target_modules[index].data = \
self.target_modules[index].data.sign().mul(m.expand(s))
(解读)二值化的权重进行前向推演之后,将存储的实值权重还原到权重参数中,以便进行对实值权重进行梯度下降,再次进行前向推演。
def restore(self):
for index in range(self.num_of_params):
self.target_modules[index].data.copy_(self.saved_params[index])
(解读)更新的梯度值计算,这里的计算过程真心没看懂,与paper的公式以及作者的notes都不对应,难搞~(希望dalao们指出)
def updateBinaryGradWeight(self):
for index in range(self.num_of_params):
weight = self.target_modules[index].data
n = weight[0].nelement()
s = weight.size()
m = weight.norm(1, 3, keepdim=True)\
.sum(2, keepdim=True).sum(1, keepdim=True).div(n).expand(s)
m[weight.lt(-1.0)] = 0
m[weight.gt(1.0)] = 0
# m = m.add(1.0/n).mul(1.0-1.0/s[1]).mul(n)
# self.target_modules[index].grad.data = \
# self.target_modules[index].grad.data.mul(m)
m = m.mul(self.target_modules[index].grad.data)
m_add = weight.sign().mul(self.target_modules[index].grad.data)
m_add = m_add.sum(3, keepdim=True)\
.sum(2, keepdim=True).sum(1, keepdim=True).div(n).expand(s)
m_add = m_add.mul(weight.sign())
self.target_modules[index].grad.data = m.add(m_add).mul(1.0-1.0/s[1]).mul(n)
1、XNOR-Net: ImageNet Classification Using Binary Convolutional Neural Networks 论文笔记
2、jiecaoyu/XNOR-Net-PyTorch/notes/notes.pdf