Connaissances d'apprentissage en profondeur 6: (modèle de compression quantitative) ---- Module personnalisé pytorch, et comprendre la méthode de définition de réseau DoReFaNet à travers celui-ci.

Référez-vous au responsable chinois, pour plus de détails, reportez-vous à: Comment personnaliser le module dans PyTorch

1. Module personnalisé

Insérez la description de l'image ici
Le module est la manière de base dont pytorch organise les réseaux de neurones. Le module contient les paramètres du modèle et la logique de calcul. La fonction porte la fonction réelle et définit la logique de calcul avant et arrière.
Ce qui suit prend la structure de réseau MLP la plus simple comme exemple pour présenter comment implémenter une structure de réseau personnalisée. Le code complet se trouve dans le repo .

1.1 Fonction

La fonction est la classe principale du mécanisme de dérivation automatique de pytorch. La fonction est sans paramètre ou sans état, elle est uniquement responsable de la réception de l'entrée et du retour de la sortie correspondante; pour l'inverse, elle reçoit le gradient correspondant de la sortie et renvoie le gradient correspondant de l'entrée.

Ici, nous nous concentrons uniquement sur la personnalisation de la fonction. Voir le code source pour la définition de Function . Voici un extrait de code simplifié:

class Function(object):
    def forward(self, *input):
        raise NotImplementedError
 
    def backward(self, *grad_output):
        raise NotImplementedError

L'entrée et la sortie de l'avant et de l'arrière sont des objets Tensor.

L'objet Function est appelable, c'est-à-dire qu'il peut être appelé par (). L'entrée et la sortie de l'appel sont des objets Variable. L'exemple de code suivant montre comment implémenter une fonction d'activation ReLU et l'appeler:


import torch
from torch.autograd import Function
 
class ReLUF(Function):
    def forward(self, input):
        self.save_for_backward(input)
 
        output = input.clamp(min=0)
        return output
 
    def backward(self, output_grad):
        input = self.to_save[0]
 
        input_grad = output_grad.clone()
        input_grad[input < 0] = 0
        return input_grad
 
## Test
if __name__ == "__main__":
      from torch.autograd import Variable
 
      torch.manual_seed(1111)  
      a = torch.randn(2, 3)
 
      va = Variable(a, requires_grad=True)
      vb = ReLUF()(va)
      print va.data, vb.data
 
      vb.backward(torch.ones(va.size()))
      print vb.grad.data, va.grad.data

Si l'entrée avant est nécessaire à l'arrière, vous devez enregistrer explicitement l'entrée requise dans l'avant. Dans le code ci-dessus, forward utilise la fonction self.save_for_backward pour enregistrer temporairement l'entrée et utilise saved_tensors (objet tuple python) pour la retirer à l'arrière.

De toute évidence, l'entrée avant doit correspondre à l'entrée arrière; en même temps, la sortie directe doit correspondre à l'entrée arrière.
Remarque:
Comme Function peut avoir besoin de stocker temporairement le tenseur d'entrée, il est recommandé de ne pas réutiliser l'objet Function pour éviter le problème de libération précoce de la mémoire.
Comme indiqué dans l' exemple de code , chaque appel au transfert régénère un objet ReLUF et ne peut pas être généré pendant l'initialisation et les appels répétés en transfert. (Cela signifie que l'exemple de code est correct, c'est que chaque fc génère un nouvel objet. Parce que chaque objet gère son propre poids et sa propre entrée )

Module 2

Semblable à Function, l'objet Module est également appelable, et l'entrée et la sortie sont également variables. La différence est que Module peut avoir des paramètres. Le module se compose de deux parties principales: les paramètres et la logique de calcul (appel de fonction). Étant donné que la fonction d'activation ReLU n'a pas de paramètres, nous prenons ici la couche entièrement connectée la plus élémentaire comme exemple pour illustrer comment personnaliser le module.
La logique de fonctionnement de la couche entièrement connectée est définie comme suit Fonction:

import torch
from torch.autograd import Function
 
class LinearF(Function):
 
     def forward(self, input, weight, bias=None):
         self.save_for_backward(input, weight, bias)
 
         output = torch.mm(input, weight.t())
         if bias is not None:
             output += bias.unsqueeze(0).expand_as(output)
 
         return output
 
     def backward(self, grad_output):
         input, weight, bias = self.saved_tensors
 
         grad_input = grad_weight = grad_bias = None
         if self.needs_input_grad[0]:
             grad_input = torch.mm(grad_output, weight)
         if self.needs_input_grad[1]:
             grad_weight = torch.mm(grad_output.t(), input)
         if bias is not None and self.needs_input_grad[2]:
             grad_bias = grad_output.sum(0).squeeze(0)
 
         if bias is not None:
             return grad_input, grad_weight, grad_bias
         else:
             return grad_input, grad_weight

needs_input_grad est un tuple avec un élément booléen, la longueur est la même que le nombre de paramètres avant, et il est utilisé pour identifier si chaque entrée est entrée pour calculer le gradient; pour les entrées qui ne nécessitent pas de gradients, les calculs inutiles peuvent être réduits .

Function (LinearF ici) définit la logique de calcul de base. Le module n'a besoin que d'allouer de l'espace mémoire pour les paramètres lors de l'initialisation et de transmettre les paramètres à l'objet Function correspondant pendant le calcul. code montrer comme ci-dessous:

import torch
import torch.nn as nn
 
class Linear(nn.Module):
 
    def __init__(self, in_features, out_features, bias=True):
         super(Linear, self).__init__()
         self.in_features = in_features
         self.out_features = out_features
         self.weight = nn.Parameter(torch.Tensor(out_features, in_features))
         if bias:
             self.bias = nn.Parameter(torch.Tensor(out_features))
         else:
            self.register_parameter('bias', None)
 
    def forward(self, input):
         return LinearF()(input, self.weight, self.bias)

Il convient de noter que le paramètre est l'espace mémoire maintenu par l'objet tensor, mais que le tenseur doit être conditionné en tant qu'objet Parameter . Parameter est une sous - classe spéciale de Variable , la seule différence est que la valeur par défaut requires_grad de Parameter est True. Varaible est la classe principale du mécanisme de dérivation automatique. Elle ne sera pas présentée ici, veuillez vous référer au tutoriel .

2.3 Réseau neuronal récurrent personnalisé (RNN)

Référence de code
exécutable : RNN
Parmi eux, Paramètres est une sous-classe de Variable, et c'est un mécanisme de dérivation automatique, donc lorsque nous définissons le réseau de codes, nous n'avons fondamentalement pas besoin d'être à l' envers , tant que l'avant est défini.

3 Définir DoReFaNet

Puisqu'il compresse les poids, nous devons seulement définir une classe de convolution, une classe de fonction d'activation, puis obtenir les poids en lui, quantifier et définir le réseau comme suit:

class AlexNet_Q(nn.Module):
  def __init__(self, wbit, abit, num_classes=1000):
    super(AlexNet_Q, self).__init__()
    Conv2d = conv2d_Q_fn(w_bit=wbit)
    Linear = linear_Q_fn(w_bit=wbit)

    self.features = nn.Sequential(
      nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
      nn.BatchNorm2d(96),
      nn.ReLU(inplace=True),
      nn.MaxPool2d(kernel_size=3, stride=2),

      Conv2d(96, 256, kernel_size=5, padding=2),
      nn.BatchNorm2d(256),
      nn.ReLU(inplace=True),
      activation_quantize_fn(a_bit=abit),
      nn.MaxPool2d(kernel_size=3, stride=2),

      Conv2d(256, 384, kernel_size=3, padding=1),
      nn.ReLU(inplace=True),
      activation_quantize_fn(a_bit=abit),

      Conv2d(384, 384, kernel_size=3, padding=1),
      nn.ReLU(inplace=True),
      activation_quantize_fn(a_bit=abit),

      Conv2d(384, 256, kernel_size=3, padding=1),
      nn.ReLU(inplace=True),
      activation_quantize_fn(a_bit=abit),
      nn.MaxPool2d(kernel_size=3, stride=2),
    )
    self.classifier = nn.Sequential(
      Linear(256 * 6 * 6, 4096),
      nn.ReLU(inplace=True),
      activation_quantize_fn(a_bit=abit),

      Linear(4096, 4096),
      nn.ReLU(inplace=True),
      activation_quantize_fn(a_bit=abit),
      nn.Linear(4096, num_classes),
    )

    for m in self.modules():
      if isinstance(m, Conv2d) or isinstance(m, Linear):
        init.xavier_normal_(m.weight.data)

  def forward(self, x):
    x = self.features(x)
    x = x.view(x.size(0), 256 * 6 * 6)
    x = self.classifier(x)
    return 
  • Les droits de personnalisation sont quantifiés comme suit:

class weight_quantize_fn(nn.Module):
  def __init__(self, w_bit):
    super(weight_quantize_fn, self).__init__()
    assert w_bit <= 8 or w_bit == 32
    self.w_bit = w_bit
    self.uniform_q = uniform_quantize(k=w_bit)

  def forward(self, x):
    if self.w_bit == 32:
      weight_q = x
    elif self.w_bit == 1:
      E = torch.mean(torch.abs(x)).detach()
      weight_q = self.uniform_q(x / E) * E
    else:
      weight = torch.tanh(x)
      weight = weight / 2 / torch.max(torch.abs(weight)) + 0.5
      weight_q = 2 * self.uniform_q(weight) - 1
    return weight_q

def conv2d_Q_fn(w_bit):
  class Conv2d_Q(nn.Conv2d):
    def __init__(self, in_channels, out_channels, kernel_size, stride=1,
                 padding=0, dilation=1, groups=1, bias=True):
      super(Conv2d_Q, self).__init__(in_channels, out_channels, kernel_size, stride,
                                     padding, dilation, groups, bias)
      self.w_bit = w_bit
      self.quantize_fn = weight_quantize_fn(w_bit=w_bit)

    def forward(self, input, order=None):
      weight_q = self.quantize_fn(self.weight)
      # print(np.unique(weight_q.detach().numpy()))
      return F.conv2d(input, weight_q, self.bias, self.stride,
                      self.padding, self.dilation, self.groups)

  return Conv2d_Q

Parmi eux, la fonction de quantification de bit uniform_quantize est:

def uniform_quantize(k):
  class qfn(torch.autograd.Function):

    @staticmethod
    def forward(ctx, input):
      if k == 32:
        out = input
      elif k == 1:
        out = torch.sign(input)
      else:
        n = float(2 ** k - 1)
        out = torch.round(input * n) / n
      return out

    @staticmethod
    def backward(ctx, grad_output):
      grad_input = grad_output.clone()
      return grad_input

  return qfn().apply

  • Une fois la couche d'activation activée, les bits de sortie sont quantifiés comme suit:
class activation_quantize_fn(nn.Module):
  def __init__(self, a_bit):
    super(activation_quantize_fn, self).__init__()
    assert a_bit <= 8 or a_bit == 32
    self.a_bit = a_bit
    self.uniform_q = uniform_quantize(k=a_bit)

  def forward(self, x):
    if self.a_bit == 32:
      activation_q = x
    else:
      activation_q = self.uniform_q(torch.clamp(x, 0, 1))
      # print(np.unique(activation_q.detach().numpy()))
    return activation_q

4. Effectuez la compression du canal du modèle, en utilisant le code yolov3 comme exemple:

La compression du canal se fait principalement par BN的aX+b=ouputla répartition du poids a de l' agencement . Après apprentissage, le poids a est trié, le seuil est obtenu en fonction du taux de recadrage, puis les canaux inférieurs au seuil sont exclus et ceux supérieurs au seuil sont conservés.

  • Obtenez l'index du nombre de couches réseau, y compris la couche BN:
def parse_module_defs(module_defs):
    CBL_idx = []
    Conv_idx = []
    for i, module_def in enumerate(module_defs):
        # 具有bn层的,才进行保存索引,然后用来压缩训练。
        if module_def['type'] == 'convolutional':
            if module_def['batch_normalize'] == '1':
                CBL_idx.append(i)
            else:
                Conv_idx.append(i)
    #排除一些不要进行压缩的层,为了保证准确度。
    ignore_idx = set()
    for i, module_def in enumerate(module_defs):
        if module_def['type'] == 'shortcut':
            ignore_idx.add(i-1)
            identity_idx = (i + int(module_def['from']))
            if module_defs[identity_idx]['type'] == 'convolutional':
                ignore_idx.add(identity_idx)
            elif module_defs[identity_idx]['type'] == 'shortcut':
                ignore_idx.add(identity_idx - 1)

    ignore_idx.add(84)
    ignore_idx.add(96)
    #获取符合要求跟需要压缩的网络层索引。
    prune_idx = [idx for idx in CBL_idx if idx not in ignore_idx]

    return CBL_idx, Conv_idx, prune_idx

  • Code pour la formation:
        for batch_i, (_, imgs, targets) in enumerate(dataloader):
            batches_done = len(dataloader) * epoch + batch_i

            imgs = imgs.to(device)
            targets = targets.to(device)

            loss, outputs = model(imgs, targets)

            optimizer.zero_grad()
            loss.backward()
            #进行模型权重的稀疏化训练,opt.s可以控制稀疏值,prune_idx是包含需要进行稀疏训练的
            #卷积层索引。
            BNOptimizer.updateBN(sr_flag, model.module_list, opt.s, prune_idx)

            optimizer.step()
  • La fonction principale dont la formation clairsemée, Principe: La opt.svaleur supplémentaire pour rendre le poids le plus important pour obtenir une canette 正梯度外加值. La fonction de code est la suivante:
class BNOptimizer():

    @staticmethod
    def updateBN(sr_flag, module_list, s, prune_idx):
        if sr_flag:
            for idx in prune_idx:
                # Squential(Conv, BN, Lrelu)
                bn_module = module_list[idx][1]
                #torch.sign()是一个跳变函数,其值只有-1,0,-1.
                #这里的s指的是附加到权重梯度grad上的值,其可以用来加大或减少梯度值,
                #从而在更新权重的时候,权值大的可以变的越来越大。
                bn_module.weight.grad.data.add_(s * torch.sign(bn_module.weight.data))  # L1

  • Une fois l'ensemble du modèle entraîné, les canaux réseau inutiles peuvent être supprimés grâce au taux d'élagage défini par vous-même. La fonction est la suivante:
percent = 0.85
def prune_and_eval(model, sorted_bn, percent=.0):
    model_copy = deepcopy(model)
    #根据裁剪比例进行获取停止的通道索引
    thre_index = int(len(sorted_bn) * percent)
    #任何根据这个停止的通道索引获取其对应的通道权重值,
    #然后作为刷选阈值。
    thre = sorted_bn[thre_index]
    print(f'Channels with Gamma value less than {thre:.4f} are pruned!')
    remain_num = 0
    for idx in prune_idx:
        bn_module = model_copy.module_list[idx][1]
        #通过阈值获取刷选通道
        mask = obtain_bn_mask(bn_module, thre)
        remain_num += int(mask.sum())
        #根据mask的0\1进行获取保留了通道的
        bn_module.weight.data.mul_(mask)
    mAP = eval_model(model_copy)[2].mean()

    print(f'Number of channels has been reduced from {len(sorted_bn)} to {remain_num}')
    print(f'Prune ratio: {1-remain_num/len(sorted_bn):.3f}')
    print(f'mAP of the pruned model is {mAP:.4f}')

    return thre

Je suppose que tu aimes

Origine blog.csdn.net/yangdashi888/article/details/104986121
conseillé
Classement