Implementación de la poda del modelo de aprendizaje profundo

    Para el aprendizaje profundo, los modelos más complejos a menudo tienen un buen efecto de reconocimiento, pero los modelos complejos a menudo requieren una potencia informática relativamente alta. En algunos escenarios de aplicaciones que requieren un alto rendimiento en tiempo real o una potencia informática relativamente pequeña, en este momento los modelos complejos a menudo no logran los resultados esperados bien En este momento, es necesario podar el modelo para mejorar la velocidad de cálculo del modelo. La poda consiste en establecer este parámetro en 0 para eliminar la conexión entre estos nodos y los siguientes, reduciendo así la cantidad de cálculo. Este artículo se basa principalmente en el combate real de la poda de modelos.

Referencia para este artículo: Compresión del modelo (poda, cuantificación) de aprendizaje profundo_Compresión del modelo de aprendizaje profundo_Algoritmo CV Blog de Enqiulu-CSDN Blog

Tabla de contenido

modelo de construcción

Explicación de la función necesaria

módulo.named_parameters()

módulo.named_buffers()

modelo.state_dict().keys()

módulo._forward_pre_ganchos

poda de una sola capa

Poda continua de una sola capa

poda mundial

poda personalizada 

modelo de construcción

    Esta parte explica principalmente el modelo utilizado en el siguiente ejemplo, que es nuestro famoso modelo LeNet, por supuesto, también son posibles otros modelos, siempre que sea una red con una estructura básica.

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        # 1: 图像的输入通道(1是黑白图像), 6: 输出通道, 3x3: 卷积核的尺寸
        self.conv1 = nn.Conv2d(1, 6, 3)
        # self.conv1 = nn.Conv2d(2, 3, 3)
        self.conv2 = nn.Conv2d(6, 16, 3)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5x5 是经历卷积操作后的图片尺寸
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, int(x.nelement() / x.shape[0]))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = LeNet().to(device=device)

Explicación de la función necesaria

    En primer lugar, necesito presentar brevemente algunas funciones que pueden aparecer con frecuencia a continuación. Si no las explica, puede estar muy confundido. Yo mismo las busqué pero no encontré una explicación muy intuitiva, así que lo haré. sigue mi entendimiento. También puedes saltar sobre él primero y volver a verlo cuando lo encuentres más tarde. Todas las explicaciones se basan en la capa convolucional, y las otras capas son similares.

 módulo.named_parameters()

    En la construcción del modelo anterior, se ha explicado que el módulo es la primera capa convolucional del modelo (por supuesto, cualquier capa es posible).Para la capa convolucional, esta función obtiene los parámetros del kernel de convolución y alguna información.

    Lo que me confundió al principio es por qué la capa de convolución de Conv2d(1, 6, 3) tiene muchos núcleos de convolución 3*3. Suponiendo que la capa convolucional es Conv2d(1, 6, 3) como se muestra arriba, lo que significa que la entrada es de 1 canal, la salida es de 6 canales y el tamaño del kernel de convolución es 3, entonces el elemento mínimo del parámetro es un Núcleo de convolución 3*3, porque la salida es de 6 canales, entonces cada canal de salida debe convolucionarse con 1 canal de entrada para obtener una salida de canal, por lo que hay 6*1 núcleos de convolución de 3*3; si la entrada es de 2 canales, entonces cada canal de salida Ambos deben convolucionarse con 2 canales de entrada, y cada canal de entrada necesita un kernel de convolución, y luego se obtiene una salida. En este momento, habrá 6 * 2 kernels de convolución de 3 * 3.

módulo.named_buffers()

    Esta función representa un búfer enmascarado. Debido a que la poda posterior está dirigida a los parámetros del núcleo de convolución, qué parámetros de posición deben marcarse para ser eliminados, y el número de este búfer corresponde a los parámetros del núcleo de convolución uno por uno. Si este parámetro se corta, entonces esta posición se marca como 0, de lo contrario es 1, y finalmente se multiplica por la matriz de parámetros, y el parámetro de posición de corte se convierte en 0.

modelo.state_dict().keys()

    Esta salida es la lista de estado actual, que puede ser algunos parámetros que deben usarse. Antes de la poda, este es un parámetro separado. Después de la poda, se convierte en una matriz de respaldo y máscara de parámetros. La función específica no es muy buena. Comprenda.

módulo._forward_pre_ganchos

Este parámetro es una lista que registra los registros del algoritmo utilizados por una determinada capa del montón, como la regularización L1.

Se introducen los siguientes cuatro métodos de poda

1. Poda de una sola capa (para una capa convolucional específica o una poda determinada)

2. Poda continua de una sola capa (poda de una sola capa para varias capas)

3. Poda global (poda global)

4. Poda personalizada (reglas de poda personalizadas)

poda de una sola capa

    El primero es podar una capa específica. Use la función prune.random_unstructured() para escribir la capa específica del modelo de poda de parámetros, como la capa convolucional. El objeto de poda es podar el sesgo de peso o sesgo, y también hay una proporción de poda, y luego las ramas se podarán de acuerdo con sus requisitos. Suponiendo que la poda es el peso, se coloca el buffer en la máscara, que marca los parámetros de que posiciones se van a podar, y estas posiciones son 0, en caso contrario son 1.

module = model.conv1
print("---修剪前的状态字典")
print(model.state_dict().keys())  # 打印修剪前的状态字典,发现有weight
print("---修剪前的参数")
print(list(module.named_parameters()))
print("---修剪前的缓冲区")
print(list(module.named_buffers()))
prune.random_unstructured(module, name="weight", amount=0.3)  # 对参数修剪
print("*" * 50)
print("---修剪后的状态字典")
print(model.state_dict().keys())  # 打印修剪前的状态字典,发现多出了 orig 和 mask
print("---修剪后的参数")
print(list(module.named_parameters()))  # 实际上还没有变,下面会解释
print("---修剪后的缓冲区")
print(list(module.named_buffers()))  # 这个就是掩码
print("---修剪算法")
print(module._forward_pre_hooks)  # 这里里面存放的每个元素是一个算法

    Puede ver la diferencia antes y después de la poda en la lista de estado, es decir, el peso ha cambiado a peso_orig y peso_mascarilla, y la máscara en realidad marca qué posiciones se van a podar . Estos datos están en el búfer, por lo que está vacío antes poda y poda Luego están los datos. peso_orig es una copia de seguridad, o el peso original , y luego la multiplicación de la máscara y peso_orig es el resultado de la poda. Se puede ver en los parámetros después de la poda que en realidad son los mismos que antes de la poda, por lo que cambiar los parámetros a después de la poda requiere el uso de la función de eliminación . Eliminar es similar al botón para confirmar la poda. Después de la ejecución, Eliminará la máscara del búfer y cambiará los parámetros a eliminar a 0. Este proceso es irreversible, después de la ejecución, si no hay una copia de seguridad adicional, los parámetros se cambiarán permanentemente. (conectado después del código anterior)

prune.remove(module, 'weight')
print("---执行remove后的参数")
print(list(module.named_parameters()))  # 此时参数变化

Poda continua de una sola capa

    La poda continua de una sola capa es similar a la poda de una sola capa. La única diferencia es que una capa convolucional se poda arriba. Ahora podemos usar un bucle para podar todas las capas convolucionales y las capas completamente conectadas. Un solo bucle De hecho, es todavía una sola capa de poda. 

print(dict(model.named_buffers()).keys())  # 打印缓冲区
print(model.state_dict().keys())  # 打印初始模型的所有状态字典
print(dict(model.named_buffers()).keys())  # 打印初始模型的mask buffers张量字典名称,发现此时为空(因为还没剪枝)
for name, module in model.named_modules():
    # 对模型中所有的卷积层执行l1_unstructured剪枝操作, 选取20%的参数剪枝
    if isinstance(module, torch.nn.Conv2d): # 比较第一个是不是第二个表示的类,这里就是判断是不是卷积层
        prune.l1_unstructured(module, name="weight", amount=0.2)
    # 对模型中所有全连接层执行ln_structured剪枝操作, 选取40%的参数剪枝
    elif isinstance(module, torch.nn.Linear):
        prune.ln_structured(module, name="weight", amount=0.4, n=2, dim=0)

# 打印多参数模块剪枝后的mask buffers张量字典名称
print(dict(model.named_buffers()).keys()) # 打印缓冲区
print(model.state_dict().keys())  # 打印多参数模块剪枝后模型的所有状态字典名称

    Se puede encontrar que hay más máscaras de peso para cada capa en el búfer, y el peso del diccionario de estado también se ha convertido en peso_orig y máscara.

poda mundial

Los dos anteriores son la poda de una capa específica, mientras que la poda global es para todo el modelo y cuántos parámetros se eliminan en todo el modelo, lo que reduce el modelo. (El código aquí está básicamente portado, gracias al tipo grande mencionado al principio del artículo)

model = LeNet().to(device=device)
parameters_to_prune = (
            (model.conv1, 'weight'),
            (model.conv2, 'weight'),
            (model.fc1, 'weight'),
            (model.fc2, 'weight'),
            (model.fc3, 'weight'))
prune.global_unstructured(parameters_to_prune, pruning_method=prune.L1Unstructured, amount=0.2)
# 统计每个层被剪枝的数量百分比(也就是统计等于0的数字占总数的比例)
print(
    "Sparsity in conv1.weight: {:.2f}%".format(
    100. * float(torch.sum(model.conv1.weight == 0))
    / float(model.conv1.weight.nelement())
    ))

print(
    "Sparsity in conv2.weight: {:.2f}%".format(
    100. * float(torch.sum(model.conv2.weight == 0))
    / float(model.conv2.weight.nelement())
    ))

print(
    "Sparsity in fc1.weight: {:.2f}%".format(
    100. * float(torch.sum(model.fc1.weight == 0))
    / float(model.fc1.weight.nelement())
    ))

print(
    "Sparsity in fc2.weight: {:.2f}%".format(
    100. * float(torch.sum(model.fc2.weight == 0))
    / float(model.fc2.weight.nelement())
    ))

print(
    "Sparsity in fc3.weight: {:.2f}%".format(
    100. * float(torch.sum(model.fc3.weight == 0))
    / float(model.fc3.weight.nelement())
    ))

print(
    "Global sparsity: {:.2f}%".format(
    100. * float(torch.sum(model.conv1.weight == 0)
               + torch.sum(model.conv2.weight == 0)
               + torch.sum(model.fc1.weight == 0)
               + torch.sum(model.fc2.weight == 0)
               + torch.sum(model.fc3.weight == 0))
         / float(model.conv1.weight.nelement()
               + model.conv2.weight.nelement()
               + model.fc1.weight.nelement()
               + model.fc2.weight.nelement()
               + model.fc3.weight.nelement())
    ))

    Después de ejecutar, se puede encontrar que los parámetros de cada capa se han cortado en diversos grados.El método de cálculo es calcular la proporción de 0 en la capa de máscara.

poda personalizada

    La personalización de la poda personalizada se refleja principalmente en el método de poda. Por ejemplo, si el parámetro es cercano a 0 o relativamente pequeño, la contribución puede ser pequeña. Entonces se puede considerar la poda en este momento y no tendrá un gran impacto. en el modelo El siguiente ejemplo adopta el método de poda alterna, es decir, poda entre sí, por supuesto, esto se puede cambiar. (Porque la referencia está escrita así) 

class myself_pruning_method(prune.BasePruningMethod):
    PRUNING_TYPE = "unstructured"

    # 内部实现compute_mask函数, 完成程序员自己定义的剪枝规则, 本质上就是如何去mask掉权重参数
    def compute_mask(self, t, default_mask):
        mask = default_mask.clone()
        # 此处定义的规则是每隔一个参数就遮掩掉一个, 最终参与剪枝的参数量的50%被mask掉
        # 当然可以自己定义
        mask.view(-1)[::2] = 0
        return mask

# 自定义剪枝方法的函数, 内部直接调用剪枝类的方法apply
def myself_unstructured_pruning(module, name):
    myself_pruning_method.apply(module, name)
    return module

# 下面开始剪枝
# 实例化模型类
model = LeNet().to(device=device)

start = time.time()  # 计时
# 调用自定义剪枝方法的函数, 对model中的第三个全连接层fc3中的偏置bias执行自定义剪枝
myself_unstructured_pruning(model.fc3, name="bias")

# 剪枝成功的最大标志, 就是拥有了bias_mask参数
print(model.fc3.bias_mask)

# 打印一下自定义剪枝的耗时
duration = time.time() - start
print(duration * 1000, 'ms')

Supongo que te gusta

Origin blog.csdn.net/weixin_60360239/article/details/129566196
Recomendado
Clasificación