01修建结构
1非结构化剪枝
1.1.1细粒度剪枝
细粒度剪枝是一种特定类型的剪枝方法,它指的是单个权重级别的剪枝。在细粒度剪枝中,模型中的每一个权重都会被独立地考虑是否需要被剪枝。这种方法的优点是可以非常精确地控制模型的大小和复杂性,因为可以精确地选择哪些权重需要被剪枝。然而,这也是一种计算复杂度较高的方法,因为需要对每一个权重都进行评估。
下面的代码直接对权重按绝对值大小来评估重要性,剪掉绝对值大小小的
以多层感知机(MLP)为例,下面是一个稍微复杂点的MLP
import torch.nn as nn
import torch
import numpy as np
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) # 输入通道数为3,输出通道数为64,卷积核大小为3,padding为1
self.bn1 = nn.BatchNorm2d(64) # 批标准化层,输入通道数为64
self.relu1 = nn.ReLU(inplace=True) # ReLU激活函数,inplace=True表示直接修改输入的张量,而不是返回一个新的张量
self.conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(64)
self.relu2 = nn.ReLU(inplace=True)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.bn3 = nn.BatchNorm2d(128)
self.relu3 = nn.ReLU(inplace=True)
self.conv4 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
self.bn4 = nn.BatchNorm2d(128)
self.relu4 = nn.ReLU(inplace=True)
self.fc1 = nn.Linear(128 * 4 * 4, 1024) # 全连接层,输入大小为128*4*4,输出大小为1024
self.fc2 = nn.Linear(1024, 10) # 全连接层,输入大小为1024,输出大小为10
def forward(self, x):
x = self.conv1(x) # 第一层卷积
x = self.bn1(x)
x = self.relu1(x)
x = self.conv2(x) # 第二层卷积
x = self.bn2(x)
x = self.relu2(x)
x = self.conv3(x) # 第三层卷积
x = self.bn3(x)
x = self.relu3(x)
x = self.conv4(x) # 第四层卷积
x = self.bn4(x)
x = self.relu4(x)
x = x.view(x.size(0), -1) # 展平
x = self.fc1(x) # 第一个全连接层
x = self.fc2(x) # 第二个全连接层
return x
接下来构建一个剪枝函数,输入是layer和prune_rate,指的是按多少比例去裁剪当前这个卷积层的权重
def prune_conv_layer(layer, prune_rate):
## 按比例prune掉当前卷积层的权重
if isinstance(layer, nn.Conv2d): ## 检查对象是否是nn.Conv2d
##取到它里面的每一个权重,取权重里面的一个data(因为weight里面除了data还有梯度),一个是数据一个是梯度
##然后运回cpu,然后用numpy做实现,后续讲解pytorch实现
weights = layer.weight.data.cpu().numpy()
print(weights.shape)
##由于我们想按比例剪枝,所以先取weight里的元素,假设有15个,先排序
## 将这15个元素展成一维,然后排序,只取前80%,剪掉后面的20%
num_weights = weights.size ## 计算大小
num_prune = int(num_weights * prune_rate) ## 计算需要剪掉多少个元素,取整
flat_weights = np.abs(weights.reshape(-1)) ## 将权重展平,并取绝对值
treshold = np.sort(flat_weights)[num_prune] ## 找到下标是num_prune的元素,保留大于等于treshold的元素
weights[abs(weights) < treshold] = 0## 将小于treshold的元素置为0
##将剪枝后的权重转换为torch张量并赋值给卷积层的权重
layer.weight.data = torch.from_numpy(weights).to(layer.weight.device)
net = Net()
prune_rate = 0.2 ## 每一层都去掉20%
for layer in net.modules():##访问每一层
prune_conv_layer(layer, prune_rate)
1.1.2向量剪枝
向量剪枝,是把当前权重的同一行,同一列全部剪掉,赋值为0
import numpy as np
import matplotlib.pyplot as plt
def vector_pruning(matrix, idx):
#取到idx的row和col
row, col = idx
pruned_matrix = matrix.copy()
#当前行和列都置为0
pruned_matrix[row, :] = 0
pruned_matrix[:, col] = 0
return pruned_matrix
matrix = np.random.randn(3, 4)
idx = (1, 2)
# prune the matrix
pruned_matrix = vector_pruning(matrix, idx)
print(matrix)
print("")
print(pruned_matrix)
1.1.3卷积核剪枝(kernel level)
对卷积层进行剪枝,将一定比例的权重设置为0
![b0059c8de2b0dd606a1fe47785388dd1](/Users/pyf/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a874608d4504d6acc7b3d847b89e39bd/nt_data/Pic/2023-06/Ori/b0059c8de2b0dd606a1fe47785388dd1.png)
# 5 * 4 * 3 * 3
def prune_conv_layer(layer, prune_rate):
if isinstance(layer, nn.Conv2d): ## 检查对象是否是nn.Conv2d
weights = layer.weight.data.cpu().numpy()
num_weights = weights.size ## 计算大小
num_prune = int(num_weights * prune_rate) ## 计算需要剪掉多少个元素,取整
#对每一组卷积计算L2范数,权重的平方求和,axis是需要求和的维度,5 * 4 * 3 * 3其中5是0维度
norm_per_filter = np.sum(weight**2, axis = (1, 2, 3))#会得到5个值
#根据L2范数排序,选择一定比例的filter,将里面的元素置为0
#先对这5个数排序
indices = np.argsort(norm_per_filter)[-num_prune:]
print(indices)
weight[indices] = 0;
layer.weight.data = torch.from_numpy(weights).to(layer.weight.device)
下面给出一个例子
import torch
import numpy as np
#定义权重张量 (3 * 2 * 2)
weight1 = np.array([[3, 2],
[3, 4]])
weight2 = np.array([[5, 6],
[7, 8]])
weight3 = np.array([[9, 0],
[1, 2]])
weight = np.stack([weight1, weight2, weight3], axis = 0)
prune_rate = 2/3 # 要prune掉
print(weight.shape)
num_prune = int(weight.shape[0] * prune_rate)
l2norm_per_kernel = np.sqrt(np.sum(weight**2, axis = (1, 2)))
print(l2norm_per_kernel)
indices = np.argsort(l2norm_per_kernel)[:num_prune]
print(indices)
weight[indices]
print(weight)
结构化剪枝
可视化方法简介
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#可视化函数
def visualize_tensor(tensor, batch_spacing=3):#batch_spacing用于控制可视化后张量与张量之间的间隔
fig = plt.figure()#创建了一个新的空白图形窗口
ax = fig.add_subplot(111, projection='3d')#在fig窗口上添加一个新的3d子图
for batch in range(tensor.shape[0]): #遍历张量的第一个维度,称之为batch
for channel in range(tensor.shape[1]): # 遍历第二个维度,称之为channel
for i in range(tensor.shape[2]): # height
for j in range(tensor.shape[3]): # width
#计算每个立方体在3d图形中的位置,位置由x,y,z三个坐标决定
#tensor.shape[3] 是 "width" 维度的大小
x, y, z = j + (batch * (tensor.shape[3] + batch_spacing)), i, channel
# 值为0是红色,否则为灰色
color = 'red' if tensor[batch, channel, i, j] == 0 else 'gray'
#bar3d用于绘制立方体,edgecolor是边缘颜色,alpha是透明度
ax.bar3d(x, z, y, 1, 1, 1, shade=True, color=color, edgecolor="black", alpha=0.9)
ax.set_xlabel('Width')
# ax.set_ylabel('B & C')
ax.set_zlabel('Height')
#ax.set_zlim(ax.get_zlim()[::-1]):这个函数设置了 z 轴的范围。具体来说,ax.get_zlim() 获取了当前 z 轴的范围,然后 [::-1] 将这个范围反转。这意味着 z 轴的方向被反转,也就是说,较大的 "channel" 值将被显示在图形的下方,而较小的 "channel" 值将被显示在图形的上方。这样做的目的是为了使图形的显示更符合常规的 3D 观察习惯。
ax.set_zlim(ax.get_zlim()[::-1])
#控制 z 轴标签和 z 轴之间的距离,将其设置为 15 是为了确保 z 轴的标签有足够的空间显示,不会与图形元素重叠。
ax.zaxis.labelpad = 15 # adjust z-axis label position
plt.show()
def prune_conv_layer(conv_layer, prune_method, percentile=20, vis=True):
pruned_layer = conv_layer.copy()
if prune_method == "fine_grained":
pruned_layer[np.abs(pruned_layer) < 0.05] = 0
if prune_method == "vector_level":
#np.linalg.norm默认计算L2范数,计算L1范数可以这样写np.linalg.norm(x, ord=1)
#axis=-1表示沿着最后一个维度来计算范数
l2_sum = np.linalg.norm(pruned_layer, axis=-1)
if prune_method == "kernel_level":
# 计算每个kernel的L2范数
l2_sum = np.linalg.norm(pruned_layer, axis=(-2, -1))
if prune_method == "filter_level":
# 计算每个filter的L2范数,因为np.linalg.norm无法对3维求范数
# 这里先平方,再求和,只是没有开根
l2_sum = np.sum(pruned_layer**2, axis=(-3, -2, -1))
if prune_method == "channel_level":
# 计算每个filter的L2范数
l2_sum = np.sum(pruned_layer**2, axis=(-4, -2, -1))
#这里对l2_sum做reshape是因为考虑的1/3/4维度,补上第一维度,后面mask才能正确索引
# add a new dimension at the front
l2_sum = l2_sum.reshape(1, -1) # equivalent to l2_sum.reshape(1, 10)
# repeat the new dimension 8 times
#np.repeat是指沿着第一个轴(axis=0)重复 l2_sum,重复的次数是 pruned_layer 的第一个维度的长度
l2_sum = np.repeat(l2_sum, pruned_layer.shape[0], axis=0)
#np.percentile 是 numpy 库中的一个函数,它用于计算给定数据的指定百分位数。
#例如,np.percentile(data, 25) 将会计算 data 的第25百分位数。
#代码的意思是计算l2_sum里百分之percentile的数是多少
threshold = np.percentile(l2_sum, percentile)
#以threshold作为阈值如果 l2_sum 中的某个元素小于 threshold,那么对应位置的布尔值就会是 True;否则,它就会是 False。
mask = l2_sum < threshold
print(pruned_layer.shape)
print(mask.shape)
print("-----------------------------")
#mask标记了哪些位置为0
pruned_layer[mask] = 0
if vis:
visualize_tensor(pruned_layer)
return pruned_layer
# 生成一个tensor
#uniform是指按照均匀分布生成随机浮点数
#np.random.uniform(low, high, size)是指生成最小值low,最大值high均匀分布的随机值
tensor = np.random.uniform(low = -1, high = 1, size = (3, 10, 4, 5))
# Prune the conv layer and visualize it
pruned_tensor = prune_conv_layer(tensor, "vector_level", vis=True)
pruned_tensor = prune_conv_layer(tensor, "kernel_level", vis=True)
pruned_tensor = prune_conv_layer(tensor, "filter_level", vis=True)
pruned_tensor = prune_conv_layer(tensor, "channel_level",percentile=40, vis=True)