YOLOv5的Tricks | 【Trick9】模型剪枝处理与Pytorch实现的剪枝策略

简介: 在yolov5项目中的torch_utils.py文件下,有prune这个函数,用来实现模型的剪枝处理。对模型裁剪,模型剪枝这方面之前没有接触到,这里用这篇笔记来学习记录一下这方面内容。

1. Yolov5代码实现


在yolov5项目中提供了两个函数:sparsity与prune,前者可以返回模型的稀疏性,后者实现对模型的裁剪处理。


原理介绍:

对于模型稀疏性的判断,其实现的思路是遍历每一层模块的参数量,当当前层的参数值非0,表示当前的神经元是被激活的在网络的前向传播中是使用到的;而当当前层的参数值为0时,表示这个为0的参数所控制的神经元是没有被激活的,也就是不参与到网络的训练上,或者说是被dropout掉了。通过这样的一种原理,将失活的神经元与整个模型的全部参数的比值,就可以判断当前模型的稀疏性。因为当失活的神经元占比越大,表示这个模型的参数是小的,训练量也是比较小的,从而实现轻量化模型的想法,加速了前向传播与后向传播的速度,加速了整个训练过程。


但是,尽管神经元被失活没有使用上,其在网络中所初始化的位置还是被保留下来的。也就说,所以这个参数在训练网络的过程中全程都没有使用上,但是模型初始化后的参数量大小是没有被改变的(通过后序实验thop计算得知)。只能说,这里yolov5实现的模型剪枝将更多的参数置0,不参与网络更新,不被激活。


剪枝代码与稀疏性测试代码如下:

def sparsity(model):
    # Return global model sparsity
    # a用来统计使用的神经元的个数, 也就是参数量个数
    # b用来统计没有使用到的神经元个数, 也就是参数为0的个数
    a, b = 0., 0.
    for p in model.parameters():
        a += p.numel()        # numel()返回数组A中元素的数量
        b += (p == 0).sum()   # 参数为0 表示没有使用到这个神经元参数
    # b / a 即可以反应模型的稀疏程度
    return b / a
def prune(model, amount=0.3):
    # Prune model to requested global sparsity
    import torch.nn.utils.prune as prune
    print('Pruning model... ', end='')
    # 对模型中的nn.Conv2d参数进行修剪
    for name, m in model.named_modules():
        if isinstance(m, nn.Conv2d):
            # 这里会对模块原来的weight构建两个缓存去, 一个是weight_orig(原参数), 另外一个是weight_mask(原参数的掩码)
            # weight_mask掩码参数有0/1构成, 1表示当前神经元不修剪, 0表示修剪当前神经元
            prune.l1_unstructured(m, name='weight', amount=amount)  # prune
            # 将name+'_orig'与name+'_mask'从参数列表中删除, 也就是将掩码mask作用于原参数上
            # 使name保持永久修剪, 同时去除参数的前向传播钩子(就是不需要前向传播)
            prune.remove(m, 'weight')  # make permanent
    # 测试模型的稀疏性
    print(' %.3g global sparsity' % sparsity(model))


测试代码:

# 功能: 测试模型参数
def model_parms(model):
    from thop import profile
    input = torch.randn(1, 3, 640, 640)
    flops, params = profile(model, inputs=(input,))
    print('flops:{}G'.format(flops / 1e9))
    print('params:{}M'.format(params / 1e6))
# 功能: 测试模型剪枝
def model_prune():
    from utils.torch_utils import prune, sparsity, model_info
    from thop import profile
    model = load_model()
    model_parms(model)
    # model_info(model, verbose=True)
    result = sparsity(model)
    print("prune before:{}".format(result))
    prune(model)
    result = sparsity(model)
    print("prune after:{}".format(result))
    model_parms(model)
    # model_info(model, verbose=True)


输出结果:

# 剪枝前的结果:(浮点计算量, 模型参数量, 模型稀疏性)
flops:7.9331296G
params:7.02772M
prune before:2.418992153252475e-06
# 剪枝后的结果:(浮点计算量, 模型参数量, 模型稀疏性)
Pruning model...  0.299 global sparsity
flops:7.9331296G
params:7.02772M
prune after:0.29918551445007324


分析:可以看见,剪枝后的模型参数为0的比例增加,失活的神经元比例增加,模型的稀疏性增加。但是模型的参数量与浮点计算量的大小没有改变。


2. 模型剪枝介绍


详细内容见参考资料1,2


2.1 剪枝方法简介

剪枝就是通过去除网络中冗余的channels,filters, neurons, or layers以得到一个更轻量级的网络,同时不影响性能。网络剪枝的步骤神经网络中的一些权重和神经元是可以被剪枝的,这是因为这些权重可能为零或者神经元的输出大多数时候为零,表明这些权重或神经元是冗余的。


模型剪枝并不是一个新的概念,其实我们从学习深度学习的第一天起就接触过,Dropout和DropConnect代表着非常经典的模型剪枝技术,模型剪枝不仅仅只有对神经元的剪枝和对权重连接的剪枝,根据粒度的不同,至少可以粗分为4个粒度。


1.细粒度剪枝(fine-grained):即对连接或者神经元进行剪枝,它是粒度最小的剪枝。

2.向量剪枝(vector-level):它相对于细粒度剪枝粒度更大,属于对卷积核内部(intra-kernel)的剪枝。

3.核剪枝(kernel-level):即去除某个卷积核,它将丢弃对输入通道中对应计算通道的响应。

4.滤波器剪枝(Filter-level):对整个卷积核组进行剪枝,会造成推理过程中输出特征通道数的改变。

image.png


细粒度剪枝(fine-grained),向量剪枝(vector-level),核剪枝(kernel-level)方法在参数量与模型性能之间取得了一定的平衡,但是网络的拓扑结构本身发生了变化,需要专门的算法设计来支持这种稀疏的运算,被称之为非结构化剪枝。

而滤波器剪枝(Filter-level)只改变了网络中的滤波器组和特征通道数目,所获得的模型不需要专门的算法设计就能够运行,被称为结构化剪枝。除此之外还有对整个网络层的剪枝,它可以被看作是滤波器剪枝(Filter-level)的变种,即所有的滤波器都丢弃。


深度学习网络模型从卷积层到全连接层存在着大量冗余的参数,大量神经元激活值趋近于0,将这些神经元去除后可以表现出同样的模型表达能力,这种情况被称为过参数化,而对应的技术则被称为模型剪枝。


剪枝步骤

网络剪枝的过程主要分以下几步:


①训练网络;

②评估权重和神经元的重要性:可以用L1、L2来评估权重的重要性,用不是0的次数来衡量神经元的重要性;

③对权重或者神经元的重要性进行排序然后移除不重要的权重或神经元;

④移除部分权重或者神经元后网络的准确率会受到一些损伤,因此我们要进行微调,也就是使用原来的训练数据更新一下参数,往往就可以复原回来;

⑤为了不会使剪枝造成模型效果的过大损伤,我们每次都不会一次性剪掉太多的权重或神经元,因此这个过程需要迭代,也就是说剪枝且微调一次后如果剪枝后的模型大小还不令人满意就回到步骤后迭代上述过程直到满意为止


2.2 剪枝合理性解释

在我之前的一篇笔记中,记录过曾经上李宏毅老师关于为什么可以进行网络剪枝的解释,笔记见:学习笔记——神经网络压缩


现在有一个问题,既然大的网络需要剪枝处理,那么为什么一开始就不训练一个小的网络呢?一个可能的感受是,小的网络比较难以去训练,然后大的网络比较容易去优化。一般来说,训练过程中存在鞍点或者局部最优解的问题。而如果网络够大,那么这种情况就不会太严重。现在有足够多的文献可以证明,只要网络够大够深,就可以用gradient descent直接找到全局最优解。所以训练一个大的网络,再剪枝处理是比较好的。解释这一想象的一个假设是大乐透假设(Lottery Ticket Hypothesis)


  • 大乐透假设(Lottery Ticket Hypothesis)

在一个大的网络结构中,其实可以看成是有很多个小的网络组成的,每一个小的网络就有可能是一种初始化的参数,而这些小的网络有些可以train起来,而有些会train不起来。所以大的网络结构中容易训练的原因可能是,其中这么多个小网络,只有有一个可以train起来了,那么大的网络就可以train起来了。paper链接:https://arxiv.org/abs/1803.03635

image.png


这是因为剪枝做成了网络结构的不规则,因此难以用GPU进行加速。在进行实验需要使用weight pruning时可以使用将被剪枝的权重设置成0的方法,也就是掩码设计的方法。


3. Pytorch剪枝策略


官方文档:https://pytorch.org/docs/stable/nn.html#utilities


剪枝可以在单层(a single layer),多层(multiple layer)或整个模型(an entire model)中进行。主要的剪枝策略如下所示:(详细见参考资料3)


  • 类方法实现:

prune.Identity 实用剪枝方法,不剪枝任何单元,但生成带有掩码的剪枝参数化。

prune.RandomUnstructured 随机修剪(当前未修剪的)张量中的单元。

prune.L1Unstructured 通过将具有最低 L1 范数的单元归零来修剪(当前未修剪)张量中的单元。

prune.RandomStructured 随机修剪张量中的整个(当前未修剪的)通道。

prune.LnStructured 根据 Ln范数在张量中修剪整个(当前未修剪的)通道。


  • 函数方法实现:

prune.identity 将修剪重新参数化应用于与调用的参数对应的张量name,module而不实际修剪任何单位。

prune.random_unstructured 通过删除随机选择的指定的(当前未修剪的)单元来修剪与调用name的参数相对应的张量。

prune.l1_unstructured 通过删除具有最低 L1 范数的指定数量的(当前未修剪的)单元来修剪与调用name的参数相对应的张量。

prune.random_structured 通过沿随机选择的指定删除指定的(当前未修剪的)通道来修剪与调用name的参数相对应的张量。

prune.ln_structured 通过沿着具有最低 L范数的指定通道移除指定的(当前未修剪的)通道,修剪与调用name的参数相对应的张量。

prune.global_unstructured parameters通过应用指定的来全局修剪与所有参数对应的张量pruning_method。

prune.custom_from_mask name通过在 中module应用预先计算的掩码来修剪与调用的参数相对应的张量mask。

prune.remove 从模块中删除修剪重新参数化,从前向钩子中删除修剪方法。


以下内容详细见参考资料4.


构建卷积网络:这里最简单的卷积网络,LeNet5为例:

import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
class LeNet5(nn.Module):
    def __init__(self):
        super(LeNet5, self).__init__()
        # 1 input image channel, 6 output channels, 3x3 square conv kernel
        self.conv1 = nn.Conv2d(1, 6, 3)
        self.conv2 = nn.Conv2d(6, 16, 3)
        self.fc1 = nn.Linear(16 * 6 * 6, 120)  # 5x5 image dimension
        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


3.1 局部剪枝

局部剪枝 主要是对个别模块(如某一层,模块等)进行剪枝操作


import torch.nn.utils.prune as prune
# 只对conv1层的权重进行随机剪枝操作
prune.random_unstructured(model.conv1, name='weight', amount=0.25)
# 也可对偏置进行剪枝操作
prune.random_unstructured(model.conv1, name='bias', amount=0.25)


3.2 迭代剪枝

剪枝可以迭代式运用,因此在实际应用中可以针对不同维度用不同的方法进行剪枝操作。

同时剪枝不仅可应用于模块中,也可以对参数进行剪枝的。


示例:对网络结构中的卷积层进行权重剪枝,对线性连接层进行不同的权重比值剪枝操作


model = LeNet5().to(device)
for name, module in model.named_modules():
    if isinstance(module, torch.nn.Conv2d):
        # Prune all 2D convolutional layers by 30%
        prune.random_unstructured(module,name='weight', amount=0.3)
    # Prune all linear layers by 50%.
    elif isinstance(module, torch.nn.Linear):
        prune.random_unstructured(module,  name='weight', amount=0.5)


3.3 全局剪枝

全局剪枝就是对整个模型进行剪枝操作。


示例:对模型的参数进行25%剪枝操作


model = LeNet5().to(device)
parameters_to_prune = (
    (model.conv1, 'weight'),
    (model.conv2, 'weight'),
    (model.fc1, 'weight'),
    (model.fc2, 'weight'),
    (model.fc3, 'weight'),
)
# prune 25% of all the parameters in the entire model
prune.global_unstructured(
    parameters_to_prune,
    pruning_method=prune.L1Unstructured,
    amount=0.25
)


3.4 自定义剪枝

如果找不到适合您需求的修剪方法,您可以创建自己的修剪方法。 为此,请从 torch.nn.utils.prune 中提供的 BasePruningMethod 类创建一个子类。

您将需要编写自己的 _ init _() 构造函数和 compute_mask() 方法来描述您的修剪方法如何计算掩码。 此外,您需要指定修剪的类型(结构化、非结构化或全局)。


If you can’t find a pruning method that suits your needs, you can create your own pruning method. To do so, create a subclass from the BasePruningMethod class provided in torch.nn.utils.prune.

you will need to write your own init() constructor and compute_mask() method to describe how your pruning method computes the mask. In addition, you’ll need to specify the type of pruning (structured, unstructured, or global).


示例:以下自定义了一个剪枝策略,就是间隔地将掩码赋值为0,mask.view(-1)[::2] = 0


class MyPruningMethod(prune.BasePruningMethod):
    PRUNING_TYPE = 'unstructured'
    def compute_mask(self, t, default_mask):
        mask = default_mask.clone()
        mask.view(-1)[::2] = 0
        return mask
def my_unstructured(module, name):
    MyPruningMethod.apply(module, name)
    return module
# 对模型进行自定义剪枝操作
model = LeNet5().to(device)
my_unstructured(model.fc1, name='bias')


查看剪枝后的编制属性与缓存区结果:


# 查看缓存区结果
print(list(model.fc1.named_buffers()))
# 输出:
[('bias_mask', tensor([0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.,
        0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.,
        0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.,
        0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.,
        0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.,
        0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.,
        0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.]))]
# 查看属性结果
print(model.fc1.bias)
# 输出:
tensor([ 0.0000, -0.0242, -0.0000,  0.0286, -0.0000,  0.0308, -0.0000, -0.0145,
         0.0000, -0.0377,  0.0000,  0.0256,  0.0000, -0.0133,  0.0000,  0.0201,
         0.0000,  0.0028,  0.0000, -0.0362, -0.0000, -0.0119,  0.0000,  0.0405,
        -0.0000, -0.0305, -0.0000,  0.0322,  0.0000, -0.0379,  0.0000,  0.0219,
         0.0000, -0.0133,  0.0000,  0.0224, -0.0000, -0.0180,  0.0000,  0.0015,
         0.0000,  0.0129,  0.0000,  0.0166,  0.0000,  0.0306,  0.0000, -0.0251,
        -0.0000, -0.0309,  0.0000, -0.0002, -0.0000,  0.0115,  0.0000,  0.0412,
         0.0000, -0.0337, -0.0000, -0.0362, -0.0000,  0.0347, -0.0000, -0.0321,
         0.0000, -0.0399,  0.0000,  0.0241, -0.0000, -0.0186, -0.0000,  0.0114,
         0.0000, -0.0283, -0.0000,  0.0292, -0.0000, -0.0048,  0.0000, -0.0317,
        -0.0000,  0.0176, -0.0000,  0.0135,  0.0000,  0.0222, -0.0000, -0.0249,
         0.0000,  0.0299, -0.0000,  0.0278, -0.0000,  0.0166, -0.0000, -0.0232,
         0.0000,  0.0028, -0.0000, -0.0310, -0.0000, -0.0015,  0.0000,  0.0247,
         0.0000,  0.0283, -0.0000, -0.0093, -0.0000,  0.0262, -0.0000, -0.0153,
         0.0000, -0.0087,  0.0000,  0.0101,  0.0000,  0.0340, -0.0000,  0.0280],
       grad_fn=<MulBackward0>)


分析:可以看见,由于自定义的掩码操作是间隔地将掩码赋值为0,所以这里的属性值与掩码缓存都是间隔数值就为0,符合我们的自定义操作想法。


4. Pytorch剪枝测试


具体见参考资料5.


这里使用LeNet5来作为模型剪枝的例子,来查看具体的一个卷积层模型去权重weight与偏置bias的变化。


# 查看模型各层结构
model = LeNet5()
for name, param in model.named_parameters():
    print(name, param.dtype)
# 输出:
conv1.weight torch.float32
conv1.bias torch.float32
conv2.weight torch.float32
conv2.bias torch.float32
fc1.weight torch.float32
fc1.bias torch.float32
fc2.weight torch.float32
fc2.bias torch.float32
fc3.weight torch.float32
fc3.bias torch.float32


4.1 剪枝前的参数

model = LeNet5()
module = model.conv1
print(list(module.named_parameters()))


输出:权重有6个矩阵,每个矩阵的size是3*3,偏差为6个value


[('weight', Parameter containing:
tensor([[[[-0.0541,  0.3125, -0.2231],
          [-0.2518,  0.0718,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.1638, -0.1794],
          [ 0.0089, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0759],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0013,  0.2150],
          [-0.1323,  0.2928,  0.2983],
          [-0.2250,  0.0447, -0.0897]]],
        [[[-0.2509, -0.1535,  0.2116],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.1805,  0.1331],
          [-0.0635,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], requires_grad=True)), ('bias', Parameter containing:
tensor([ 0.0172, -0.0222,  0.0460,  0.2165,  0.0469,  0.0145],
       requires_grad=True))]


检测是否有缓冲区

print(list(module.named_buffers()))


输出为空矩阵:[]


ps:parameter和buffer的区别


模型中需要保存下来的参数包括两种:

1)一种是反向传播需要被optimizer更新的,称之为 parameter

2)一种是反向传播不需要被optimizer更新,称之为 buffer


4.2 对卷积层权重剪枝

目标:我们将在conv1层中名为weight的参数中随机修剪 30%的连接


  1. 从torch.nn.utils.prune选择修建技术
  2. 指定模块和该模块中需要修剪的参数名称
  3. 使用所选修剪技术所需的适当关键字参数,指定修剪参数。
# 参数说明: 对model.conv1中的权重"weight"剪枝30%的参数
prune.random_unstructured(module, name="weight", amount=0.3)
# Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))


修剪函数执行时候的内部原理:


修剪是通过从参数中删除weight并将其替换为名为weight_orig的新参数(即,将"_orig"附加到初始参数name)来进行的。 weight_orig存储未修剪的张量版本。 bias未修剪,因此它将保持完整。


print(list(module.named_parameters()))
# 输出:
[('bias', Parameter containing:
tensor([ 0.0172, -0.0222,  0.0460,  0.2165,  0.0469,  0.0145],
       requires_grad=True)), ('weight_orig', Parameter containing:
tensor([[[[-0.0541,  0.3125, -0.2231],
          [-0.2518,  0.0718,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.1638, -0.1794],
          [ 0.0089, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0759],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0013,  0.2150],
          [-0.1323,  0.2928,  0.2983],
          [-0.2250,  0.0447, -0.0897]]],
        [[[-0.2509, -0.1535,  0.2116],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.1805,  0.1331],
          [-0.0635,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], requires_grad=True))]


通过以上选择的修剪技术生成的修剪掩码将保存为名为weight_mask的模块缓冲区,存储在named_buffers中,因为掩码不需要反向传播,永久保存。


print(list(module.named_buffers()))
# 输出:
[('weight_mask', tensor([[[[1., 1., 0.],
          [0., 0., 1.],
          [1., 1., 1.]]],
        [[[1., 0., 0.],
          [0., 1., 1.],
          [1., 1., 1.]]],
        [[[1., 1., 0.],
          [1., 1., 1.],
          [1., 1., 1.]]],
        [[[1., 0., 1.],
          [0., 1., 1.],
          [0., 1., 0.]]],
        [[[0., 0., 0.],
          [1., 1., 1.],
          [1., 1., 1.]]],
        [[[1., 0., 1.],
          [0., 1., 1.],
          [1., 1., 1.]]]]))]


需要注意:mask里标记为0的位置对应的weight是被pruned掉的,在retrained的时候保持为0。而如果这时候打印weight,会得到掩码和原始参数结合的版本(即pruned的权重变为0)。注意这里的weight不是一个参数,只是一个属性。


print(module.weight)
# 输出:
tensor([[[[-0.0541,  0.3125, -0.0000],
          [-0.0000,  0.0000,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.0000, -0.0000],
          [ 0.0000, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0000],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0000,  0.2150],
          [-0.0000,  0.2928,  0.2983],
          [-0.0000,  0.0447, -0.0000]]],
        [[[-0.0000, -0.0000,  0.0000],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.0000,  0.1331],
          [-0.0000,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], grad_fn=<MulBackward0>)



剪枝需要在每次前向传播之前被应用。通过PyTorch 的forward_pre_hooks可以应用剪枝。

当模型被剪枝时,它将为与该模型关联的每个参数获取forward_pre_hook进行修剪。(注意,在这里模型不是指整个网络模型,而是指被剪枝的子模型,比如在这里是指conv1)


在这种情况下,由于到目前为止我们只修剪了名称为weight的原始参数,因此只会出现一个钩子。


print(module._forward_pre_hooks)
# 输出:
OrderedDict([(35, <torch.nn.utils.prune.RandomUnstructured object at 0x7f935dfe1c70>)])


4.3 对卷积层偏置剪枝

现在同样对bias进行剪枝处理,以查看module的参数parameter,缓冲区buffer,挂钩hook和属性property如何变化。

在这里我们尝试另一种修剪方法,按 L1 范数修剪掉最小的3个偏差bias


prune.random_unstructured(module, name="bias", amount=3)


现在查看各参数变化:


print(list(module.named_parameters()))
# 输出:
[('weight_orig', Parameter containing:
tensor([[[[-0.0541,  0.3125, -0.2231],
          [-0.2518,  0.0718,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.1638, -0.1794],
          [ 0.0089, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0759],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0013,  0.2150],
          [-0.1323,  0.2928,  0.2983],
          [-0.2250,  0.0447, -0.0897]]],
        [[[-0.2509, -0.1535,  0.2116],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.1805,  0.1331],
          [-0.0635,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], requires_grad=True)), 
('bias_orig', Parameter containing:
tensor([ 0.0172, -0.0222,  0.0460,  0.2165,  0.0469,  0.0145],
       requires_grad=True))]


缓冲区:


print(list(module.named_buffers()))
# 输出:
[('weight_mask', tensor([[[[1., 1., 0.],
          [0., 0., 1.],
          [1., 1., 1.]]],
        [[[1., 0., 0.],
          [0., 1., 1.],
          [1., 1., 1.]]],
        [[[1., 1., 0.],
          [1., 1., 1.],
          [1., 1., 1.]]],
        [[[1., 0., 1.],
          [0., 1., 1.],
          [0., 1., 0.]]],
        [[[0., 0., 0.],
          [1., 1., 1.],
          [1., 1., 1.]]],
        [[[1., 0., 1.],
          [0., 1., 1.],
          [1., 1., 1.]]]])), 
('bias_mask', tensor([1., 1., 0., 0., 0., 1.]))]


属性值:


print(module.bias)
# 输出:
tensor([ 0.0172, -0.0222,  0.0000,  0.0000,  0.0000,  0.0145],
       grad_fn=<MulBackward0>)


钩子:可以看到有两个钩子


print(module._forward_pre_hooks)
# 输出:
OrderedDict([(35, <torch.nn.utils.prune.RandomUnstructured object at 0x7f935dfe1c70>), 
    (36, <torch.nn.utils.prune.RandomUnstructured object at 0x7f935dfe1b20>)])


4.4 对卷积层权重删除修剪

现在对利用prune.remove函数从模块中删除修剪重新参数化,从前向钩子中删除修剪方法。来查看个参数的变化,以了解prune.remove函数的作用。


# 参数说明: 删除module中对"weight"的剪枝处理
prune.remove(module, "weight")
# Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))


各参数变化:weight替换掉了原来的weight_orig,表示参数的更新,只不过由于剪枝后所以部分的参数为0


print(list(module.named_parameters()))
# 输出:
[('bias_orig', Parameter containing:
tensor([ 0.0172, -0.0222,  0.0460,  0.2165,  0.0469,  0.0145],
       requires_grad=True)), ('weight', Parameter containing:
tensor([[[[-0.0541,  0.3125, -0.0000],
          [-0.0000,  0.0000,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.0000, -0.0000],
          [ 0.0000, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0000],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0000,  0.2150],
          [-0.0000,  0.2928,  0.2983],
          [-0.0000,  0.0447, -0.0000]]],
        [[[-0.0000, -0.0000,  0.0000],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.0000,  0.1331],
          [-0.0000,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], requires_grad=True))]


缓冲区:


print(list(module.named_buffers()))
# 输出:
[('bias_mask', tensor([1., 1., 0., 0., 0., 1.]))]


属性值:由grad_fn=变为requires_grad=True,现在变成了需要更新的参数


print(module.weight)
# 输出:
Parameter containing:
tensor([[[[-0.0541,  0.3125, -0.0000],
          [-0.0000,  0.0000,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.0000, -0.0000],
          [ 0.0000, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0000],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0000,  0.2150],
          [-0.0000,  0.2928,  0.2983],
          [-0.0000,  0.0447, -0.0000]]],
        [[[-0.0000, -0.0000,  0.0000],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.0000,  0.1331],
          [-0.0000,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], requires_grad=True)


钩子:只剩下对bias剪枝的钩子


print(module._forward_pre_hooks)
# 输出:
OrderedDict([(36, <torch.nn.utils.prune.RandomUnstructured object at 0x7f935dfe1b20>)])


4.5 对卷积层偏置删除修剪

prune.remove(module, "bias")


同样,四部曲分别查看参数,缓存区,属性,钩子,变化如下:


# 1. 参数查看
print(list(module.named_parameters()))
# 输出:
[('weight', Parameter containing:
tensor([[[[-0.0541,  0.3125, -0.0000],
          [-0.0000,  0.0000,  0.1464],
          [-0.2506,  0.0921,  0.1507]]],
        [[[-0.0208, -0.0000, -0.0000],
          [ 0.0000, -0.2395, -0.0704],
          [ 0.0643, -0.1986, -0.1765]]],
        [[[-0.1040, -0.1539, -0.0000],
          [-0.3282,  0.0630, -0.3091],
          [-0.0085,  0.2010,  0.0239]]],
        [[[-0.2581, -0.0000,  0.2150],
          [-0.0000,  0.2928,  0.2983],
          [-0.0000,  0.0447, -0.0000]]],
        [[[-0.0000, -0.0000,  0.0000],
          [-0.0049,  0.0929, -0.2913],
          [ 0.1029,  0.1205, -0.1133]]],
        [[[-0.3326,  0.0000,  0.1331],
          [-0.0000,  0.0032, -0.2707],
          [-0.2712, -0.2373,  0.2067]]]], requires_grad=True)), ('bias', Parameter containing:
tensor([ 0.0172, -0.0222,  0.0000,  0.0000,  0.0000,  0.0145],
       requires_grad=True))]
# 2. 缓存区查看
print(list(module.named_buffers()))
# 输出: [] 一个空列表
# 3. 属性查看
print(module.bias)
# 输出: 
Parameter containing:
tensor([ 0.0172, -0.0222,  0.0000,  0.0000,  0.0000,  0.0145],
       requires_grad=True)
# 4. 钩子查看
print(module._forward_pre_hooks)
# 输出:
OrderedDict()


看到这里应该就能知道prune.remove的作用了,就是将剪枝的模块重新复制更新,取消掩码钩子等操作,变成普通网络的训练参数。


4.6 剪枝对参数量与浮点计算量的变化

这里对yolov5的代码进行参考,测试剪枝前后模型参数的变化与浮点计算量的变化,需要主要的是这里遍历每一个模块之后是对剪枝进行remove更新的,也就是说剪枝后权重与偏置为0的参数是直接被赋值到模型中,这样就可以计算模型剪枝后的稀疏度。


参考代码:


# 功能: 测试模型参数
def model_parms(model):
    from thop import profile
    input = torch.randn(1, 1, 32, 32)
    flops, params = profile(model, inputs=(input,))
    print('flops:{}M'.format(flops / 1e6))
    print('params:{}Kb'.format(params / 1e3))
# 功能:测试模型稀疏度
def sparsity(model):
    # Return global model sparsity
    # a用来统计使用的神经元的个数, 也就是参数量个数
    # b用来统计没有使用到的神经元个数, 也就是参数为0的个数
    a, b = 0., 0.
    for p in model.parameters():
        a += p.numel()        # numel()返回数组A中元素的数量
        b += (p == 0).sum()   # 参数为0 表示没有使用到这个神经元参数
    # b / a 即可以反应模型的稀疏程度
    return b / a
# 功能: 对模型的卷积层与全连接层进行剪枝操作
def model_prune(model):
  import torch.nn.utils.prune as prune
    for name, module in model.named_modules():
        # Prune all 2D convolutional layers by 30%
        if isinstance(module, torch.nn.Conv2d):
            print("torch.nn.Conv2d:", name)
            prune.random_unstructured(module,name='weight', amount=0.3)
            prune.random_unstructured(module,name='bias', amount=3)
            prune.remove(module, 'weight')
            prune.remove(module, 'bias')
        # Prune all linear layers by 50%.
        elif isinstance(module, torch.nn.Linear):
            print("torch.nn.Linear:", name)
            prune.random_unstructured(module,name='weight', amount=0.5)
            prune.random_unstructured(module,name='bias', amount=3)
            prune.remove(module, 'weight')
            prune.remove(module, 'bias')
# 测试函数
if __name__ == '__main__':
  model = LeNet5()
  # 剪枝前参数测试
  model_parms(model)
  print(sparsity(model))
  model_prune(model)
  # 剪枝后参数测试
  model_parms(model)
  print(sparsity(model))


输出:


# 剪枝前
[INFO] Register count_convNd() for <class 'torch.nn.modules.conv.Conv2d'>.
[INFO] Register count_linear() for <class 'torch.nn.modules.linear.Linear'>.
[WARN] Cannot find rule for <class '__main__.LeNet5'>. Treat it as zero Macs and zero Params.
flops:0.28276M
params:81.194Kb
tensor(0.)
# 剪枝后
torch.nn.Conv2d: conv1
torch.nn.Conv2d: conv2
torch.nn.Linear: fc1
torch.nn.Linear: fc2
torch.nn.Linear: fc3
[INFO] Register count_convNd() for <class 'torch.nn.modules.conv.Conv2d'>.
[INFO] Register count_linear() for <class 'torch.nn.modules.linear.Linear'>.
[WARN] Cannot find rule for <class '__main__.LeNet5'>. Treat it as zero Macs and zero Params.
flops:0.28276M
params:81.194Kb
tensor(0.4965)


可以看见,这里由于是对模型进行掩码的操作,所以模型的参数量是没有改变的,因为正如上诉所说权重并不是真的被剪掉了,只是掩码的作用让他失活不对模型更新产生作用。从稀疏度的变化就可以了解到了,当前的模型有很多神经元被失活置为0。


参考资料:


1. 模型剪枝简介


2. 模型剪枝


3. Pytorch Utils 总结


4. Pytorch袖珍手册之十四


5. Pytorch剪枝代码示例和注释


6. 学习笔记——神经网络压缩


目录
相关文章
|
2月前
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
363 2
|
19天前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
35 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
2月前
|
机器学习/深度学习 自然语言处理 监控
利用 PyTorch Lightning 搭建一个文本分类模型
利用 PyTorch Lightning 搭建一个文本分类模型
69 8
利用 PyTorch Lightning 搭建一个文本分类模型
|
2月前
|
机器学习/深度学习 自然语言处理 数据建模
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
本文深入探讨了Transformer模型中的三种关键注意力机制:自注意力、交叉注意力和因果自注意力,这些机制是GPT-4、Llama等大型语言模型的核心。文章不仅讲解了理论概念,还通过Python和PyTorch从零开始实现这些机制,帮助读者深入理解其内部工作原理。自注意力机制通过整合上下文信息增强了输入嵌入,多头注意力则通过多个并行的注意力头捕捉不同类型的依赖关系。交叉注意力则允许模型在两个不同输入序列间传递信息,适用于机器翻译和图像描述等任务。因果自注意力确保模型在生成文本时仅考虑先前的上下文,适用于解码器风格的模型。通过本文的详细解析和代码实现,读者可以全面掌握这些机制的应用潜力。
121 3
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
|
3月前
|
机器学习/深度学习 PyTorch 调度
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
在深度学习中,学习率作为关键超参数对模型收敛速度和性能至关重要。传统方法采用统一学习率,但研究表明为不同层设置差异化学习率能显著提升性能。本文探讨了这一策略的理论基础及PyTorch实现方法,包括模型定义、参数分组、优化器配置及训练流程。通过示例展示了如何为ResNet18设置不同层的学习率,并介绍了渐进式解冻和层适应学习率等高级技巧,帮助研究者更好地优化模型训练。
203 4
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
|
3月前
|
机器学习/深度学习 监控 PyTorch
PyTorch 模型调试与故障排除指南
在深度学习领域,PyTorch 成为开发和训练神经网络的主要框架之一。本文为 PyTorch 开发者提供全面的调试指南,涵盖从基础概念到高级技术的内容。目标读者包括初学者、中级开发者和高级工程师。本文探讨常见问题及解决方案,帮助读者理解 PyTorch 的核心概念、掌握调试策略、识别性能瓶颈,并通过实际案例获得实践经验。无论是在构建简单神经网络还是复杂模型,本文都将提供宝贵的洞察和实用技巧,帮助开发者更高效地开发和优化 PyTorch 模型。
54 3
PyTorch 模型调试与故障排除指南
|
2月前
|
存储 并行计算 PyTorch
探索PyTorch:模型的定义和保存方法
探索PyTorch:模型的定义和保存方法
|
4月前
|
机器学习/深度学习 PyTorch 编译器
PyTorch 与 TorchScript:模型的序列化与加速
【8月更文第27天】PyTorch 是一个非常流行的深度学习框架,它以其灵活性和易用性而著称。然而,当涉及到模型的部署和性能优化时,PyTorch 的动态计算图可能会带来一些挑战。为了解决这些问题,PyTorch 引入了 TorchScript,这是一个用于序列化和优化 PyTorch 模型的工具。本文将详细介绍如何使用 TorchScript 来序列化 PyTorch 模型以及如何加速模型的执行。
169 4
|
4月前
|
机器学习/深度学习 边缘计算 PyTorch
PyTorch 与边缘计算:将深度学习模型部署到嵌入式设备
【8月更文第29天】随着物联网技术的发展,越来越多的数据处理任务开始在边缘设备上执行,以减少网络延迟、降低带宽成本并提高隐私保护水平。PyTorch 是一个广泛使用的深度学习框架,它不仅支持高效的模型训练,还提供了多种工具帮助开发者将模型部署到边缘设备。本文将探讨如何将PyTorch模型高效地部署到嵌入式设备上,并通过一个具体的示例来展示整个流程。
756 1
|
4月前
|
机器学习/深度学习 自然语言处理 PyTorch
PyTorch与Hugging Face Transformers:快速构建先进的NLP模型
【8月更文第27天】随着自然语言处理(NLP)技术的快速发展,深度学习模型已经成为了构建高质量NLP应用程序的关键。PyTorch 作为一种强大的深度学习框架,提供了灵活的 API 和高效的性能,非常适合于构建复杂的 NLP 模型。Hugging Face Transformers 库则是目前最流行的预训练模型库之一,它为 PyTorch 提供了大量的预训练模型和工具,极大地简化了模型训练和部署的过程。
239 2
下一篇
DataWorks