论文:
Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference

下载地址:https://arxiv.org/pdf/1712.05877.pdf

更新:量化感知训练的博客文章是在线的,并在这里链接,通过它我们可以训练和量化我们的模型以运行在4比特!
你好,我想分享我如何能够使用定点算术(8位算术)运行神经网络推理的旅程。截至目前,Pytorch的状态只允许32位或16位浮点训练和推理。如果现在想要使用量化压缩Pytorch神经网络,他/她需要将其导入onnx,转换为caffe,并在计算图上运行辉光量化编译器,最终产生量化网络。
在深入研究如何量化一个网络之前,让我们看看为什么我们需要量化一个网络。简单的答案是提高推理速度,浮点运算通常比定点(整数)运算需要更长的计算时间。另一个优势是节省空间,浮点网络的大小是8位量化网络的4倍。这与边缘设备(手机、物联网)尤其相关,因为低存储空间和计算需求对其成为可生产的解决方案至关重要。

在继续之前,这里有一个工作的Colab笔记本,供那些只想查看代码的人运行和验证这个量化网络。此示例在普通Pytorch中从头开始实现量化(没有外部库或框架)

现在我们已经证明了量化的必要性,让我们看看如何量化一个简单的MNIST模型。让我们使用一个简单的模型架构来解决MNIST,它使用2个conv层和2个全连接层。

class Net(nn.Module):
    def __init__(self, mnist=True):
      
        super(Net, self).__init__()
        if mnist:
          num_channels = 1
        else:
          num_channels = 3
          
        self.conv1 = nn.Conv2d(num_channels, 20, 5, 1)
        self.conv2 = nn.Conv2d(20, 50, 5, 1)
        self.fc1 = nn.Linear(4*4*50, 500)
        self.fc2 = nn.Linear(500, 10)

      
    def forward(self, x):
        


        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2, 2)
        x = x.view(-1, 4*4*50)   
        x = F.relu(self.fc1(x))
        x = self.fc2(x)

        return F.log_softmax(x, dim=1)

让我们使用下面的简单训练脚本来训练这个网络:

def train(args, model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()

        if batch_idx % args["log_interval"] == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))
    
def main():
 
    batch_size = 64
    test_batch_size = 64
    epochs = 10
    lr = 0.01
    momentum = 0.5
    seed = 1
    log_interval = 500
    save_model = False
    no_cuda = False
    
    use_cuda = not no_cuda and torch.cuda.is_available()

    torch.manual_seed(seed)

    device = torch.device("cuda" if use_cuda else "cpu")

    kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
    train_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=True, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=batch_size, shuffle=True, **kwargs)
    
    test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=False, transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=test_batch_size, shuffle=True, **kwargs)
    

    model = Net().to(device)
    optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum)
    args = {}
    args["log_interval"] = log_interval
    for epoch in range(1, epochs + 1):
        train(args, model, device, train_loader, optimizer, epoch)
        test(args, model, device, test_loader)

    if (save_model):
        torch.save(model.state_dict(),"mnist_cnn.pt")
    
    return model

现在,我们可以使用简单的```model = main()``命令来训练这个网络。一旦模型被训练了10个epoch,让我们通过以下测试函数来测试这个模型。

def test(args, model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
            pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)

    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

经过测试,我们得到了99%的准确率。(~9900/10000)正确分类。现在让我们研究一下通过一种称为后训练量化的技术来执行量化。

要点是我们将神经网络的激活和权重转换为8位整数(范围为0到255)。因此,我们在不动点上执行所有算法,希望精度不会显著下降。

为了量化和去量化一个张量,我们使用以下公式:

x_Float =缩放*(x_Quant -zero_point)。因此,

x_Quant = (x_Float/scale) +零点。

这里缩放等于(max_val - min_val) / (qmax - qmin)

其中max_val和min_val分别是X张量的最大值和最小值。Qmin和q_max表示8位数字的范围(分别为0和255)。刻度会缩放量化网络,零点会移动数字。下面给出的去量化和量化函数更清楚地说明了浮点张量如何转换为8位张量,反之亦然。

QTensor = namedtuple('QTensor', ['tensor', 'scale', 'zero_point'])


def quantize_tensor(x, num_bits=8):
    qmin = 0.
    qmax = 2.**num_bits - 1.
    min_val, max_val = x.min(), x.max()

    scale = (max_val - min_val) / (qmax - qmin)

    initial_zero_point = qmin - min_val / scale

    zero_point = 0
    if initial_zero_point < qmin:
        zero_point = qmin
    elif initial_zero_point > qmax:
        zero_point = qmax
    else:
        zero_point = initial_zero_point

    zero_point = int(zero_point)
    q_x = zero_point + x / scale
    q_x.clamp_(qmin, qmax).round_()
    q_x = q_x.round().byte()
    return QTensor(tensor=q_x, scale=scale, zero_point=zero_point)


def dequantize_tensor(q_x):
    return q_x.scale * (q_x.tensor.float() - q_x.zero_point)

需要注意的是,刻度是浮点数,而零点是整数(8位)。然而,现代实现通过一些花哨的位技巧(即近似)绕过了这种规模的浮点乘法,这些技巧被证明对网络的精度影响可以忽略不计。

现在我们已经准备好了这些函数,我们可以通过修改MNIST网络的前向传递来量化我们的权重和激活。修改后的前向传球看起来像这样。

def calcScaleZeroPoint(min_val, max_val,num_bits=8):
  # Calc Scale and zero point of next 
  qmin = 0.
  qmax = 2.**num_bits - 1.

  scale_next = (max_val - min_val) / (qmax - qmin)

  initial_zero_point = qmin - min_val / scale_next
  
  zero_point_next = 0
  if initial_zero_point < qmin:
      zero_point_next = qmin
  elif initial_zero_point > qmax:
      zero_point_next = qmax
  else:
      zero_point_next = initial_zero_point

  zero_point_next = int(zero_point_next)

  return scale_next, zero_point_next
  
def quantizeLayer(x, layer, stat, scale_x, zp_x):
  # for both conv and linear layers
  W = layer.weight.data
  B = layer.bias.data

  # scale_x = x.scale
  # zp_x = x.zero_point
  w = quantize_tensor(layer.weight.data) 
  b = quantize_tensor(layer.bias.data)

  layer.weight.data = w.tensor.float()
  layer.bias.data = b.tensor.float()

  ####################################################################
  # This is Quantisation !!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  scale_w = w.scale
  zp_w = w.zero_point
  
  scale_b = b.scale
  zp_b = b.zero_point
  

  scale_next, zero_point_next = calcScaleZeroPoint(min_val=stat['min'], max_val=stat['max'])

  # Perparing input by shifting
  X = x.float() - zp_x
  layer.weight.data = (scale_x * scale_w/scale_next)*(layer.weight.data - zp_w)
  layer.bias.data = (scale_b/scale_next)*(layer.bias.data + zp_b)

  # All int

  x = layer(X) + zero_point_next
    
  x = F.relu(x)

  # Reset
  layer.weight.data = W
  layer.bias.data = B
  
  return x, scale_next, zero_point_next


def quantForward(model, x, stats):
  
  # Quantise before inputting into incoming layers
  x = quantize_tensor_act(x, stats['conv1'])

  x, scale_next, zero_point_next = quantizeLayer(x.tensor, model.conv1, stats['conv2'], x.scale, x.zero_point)

  x = F.max_pool2d(x, 2, 2)
  
  x, scale_next, zero_point_next = quantizeLayer(x, model.conv2, stats['fc1'], scale_next, zero_point_next)

  x = F.max_pool2d(x, 2, 2)

  x = x.view(-1, 4*4*50)

  x, scale_next, zero_point_next = quantizeLayer(x, model.fc1, stats['fc2'], scale_next, zero_point_next)
  
  # Back to dequant for final layer
  x = dequantize_tensor(QTensor(tensor=x, scale=scale_next, zero_point=zero_point_next))
   
  x = model.fc2(x)

  return F.log_softmax(x, dim=1)

在这里,我们在输入卷积层conv1之前对激活进行量化,并使用名为quantizeLayer的函数,该函数接受conv或线性层以及量化激活的激活、缩放和零点,quantizeLayer()函数执行完全量化的层的前向传递。如果您有任何疑问,请查看上面的代码。您可能想知道quantize_tensor_act()函数是做什么的,它只是通过遍历1000个示例并平均结果,使用张量x通常具有的最小值和最大值对激活x进行量化。它使用这些统计数据来计算尺度,从而计算零点,这是量化张量的必要条件。现在,让我们将所有这些放在一起,并使用这种新的quantForward方法运行网络,并检查最终的准确性。

仍然是99% !当然,这只是一个玩具例子,我已经严重跳过了量化理论,但这是神经网络中如何执行量化的基本要点。它不是巫毒魔法,而是简单的线性代数和一些巧妙的技巧来绕过pytorch层。

希望这对你们来说是一个有趣的旅程,请查看这个正在工作的Colab笔记本,以运行和验证这个量化网络!

如果你们中有人想了解更多关于量化的知识,我已经把我从下面学到的资源嵌入其中。它们的确是无价的。

10-13 17:32