文章以剪枝后的梯度范数为标准,对剪枝后梯度范数下降最小的权重进行剪枝,因为文章依赖于保留梯度流来修剪网络,所以作者将他们的方法命名为梯度信号保存(Grasp)。
一、算法思路
从数学上讲,梯度范数越大,表示在一阶内,每次梯度更新的损失减少越大,可以表述为:
由于文章只关心修剪网络的性能,因此目标是保留甚至增加修剪后的梯度流(即修剪网络的梯度流)。为了实现这一点,他们将修剪操作转换为在初始权重是增加扰动,以表征去除一个权重将如何影响修剪后的梯度流。
其中是
的函数,它表示(1)式对初始值
的
扰动的变化。黑森矩阵H捕获每个权重之间的相关性,从而调节修剪对剩余权重的影响,当H为恒等时,上述判断依据恢复SNIP到绝对值(回想SNIP判据为
),然而,已经观察到不同的权重时高度耦合的,这表明Hessian不相同。在实际应用中,我们采用(2)作为衡量各权重重要性的尺度,具体来说,如果
是负的,那么去掉相应的权重会减少梯度流动,否则不会。因此,我们倾向于先去掉那些不会降低梯度流的权重,对于每个权重,其重要性可以通过以下方式计算:
对于给定的简直比例p,我们可以通过一次计算剪枝条件,并对其进行排序,然后去除权重的前p%来得到最终的剪枝掩码。粗略的说,Grasp考虑了剪枝后梯度流的变化,而SNIP只保留剪枝后的损失,这可能不会保持梯度流。
二、原文部分培养代码展示
# 定义一个函数GraSP,用于实现GraSP算法,该算法是一种基于梯度的神经网络剪枝方法
# 参数说明:
# net: 一个神经网络对象
# ratio: 一个浮点数,表示要保留的权重比例
# train_dataloader: 一个数据加载器对象,用于获取训练数据
# device: 一个字符串或者torch.device对象,表示要使用的设备(CPU或GPU)
# num_classes: 一个整数,表示类别的数量,默认为10
# samples_per_class: 一个整数,表示每个类别要采样的样本数量,默认为25
# num_iters: 一个整数,表示要进行的迭代次数,默认为1
# T: 一个整数,表示温度参数,默认为200
# reinit: 一个布尔值,表示是否要重新初始化线性层的权重,默认为True
def GraSP(net, ratio, train_dataloader, device, num_classes=10, samples_per_class=25, num_iters=1, T=200, reinit=True):
eps = 1e-10 # 定义一个很小的正数eps,用于避免除以零或者其他数值稳定性问题
keep_ratio = 1-ratio # 计算要剪枝掉的权重比例
old_net = net # 复制原始网络
net = copy.deepcopy(net) # .eval() 深度复制原始网络,并将其赋值给net变量
net.zero_grad()
weights = [] # 创建一个空列表weights,用于存储net中所有卷积层和线性层的权重张量
total_parameters = count_total_parameters(net)
fc_parameters = count_fc_parameters(net)
# rescale_weights(net)
for layer in net.modules():
if isinstance(layer, nn.Conv2d) or isinstance(layer, nn.Linear):
if isinstance(layer, nn.Linear) and reinit:
nn.init.xavier_normal(layer.weight)
weights.append(layer.weight)
inputs_one = [] # 创建一个空列表inputs_one,用于存储每次迭代的输入数据的一半
targets_one = [] # 创建一个空列表targets_one,用于存储每次迭代的目标数据的一半
grad_w = None # 创建一个None对象grad_w,用于存储权重张量的梯度和
for w in weights:
w.requires_grad_(True)
print_once = False#创建一个布尔值变量print_once,并赋值为False,表示是否打印过输出概率分布
for it in range(num_iters):
print("(1): Iterations %d/%d." % (it, num_iters))
inputs, targets = GraSP_fetch_data(train_dataloader, num_classes, samples_per_class) # 调用GraSP_fetch_data函数,从train_dataloader中获取输入数据和目标数据,每个类别采样samples_per_class个样本,并赋值给inputs和targets变量
N = inputs.shape[0]
din = copy.deepcopy(inputs)
dtarget = copy.deepcopy(targets)
inputs_one.append(din[:N//2])
targets_one.append(dtarget[:N//2])
inputs_one.append(din[N // 2:])
targets_one.append(dtarget[N // 2:])
inputs = inputs.to(device)
targets = targets.to(device)
outputs = net.forward(inputs[:N//2])/T # 调用net的forward方法,将inputs的前一半(即前N//2个样本)作为输入,得到输出,并除以T,得到一个输出概率分布,并赋值给outputs变量
if print_once:
x = F.softmax(outputs)
print(x)
print(x.max(), x.min())
print_once = False # 将print_once设置为False,表示已经打印过了
loss = F.cross_entropy(outputs, targets[:N//2])
# ===== debug ================
grad_w_p = autograd.grad(loss, weights) # 使用autograd.grad函数计算loss对weights中的每个权重张量的梯度,并返回一个梯度列表,并赋值给grad_w_p变量
if grad_w is None:
grad_w = list(grad_w_p)
else:
for idx in range(len(grad_w)):
grad_w[idx] += grad_w_p[idx]
outputs = net.forward(inputs[N // 2:])/T
loss = F.cross_entropy(outputs, targets[N // 2:])
grad_w_p = autograd.grad(loss, weights, create_graph=False)
if grad_w is None:
grad_w = list(grad_w_p)
else:
for idx in range(len(grad_w)):
grad_w[idx] += grad_w_p[idx]
ret_inputs = []
ret_targets = []
for it in range(len(inputs_one)):
print("(2): Iterations %d/%d." % (it, num_iters))
inputs = inputs_one.pop(0).to(device) # 从输入列表中弹出第一个元素
targets = targets_one.pop(0).to(device) # 从输入列表中弹出第一个元素
ret_inputs.append(inputs)
ret_targets.append(targets)
outputs = net.forward(inputs)/T
loss = F.cross_entropy(outputs, targets)
grad_f = autograd.grad(loss, weights, create_graph=True)
z = 0
count = 0
for layer in net.modules():
if isinstance(layer, nn.Conv2d) or isinstance(layer, nn.Linear):
z += (grad_w[count].data * grad_f[count]).sum() # 计算当前层权重梯度和grad_w中对应索引位置的梯度之间的点积,并累加到z上
count += 1
z.backward() # 对z求反向传播,更新权重梯度
grads = dict() # 定义一个字典,用来存储每一层权重更新后的值(-theta_q Hg)
old_modules = list(old_net.modules())
for idx, layer in enumerate(net.modules()):
if isinstance(layer, nn.Conv2d) or isinstance(layer, nn.Linear):
grads[old_modules[idx]] = -layer.weight.data * layer.weight.grad # -theta_q Hg
# 将grads字典中所有值展平并拼接成一个张量
all_scores = torch.cat([torch.flatten(x) for x in grads.values()])
# 计算所有分数绝对值之和,并加上一个很小的正数eps(防止除零错误)
norm_factor = torch.abs(torch.sum(all_scores)) + eps
print("** norm factor:", norm_factor)
all_scores.div_(norm_factor) # 将所有分数除以归一化因子
num_params_to_rm = int(len(all_scores) * (1-keep_ratio)) # 计算要移除参数个数(总参数个数乘以保留比例)
threshold, _ = torch.topk(all_scores, num_params_to_rm, sorted=True) # 获取所有分数中最大的num_params_to_rm个值,并排序
acceptable_score = threshold[-1]# 获取最后一个值,作为可接受的分数阈值
print('** accept: ', acceptable_score) # 打印可接受的分数阈值
keep_masks = dict()
for m, g in grads.items():
# 将当前层的梯度除以归一化因子,与可接受的分数阈值比较,得到一个布尔张量,然后转换为浮点型张量,添加到keep_masks字典中
keep_masks[m] = ((g / norm_factor) <= acceptable_score).float()
print(torch.sum(torch.cat([torch.flatten(x == 1) for x in keep_masks.values()])))
return keep_masks