任务要求

对CNN分类神经网络进行权重剪枝实现模型压缩。

任务实现

实现原理

研究表明,大多数神经网络的权重分布接近正态分布,即权值越接近0,权重的数量就越多。这表明网络中许多参数对最终预测结果的贡献较小。

因此,可以通过一些剪枝算法识别并移除这些不必要的参数,减少模型的复杂度。同时剪枝后,模型的权重矩阵变得更加稀疏,提高计算效率并减少存储需求。

根据任务提供的权重剪枝实现,可得任务要求的权重剪枝具体步骤如下

  • 计算测试集中最后一层卷积层每个输出特征图的的神经元激活值。
  • 根据得出的神经元激活值进行排序。
  • 对于含有P个特征图的卷积层,按照激活值从小到大的顺序,逐步将一个神经元的权重和偏置设为0,直至仅剩下激活值最高的神经元。
  • 在上述过程中,每剪枝一个神经元,记录当前模型在测试集上的准确率。
实现模型

一个简单的分类模型,使用MINIST数据集,具体网络结构如下

层类型 输入形状 输出形状 参数
Conv2d (, 2, 28, 28) (, 8, 28, 28) stride=1, padding=1
LeakyReLU \ \ -
Conv2d (, 8, 28, 28) (, 16, 28, 28) stride=1, padding=1
LeakyReLU \ \ -
MaxPool2d (, 16, 28, 28) (, 16, 14, 14) kernel_size=2, stride=2
Dropout \ \ p=0.1
Conv2d (, 16, 14, 14) (, 32, 14, 14) stride=1, padding=1
LeakyReLU \ \ -
MaxPool2d (, 32, 14, 14) (, 32, 7, 7) kernel_size=2, stride=2
Dropout \ \ p=0.1
Conv2d (, 32, 7, 7) (, 64, 5, 5) stride=1, padding=0
LeakyReLU \ \ -
Dropout \ \ p=0.1
Conv2d (, 64, 5, 5) (, 64, 5, 5) stride=1, padding=1
LeakyReLU \ \ -
Dropout \ \ p=0.1
Flatten (, 64, 5, 5) (, 1600) -
Linear (, 1600) (, 64) in_features=1600, out_features=64
LeakyReLU \ \ -
Linear (, 64) (, 32) in_features=64, out_features=32
LeakyReLU \ \ -
Linear (, 32) (, 1) in_features=32, out_features=1
实现过程

为提取最后一层卷积层的输出(经过激活函数处理),在该层的输出定义一个钩子函数

1
2
3
# 定义钩子函数,用于提取conv4的输出值
def hook(model, input, output):
conv4_output["conv4"] = output.detach()

由于激活函数为LeakyReLU,因此输出特征值可能为负数。在生成平均输出特征图时不对卷积层输出值取绝对值,但对于64个输出特征图的神经元激活,若直接对输出相加取平均,则可能导致正值和负值相互抵消,从而低估了激活强度。因此,在计算神经元激活值时,对输出特征值取绝对值后累加求均值。具体实现代码如下

1
2
3
4
5
6
7
8
9
10
11
for img, label in test_loader:
label = label.view(-1, 1)
output = model(img)
# 对输出特征图进行累加求均值
# 对64个神经元的激活值取绝对值然后累加求均值
for i in range(64):
# 计算神经元激活
neuron_activation[str(i)] += abs(conv4_output["conv4"][0][i])
.sum().item() / (25.0 * len(test_loader))
# 计算输出特征图
avg_output[i] += conv4_output["conv4"][0][i].detach().numpy() / len(test_loader)

最后得到剪枝前最后一层卷积层在整个测试数据集上的平均输出特征图(尺寸64*5*5),具体情况如图1所示

平均输出特性图
图 1 平均输出特征图

然后根据神经元激活计算结果,对64个输出特征图的神经元激活值进行从小到达的排序,结果如图2所示

损失函数
图2 神经元激活曲线

从图中可以看出,最小的神经元激活值为0.0825,最大为0.4462。这表明前者对预测结果的影响较小,而后者对预测结果的影响较大。

在进行权重剪枝前,测试集准确率为**97.26%**。接下来,对测试集进行63次剪枝并记录准确率,每次按照神经元激活值从小到大的顺序,对对应的神经元权重进行剪枝(将神经元权重和偏置设为0)。具体代码为

1
2
3
4
5
6
7
8
9
10
11
12
# compute_accuracy()函数用于计算模型的准确率
accuracy_pruning = []
accuracy_pruning.append(compute_accuracy())

# 计算剪枝后的准确率
for i in range(63):
# 对第i个神经元进行剪枝,前i个神经元权重置0
with torch.no_grad():
model.conv4[0].weight[int(neuron_activation[i][0])].fill_(0)
# 记录剪枝后的准确率
accuracy = compute_accuracy()
accuracy_pruning.append(accuracy)

最终生成准确率与剪枝数量的折线图,如图3所示

损失函数
图3 准确率-剪枝数折线图
结果分析

观察剪枝前的平均输出特征图,除了少数几个特征图的值偏小外,其余特征图并没有表现出显著差异。但通过观察准确率-剪枝数折线图可知,

  • 对激活值最小的五个神经元权重进行剪枝,模型在测试集上的预测准确率几乎没有变化。

  • 甚至剪枝掉一半的神经元,模型的测试集准确率依旧在**95%**以上。

  • 随着剪枝的神经元激活值逐渐增加,模型性能的下降速度显著加快:准确率从 97.27% 降至 95.01% 过程中,剪枝掉了37个神经元;准确率从 95.01% 降到 80.84% 过程中,剪枝掉了 17 个神经元;而到了后期,准确率从 80.84%60.11% 过程中,只剪枝掉了5个神经元。

因此可知,激活值越小的神经元,其权重对模型性能的影响越小。通过剪枝这些激活值较低的神经元,不仅能够在几乎不影响模型性能的情况下显著减少模型参数,还能够减小模型参数,提高计算效率。