基于飞桨实现二值神经网络的构建,提升模型存储效率和预测速度

点击左上方蓝字关注我们

项目背景

随着近年深度学习的发展,卷积神经网络在各项计算机视觉任务中都展现出了优异的性能。而随着性能要求越来越高,早期的卷积神经网络如AlexNet已经无法满足大家的需求,于是各种性能更优越的CNN网络被相继提出。虽然网络性能得到了提高,但也因此引发了效率问题。效率问题主要表现在模型的存储问题和模型进行预测的速度问题。

存储问题方面,由于数百层网络有着大量的权值参数,保存大量权值参数对设备的内存要求很高。

速度问题方面,数十兆次的浮点计算即使是在GPU(除非是V100这种)上进行也很难达到实时性要求(处理单张图片时间小于30ms)。同时,这些复杂网络带来的能耗问题,严重阻碍了神经网络在边缘移动设备上的部署应用。

因此,学术界对神经网络压缩和加速展开了研究。量化就是神经网络压缩和加速的重要技术之一,常见的量化形式有将float32量化为float16,将float32量化为int8。这些量化技术都使得网络参数的存储空间占用大幅减小,并提高了网络的计算速度。

二值神经网络(BNN)与一般的量化有两大不同之处。第一,二值化是量化的最极端的情况,其对存储空间需求最小且加速提升最明显。第二,一般量化是在一个已经训练好的float32型神经网络基础上,直接进行量化(会导致精度下降较多)或是量化训练(精度下降不多,甚至有时还能提高);而二值神经网络是在训练的全过程中进行量化,即训练得到的权重参数就是二值化的。

本文基于飞桨开源框架v2.1,使用动态图的方式实现了二值神经网络在图像分类任务上的应用。

项目在AI Studio上已经公开,Fork之后可以直接运行,相关链接为:

https://aistudio.baidu.com/aistudio/projectdetail/2174721

二值神经网络原理

二值化网络只是将网络的权重参数和激活值二值化,并没有改变网络结构。

二值化方法公式如下:

上式中x可以代表权重参数或数据,代表相应的二值化后的结果。

在前馈传播的过程中,先将float32型权重参数二值化得到二值型权值参数,即。然后利用二值化后的参数计算得到实数型的中间向量,该向量再通过Batch Normalization操作,得到实数型的隐藏层激活向量。如果不是输出层的话,就将该向量做二值化激活。完整公式如下所示:

上式中表示第k层经过二值化激活后的结果。

值得一提的是,在训练过程中,权重参数是以全精度float32类型存储并更新的。但是在前馈传播过程中,权重参数会先二值化再进行运算。

由于的导数(几乎)处处为零,因此,通过BP算法得到的误差为零,因此不能直接用来更新权值。为解决这个问题采用 straight-through estimator的方法,该方法将进行宽松。这样,函数就变成可以求导的了,求导近似函数如下,其实就是在-1和1之间加一个线性变化过程。

在编程中,从代码形式上看是激活函数,但是在除第一层的每层中,都会将该层的输入先做二值化。由于,所以本质上的激活函数还是

在进行参数更新时,要时时刻刻把超出[-1,1]的部分裁剪,即权重参数始终是[-1,1]之间的实数。因为,权重参数过大对训练的稳定性和收敛速度不利。

图像数据一般都是uint8类型的,其直接输入神经网络的第一层。如果在第一层输入处,与其他层一样对输入做二值化显然不合理,因为这样会让数据全变为1,从而导致没有有效信息送入网络。通常是用均值和标准差,对图像进行归一化处理。然后将全精度的图像数据送入第一层,直接与二值化的权重参数进行运算。

代码实现

这里,将详细对全连接网络在MNIST数据集上实现的核心代码部分进行解析。因为篇幅原因,关于数据准备、结果显示等部分的代码将略过,欢迎查看原项目。

首先,对飞桨开源框架下的tensor类型量(权重、激活值)进行二值化只需要调用paddle.sign函数即可,这里为方便对其进行封装。

def binarize(tensor): 
    return paddle.sign(tensor)

二值全连接层没有Paddle的底层函数可以调用,需要自行实现。代码如下:

class BinarizeLinear(paddle.nn.Linear): 

    def __init__(self, *args, **kwargs): 
        super(BinarizeLinear, self).__init__(*kargs, **kwargs) 

    def forward(self, input): 
        if input.shape[1] != 784: 
            input_data = paddle.assign(input) 
            input_data.stop_gradient = True 
            paddle.assign(binarize(input_data), input) 
        if not hasattr(self.weight, 'org'): 
            self.weight.org = paddle.assign(self.weight)
            self.weight.org.stop_gradient = True 
        paddle.assign(binarize(self.weight.org), self.weight) 
        out = paddle.nn.functional.linear(input, self.weight, self.bias) 
        return out

forward下的第一个if用于判断是否为第一层,如果是第一层则不对输入数据进行二值化。注意,因为训练中前馈传播过程与反向传播相对独立,即构建计算图时不能让二值化出现在其中,否则求导结果为0。只能利用paddle.assign直接对tensor中的数值进行二值化,这样不会影响求导过程。self.weight.org中存放的是前一轮训练中更新后的权重参数,为float32类型;self.weight中存放的是二值化后的权重参数,可以直接取出用于预测。

构建网络结构的部分与常规的完全一直,只要把paddle.nn.Linear换为以上自行实现的BinarizeLinear即可。

class LeNet(paddle.nn.Layer): 
    def __init__(self): 
        super(LeNet, self).__init__() 
        self.infl_ratio = 1 
        self.fc1 = BinarizeLinear(784, 2048*self.infl_ratio) 
        self.htanh1 = paddle.nn.Hardtanh() 
        self.bn1 = paddle.nn.BatchNorm1D(2048*self.infl_ratio) 
        self.fc2 = BinarizeLinear(2048*self.infl_ratio, 2048*self.infl_ratio) 
        self.htanh2 = paddle.nn.Hardtanh() 
        self.bn2 = paddle.nn.BatchNorm1D(2048*self.infl_ratio) 
        self.fc3 = BinarizeLinear(2048*self.infl_ratio, 2048*self.infl_ratio) 
        self.htanh3 = paddle.nn.Hardtanh() 
        self.bn3 = paddle.nn.BatchNorm1D(2048*self.infl_ratio) 
        self.fc4 = BinarizeLinear(2048*self.infl_ratio, 10) 
        self.drop = paddle.nn.Dropout(0.5) 
        self.softmax = paddle.nn.Softmax() 

    def forward(self, x): 
        x = paddle.reshape(x, [-1, 28*28]) 
        x = self.fc1(x) 
        x = self.bn1(x) 
        x = self.htanh1(x) 
        x = self.fc2(x) 
        x = self.bn2(x) 
        x = self.htanh2(x) 
        X = self.fc3(x) 
        x = self.drop(x) 
        x = self.bn3(x) 
        x = self.htanh3(x) 
        x = self.fc4(x) 
        return x

训练部分的核心代码如下:

for epoch in range(epoch_num): 
    for data in train_loader():
        x_data = paddle.cast(data[0], 'float32') 
        y_data = paddle.cast(data[1], 'int64') 
        y_data = paddle.reshape(y_data, (-1, 1)) 
        opt.clear_grad() 
        y_predict = model(x_data) 
        loss = F.cross_entropy(y_predict, y_data) 
        loss.backward() 
        for p in list(model.parameters()): 
            if hasattr(p, 'org'): 
                paddle.assign(x=p.org, output=p) 
        opt.step() 
        for p in list(model.parameters()): 
            if hasattr(p, 'org'): 
                paddle.assign(x=p.clip(min=-1, max=1), output=p.org) 

与常规的训练流程不同,由于参数p中存放的是二值化后的权重,p.org中存放的是非二值化的权重;而二值神经网络训练中,是对非二值化的权重进行优化,所以需要在前馈传播后,用p.org中的值覆盖掉p中的值,为后续的opt.step()做准备。而权重更新后,需要将权重参数范围规定在[-1,1]之间,并再存入p.org中。

关于二值化卷积层的实现这里就不赘述,原理类似,详细代码请查看原项目。

总结

总的来说,二值神经网络有以下优势:

首先,二值神经网络的运算速度更快。因为权重和激活值都是经过了二值化的,所以可以用异或XNOR操作来代替32位浮点乘法操作,而异或这一逻辑运算的速度显然快于32位浮点乘法。遗憾的是,由于GPU的浮点运算速度也很快,且本项目所用的网络规模不是很大,所以很难在GPU上明显地展示出二值神经网络速度的优势。(建议此推理步骤中的异或自行实现一下,因为如果用paddle的底层函数这里还是只能用乘法来操作)。

其次,二值神经网络适用的硬件平台更广。对于算力明显不足的嵌入式平台,如单片机或是一些性能较差的ARM芯片,二值神经网络依然可以在其上实现部署,并保证较高的推理速度。

第三,二值神经网络占用的存储空间和运行内存更小。权重参数所占存储空间仅为全精度时的1/32;且由于激活值只有1bit,所以运行内存也非常小。

第四,二值神经网络的功耗更低,适合于移动端部署应用。

本文属于实验层面的实现,将模型中的参数限制为二值,从而模拟BNN。但是实际运算的参数并不是二值类型的,所以不具备BNN的速度、存储等优势。可以尝试将参数导出后直接进行转换,并用C语言实现二值全连接层和二值卷积层,从而显示出BNN的优势。

参考文献

Binarized Neural Networks: Training Neural Networks with Weights and Activations Constrained to +1 or −1(https://arxiv.org/abs/1602.02830)

如有飞桨相关技术问题,欢迎在飞桨论坛中提问交流:

http://discuss.paddlepaddle.org.cn/

欢迎加入官方QQ群获取最新活动资讯:793866180。

如果您想详细了解更多飞桨的相关内容,请参阅以下文档。

·飞桨官网地址·

https://www.paddlepaddle.org.cn/

·飞桨开源框架项目地址·

GitHub: https://github.com/PaddlePaddle/Paddle 

Gitee: https://gitee.com/paddlepaddle/Paddle

????长按上方二维码立即star!????

飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件和丰富的工具组件于一体,是中国首个自主研发、功能丰富、开源开放的产业级深度学习平台。飞桨企业版针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值