注意
转到末尾 下载完整的示例代码。
剪枝教程#
创建日期:2019 年 7 月 22 日 | 最后更新:2023 年 11 月 02 日 | 最后验证:2024 年 11 月 05 日
最先进的深度学习技术依赖于过度参数化的模型,这些模型难以部署。相反,生物神经网络以使用高效的稀疏连接而闻名。识别通过减少参数数量来压缩模型的最佳技术非常重要,以在不牺牲准确性的前提下减少内存、电池和硬件消耗。这反过来又允许您在设备上部署轻量级模型,并利用设备上的私有计算来保证隐私。在研究方面,剪枝用于研究过度参数化和欠参数化网络之间学习动态的差异,研究幸运稀疏子网络和初始化(“彩票”)作为一种破坏性神经架构搜索技术的作用等等。
在本教程中,您将学习如何使用 torch.nn.utils.prune
来稀疏化您的神经网络,以及如何扩展它来实现您自己的自定义剪枝技术。
要求#
"torch>=1.4.0a0+8e8a5e0"
import torch
from torch import nn
import torch.nn.utils.prune as prune
import torch.nn.functional as F
创建模型#
在本教程中,我们使用 LeCun 等人于 1998 年提出的 LeNet 架构。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
# 1 input image channel, 6 output channels, 5x5 square conv kernel
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 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
model = LeNet().to(device=device)
检查模块#
让我们检查一下 LeNet 模型中(未剪枝的)conv1
层。它将包含两个参数 weight
和 bias
,目前没有缓冲区。
module = model.conv1
print(list(module.named_parameters()))
[('weight', Parameter containing:
tensor([[[[ 0.0644, 0.0714, 0.0409, 0.1549, -0.1539],
[-0.0971, 0.0783, 0.1766, -0.1087, -0.1955],
[-0.0273, 0.0593, 0.1937, 0.1335, -0.0627],
[ 0.0248, -0.1581, 0.1811, 0.1400, 0.1427],
[-0.0918, -0.0486, -0.1368, -0.0864, -0.0218]]],
[[[ 0.1182, -0.0398, 0.0982, -0.1954, -0.1851],
[ 0.1095, -0.1790, -0.0277, -0.1760, -0.0027],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.1394, 0.1836, -0.0859, 0.1002],
[-0.1051, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.1190, -0.0952, 0.0192, 0.1990, 0.0912],
[ 0.1574, -0.0854, 0.1001, 0.1326, 0.1858],
[-0.0444, -0.1750, 0.1469, 0.1091, 0.0399],
[ 0.1067, -0.0445, 0.1775, 0.0061, 0.0998],
[-0.1501, -0.0217, 0.0253, -0.0187, -0.0365]]],
[[[ 0.0145, -0.1725, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.1170, -0.0113, 0.1785, -0.0537],
[ 0.1013, 0.0608, 0.1382, 0.1564, -0.1048],
[-0.0665, -0.1112, -0.1837, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.1175],
[ 0.0294, -0.1602, -0.1325, 0.1916, -0.1814],
[ 0.1835, 0.1552, -0.0839, -0.1157, 0.1017],
[-0.0063, -0.1077, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0890, -0.1351]]],
[[[-0.1150, 0.1457, 0.0952, 0.1852, 0.0734],
[ 0.0109, -0.0772, -0.0722, -0.1909, 0.1345],
[ 0.0715, -0.0234, -0.0349, -0.1835, -0.1106],
[ 0.0400, 0.1856, -0.1763, 0.1291, -0.0970],
[-0.0257, 0.0063, 0.1400, -0.0588, 0.0294]]]], device='cuda:0',
requires_grad=True)), ('bias', Parameter containing:
tensor([-0.1182, 0.1782, -0.0510, -0.0680, -0.0784, -0.0060], device='cuda:0',
requires_grad=True))]
print(list(module.named_buffers()))
[]
剪枝模块#
要剪枝一个模块(在本例中为 LeNet 架构的 conv1
层),首先从 torch.nn.utils.prune
中选择一种剪枝技术(或通过继承 BasePruningMethod
实现您自己的剪枝技术)。然后,指定模块和该模块内要剪枝的参数名称。最后,使用所选剪枝技术所需的关键字参数,指定剪枝参数。
在本例中,我们将随机剪枝 conv1
层中名为 weight
的参数的 30% 连接。模块作为函数的第一个参数传入;name
使用其字符串标识符识别该模块内的参数;amount
表示要剪枝的连接百分比(如果是 0. 到 1. 之间的浮点数),或要剪枝的连接的绝对数量(如果是非负整数)。
prune.random_unstructured(module, name="weight", amount=0.3)
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
剪枝通过从参数中移除 weight
并用名为 weight_orig
的新参数(即在初始参数 name
后添加 "_orig"
)替换它来实现。weight_orig
存储张量的未剪枝版本。bias
未被剪枝,因此它将保持不变。
print(list(module.named_parameters()))
[('bias', Parameter containing:
tensor([-0.1182, 0.1782, -0.0510, -0.0680, -0.0784, -0.0060], device='cuda:0',
requires_grad=True)), ('weight_orig', Parameter containing:
tensor([[[[ 0.0644, 0.0714, 0.0409, 0.1549, -0.1539],
[-0.0971, 0.0783, 0.1766, -0.1087, -0.1955],
[-0.0273, 0.0593, 0.1937, 0.1335, -0.0627],
[ 0.0248, -0.1581, 0.1811, 0.1400, 0.1427],
[-0.0918, -0.0486, -0.1368, -0.0864, -0.0218]]],
[[[ 0.1182, -0.0398, 0.0982, -0.1954, -0.1851],
[ 0.1095, -0.1790, -0.0277, -0.1760, -0.0027],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.1394, 0.1836, -0.0859, 0.1002],
[-0.1051, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.1190, -0.0952, 0.0192, 0.1990, 0.0912],
[ 0.1574, -0.0854, 0.1001, 0.1326, 0.1858],
[-0.0444, -0.1750, 0.1469, 0.1091, 0.0399],
[ 0.1067, -0.0445, 0.1775, 0.0061, 0.0998],
[-0.1501, -0.0217, 0.0253, -0.0187, -0.0365]]],
[[[ 0.0145, -0.1725, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.1170, -0.0113, 0.1785, -0.0537],
[ 0.1013, 0.0608, 0.1382, 0.1564, -0.1048],
[-0.0665, -0.1112, -0.1837, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.1175],
[ 0.0294, -0.1602, -0.1325, 0.1916, -0.1814],
[ 0.1835, 0.1552, -0.0839, -0.1157, 0.1017],
[-0.0063, -0.1077, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0890, -0.1351]]],
[[[-0.1150, 0.1457, 0.0952, 0.1852, 0.0734],
[ 0.0109, -0.0772, -0.0722, -0.1909, 0.1345],
[ 0.0715, -0.0234, -0.0349, -0.1835, -0.1106],
[ 0.0400, 0.1856, -0.1763, 0.1291, -0.0970],
[-0.0257, 0.0063, 0.1400, -0.0588, 0.0294]]]], device='cuda:0',
requires_grad=True))]
上面选择的剪枝技术生成的剪枝掩码保存为名为 weight_mask
的模块缓冲区(即在初始参数 name
后添加 "_mask"
)。
print(list(module.named_buffers()))
[('weight_mask', tensor([[[[1., 0., 1., 0., 1.],
[1., 0., 1., 1., 1.],
[0., 1., 1., 0., 0.],
[1., 0., 1., 0., 0.],
[1., 1., 0., 1., 1.]]],
[[[1., 0., 1., 0., 1.],
[1., 1., 0., 1., 0.],
[1., 1., 1., 1., 1.],
[1., 0., 1., 1., 0.],
[0., 1., 1., 1., 1.]]],
[[[0., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 0., 0., 1., 1.],
[1., 0., 0., 1., 1.],
[1., 1., 1., 0., 1.]]],
[[[1., 0., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 0., 1., 0., 1.],
[1., 0., 1., 1., 0.],
[0., 1., 0., 1., 1.]]],
[[[1., 1., 1., 1., 0.],
[1., 1., 1., 0., 0.],
[1., 0., 0., 1., 1.],
[0., 0., 1., 1., 1.],
[1., 1., 1., 0., 1.]]],
[[[1., 1., 1., 1., 1.],
[0., 1., 0., 1., 1.],
[0., 1., 1., 0., 0.],
[1., 1., 1., 0., 1.],
[1., 1., 1., 1., 0.]]]], device='cuda:0'))]
为了使前向传播无需修改即可工作,weight
属性需要存在。torch.nn.utils.prune
中实现的剪枝技术计算权重的剪枝版本(通过将掩码与原始参数组合)并将其存储在属性 weight
中。请注意,这不再是 module
的参数,它现在只是一个属性。
print(module.weight)
tensor([[[[ 0.0644, 0.0000, 0.0409, 0.0000, -0.1539],
[-0.0971, 0.0000, 0.1766, -0.1087, -0.1955],
[-0.0000, 0.0593, 0.1937, 0.0000, -0.0000],
[ 0.0248, -0.0000, 0.1811, 0.0000, 0.0000],
[-0.0918, -0.0486, -0.0000, -0.0864, -0.0218]]],
[[[ 0.1182, -0.0000, 0.0982, -0.0000, -0.1851],
[ 0.1095, -0.1790, -0.0000, -0.1760, -0.0000],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.0000, 0.1836, -0.0859, 0.0000],
[-0.0000, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.0000, -0.0952, 0.0192, 0.1990, 0.0912],
[ 0.1574, -0.0854, 0.1001, 0.1326, 0.1858],
[-0.0444, -0.0000, 0.0000, 0.1091, 0.0399],
[ 0.1067, -0.0000, 0.0000, 0.0061, 0.0998],
[-0.1501, -0.0217, 0.0253, -0.0000, -0.0365]]],
[[[ 0.0145, -0.0000, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.0000, -0.0113, 0.0000, -0.0537],
[ 0.1013, 0.0000, 0.1382, 0.1564, -0.0000],
[-0.0000, -0.1112, -0.0000, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.0000],
[ 0.0294, -0.1602, -0.1325, 0.0000, -0.0000],
[ 0.1835, 0.0000, -0.0000, -0.1157, 0.1017],
[-0.0000, -0.0000, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0000, -0.1351]]],
[[[-0.1150, 0.1457, 0.0952, 0.1852, 0.0734],
[ 0.0000, -0.0772, -0.0000, -0.1909, 0.1345],
[ 0.0000, -0.0234, -0.0349, -0.0000, -0.0000],
[ 0.0400, 0.1856, -0.1763, 0.0000, -0.0970],
[-0.0257, 0.0063, 0.1400, -0.0588, 0.0000]]]], device='cuda:0',
grad_fn=<MulBackward0>)
最后,剪枝在使用 PyTorch 的 forward_pre_hooks
进行每次前向传播之前应用。具体来说,当 module
被剪枝时(如我们在此处所做),对于与其关联的每个被剪枝的参数,它将获得一个 forward_pre_hook
。在这种情况下,由于我们目前只剪枝了名为 weight
的原始参数,因此只会存在一个钩子。
print(module._forward_pre_hooks)
OrderedDict([(0, <torch.nn.utils.prune.RandomUnstructured object at 0x7f0418cd2740>)])
为了完整性,我们现在也可以剪枝 bias
,看看模块的参数、缓冲区、钩子和属性如何变化。为了尝试另一种剪枝技术,我们在这里使用 l1_unstructured
剪枝函数中实现的 L1 范数,剪枝偏差中最小的 3 个条目。
prune.l1_unstructured(module, name="bias", amount=3)
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
我们现在期望命名参数包括 weight_orig
(之前)和 bias_orig
。缓冲区将包括 weight_mask
和 bias_mask
。两个张量的剪枝版本将作为模块属性存在,并且该模块现在将有两个 forward_pre_hooks
。
print(list(module.named_parameters()))
[('weight_orig', Parameter containing:
tensor([[[[ 0.0644, 0.0714, 0.0409, 0.1549, -0.1539],
[-0.0971, 0.0783, 0.1766, -0.1087, -0.1955],
[-0.0273, 0.0593, 0.1937, 0.1335, -0.0627],
[ 0.0248, -0.1581, 0.1811, 0.1400, 0.1427],
[-0.0918, -0.0486, -0.1368, -0.0864, -0.0218]]],
[[[ 0.1182, -0.0398, 0.0982, -0.1954, -0.1851],
[ 0.1095, -0.1790, -0.0277, -0.1760, -0.0027],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.1394, 0.1836, -0.0859, 0.1002],
[-0.1051, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.1190, -0.0952, 0.0192, 0.1990, 0.0912],
[ 0.1574, -0.0854, 0.1001, 0.1326, 0.1858],
[-0.0444, -0.1750, 0.1469, 0.1091, 0.0399],
[ 0.1067, -0.0445, 0.1775, 0.0061, 0.0998],
[-0.1501, -0.0217, 0.0253, -0.0187, -0.0365]]],
[[[ 0.0145, -0.1725, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.1170, -0.0113, 0.1785, -0.0537],
[ 0.1013, 0.0608, 0.1382, 0.1564, -0.1048],
[-0.0665, -0.1112, -0.1837, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.1175],
[ 0.0294, -0.1602, -0.1325, 0.1916, -0.1814],
[ 0.1835, 0.1552, -0.0839, -0.1157, 0.1017],
[-0.0063, -0.1077, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0890, -0.1351]]],
[[[-0.1150, 0.1457, 0.0952, 0.1852, 0.0734],
[ 0.0109, -0.0772, -0.0722, -0.1909, 0.1345],
[ 0.0715, -0.0234, -0.0349, -0.1835, -0.1106],
[ 0.0400, 0.1856, -0.1763, 0.1291, -0.0970],
[-0.0257, 0.0063, 0.1400, -0.0588, 0.0294]]]], device='cuda:0',
requires_grad=True)), ('bias_orig', Parameter containing:
tensor([-0.1182, 0.1782, -0.0510, -0.0680, -0.0784, -0.0060], device='cuda:0',
requires_grad=True))]
print(list(module.named_buffers()))
[('weight_mask', tensor([[[[1., 0., 1., 0., 1.],
[1., 0., 1., 1., 1.],
[0., 1., 1., 0., 0.],
[1., 0., 1., 0., 0.],
[1., 1., 0., 1., 1.]]],
[[[1., 0., 1., 0., 1.],
[1., 1., 0., 1., 0.],
[1., 1., 1., 1., 1.],
[1., 0., 1., 1., 0.],
[0., 1., 1., 1., 1.]]],
[[[0., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 0., 0., 1., 1.],
[1., 0., 0., 1., 1.],
[1., 1., 1., 0., 1.]]],
[[[1., 0., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 0., 1., 0., 1.],
[1., 0., 1., 1., 0.],
[0., 1., 0., 1., 1.]]],
[[[1., 1., 1., 1., 0.],
[1., 1., 1., 0., 0.],
[1., 0., 0., 1., 1.],
[0., 0., 1., 1., 1.],
[1., 1., 1., 0., 1.]]],
[[[1., 1., 1., 1., 1.],
[0., 1., 0., 1., 1.],
[0., 1., 1., 0., 0.],
[1., 1., 1., 0., 1.],
[1., 1., 1., 1., 0.]]]], device='cuda:0')), ('bias_mask', tensor([1., 1., 0., 0., 1., 0.], device='cuda:0'))]
print(module.bias)
tensor([-0.1182, 0.1782, -0.0000, -0.0000, -0.0784, -0.0000], device='cuda:0',
grad_fn=<MulBackward0>)
print(module._forward_pre_hooks)
OrderedDict([(0, <torch.nn.utils.prune.RandomUnstructured object at 0x7f0418cd2740>), (1, <torch.nn.utils.prune.L1Unstructured object at 0x7f0418cd33d0>)])
迭代剪枝#
模块中的相同参数可以被多次剪枝,各种剪枝调用的效果等同于串联应用的各种掩码的组合。新掩码与旧掩码的组合由 PruningContainer
的 compute_mask
方法处理。
例如,假设我们现在想进一步剪枝 module.weight
,这次是根据张量第 0 轴(第 0 轴对应于卷积层的输出通道,conv1
的维度为 6)的 L2 范数进行结构化剪枝。这可以使用 ln_structured
函数,并设置 n=2
和 dim=0
来实现。
prune.ln_structured(module, name="weight", amount=0.5, n=2, dim=0)
# As we can verify, this will zero out all the connections corresponding to
# 50% (3 out of 6) of the channels, while preserving the action of the
# previous mask.
print(module.weight)
tensor([[[[ 0.0000, 0.0000, 0.0000, 0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, -0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, 0.0000, -0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, -0.0000, -0.0000, -0.0000]]],
[[[ 0.1182, -0.0000, 0.0982, -0.0000, -0.1851],
[ 0.1095, -0.1790, -0.0000, -0.1760, -0.0000],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.0000, 0.1836, -0.0859, 0.0000],
[-0.0000, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, 0.0000, -0.0000, -0.0000]]],
[[[ 0.0145, -0.0000, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.0000, -0.0113, 0.0000, -0.0537],
[ 0.1013, 0.0000, 0.1382, 0.1564, -0.0000],
[-0.0000, -0.1112, -0.0000, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.0000],
[ 0.0294, -0.1602, -0.1325, 0.0000, -0.0000],
[ 0.1835, 0.0000, -0.0000, -0.1157, 0.1017],
[-0.0000, -0.0000, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0000, -0.1351]]],
[[[-0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, -0.0000, -0.0000, 0.0000],
[ 0.0000, -0.0000, -0.0000, -0.0000, -0.0000],
[ 0.0000, 0.0000, -0.0000, 0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, -0.0000, 0.0000]]]], device='cuda:0',
grad_fn=<MulBackward0>)
相应的钩子现在将是 torch.nn.utils.prune.PruningContainer
类型,并将存储应用于 weight
参数的剪枝历史。
[<torch.nn.utils.prune.RandomUnstructured object at 0x7f0418cd2740>, <torch.nn.utils.prune.LnStructured object at 0x7f0418cd0550>]
序列化剪枝模型#
所有相关张量,包括掩码缓冲区和用于计算剪枝张量的原始参数,都存储在模型的 state_dict
中,因此可以轻松地序列化和保存,如果需要的话。
print(model.state_dict().keys())
odict_keys(['conv1.weight_orig', 'conv1.bias_orig', 'conv1.weight_mask', 'conv1.bias_mask', 'conv2.weight', 'conv2.bias', 'fc1.weight', 'fc1.bias', 'fc2.weight', 'fc2.bias', 'fc3.weight', 'fc3.bias'])
移除剪枝重参数化#
为了使剪枝永久化,移除 weight_orig
和 weight_mask
的重参数化,并移除 forward_pre_hook
,我们可以使用 torch.nn.utils.prune
中的 remove
功能。请注意,这并不能撤销剪枝,就像它从未发生过一样。它只是通过将参数 weight
重新分配给模型参数(以其剪枝版本)来使其永久化。
移除重参数化之前
print(list(module.named_parameters()))
[('weight_orig', Parameter containing:
tensor([[[[ 0.0644, 0.0714, 0.0409, 0.1549, -0.1539],
[-0.0971, 0.0783, 0.1766, -0.1087, -0.1955],
[-0.0273, 0.0593, 0.1937, 0.1335, -0.0627],
[ 0.0248, -0.1581, 0.1811, 0.1400, 0.1427],
[-0.0918, -0.0486, -0.1368, -0.0864, -0.0218]]],
[[[ 0.1182, -0.0398, 0.0982, -0.1954, -0.1851],
[ 0.1095, -0.1790, -0.0277, -0.1760, -0.0027],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.1394, 0.1836, -0.0859, 0.1002],
[-0.1051, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.1190, -0.0952, 0.0192, 0.1990, 0.0912],
[ 0.1574, -0.0854, 0.1001, 0.1326, 0.1858],
[-0.0444, -0.1750, 0.1469, 0.1091, 0.0399],
[ 0.1067, -0.0445, 0.1775, 0.0061, 0.0998],
[-0.1501, -0.0217, 0.0253, -0.0187, -0.0365]]],
[[[ 0.0145, -0.1725, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.1170, -0.0113, 0.1785, -0.0537],
[ 0.1013, 0.0608, 0.1382, 0.1564, -0.1048],
[-0.0665, -0.1112, -0.1837, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.1175],
[ 0.0294, -0.1602, -0.1325, 0.1916, -0.1814],
[ 0.1835, 0.1552, -0.0839, -0.1157, 0.1017],
[-0.0063, -0.1077, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0890, -0.1351]]],
[[[-0.1150, 0.1457, 0.0952, 0.1852, 0.0734],
[ 0.0109, -0.0772, -0.0722, -0.1909, 0.1345],
[ 0.0715, -0.0234, -0.0349, -0.1835, -0.1106],
[ 0.0400, 0.1856, -0.1763, 0.1291, -0.0970],
[-0.0257, 0.0063, 0.1400, -0.0588, 0.0294]]]], device='cuda:0',
requires_grad=True)), ('bias_orig', Parameter containing:
tensor([-0.1182, 0.1782, -0.0510, -0.0680, -0.0784, -0.0060], device='cuda:0',
requires_grad=True))]
print(list(module.named_buffers()))
[('weight_mask', tensor([[[[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]]],
[[[1., 0., 1., 0., 1.],
[1., 1., 0., 1., 0.],
[1., 1., 1., 1., 1.],
[1., 0., 1., 1., 0.],
[0., 1., 1., 1., 1.]]],
[[[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]]],
[[[1., 0., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 0., 1., 0., 1.],
[1., 0., 1., 1., 0.],
[0., 1., 0., 1., 1.]]],
[[[1., 1., 1., 1., 0.],
[1., 1., 1., 0., 0.],
[1., 0., 0., 1., 1.],
[0., 0., 1., 1., 1.],
[1., 1., 1., 0., 1.]]],
[[[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]]]], device='cuda:0')), ('bias_mask', tensor([1., 1., 0., 0., 1., 0.], device='cuda:0'))]
print(module.weight)
tensor([[[[ 0.0000, 0.0000, 0.0000, 0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, -0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, 0.0000, -0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, -0.0000, -0.0000, -0.0000]]],
[[[ 0.1182, -0.0000, 0.0982, -0.0000, -0.1851],
[ 0.1095, -0.1790, -0.0000, -0.1760, -0.0000],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.0000, 0.1836, -0.0859, 0.0000],
[-0.0000, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, 0.0000, -0.0000, -0.0000]]],
[[[ 0.0145, -0.0000, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.0000, -0.0113, 0.0000, -0.0537],
[ 0.1013, 0.0000, 0.1382, 0.1564, -0.0000],
[-0.0000, -0.1112, -0.0000, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.0000],
[ 0.0294, -0.1602, -0.1325, 0.0000, -0.0000],
[ 0.1835, 0.0000, -0.0000, -0.1157, 0.1017],
[-0.0000, -0.0000, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0000, -0.1351]]],
[[[-0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, -0.0000, -0.0000, 0.0000],
[ 0.0000, -0.0000, -0.0000, -0.0000, -0.0000],
[ 0.0000, 0.0000, -0.0000, 0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, -0.0000, 0.0000]]]], device='cuda:0',
grad_fn=<MulBackward0>)
移除重参数化之后
prune.remove(module, 'weight')
print(list(module.named_parameters()))
[('bias_orig', Parameter containing:
tensor([-0.1182, 0.1782, -0.0510, -0.0680, -0.0784, -0.0060], device='cuda:0',
requires_grad=True)), ('weight', Parameter containing:
tensor([[[[ 0.0000, 0.0000, 0.0000, 0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, -0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, 0.0000, -0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, -0.0000, -0.0000, -0.0000]]],
[[[ 0.1182, -0.0000, 0.0982, -0.0000, -0.1851],
[ 0.1095, -0.1790, -0.0000, -0.1760, -0.0000],
[ 0.1409, 0.1320, 0.0287, -0.0701, 0.0778],
[-0.1823, -0.0000, 0.1836, -0.0859, 0.0000],
[-0.0000, 0.0863, -0.0547, 0.1020, 0.1746]]],
[[[-0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, 0.0000, 0.0000, 0.0000],
[-0.0000, -0.0000, 0.0000, -0.0000, -0.0000]]],
[[[ 0.0145, -0.0000, -0.1936, 0.1588, 0.1175],
[ 0.1840, -0.0485, -0.0800, 0.1318, 0.1662],
[ 0.1660, 0.0000, -0.0113, 0.0000, -0.0537],
[ 0.1013, 0.0000, 0.1382, 0.1564, -0.0000],
[-0.0000, -0.1112, -0.0000, 0.1566, -0.1820]]],
[[[ 0.1103, 0.1611, -0.1615, 0.1834, -0.0000],
[ 0.0294, -0.1602, -0.1325, 0.0000, -0.0000],
[ 0.1835, 0.0000, -0.0000, -0.1157, 0.1017],
[-0.0000, -0.0000, -0.0003, 0.0934, -0.0693],
[-0.0236, -0.1327, -0.1541, -0.0000, -0.1351]]],
[[[-0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, -0.0000, -0.0000, -0.0000, 0.0000],
[ 0.0000, -0.0000, -0.0000, -0.0000, -0.0000],
[ 0.0000, 0.0000, -0.0000, 0.0000, -0.0000],
[-0.0000, 0.0000, 0.0000, -0.0000, 0.0000]]]], device='cuda:0',
requires_grad=True))]
print(list(module.named_buffers()))
[('bias_mask', tensor([1., 1., 0., 0., 1., 0.], device='cuda:0'))]
剪枝模型中的多个参数#
通过指定所需的剪枝技术和参数,我们可以轻松地剪枝网络中的多个张量,也许可以根据它们的类型,正如我们在此示例中将看到的。
new_model = LeNet()
for name, module in new_model.named_modules():
# prune 20% of connections in all 2D-conv layers
if isinstance(module, torch.nn.Conv2d):
prune.l1_unstructured(module, name='weight', amount=0.2)
# prune 40% of connections in all linear layers
elif isinstance(module, torch.nn.Linear):
prune.l1_unstructured(module, name='weight', amount=0.4)
print(dict(new_model.named_buffers()).keys()) # to verify that all masks exist
dict_keys(['conv1.weight_mask', 'conv2.weight_mask', 'fc1.weight_mask', 'fc2.weight_mask', 'fc3.weight_mask'])
全局剪枝#
到目前为止,我们只研究了通常所说的“局部”剪枝,即逐个剪枝模型中的张量,通过将每个条目的统计数据(权重大小、激活、梯度等)仅与该张量中的其他条目进行比较。然而,一种常见且可能更强大的技术是同时剪枝整个模型,通过移除(例如)整个模型中最低的 20% 连接,而不是移除每个层中最低的 20% 连接。这可能会导致每层的剪枝百分比不同。让我们看看如何使用 torch.nn.utils.prune
中的 global_unstructured
来实现。
model = LeNet()
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,
)
现在我们可以检查每个剪枝参数中引入的稀疏度,它在每个层中都不会等于 20%。但是,全局稀疏度将(大约)为 20%。
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()
)
)
)
Sparsity in conv1.weight: 6.67%
Sparsity in conv2.weight: 13.21%
Sparsity in fc1.weight: 22.20%
Sparsity in fc2.weight: 12.14%
Sparsity in fc3.weight: 10.24%
Global sparsity: 20.00%
使用自定义剪枝函数扩展 torch.nn.utils.prune
#
要实现您自己的剪枝函数,您可以通过继承 BasePruningMethod
基类来扩展 nn.utils.prune
模块,就像所有其他剪枝方法一样。基类为您实现了以下方法:__call__
、apply_mask
、apply
、prune
和 remove
。除了某些特殊情况,您不应该为您的新剪枝技术重新实现这些方法。但是,您必须实现 __init__
(构造函数)和 compute_mask
(根据您的剪枝技术逻辑计算给定张量掩码的指令)。此外,您必须指定此技术实现哪种类型的剪枝(支持的选项有 global
、structured
和 unstructured
)。这在迭代应用剪枝时需要确定如何组合掩码。换句话说,当剪枝预剪枝的参数时,当前的剪枝技术预计会对参数的未剪枝部分起作用。指定 PRUNING_TYPE
将使 PruningContainer
(处理剪枝掩码的迭代应用)能够正确识别要剪枝的参数切片。
例如,假设您想实现一种剪枝技术,该技术剪枝张量中(或如果张量以前被剪枝过,则在张量的剩余未剪枝部分中)的每个隔行条目。这将是 PRUNING_TYPE='unstructured'
,因为它作用于层中的单个连接,而不是整个单元/通道('structured'
)或跨不同参数('global'
)。
class FooBarPruningMethod(prune.BasePruningMethod):
"""Prune every other entry in a tensor
"""
PRUNING_TYPE = 'unstructured'
def compute_mask(self, t, default_mask):
mask = default_mask.clone()
mask.view(-1)[::2] = 0
return mask
现在,要将其应用于 nn.Module
中的参数,您还应该提供一个简单的函数来实例化该方法并应用它。
def foobar_unstructured(module, name):
"""Prunes tensor corresponding to parameter called `name` in `module`
by removing every other entry in the tensors.
Modifies module in place (and also return the modified module)
by:
1) adding a named buffer called `name+'_mask'` corresponding to the
binary mask applied to the parameter `name` by the pruning method.
The parameter `name` is replaced by its pruned version, while the
original (unpruned) parameter is stored in a new parameter named
`name+'_orig'`.
Args:
module (nn.Module): module containing the tensor to prune
name (string): parameter name within `module` on which pruning
will act.
Returns:
module (nn.Module): modified (i.e. pruned) version of the input
module
Examples:
>>> m = nn.Linear(3, 4)
>>> foobar_unstructured(m, name='bias')
"""
FooBarPruningMethod.apply(module, name)
return module
我们来试试看!
model = LeNet()
foobar_unstructured(model.fc3, name='bias')
print(model.fc3.bias_mask)
tensor([0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])
脚本总运行时间: (0 分钟 0.492 秒)