二值神经网络

  • 目录
  • 二值神经网络简介
  • 同或运算和比特计数运算加速原理
  • Pytorch框架下训练二值神经网络
  • 利用CIFAR10数据集测试二值神经网络性能
  • 利用CIFAR10图片测试二值神经网络性能
  • 提取训练模型参数以供其他平台使用


目录

二值神经网络简介

定义:网络参数使用+1或-1表示的极限量化模型。
量化方法:

  • 确定性二值化:当输入值大于设定的阈值时,输出值为+1,否则输出值则为-1。
    二值神经网络 pytorch 二值神经网络英语_二值神经网络 pytorch

其中二值神经网络 pytorch 二值神经网络英语_神经网络_02表示输入的浮点型参数, 二值神经网络 pytorch 二值神经网络英语_神经网络_03

二值神经网络 pytorch 二值神经网络英语_2d_04

  • 随机性二值化:当输入x满足一定的概率时,输出值为+1,否则输出值为-1。
    二值神经网络 pytorch 二值神经网络英语_2d_05

其中二值神经网络 pytorch 二值神经网络英语_二值化_06。虽然随机性二值化方法更符合实际情况,但是在实际操作中使用硬件生成随机数存在困难,所以仍常对参数执行确定性二值化。

二值化优势:

  • 相比于浮点数卷积神经网络而言,参数仅占用1比特,内存压缩率高;
  • 可以采用同或运算和比特计数运算代替卷积神经网络中的浮点数乘累加运算,卷积运算速度快,适合硬件加速;
  • 具有直接部署在嵌入式设备上的潜力,也是最有前途实际工程化应用的网络模型之一。

二值化劣势:

  • 有效信息丢失,网络模型精度有所降低;
  • 常用于简单小型的处理任务,难以胜任大型挑战。

同或运算和比特计数运算加速原理

在二值神经网络中,卷积运算只是+1和-1的乘累加操作,因此衍生出了采用同或(XNOR)运算替代传统卷积操作的新方法。

在前向推理过程中,可以采用0代替-1的方式优化运算过程。下图展示了乘法运算和同或运算的真值表,左侧为二值乘法运算真值表,右侧为二值同或运算真值表(相同为真,不同为假)。

二值神经网络 pytorch 二值神经网络英语_深度学习_07


卷积运算不仅包括乘法运算,还包括累加运算。在二值神经网络中采用Popcount比特计数算法代替累加运算。Popcount算法的全称是Population count,其实质是计算二进制同或结果中1的位数。激活值和权重值乘累加的示意图如下图所示:

二值神经网络 pytorch 二值神经网络英语_深度学习_08


上述激活值和权重值按照乘累加的方式计算的结果为-1

将激活值和权重中的-1用0代替后,激活值变为10110,权重值变为01100,两者执行同或运算和Popcount比特计数代替卷积操作的示意图:

二值神经网络 pytorch 二值神经网络英语_二值神经网络 pytorch_09


length表示二进制数据的总位宽,激活值和权重值的同或结果为00101,其Popcount比特计数结果为2,再通过(a)式可得到最终的二值卷积结果:-5+2×2=-1。激活值和权重值的异或结果为11010,其Popcount比特计数结果为3,再通过(b)式即可得到同样的二值卷积结果:-(2×3-5)=-1

经过计算验证后可得,上述激活值和权重值按照乘累加运算的结果与二值卷积结果相等


Pytorch框架下训练二值神经网络

二值神经网络适用于较小的数据集,因此将利用CIFAR10数据集对其进行训练和测试。

model模型是二值神经网络的核心代码,代码中的参数可自行调整。

import torch
import torch.nn as nn
import torchvision.transforms as transforms
from torch.autograd import Function
import torch.nn.functional as F
from .binarized_modules import BinarizeLinear, BinarizeConv2d


class vgg_cifar10(nn.Module):

    def __init__(self, num_classes=1000):
        super(vgg_cifar10, self).__init__()
        self.infl_ratio=1;
        self.features = nn.Sequential(
            BinarizeConv2d(3, 128*self.infl_ratio, kernel_size=3, stride=1, padding=1, bias=True),
            nn.BatchNorm2d(128*self.infl_ratio),
            nn.Hardtanh(inplace=True),
            
            BinarizeConv2d(128*self.infl_ratio, 128*self.infl_ratio, kernel_size=3, padding=1, bias=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.BatchNorm2d(128*self.infl_ratio),
            nn.Hardtanh(inplace=True),

            BinarizeConv2d(128*self.infl_ratio, 256*self.infl_ratio, kernel_size=3, padding=1, bias=True),
            nn.BatchNorm2d(256*self.infl_ratio),
            nn.Hardtanh(inplace=True),

            BinarizeConv2d(256*self.infl_ratio, 256*self.infl_ratio, kernel_size=3, padding=1, bias=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.BatchNorm2d(256*self.infl_ratio),
            nn.Hardtanh(inplace=True),

            BinarizeConv2d(256*self.infl_ratio, 512*self.infl_ratio, kernel_size=3, padding=1, bias=True),
            nn.BatchNorm2d(512*self.infl_ratio),
            nn.Hardtanh(inplace=True),

            BinarizeConv2d(512*self.infl_ratio, 512, kernel_size=3, padding=1, bias=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.BatchNorm2d(512),
            nn.Hardtanh(inplace=True)
        )

        self.classifier = nn.Sequential(
            BinarizeLinear(512 * 4 * 4, num_classes, bias=True),
            nn.BatchNorm1d(num_classes, affine=False),
            nn.LogSoftmax()
        )
        self.regime = {
            0: {'optimizer': 'Adam', 'betas': (0.9, 0.999), 'lr': 5e-3},
            40: {'lr': 1e-3},
            80: {'lr': 5e-4},
            100: {'lr': 1e-4},
            120: {'lr': 5e-5},
            140: {'lr': 1e-5}
        }

    def forward(self, x):
        x = self.features(x)
        x = x.view(-1, 512 * 4 * 4)
        x = self.classifier(x)
        return x

def vgg_cifar10_binary(**kwargs):
    num_classes = kwargs.get('num_classes', 10)
    return vgg_cifar10(num_classes)

上述代码中使用到的BinarizeConv2d函数定义如下:

class BinarizeConv2d(nn.Conv2d):

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


    def forward(self, input):
        if input.size(1) != 3:
            input.data = Binarize(input.data)
        if not hasattr(self.weight, 'org'):
            self.weight.org = self.weight.data.clone()
        self.weight.data = Binarize(self.weight.org)

        out = nn.functional.conv2d(input, self.weight, None, self.stride,
                                   self.padding, self.dilation, self.groups)

        if not self.bias is None:
            self.bias.org = self.bias.data.clone()
            out += self.bias.view(1, -1, 1, 1).expand_as(out)

        return out

详细完整的代码可以参考Github:二值神经网络


利用CIFAR10数据集测试二值神经网络性能

import torch
import cv2
import torch.nn.functional as F
import torch.nn as nn
import numpy as np
import torchvision
import torch.utils.data as data
import torchvision.transforms as transforms
from torchvision import datasets, transforms
from models.binarized_modules import BinarizeLinear, BinarizeConv2d
from models.vgg_cifar10_binary import VGG_Cifar10
# 调用网络模型

transform = transforms.Compose([
    transforms.ToTensor(),
    # torchvision datasets are PILImage images of range [0, 1]
    # Tensors of normalized range [-1, 1]
    transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])

testset = torchvision.datasets.CIFAR10(root=r'./Datasets/CIFAR10', train=False, download=True, transform=transform)
testLoader = data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=0)

classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')


def test_every():
    class_correct = list(0. for i in range(10))
    # print(class_correct)
    class_total = list(0. for i in range(10))
    # print(class_total)
    with torch.no_grad():
        for data in testLoader:
            images, labels = data
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs, 1)
            c = (predicted == labels).squeeze()  # 去掉为1的维度
            for i in range(4):
                label = labels[i]
                class_correct[label] += c[i].item()
                class_total[label] += 1

    for i in range(10):
        print('Accuracy of %5s : %f %%' % (classes[i], 100 * class_correct[i] / class_total[i]))


def test_all():
    corret = 0
    total = 0
    with torch.no_grad():
        for data in testLoader:
            images, labels = data
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            corret += (predicted == labels).sum().item()

    print('Accuracy of the network on the 10000 test images: %f %%' % (100 * corret / total))

def evaluteTop5():
    correct = 0
    total = 0
    for x, y in testLoader:
        x, y = x.to(device), y.to(device)
        total += y.size(0)
        with torch.no_grad():
            logits = model(x)
            maxk = max((1, 5))
            y_resize = y.view(-1, 1)
            _, pred = logits.topk(maxk, 1, True, True)
            correct += torch.eq(pred, y_resize).sum().float().item()

    print('Accuracy of the network on the 10000 test images(TOP5): %f %%' % (100 * correct / total))

if __name__ == '__main__':
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    ckpt = torch.load('./results/model_best.pth.tar')
    model = VGG_Cifar10(num_classes=10)
    model.load_state_dict(ckpt['state_dict'])
    model = model.to(device)
    model.eval()  # 把模型转为test模式

    test_all()
    test_every()
    evaluteTop5()

其中:test_every()函数表示测试CIFAR10中每一类目标的TOP-1分类精度;
test_all()函数表示测试CIFAR10的整体TOP-1分类精度;
evaluteTop5()函数表示测试CIFAR10的整体TOP-5分类精度;

利用CIFAR10图片测试二值神经网络性能

为了更直观地看到CIFAR10数据集中的图片,以及验证二值神经网络的性能,故可以先将二进制的CIFAR10数据集转换为图片格式(.bmp等),再将所有图片依次送入网络模型进行测试。将CIFAR10数据集转换为图片形式,也有利于其他平台(Matlab、FPGA等)的使用。

将CIFAR10转换为图片格式的代码如下:(根据自己的情况更改数据集路径)

#  encoding:utf-8

from scipy.misc import imsave
import numpy as np


#将cifar数据集可视化,转换成图片

def unpickle(file):
    # 解压缩,返回解压后的字典
    import pickle
    fo = open(file, 'rb')
    dict = pickle.load(fo, encoding='iso-8859-1')
    fo.close()
    return dict


for j in range(1, 6):
    # 生成训练集图片,如果需要png格式,只需要改图片后缀名即可。
    dataName = "./Datasets/CIFAR10/data_batch_" + str(j)  # 读取当前目录下的data_batch12345文件,dataName其实也是data_batch文件的路径,本文和脚本文件在同一目录下。
    Xtr = unpickle(dataName)
    print(dataName + " is loading...")

    for i in range(0, 10000):
        img = np.reshape(Xtr['data'][i], (3, 32, 32))  # Xtr['data']为图片二进制数据
        img = img.transpose(1,  2,  0)  # 读取image
        picName = './test_cifar/cifar-10-bmp/train/' + str(Xtr['labels'][i]) + '_' + str(i + (j - 1)*10000) + '.bmp'  # Xtr['labels']为图片的标签,值范围0-9,本文中,train文件夹需要存在,并与脚本文件在同一目录下。
        imsave(picName, img)
    print(dataName + " loaded.")

print("test_batch is loading...")

# 生成测试集图片
testXtr = unpickle("./Datasets/CIFAR10/test_batch")
for i in range(0, 10000):
    img = np.reshape(testXtr['data'][i], (3, 32, 32))
    img = img.transpose(1, 2, 0)
    picName = './test_cifar/cifar-10-bmp/test/' + str(testXtr['labels'][i]) + '_' + str(i) + '.bmp'
    imsave(picName, img)
print("test_batch loaded.")

将所有CIFAR10图片送入二值神经网络进行测试的代码如下:

import torch
import cv2
import torch.nn.functional as F
import torch.nn as nn
import os
import numpy as np
import time
from torchvision import datasets, transforms
from models.binarized_modules import BinarizeLinear, BinarizeConv2d
from models.vgg_cifar10_binary import VGG_Cifar10
# 调用网络模型

classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

time_start = time.time()

if __name__ == '__main__':
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    ckpt = torch.load('./results/model_best.pth.tar')  # 加载训练好的模型
    model = VGG_Cifar10(num_classes=10) # 定义模型
    model.load_state_dict(ckpt['state_dict'])
    # print(model.state_dict().keys())
    # print(model.state_dict())
    # exit()

    model = model.to(device)
    model.eval()  # 把模型转为test模式

    datapath = './test_cifar'  # 测试图片所在的路径

    transform = transforms.Compose([
        transforms.ToTensor(),
        # torchvision datasets are PILImage images of range [0, 1]
        # Tensors of normalized range [-1, 1]
        transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
    ]) #数据归一化

    # str = datapath + '/*.bmp'
    correct_class = 0
    error_class = 0
    error_list = []
    for filename in os.listdir(datapath):
        ture_class = int(filename[0])
        img1 = cv2.imread(datapath + "/" + filename)[:, :, ::-1] # 读取大量测试图片,opencv读取进来的是BGR,需要转换成RGB
        # img = cv2.imread("./test_cifar/0_1023.bmp")  # 读取单幅测试图片
        img = np.ascontiguousarray(img1)
        img = transform(img)
        img = img.to(device)
        img = img.unsqueeze(0)  # 图片扩展多一维,因为输入到保存的模型中是4维的[batch_size,通道,长,宽],而普通图片只有三维,[通道,长,宽]
        output = model(img)
        # print(output)
        value, predicted = torch.max(output.data, 1)
        pred_class = classes[predicted.item()]
        print(pred_class)

        if ture_class == predicted.int():
            correct_class = correct_class + 1
        else:
            error_class = error_class + 1
            error_list.append(filename)

    print('Accuracy of the network on the 10000 test images(TOP1): %f %%' % (100 * correct_class / 10000))
    print('Error of the network on the 10000 test images(TOP1): %f %%' % (100 * error_class / 10000))
    print(error_list) # 打印错误分类图片

    time_end = time.time() # 结束运行计时
    print('time cost', time_end - time_start, 's')

提取训练模型参数以供其他平台使用

其他平台想要使用训练好的参数时,就需要将pth文件中的参数重新读取并保存,既可以保存为matlab便于使用的mat形式,也可以保存为更通用的txt文本形式。

from models.vgg_cifar10_binary15 import VGG_Cifar10
# 选择模型
import numpy as np
import scipy.io as io
import torch

ckpt = torch.load('./results/model_best.pth.tar')
model = VGG_Cifar10(10)
model.load_state_dict(ckpt['state_dict'])
model.eval()

for k, v in model.state_dict().items():
    # torch.set_printoptions(threshold=np.inf)
    # print(v)
    print(k, ',', v.size(), sep='')
    # print(k, ',', v, sep='')
    vv = np.array(v)
    io.savemat('./results/tiqu_mat/' + str(k) + '.mat', {'array': vv})