模型压缩
为什么需要模型压缩
量化
剪枝
蒸馏
总结 | 方法 |
---|---|
线性或者非线性量化 | 1/2bits,int8和fp16等 |
结构或非结构剪枝 | deep compression,channel pruning和network slimming等 |
网络结构搜索 | NAS:Network Architecture Search:DARTS,DetNAS和NAS-FCOS |
权重矩阵的低秩分解 | 知识蒸馏与网络结构简化(Squeeze-net,mobile-net,shuffle-net) |
剪枝方法 | 依据 |
---|---|
随机裁剪 | 随机 |
裁剪向量 | 计算二范数大小 |
裁剪核 | 对核进行裁剪 |
裁剪组 | 针对固定组的剪 |
裁剪通道 | 正常情况下用这个 |
方法 | 描述 |
---|---|
非结构剪枝 | 通常是连接级、细粒度的剪枝方法、精度相对较高,但是依赖于特定的算法库或者硬件平台的支持,如Deep Compression,Sparse-Winograd |
结构剪枝 | filter级或者layer级、粗粒度的剪枝方法,精度相对降低,但是剪枝策略更为有效,不需要特定算法库或者硬件平台的支持,能够直接在成熟深度学习框架上运行 |
import torch
from torch import nn
import torch.nn.utils.prune as prune
import torch.nn.functional as F
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, 3x3 square conv kernel
self.conv1 = nn.Conv2d(1, 6, 3)
self.conv2 = nn.Conv2d(6, 16, 3)
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)
按照pytorch上的代码先定义了一个网络结构,按照下一步代码main函数运行可以得到得到对应结果。
# 定义模型一个卷积层
module = model.conv1
# 打印该层模型的参数
print(list(module.named_parameters()))
# 用于剪枝时临时缓存的一些数据
print(list(module.named_buffers()))
结果:[]
'''要修剪模块(conv1LeNet体系结构的层),请首先从中可用的修剪技术中选择修剪技术torch.nn.utils.prune(或 通过子类实现您自己 的修剪技术 BasePruningMethod)。然后,指定模块以及该模块中要修剪的参数的名称。最后,使用所选修剪技术所需的适当关键字参数,指定修剪参数。'''
from torch.nn.utils import prune
# 随机剪枝方法,针对这个模型的weight进行剪枝,剪枝比例0.3
prune.random_unstructured(module, name="weight", amount=0.3)
下列运行代码:
# 函数运行
if __name__ == '__main__':
model = LeNet().to(device=device)
# 获取卷积层
module = model.conv1
# 打印参数,包括weight和偏值bias
print(list(module.named_parameters()))
# 剪枝时临时更改存储的数据掩码
print(list(module.named_buffers()))
# 按照0.3的权重比例,对网络该层的weight随机裁剪
prune.random_unstructured(module, name="weight", amount=0.3)
# 查看变化,看着值还没更改,但是已经生成了更改掩码,weight--->weight_orig(即追加"_orig"到初始参数name)来进行的。weight_orig存储未经修剪的张量版本。
# print(list(module.named_parameters()))
# 通过上面选择的修剪技术生成的修剪掩码被保存为名为weight_mask(即附加"_mask"到初始参数name)的模块缓冲区。
# print(list(module.named_buffers()))
# 已经改了
print(module.weight)
# 只显示对weight出现操作
print(module._forward_pre_hooks)
# L1剪枝。针对bias,剪3个最小的
prune.l1_unstructured(module, name="bias", amount=3)
# 打印,现在已经改为weight_orig 和 bias_orig
print(list(module.named_parameters()))
print(list(module.named_buffers()))
print(module._forward_pre_hooks)
# 迭代修剪
model = LeNet().to(device=device)
# 获取卷积层
module = model.conv1
# dim :针对输出还是输入去剪枝,具体看形状 n:1,2 表示是L1,还是L2剪枝 name:针对weight还是bias
prune.ln_structured(module, name="weight", amount=0.5, n=2, dim=0)
print(module.weight)
# 所有相关的张量,包括掩码缓冲区和用于计算修剪张量的原始参数,都存储在模型中state_dict ,因此可以根据需要轻松地序列化和保存。
print(model.state_dict().keys())
# 修剪永久化
prune.remove(module, 'weight')
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
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%。'''
)
神经网络 | 方法 |
---|---|
二值神经网络 | 在运行时权重和激活只取两种值(例如+1,-1)的神经网络,以及在训练师计算参数的梯度 |
三元权重网络 | 权重约束为+1,0,-1的神经网络 |
XNOR网络 | 过滤器和卷积层的输入是二进制的。XNOR网络主要视同二进制运算来近似卷积 |
在特征层加上l2正则化损失,从特征层就开始学习,最后总损失是损失+特征层损失。