门户网站的营销方式,让别人做网站要注意什么,网站后台进不去的原因,安徽网站开发费用文章目录 一、环境配置二、模型压缩2.1 模型压缩简介2.2 模型压缩评价指标 三、 模型剪枝3.1 模型剪枝简介3.2 何为剪枝#xff08;What is Pruning?#xff09;3.3 剪枝标准#xff08;How to prune?#xff09;3.4 剪枝频率#xff08;How often?#xff09;3.5 剪枝… 文章目录 一、环境配置二、模型压缩2.1 模型压缩简介2.2 模型压缩评价指标 三、 模型剪枝3.1 模型剪枝简介3.2 何为剪枝What is Pruning?3.3 剪枝标准How to prune?3.4 剪枝频率How often?3.5 剪枝时机When to prune?3.6 剪枝比例 四、代码实践4.1 剪枝粒度实践4.1.1 细粒度剪枝4.1.2 基于模式的剪枝4.1.3 向量级别剪枝4.1.4 卷积核级别剪枝4.1.5 通道级别剪枝4.1.6 滤波器级别剪枝4.1.7 汇总 4.2 剪枝标准实践4.2.1 定义初始网络画出权重分布图和密度直方图4.2.2 基于L1权重大小的剪枝4.2.3 基于L2权重大小的剪枝4.2.4 基于梯度大小的剪枝 4.3 剪枝时机实践训练后剪枝4.3.1 加载LeNet网络评估其权重分布和模型指标4.3.2 模型剪枝4.3.3 对剪枝后的模型进行微调4.3.3.1 PyTorch保存模型的两种方式4.3.3.2 回调函数 4.3.4 对比剪枝前后的模型指标 五、PyTorch模型剪枝教程 《datawhale2411组队学习之模型压缩技术1模型剪枝上》介绍模型压缩的几种技术模型剪枝基本概念、分类方式、剪枝标准、剪枝频次、剪枝后微调等内容《datawhale11月组队学习 模型压缩技术2PyTorch模型剪枝教程》介绍PyTorch的prune模块具体用法《datawhale11月组队学习 模型压缩技术324结构稀疏化BERT模型》介绍基于模式的剪枝——24结构稀疏化及其在BERT模型上的测试效果
一、环境配置 项目地址awesome-compression、在线阅读 整个项目我是在Autodl上跑的。下面是拉取并配置环境的代码
conda create -n compression python3.10
conda activate compressiongit clone https://github.com/datawhalechina/awesome-compression.git
cd awesome-compression
pip install -r ./docs/notebook/requirements.txt测试第三章代码使用plt画图报错没有此字体
import torch
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D# plt.rcParams[font.sans-serif] [SimHei] # 解决中文乱码
plt.rcParams[font.sans-serif] [Arial Unicode MS]def plot_tensor(tensor, title):...# 创建一个矩阵weight
weight torch.rand(8, 8)
plot_tensor(weight, 剪枝前weight)打印出系统中所有matplotlib已注册的地址及其位置
from matplotlib import font_manager
for font in font_manager.fontManager.ttflist:print(font.name,font.fname)DejaVu Sans /root/miniconda3/lib/python3.8/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf
DejaVu Sans Mono /root/miniconda3/lib/python3.8/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf
...
...直接将windows10系统字体文件夹C:\Windows\Fonts中的中文字体文件msyh.ttc微软雅黑、simfang.ttf华文仿宋等上传到系统matplotlib字体文件夹/root/miniconda3/lib/python3.8/site-packages/matplotlib/mpl-data/fonts/ttf下。如果是启用虚拟环境就放到对应的虚拟环境matplotlib字体文件夹 如果字体文件不在matplotlib字体文件夹在第三步会报错。 加载中文字体
# 清除字体缓存
font_manager._load_fontmanager(try_read_cacheFalse)# 下面做主要是根据读取字体文件来获取字体名即prop.get_name()Microsoft YaHei
# 如果你已经直到字体名直接写plt.rcParams[font.family] Microsoft YaHei就行
font_path /root/miniconda3/lib/python3.8/site-packages/matplotlib/mpl-data/fonts/ttf/msyh.ttc # 替换为你的字体文件路径
prop font_manager.FontProperties(fnamefont_path)
plt.rcParams[font.family] prop.get_name()prop.get_name()用于获取字体名称此名称和字体文件名不一样。试验了几种字体微软雅黑效果较好。
二、模型压缩
2.1 模型压缩简介 下图是近年来模型大小与GPU发展的趋势从图中可以看出GPU硬件发展的速度远远跟不上模型大小的增长速度这也导致了大模型训练和推理的困难。而模型压缩技术可以弥补这个差距使得大模型可以在有限的硬件资源上运行。 模型压缩技术包括 模型剪枝研究发现神经元可能会出现两种冗余情况一部分神经元会“坍缩”成功能相似的神经元共同负责类似的任务另一些神经元则在优化过程中被忽视。模型剪枝的目标是将这些冗余神经元删除或合并成等效神经元从而减少模型的参数量和计算量而不会损害模型的性能甚至在某些情况下可能会提高性能。 模型量化神经网络通常使用浮点数进行存储和计算而减少计算精度如降低位宽不会显著影响最终结果。量化技术通过减少模型的数值精度从而减小存储和计算开销提高计算速度。 蒸馏学习将大模型的知识迁移到小模型中使得小模型能够在不损失性能的情况下具有类似大模型的表现。 神经网络架构搜索NAS通过自动化搜索优化神经网络架构找到适合的、较小的网络结构。
2.2 模型压缩评价指标
指标描述备注准确率 (Accuracy)对比模型压缩前后在特定任务上的准确度如分类准确率、检测精度等。通常计算在零样本Zero-shot数据集上的准确率参数量 (Params)模型中可训练参数的总数包括所有权重和偏置。模型大小通常是通过模型的总参数数量来衡量的参数越多计算资源和内存消耗也越多模型大小 (Model Size)以存储大小如MB衡量压缩效果计算公式为大小 参数量 * 带宽。一个模型的参数量为70B假设使用32位浮点数存储那么其模型大小为 70 B ∗ 4 B y t e s ( 32 b i t s ) 280 G B ( 280 ∗ 1 0 9 B y t e s ) 70B * 4Bytes(32bits) 280GB(280 * 10^9 Bytes) 70B∗4Bytes(32bits)280GB(280∗109Bytes)乘累加操作 (MACs)浮点运算的基本单位包括一个乘法操作和一个加法操作可描述CNN卷积操作的计算量。卷积层前向传播时每个卷积核都会与输入特征图进行点积然后累加所有点积结果得到输出特征图。此过程就是一次MAC操作。浮点运算 (FLOPsNumber of Floating Point Operations)模型执行一次前向推理需要的浮点运算次数。代表LLM执行一个实例所需的浮点运算数量与模型需要计算资源成正比操作数 (OPs)神经网络中的激活或权重计算也不总是浮点运算OPS(Operation Per Second)代表每秒执行的操作数压缩比 (Compression Ratio)原始模型大小与压缩后模型大小的比值越高压缩效果越好但可能伴随性能损失。推理时间 (Inference Time)模型处理输入并生成输出的时间。压缩后通常推理时间会减少提升响应速度。吞吐量 (Throughput)模型单位时间内处理的数据量衡量压缩后模型的效率。通常与准确率和延迟一起考量平衡模型精度和推理速度之间的关系
三、 模型剪枝
3.1 模型剪枝简介 概念模型剪枝通过移除模型中不重要的权重即神经网络中的突触Synapses和分支网络中的神经元Neurons将网络结构稀疏化减少模型的参数量和计算复杂度从而降低内存消耗和加速模型推理过程。这种技术对于在资源受限的设备上部署模型尤为重要。剪枝过程如下图所示 目标在减少模型大小的同时尽量保持模型性能找到模型大小和性能之间的最佳平衡点。 数学表示对于一个简单的线性神经网络其公式可表示为 Y W X YW X YWX 其中 W W W为权重 X X X为输入即神经元。模型剪枝可以看作是将权重矩阵中的一些元素设置为零这些剪枝后具有大量零元素的矩阵被称为稀疏矩阵反之绝大部分元素非零的矩阵被称为稠密矩阵。
剪枝的理论基础
彩票假说大型神经网络中存在一个子网络如果独立训练可以达到与完整网络相似的性能。网络稀疏性许多深度神经网络参数呈现出稀疏性即大部分参数值接近于零。这种稀疏性启发了剪枝技术即通过移除这些非显著的参数来简化模型。正则化L1正则化鼓励网络学习稀疏的参数分布这些参数权重接近于零可以安全移除。
3.2 何为剪枝What is Pruning?
剪枝可以按照不同标准进行划分下面主要从剪枝类型、剪枝范围、剪枝粒度三方面来阐述。 剪枝类型 非结构化剪枝不关心权重在网络中的位置仅根据权重的大小决定是否剪枝剪掉较小的权重导致模型稀疏。这种方法会破坏原有模型的结构不利于现有硬件加速需要设计特定的硬件。结构化剪枝考虑了网络的结构通常是移除整个神经元、卷积核或层易于硬件加速。结构化剪枝的实现通常更加复杂需要更多的计算资源和对模型的深入理解半结构化剪枝两者之间的折中在剪枝时部分考虑结构但不完全按整体结构剪去某个单元试图在保持计算加速的同时减少性能损。比如移除整个神经元或过滤器的一部分而不是全部。 NVIDIA A100 GPU在稀疏加速方面的一个显著特性是其支持的2:4 50%稀疏模式这是一种特定形式的半结构化剪枝。在这种模式下模型中的每个权重块通常是 4 个连续的权重值中有 2 个权重为非零值剩下 2 个权重置为0。 剪枝范围 局部剪枝局部剪枝的操作是独立进行的不依赖于模型的整体结构直接移除那些对模型输出影响较小的权重。可以是权重剪枝也可以是神经元剪枝甚至是通道剪枝CNN中。全局剪枝考虑模型的整体结构和性能可能会移除整个神经元、卷积核、层或更复杂的结构通常需要对模型有深入的理解且可能涉及重设计模型架构。 剪枝粒度按照剪枝粒度进行划分剪枝可分为 细粒度剪枝Fine-grained Pruning移除权重矩阵中的任意值可以实现高压缩比但对硬件不友好因此速度增益有限。基于模式的剪枝Pattern-based Pruning在第四章剪枝实践中进行详细介绍向量级剪枝Vector-level Pruning以行或列为单位对权重进行裁剪。内核级剪枝Kernel-level Pruning卷积核滤波器为单位对权重进行裁剪。通道级剪枝Channel-level Pruning以通道为单位对权重进行裁剪。 通道级剪枝是改变了网络中的滤波器组和特征通道数目属于结构化剪枝。其它四者使网络的拓扑结构本身发生了变化需要专门的算法设计来支持这种稀疏的运算属于非结构化剪枝。 3.3 剪枝标准How to prune? 剪枝算法通常基于权重的重要性来决定是否剪枝。权重的重要性可以通过多种方式评估例如权重的大小、权重对损失函数的梯度、或者权重对输入的激活情况等。 基于权重大小直接根据每个元素的权重绝对值大小来计算权重的重要性移除绝对值较小的权重整个剪枝过程如下 此外也可以根据权重的L1和L2正则化来指导指导模型剪枝。具体来说以行为单位计算每行的重要性移除权重中那些重要性较小的行 基于梯度大小 以权值大小为依据的进行剪枝很容易剪掉重要的权值。以人脸识别为例在人脸的诸多特征中眼睛的细微变化如颜色、大小、形状对于人脸识别的结果有很大影响不应该被剪掉。在模型训练过程中梯度是损失函数对权值的偏导数反映了损失对权值的敏感程度梯度越大的权重越重要所以应该去除较小梯度的权重。 基于尺度(Scaling-based)利用BN层中的缩放因子来实现稀疏性识别并剪枝那些对模型输出影响不大的整个通道。 在标准的CNN训练中批归一化BN层计算公式如下 x ^ x − μ σ 2 ϵ \hat{x} \frac{x - \mu}{\sqrt{\sigma^2 \epsilon}} x^σ2ϵ x−μ 其中 μ \mu μ 是批次均值 σ 2 \sigma^2 σ2 是批次方差 ϵ \epsilon ϵ 是为了数值稳定而加的一个小值。 在此基础上BN层会应用一个可训练的缩放因子 γ \gamma γ(调节标准化后数据的尺度影响输出的幅度) 和一个可训练的偏移量 β \beta β(平移标准化后数据的分布) y γ x ^ β y \gamma \hat{x} \beta yγx^β Network Slimming 方法中在损失函数中添加一个L1正则化项来鼓励 γ \gamma γ趋向于零从而可以识别不重要的通道并进行剪枝。 基于二阶梯度Second-Order-based最具代表性的是最优脑损伤Optimal Brain DamageOBD它使用 Hessian 矩阵损失函数对网络权重的二阶导数矩阵来判断每个权重的重要性。 使用条件假设神经网络的损失函数可以在一个局部区域内近似为一个 二次函数神经网络的训练已收敛 删除每个参数所导致的误差是独立的。误差计算当某个权重 w i w_i wi 被移除时模型的损失函数变化可用公式近似 δ L i ≈ 1 2 h i i w i 2 \delta L_i \approx \frac{1}{2} h_{ii} w_i^2 δLi≈21hiiwi2其中 h i i h_{ii} hii 是 Hessian 矩阵的对角元素表示损失函数关于权重 w i w_i wi 的二阶导数即 h i i ∂ 2 L ∂ w i 2 h_{i i}\frac{\partial^2 L}{\partial w_i^2} hii∂wi2∂2L剪枝原则误差公式表明权重的重要性由其对应的 Hessian 对角线元素和其平方决定。对于较小的 ∣ δ L i ∣ |\delta L_i| ∣δLi∣权重的重要性较低可以被剪去。
OBD剪枝的优点有
评估更准确相比基于一阶梯度如 L1 或 L2 正则化方法提供的梯度方向和大小二阶导数考虑了梯度的变化速率而不仅依赖于当前梯度这使得它能够更精确地评估权重的重要性。适合收敛后的模型在模型已经收敛的情况下一阶梯度可能接近零但权重的二阶效应如曲率仍能反映权重的重要性。
OBD剪枝的缺点是
计算复杂度高纵使OBD 只需要考虑 Hessian 矩阵的对角线元素 h i i h_{ii} hii而无需考虑整个 Hessian 矩阵但仍然比计算一阶梯度复杂。近似性限制OBD 假设损失函数在当前权重点附近是二次的这在一些复杂的非线性模型中可能并不准确。局限于收敛模型OBD 的假设前提之一是模型已经收敛对于训练初期的网络二阶信息的作用可能较小。 总结来说OBD 使用 Hessian 矩阵来判断权重的重要性是为了利用其提供的曲率信息进行更精确的剪枝评估。虽然计算复杂度较高但能避免简单一阶梯度方法的局限性适用于在模型已经收敛的情况下进行精细剪枝。
3.4 剪枝频率How often?
迭代剪枝Iterative Pruning逐步移除权重可以更细致地评估每一次剪枝对模型性能的影响并允许模型有机会调整其余权重来补偿被剪除的权重。 训练模型首先训练一个完整的、未剪枝的模型使其在训练数据上达到一个良好的性能水平。剪枝使用一个预定的剪枝策略来轻微剪枝网络移除一小部分权重。微调使用原始训练数据集重新训练模型以恢复由于剪枝引起的性能损失。评估在验证集上评估剪枝后模型的性能确保模型仍然能够维持良好的性能。重复重复步骤2到步骤4每次迭代剪掉更多的权重并进行微调直到达到一个预定的性能标准或剪枝比例。 单次剪枝One-Shot Pruning模型在被训练到收敛后对其进行一次性的剪枝操作优点是高效直接缺点是会极大地受到噪声的影响。 迭代式剪枝在每次迭代之后只会删除掉少量的权重然后周而复始地进行其他轮的评估和删除能够在一定程度上减少噪声对于整个剪枝过程的影响。但对于大模型来说由于微调的成本太高所以更倾向于使用单次剪枝方法。 3.5 剪枝时机When to prune?
训练后剪枝先训练一个模型然后对模型进行剪枝最后对剪枝后模型进行微调。训练时剪枝直接在模型训练过程中进行剪枝最后对剪枝后模型进行微调。训练前剪枝在模型训练前进行剪枝然后从头训练剪枝后的模型。
3.6 剪枝比例
均匀分层剪枝Uniform Layer-Wise Pruning在神经网络的每一层中都应用相同的剪枝率。这种方法实现简单剪枝率容易控制但它忽略了每一层对模型整体性能的重要性差异。非均匀分层剪枝Non-Uniform Layer-Wise Pruning根据每一层的不同特点来分配不同的剪枝率。例如可以根据梯度信息、权重的大小、或者其他指标如信息熵、Hessian矩阵等来确定每一层的剪枝率越重要的层保留的参数越多。非均匀剪枝往往比均匀剪枝的性能更好。
四、代码实践 原始代码见awesome-compression/docs/notebook/ch03 下面先讲点前置知识方便后面剪枝粒度的理解。
Kernel卷积核卷积层中用于特征提取的小矩阵其size为height,width)Channel通道通常指数据的深度维度。例如彩色图像有RGB三个颜色通道。在CNN中输入层的Channel数对应于图像的颜色通道数而隐藏层的Channel数则对应于该层Filter的数量即每个Filter产生的特征图数量。Filter滤波器。在对多通道输入进行卷积时每个Channel都有其对应的Kernel它们的集合构成了一个Filter。例如对于一个RGB彩色图像一个Filter将包含三个卷积核每个用于一个颜色通道。最终输出是一个二维的特征图Feature Map。Filter能够检测特定类型的特征不同的Filter可以捕捉到不同的特征。Feature Map特征图指的是从输入数据如图像中通过特定的卷积滤波器Filter提取出的特征表示。LayerCNN由多个层组成每个层可以是卷积层、池化层、全连接层等。每个卷积层由多个Filter组成每个Filter通过卷积操作生成一个特征图所有特征图堆叠在一起形成该层的输出。Kernel/Filter关注的是局部特征的提取Channel关注的是特征的多样性和表示而Layer则是网络结构的组成部分
下图是对一个3通道的图片做卷积操作即对三通道图片使用一个Filter 如果是对三通道图片使用两个Filter结果就是两个特征图
4.1 剪枝粒度实践
import torch
import matplotlib.pyplot as plt
from matplotlib import font_manager
from mpl_toolkits.mplot3d import Axes3D#font_manager._load_fontmanager(try_read_cacheFalse)
plt.rcParams[font.family] Microsoft YaHei# 创建一个可视化2维矩阵函数将值为0的元素与其他区分开
def plot_tensor(tensor, title):# 创建一个新的图像和和一组子图轴axsubplots函数返回一个图像对象和一个轴对象fig, ax plt.subplots()# tensor.cpu().numpy()将张量转为numpy数组ax.imshow(tensor.cpu().numpy() 0, vmin0, vmax1, cmaptab20c)ax.set_title(title)ax.set_yticklabels([])ax.set_xticklabels([])# 遍历矩阵中的每个元素并添加文本标签for i in range(tensor.shape[1]):for j in range(tensor.shape[0]):text ax.text(j, i, f{tensor[i, j].item():.2f}, hacenter, vacenter, colork)# 显示图像plt.show()cmaptab20c是一个包含20种颜色颜色映射方案vmin和vmax参数设置颜色映射的值范围超过此范围的数值被裁剪到最大值或最小值。在tab20c颜色映射中True值1通常会被映射到颜色映射中的一个颜色而False值0会被映射到另一种颜色。text ax.text(j, i, f{tensor[i, j].item():.2f}, hacenter, vacenter, colork)在每个元素的位置添加文本标签显示该元素的值保留两位小数。hacenter,vacenter参数设置文本的水平和垂直对齐方式为居中。colork设置文本颜色为黑色。
4.1.1 细粒度剪枝
# 1. 创建一个随机矩阵weight
weight torch.rand(8, 8)
plot_tensor(weight, 剪枝前weight)# 2. 随机定义一个剪枝规则比如将Tensor里的值小于0.5的都置为0
def _fine_grained_prune(tensor: torch.Tensor, threshold : float) - torch.Tensor::param tensor: 输入张量包含需要剪枝的权重。:param threshold: 阈值用于判断权重的大小。:return: 剪枝后的张量。for i in range(tensor.shape[1]):for j in range(tensor.shape[0]):if tensor[i, j] threshold:tensor[i][j] 0return tensorpruned_weight _fine_grained_prune(weight, 0.5)
plot_tensor(weight, 细粒度剪枝后weight)用for循环遍历去实现虽然结果是对的但如果参数太大的话肯定会影响到速度。剪枝中常用的一种方法是使用mask掩码矩阵来实现。
# 3. 使用mask矩阵实现细粒度剪枝
def fine_grained_prune(tensor: torch.Tensor, threshold : float) - torch.Tensor:创建一个掩码张量指示哪些权重不应被剪枝应保持非零。:param tensor: 输入张量包含需要剪枝的权重。:param threshold: 阈值用于判断权重的大小。:return: 剪枝后的张量。mask torch.gt(tensor, threshold) # 所有大于threshold的位置返回Truetensor.mul_(mask) # mul_表示原地更新的乘法操作return tensor
pruned_weight fine_grained_prune(weight, 0.5)
plot_tensor(pruned_weight, 细粒度剪枝后weight)4.1.2 基于模式的剪枝 N:M 稀疏度表示 DNN 的稀疏度即每M个连续权重中固定有N个非零值其余元素均置为0。这种结构可以利用NVIDIA的稀疏张量核心加速矩阵乘法比如NVIDIA Ampere A100 GPU通过稀疏张量核心支持2:4稀疏度实现高达2倍的吞吐量提升而不影响计算的准确性。 稀疏矩阵W首先会被压缩压缩后的矩阵存储着非零的数据值而metadata则存储着对应非零元素在原矩阵W中的索引信息非零元素的行号和列号压缩成两个独立的一维数组如下图所示 如下图所示以NVIDIA 24为例
创建一个patterns从4个中取出2个为非零值可以算出一共有6种不同的模式代表每行有6种剪枝方式将weight matrix变换成nx4的格式方便与pattern进行矩阵运算运算后的结果为nx6的矩阵在n的维度上进行argmax取得最大的索引表示这一行剪枝的最佳方式将索引对应的pattern值填充到mask中。将mask reshape到weight matrix相同的形式二者进行矩阵乘法得到剪枝后的结果 # 创建一个矩阵weight
weight torch.rand(8, 8)
plot_tensor(weight, 剪枝前weight)from itertools import permutationsdef reshape_1d(tensor, m):将输入的二维张量 tensor 重新塑形为列数为 m 的矩阵。如果原始张量的列数不能被 m 整除它会填充零以使列数成为 m 的倍数。if tensor.shape[1] % m 0:mat torch.FloatTensor(tensor.shape[0], tensor.shape[1] (m - tensor.shape[1] % m)).fill_(0)mat[:, : tensor.shape[1]] tensorreturn mat.view(-1, m)else:return tensor.view(-1, m)def compute_valid_1d_patterns(m, n):# 创建一个长度为 m 的全零张量然后将前 n 个元素设置为 1patterns torch.zeros(m)patterns[:n] 1# 使用 permutations 生成所有可能的排列并转换为 torch.Tensor。valid_patterns torch.Tensor(list(set(permutations(patterns.tolist()))))# 最终返回6行4列的矩阵每行是一种42的模式return valid_patternsdef compute_mask(tensor, m, n):# 计算所有可能的模式patterns compute_valid_1d_patterns(m,n)# 初始化一个与tensor形状相同的全1张量然后reshape到m列(8*8 —— 16*4)mask torch.IntTensor(tensor.shape).fill_(1).view(-1,m)mat reshape_1d(tensor, m)# 计算mat的绝对值与所有可能模式的转置patterns.t()的矩阵乘积然后得到最佳索引pmax torch.argmax(torch.matmul(mat.abs(), patterns.t()), dim1)# 从所有可能的模式中选择最佳模式并更新 mask 张量。这样mask 中的每个元素将反映输入张量中相应元素的最佳模式。mask[:] patterns[pmax[:]]# 将 mask 张量的形状调整回与输入张量 tensor 相同的形状以便可以直接应用于原始张量。mask mask.view(tensor.shape)return maskdef pattern_pruning(tensor, m, n):mask compute_mask(tensor, m, n)tensor.mul_(mask)return tensorpruned_weight pattern_pruning(weight, 4, 2)
plot_tensor(pruned_weight, 剪枝后weight)4.1.3 向量级别剪枝
# 创建一个矩阵weight
weight torch.rand(8, 8)
plot_tensor(weight, 剪枝前weight)# 剪枝某个点所在的行与列
def vector_pruning(weight, point):row, col pointprune_weight weight.clone()prune_weight[row, :] 0prune_weight[:, col] 0return prune_weight
point (1, 1)
prune_weight vector_pruning(weight, point)
plot_tensor(prune_weight, 向量级剪枝后weight)4.1.4 卷积核级别剪枝
# 定义可视化4维张量的函数
def visualize_tensor(tensor, title, batch_spacing3):fig plt.figure() # 创建一个新的matplotlib图形ax fig.add_subplot(111, projection3d) # 向图形中添加一个3D子图# 遍历张量的批次维度for batch in range(tensor.shape[0]):# 遍历张量的通道维度for channel in range(tensor.shape[1]):# 遍历张量的高度维度for i in range(tensor.shape[2]):# 遍历张量的宽度维度for j in range(tensor.shape[3]):# 计算条形的x位置考虑到不同批次间的间隔x j (batch * (tensor.shape[3] batch_spacing))y i # 条形的y位置即张量的高度维度z channel # 条形的z位置即张量的通道维度# 如果张量在当前位置的值为0则设置条形颜色为红色否则为绿色color red if tensor[batch, channel, i, j] 0 else green# 绘制单个3D条形ax.bar3d(x, y, z, 1, 1, 1, shadeTrue, colorcolor, edgecolorblack, alpha0.9)ax.set_title(title) # 设置3D图形的标题ax.set_xlabel(Width) # 设置x轴标签对应张量的宽度维度ax.set_ylabel(Height) # 设置y轴标签对应张量的高度维度ax.set_zlabel(Channel) # 设置z轴标签对于张量的通道维度ax.set_zlim(ax.get_zlim()[::-1]) # 反转z轴方向ax.zaxis.labelpad 15 # 调整z轴标签的填充plt.show() # 显示图形def prune_conv_layer(conv_layer,title, percentile0.2, visTrue, dimNone):conv_layer: 输入的卷积层或张量。title: 用于可视化的标题默认为空字符串。percentile: 用于确定剪枝阈值的百分位数默认为 0.2。vis: 是否进行可视化默认为 True。dim: 计算 L2 范数的维度默认为 None如果指定将在这个维度上计算 L2 范数prune_layer conv_layer.clone()l2_norm Nonemask None# 计算每个kernel的L2范数,keepdimTrue 保持输出的维度与输入一致。l2_norm torch.norm(prune_layer, p2, dimdim, keepdimTrue)# 计算L2范数的20%位数threshold torch.quantile(l2_norm, percentile)mask l2_norm thresholdprune_layer prune_layer * mask.float()visualize_tensor(prune_layer,titletitle) torch.quantile(input, q) 是 PyTorch 提供的一个函数用于计算输入张量input在给定百分位数q处的值。比如torch.quantile(tensor, 0.5)表示中位数torch.quantile(tensor, 0.75)表示75%位数。
下面使用PyTorch创建一个四维张量(N, C, H, W)每个维度分别代表(batch_size,channel,height,width)然后进行kernel级别剪枝
tensor torch.rand((3, 10, 4, 5))
# 调用函数进行剪枝dim(-2, -1)表示高和宽两个维度
pruned_tensor prune_conv_layer(conv_layertensor, titleKernel级别剪枝,dim(2, 3), visTrue)4.1.5 通道级别剪枝
pruned_tensor prune_conv_layer(conv_layertensor, titleChannel级别剪枝,dim(0, 2, 3), visTrue)4.1.6 滤波器级别剪枝
pruned_tensor prune_conv_layer(conv_layertensor, titleFilter级别剪枝,dim(1, 2, 3), visTrue)4.1.7 汇总
# 返回一个mask
def get_threshold_and_mask(norms, percentile):threshold torch.quantile(norms, percentile)return norms thresholddef prune_conv_layer(conv_layer, prune_method, title , percentile0.2, visTrue):prune_layer conv_layer.clone()mask Noneif prune_method fine_grained:prune_layer[torch.abs(prune_layer) percentile] 0elif prune_method vector_level:mask get_threshold_and_mask(torch.norm(prune_layer, p2, dim-1), percentile).unsqueeze(-1)elif prune_method kernel_level:mask get_threshold_and_mask(torch.norm(prune_layer, p2, dim(-2, -1), keepdimTrue), percentile)elif prune_method filter_level:mask get_threshold_and_mask(torch.norm(prune_layer, p2, dim(1, 2, 3), keepdimTrue), percentile)elif prune_method channel_level:mask get_threshold_and_mask(torch.norm(prune_layer, p2, dim(0, 2, 3), keepdimTrue), percentile)if mask is not None:prune_layer prune_layer * mask.float()if vis:visualize_tensor(prune_layer, titletitle) # 实现可视化的函数return prune_layer# 使用PyTorch创建一个张量
tensor torch.rand((3, 10, 4, 5)) # 调用函数进行剪枝
pruned_tensor prune_conv_layer(tensor, fine_grained, 细粒度剪枝, visTrue)
pruned_tensor prune_conv_layer(tensor, vector_level, Vector级别剪枝, visTrue)
pruned_tensor prune_conv_layer(tensor, kernel_level, Kernel级别剪枝, visTrue)
pruned_tensor prune_conv_layer(tensor, filter_level, Filter级别剪枝, visTrue)
pruned_tensor prune_conv_layer(tensor, channel_level, Channel级别剪枝, visTrue)4.2 剪枝标准实践
4.2.1 定义初始网络画出权重分布图和密度直方图
import copy
import math
import random
import timeimport torch
import torch.nn as nn
import numpy as np
from matplotlib import pyplot as plt
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
import torch.nn.functional as F# 设置 matplotlib 使用支持负号的字体
plt.rcParams[font.family] DejaVu Sans# 1. 定义一个LeNet网络
class LeNet(nn.Module):def __init__(self, num_classes10):super(LeNet, self).__init__()self.conv1 nn.Conv2d(in_channels1, out_channels6, kernel_size5)self.conv2 nn.Conv2d(in_channels6, out_channels16, kernel_size5)self.maxpool nn.MaxPool2d(kernel_size2, stride2)self.fc1 nn.Linear(in_features16 * 4 * 4, out_features120)self.fc2 nn.Linear(in_features120, out_features84)self.fc3 nn.Linear(in_features84, out_featuresnum_classes)def forward(self, x):x self.maxpool(F.relu(self.conv1(x)))x self.maxpool(F.relu(self.conv2(x)))x x.view(x.size()[0], -1)x F.relu(self.fc1(x))x F.relu(self.fc2(x))x self.fc3(x)return x
device torch.device(cuda if torch.cuda.is_available() else cpu)
model LeNet().to(devicedevice)
modelLeNet((conv1): Conv2d(1, 6, kernel_size(5, 5), stride(1, 1))(conv2): Conv2d(6, 16, kernel_size(5, 5), stride(1, 1))(maxpool): MaxPool2d(kernel_size2, stride2, padding0, dilation1, ceil_modeFalse)(fc1): Linear(in_features256, out_features120, biasTrue)(fc2): Linear(in_features120, out_features84, biasTrue)(fc3): Linear(in_features84, out_features10, biasTrue)
)# 2. 加载模型
checkpoint torch.load(../ch02/model.pt)
# 加载状态字典到模型
model.load_state_dict(checkpoint)
origin_model copy.deepcopy(model)# 3. 绘制权重分布图
def plot_weight_distribution(model, bins256, count_nonzero_onlyFalse):fig, axes plt.subplots(2,3, figsize(10, 6)) # 创建了一个2行3列的子图布局整个图形的大小为10x6英寸 fig.delaxes(axes[1][2]) # 实际只绘制5个子图所以我们删除第6个子图 axes axes.ravel() # 将多维数组axes展平成一维数组方便后续索引plot_index 0 # 初始化一个索引变量plot_index用于跟踪当前正在绘制的子图# 遍历模型的所有参数参数名称和参数本身的迭代器for name, param in model.named_parameters():if param.dim() 1:ax axes[plot_index]# 如果count_nonzero_only为True则只考虑非零权重if count_nonzero_only:# 将参数从计算图中分离出来展平成一维数组并移动到CPU上param_cpu param.detach().view(-1).cpu()# 筛选出非零的权重param_cpu param_cpu[param_cpu ! 0].view(-1)# 在当前子图上绘制直方图bins指定柱状区间的数量# densityTrue表示显示概率密度color和alpha分别设置柱状图的颜色和透明度。ax.hist(param_cpu, binsbins, densityTrue, color green, alpha 0.5)else:# 如果count_nonzero_only为False则绘制所有权重的直方图ax.hist(param.detach().view(-1).cpu(), binsbins, densityTrue, color green, alpha 0.5)ax.set_xlabel(name)ax.set_ylabel(density)plot_index 1# 设置整个图形的标题并自动调整子图参数使之填充整个图形区域。fig.suptitle(Histogram of Weights)fig.tight_layout()fig.subplots_adjust(top0.925)plt.show()plot_weight_distribution(model)最终显示的是模型各层权重的分布情况x 轴是权重值y 轴是概率密度。这些图也有助于理解模型的权重在训练过程中的情况例如是否有梯度消失权重值集中在零附近或爆炸权重分布会出现极大值的问题。
# 4. 计算每一层网络的稠密程度非0参数的占比
def plot_num_parameters_distribution(model):# 创建一个空字典用于存储每一层的名称和对应的参数密度。num_parameters dict()# 初始化两个变量分别用于存储每一层中非零参数的数量和总参数数量num_nonzeros, num_elements 0, 0for name,param in model.named_parameters():if param.dim() 1:# 计算当前参数中非零元素的数量和总数量num_nonzeros param.count_nonzero()num_elements param.numel()dense float(num_nonzeros) / num_elementsnum_parameters[name] dense#创建一个大小为8x6英寸的图形在图形上添加y轴网格线fig plt.figure(figsize(8, 6))plt.grid(axisy)# 使用柱状图显示每一层的参数密度x轴为参数名称y轴为参数密度。bars plt.bar(list(num_parameters.keys()), list(num_parameters.values()))# 在柱状图上添加数据标签for bar in bars:# 获取柱状图的高度值。yval bar.get_height()# 在每个柱状图上方添加文本标签显示柱状图的高度值。plt.text(bar.get_x() bar.get_width()/2.0, yval, yval, vabottom) plt.title(#Parameter Distribution)plt.ylabel(Number of Parameters)# 将x轴的标签旋转60度以便更好地显示参数名称plt.xticks(rotation60)# 自动调整子图参数使之填充整个图形区域。plt.tight_layout()plt.show()# 绘制模型中每一层参数密度的分布柱状图
plot_num_parameters_distribution(model)4.2.2 基于L1权重大小的剪枝
torch.no_grad()
def prune_l1(weight, percentile0.5):num_elements weight.numel() # 权重总数 num_zeros round(num_elements * percentile) # 将50%的权重置为0 importance weight.abs() # 计算权重的绝对值作为权重的重要性指标 threshold importance.view(-1).kthvalue(num_zeros).values # 计算裁剪阈值 mask torch.gt(importance, threshold) # 创建mask大于阈值的权重为True weight.mul_(mask) # 计算mask后的weightreturn weightimportance.view(-1).kthvalue(num_zeros).values先将importance展平为一维向量然后求其第k小的元素knum_zeros的值values。
weight_pruned prune_l1(model.conv2.weight, percentile0.5) # 裁剪conv2层
model.conv2.weight.data weight_pruned # 替换原有model层
plot_weight_distribution(model) # 画出weight直方图# 画出weight稠密度直方图
plot_num_parameters_distribution(model)4.2.3 基于L2权重大小的剪枝
torch.no_grad()
def prune_l2(weight, percentile0.5):num_elements weight.numel() num_zeros round(num_elements * percentile) # 计算值为0的数量 importance weight.pow(2) # 计算weight的重要性使用L2范数即各元素的平方 threshold importance.view(-1).kthvalue(num_zeros).values # 计算裁剪阈值 mask torch.gt(importance, threshold) # 计算mask weight.mul_(mask) # 计算mask后的weightreturn weightweight_pruned prune_l2(model.fc1.weight, percentile0.4) # 裁剪fc1层
model.fc1.weight.data weight_pruned # 替换原有model层
plot_weight_distribution(model # 画出weight直方图# 画出weight稠密度直方图
plot_num_parameters_distribution(model)
# 保存裁剪后的weight
torch.save(model.state_dict(), ./model_pruned.pt)4.2.4 基于梯度大小的剪枝
del model
model LeNet().to(devicedevice)gradients torch.load(../ch02/model_gradients.pt) # 加载梯度信息
checkpoint torch.load(../ch02/model.pt) # 加载参数信息
model.load_state_dict(checkpoint) # 加载状态字典到模型torch.no_grad()
def gradient_magnitude_pruning(weight, gradient, percentile0.5):num_elements weight.numel() num_zeros round(num_elements * percentile) # 计算值为0的数量 importance gradient.abs() # 计算weight的重要性使用L1范数 threshold importance.view(-1).kthvalue(num_zeros).values # 计算裁剪阈值 mask torch.gt(importance, threshold) # 计算mask weight.mul_(mask) # 计算mask后的weightreturn weight# 对fc2应用梯度裁剪
gradient_magnitude_pruning(model.fc2.weight, gradients[fc2.weight], percentile0.5)
# 列出weight直方图
plot_weight_distribution(model)plot_num_parameters_distribution(model)4.3 剪枝时机实践训练后剪枝
模型训练后进行剪枝的步骤如下
初始训练使用反向传播算法训练神经网络学习权重和网络结构。识别重要连接训练后识别出对输出影响显著的连接通常是权重较大的连接。设置阈值选择一个阈值低于该阈值的连接被视为不重要。剪枝移除所有权重低于阈值的连接形成稀疏层。重新训练剪枝后重新训练网络调整剩余连接的权重以保持准确性。迭代剪枝重复剪枝和重新训练的过程直到达到在不显著损失准确性的情况下尽可能减少连接的平衡点。 下面还是以上一章节定义的LeNet网络为例根据权重绝对值大小进行剪枝并评估其剪枝前后在mnist数据集上的性能以及模型指标。
4.3.1 加载LeNet网络评估其权重分布和模型指标
定义一些神经网络模型的的属性函数包括模型的MACs乘累加操作数稀疏度参数总数和模型大小。
from torchprofile import profile_macs# 1. 定义一个函数用于获取模型的MACs乘累加操作数。
def get_model_macs(model, inputs) - int:return profile_macs(model, inputs)# 2. 定义一个函数用于计算给定张量的稀疏度即张量中零元素的占比。
def get_sparsity(tensor: torch.Tensor) - float:# 计算张量中非零元素的数量num_nonzeros tensor.count_nonzero()# 计算张量中总元素的数量num_elements tensor.numel()# 计算稀疏度并返回return 1 - float(num_nonzeros) / num_elements# 3. 定义一个函数用于计算给定模型的稀疏度。
def get_model_sparsity(model: nn.Module) - float:num_nonzeros, num_elements 0, 0for param in model.parameters():num_nonzeros param.count_nonzero()num_elements param.numel()return 1 - float(num_nonzeros) / num_elements# 4. 定义一个函数用于计算模型的总参数数量。
def get_num_parameters(model: nn.Module, count_nonzero_onlyFalse) - int:# 参数count_nonzero_only决定是否只计算非零权重的参数。num_counted_elements 0for param in model.parameters():if count_nonzero_only:num_counted_elements param.count_nonzero()else:num_counted_elements param.numel()return num_counted_elements# 5. 定义一个函数用于计算模型的大小单位为比特。
def get_model_size(model: nn.Module, data_width32, count_nonzero_onlyFalse) - int:# 参数data_width表示每个元素的位数count_nonzero_only决定是否只计算非零权重。return get_num_parameters(model, count_nonzero_only) * data_width# 6. 定义一些常用的数据单位常量
Byte 8
KiB 1024 * Byte
MiB 1024 * KiB
GiB 1024 * MiB数据预处理略 定义transform 方式使用datasets.MNIST加载mnist训练集和测试集数据将数据装入train_loader 和test_loader 定义训练函数train和评估函数evaluate略定义一个LeNet网络加载模型。初始模型的大小和准确率为
# 5. 加载训练后的模型
model LeNet()
checkpoint torch.load(../ch02/model.pt)
# 加载状态字典到模型
model.load_state_dict(checkpoint)# 6. 备份并评估model
origin_model copy.deepcopy(model)
origin_model_accuracy evaluate(origin_model, test_loader)
origin_model_size get_model_size(origin_model)
print(fdense model has accuracy{origin_model_accuracy:.2f}%)
print(fdense model has size{origin_model_size/MiB:.2f}%MiB)eval: 0%| | 0/157 [00:00?, ?it/s]
dense model has accuracy97.99%
dense model has size0.17%MiB# 7. 绘制weight分布图和稀疏度直方图
plot_weight_distribution(model)
plot_num_parameters_distribution(model)4.3.2 模型剪枝
定义细粒度剪枝函数fine_grained_prune对单个张量进行基于绝对值的剪枝。 定义剪枝比例sparsity根据权重的绝对值importance tensor.abs()来确定每个权重的重要性。根据剪枝比例计算阈值threshold使用torch.gt(importance, threshold)得到掩码对张量进行原地更新tensor.mul_(mask)将小于阈值的权重置零完成张量剪枝。
def fine_grained_prune(tensor: torch.Tensor, sparsity : float) - torch.Tensor:magnitude-based pruning for single tensor:param tensor: torch.(cuda.)Tensor, weight of conv/fc layer:param sparsity: float, pruning sparsitysparsity #zeros / #elements 1 - #nonzeros / #elements:return:torch.(cuda.)Tensor, mask for zeros# 确保稀疏率在 [0.0, 1.0] 之间sparsity min(max(0.0, sparsity), 1.0) # 如果 sparsity 为 1.0所有元素被置零并返回全零掩码 if sparsity 1.0:tensor.zero_()return torch.zeros_like(tensor)# 如果 sparsity 为 0.0返回全一掩码不进行剪枝elif sparsity 0.0:return torch.ones_like(tensor)num_elements tensor.numel()num_zeros round(num_elements * sparsity)importance tensor.abs()threshold importance.view(-1).kthvalue(num_zeros).valuesmask torch.gt(importance, threshold)tensor.mul_(mask)return mask定义一个细粒度剪枝类FineGrainedPruner用于管理剪枝过程。 init方法 接收一个模型和一个剪枝字典sparsity_dict字典中指定了每层的剪枝比例。使用静态方法 prune 对模型进行剪枝并保存掩码 prune方法遍历模型的参数对每个参数应用fine_grained_prune函数生成并存储掩码且进行剪枝。apply方法重新应用剪枝掩码。这通常是为了在训练期间例如在每个训练步骤之后确保剪枝效果仍然有效。
class FineGrainedPruner:def __init__(self, model, sparsity_dict):self.masks FineGrainedPruner.prune(model, sparsity_dict)torch.no_grad()def apply(self, model):# 使用保存的掩码对模型的参数进行掩码操作进行剪枝for name, param in model.named_parameters():if name in self.masks:param * self.masks[name]staticmethodtorch.no_grad()def prune(model, sparsity_dict):masks dict()for name, param in model.named_parameters():if param.dim() 1: # we only prune conv and fc weightsmasks[name] fine_grained_prune(param, sparsity_dict[name])return masks# 3. 设置剪枝字典
sparsity_dict {conv1.weight: 0.85,conv2.weight: 0.8,fc1.weight: 0.75,fc2.weight: 0.7,fc3.weight: 0.8,
}# 4. 应用剪枝
pruner FineGrainedPruner(model, sparsity_dict)
print(fAfter pruning with sparsity dictionary)# 5. 打印模型各层剪枝后的稀疏度
print(fThe sparsity of each layer becomes)
for name, param in model.named_parameters():if name in sparsity_dict:print(f {name}: {get_sparsity(param):.2f})# 6. 测试模型大小和mnist数据集上的准确率
sparse_model_size get_model_size(model, count_nonzero_onlyTrue)
print(fSparse model has size{sparse_model_size / MiB:.2f} MiB {sparse_model_size / origin_model_size * 100:.2f}% of orgin model size)
sparse_model_accuracy evaluate(model, test_loader)
print(fSparse model has accuracy{sparse_model_accuracy:.2f}% before fintuning)The sparsity of each layer becomesconv1.weight: 0.85conv2.weight: 0.80fc1.weight: 0.75fc2.weight: 0.70fc3.weight: 0.80
Sparse model has size0.04 MiB 26.13% of orgin model size
eval: 0%| | 0/157 [00:00?, ?it/s]
Sparse model has accuracy66.46% before fintuning# 7. 画出剪枝后模型权重分布图和稀疏度直方图
plot_weight_distribution(model, count_nonzero_onlyTrue)
plot_num_parameters_distribution(model)4.3.3 对剪枝后的模型进行微调
使用 SGD 优化器和交叉熵损失函数对剪枝后的模型进行微调训练即使训练过程中模型可能尝试恢复稀疏权重所以需要在每个训练迭代结束后调用 pruner.apply(model) 保持模型的稀疏性每个训练周期后评估模型并在模型达到新的最佳精度时保存模型的权重。
# 1. 设置训练参数
lr 0.01
momentum 0.5
num_finetune_epochs 5
# 设置SGD优化器和交叉熵损失函数
optimizer torch.optim.SGD(model.parameters(), lrlr, momentummomentum)
criterion nn.CrossEntropyLoss() best_sparse_model_checkpoint dict()
best_accuracy 0
print(fFinetuning Fine-grained Pruned Sparse Model)# 2. 微调模型
for epoch in range(num_finetune_epochs):train(model, train_loader, criterion, optimizer,callbacks[lambda: pruner.apply(model)])accuracy evaluate(model, test_loader)is_best accuracy best_accuracyif is_best:best_sparse_model_checkpoint[state_dict] copy.deepcopy(model.state_dict())best_accuracy accuracyprint(f Epoch {epoch1} Accuracy {accuracy:.2f}% / Best Accuracy: {best_accuracy:.2f}%)Finetuning Fine-grained Pruned Sparse Model
train: 0%| | 0/938 [00:00?, ?it/s]
eval: 0%| | 0/157 [00:00?, ?it/s]Epoch 1 Accuracy 97.48% / Best Accuracy: 97.48%
train: 0%| | 0/938 [00:00?, ?it/s]
eval: 0%| | 0/157 [00:00?, ?it/s]Epoch 2 Accuracy 97.52% / Best Accuracy: 97.52%
train: 0%| | 0/938 [00:00?, ?it/s]
eval: 0%| | 0/157 [00:00?, ?it/s]Epoch 3 Accuracy 97.79% / Best Accuracy: 97.79%
train: 0%| | 0/938 [00:00?, ?it/s]
eval: 0%| | 0/157 [00:00?, ?it/s]Epoch 4 Accuracy 97.79% / Best Accuracy: 97.79%
train: 0%| | 0/938 [00:00?, ?it/s]
eval: 0%| | 0/157 [00:00?, ?it/s]Epoch 5 Accuracy 97.95% / Best Accuracy: 97.95%# 3. 评估微调后的最优模型
model.load_state_dict(best_sparse_model_checkpoint[state_dict])
sparse_model_size get_model_size(model, count_nonzero_onlyTrue)
print(fSparse model has size{sparse_model_size / MiB:.2f} MiB {sparse_model_size / origin_model_size * 100:.2f}% of dense model size)
sparse_model_accuracy evaluate(model, test_loader)
print(fSparse model has accuracy{sparse_model_accuracy:.2f}% after fintuning)Sparse model has size0.04 MiB 26.13% of dense model size
eval: 0%| | 0/157 [00:00?, ?it/s]
Sparse model has accuracy97.95% after fintuning# 4. 绘制微调后模型权重分布图和稀疏度直方图
plot_weight_distribution(model)
plot_num_parameters_distribution(model)4.3.3.1 PyTorch保存模型的两种方式 在 Python 中当你将一个对象赋值给另一个变量时默认情况下它们共享同一个内存地址。如果你只是做了一个简单的赋值例如
best_sparse_model_checkpoint[state_dict] model.state_dict()这实际上存储的是 model.state_dict() 的引用内存地址。因此如果在后续的训练或修改过程中模型参数发生变化best_sparse_model_checkpoint 中的 state_dict 也会跟着改变因为它们指向的是同一个对象。 copy.deepcopy() 会创建一个独立的深拷贝也就是将对象的内容复制到一个新的内存空间里。这样一来即使后续修改了 model 的参数已经存储的 state_dict 不会受到影响。 你也可以直接使用 torch.save 保存最优模型。使用 torch.save 时模型的 state_dict 会被序列化并存储在磁盘上。保存完成后即使模型参数在内存中被修改磁盘上的文件也不会受到影响。
# 保存最佳模型
if is_best:torch.save(model.state_dict(), best_model.pth)# 恢复模型
model.load_state_dict(torch.load(best_model.pth))维度torch.savecopy.deepcopy保存位置磁盘内存存取速度磁盘 I/O 相对较慢频繁保存和加载模型会影响训练效率。快速内存操作内存占用不占用额外内存适合大规模模型或内存有限的环境占用额外内存持久性持久化存储程序结束后仍可使用非持久化程序结束后数据丢失适用场景训练后使用最优模型进行推理或分发时训练过程中定期保存模型的快照以备训练中断时恢复训练过程中的临时保存与恢复适合在训练中动态保存最佳状态实现复杂性需要指定保存路径及文件简单直接复制内存中的模型
4.3.3.2 回调函数 callbacks是深度学习训练中的一种设计模式用于在训练过程中如每个epoch或batch结束后插入自定义的代码逻辑增强训练的可扩展性和灵活性而不用修改 train 函数本身。例如使用callbacks可以实现以下功能
保存检查点在模型达到某个性能阈值时保存模型。早停当验证集的性能不再提高时停止训练。日志记录在训练期间记录损失和精度变化。动态调整学习率根据当前训练的表现动态调整学习率。模型修剪或剪枝保持模型稀疏性或定期应用剪枝掩码如示例中pruner.apply(model)。 在上述代码中callbacks[lambda: pruner.apply(model)]表示在每个train()调用结束时执行pruner.apply(model)确保模型在每次训练循环后保持稀疏性。
4.3.4 对比剪枝前后的模型指标 下面测试模型的延迟latency、计算量以乘累加操作数即MACs表示和模型大小以参数数量表示。
# 1. 测量模型延迟
torch.no_grad()
def measure_latency(model, dummy_input, n_warmup20, n_test100):model.eval()# 预热阶段避免测量结果受到模型权重初始化的影响for _ in range(n_warmup):_ model(dummy_input)# 真实测试阶段计算模型的平均延迟t1 time.time()for _ in range(n_test):_ model(dummy_input)t2 time.time()return (t2 - t1) / n_test # 创建格式化模板左对齐宽度为15个字符不到15填充空格
table_template {:15} {:15} {:15} {:15}
print (table_template.format(, Original,Pruned,Reduction Ratio))dummy_input torch.randn(64, 1, 28, 28)pruned_latency measure_latency(model, dummy_input)
original_latency measure_latency(origin_model, dummy_input)
print(table_template.format(Latency (ms),round(original_latency * 1000, 1),round(pruned_latency * 1000, 1),round(original_latency / pruned_latency, 1)))# 2. 测量模型MACs
original_macs get_model_macs(origin_model, dummy_input)
pruned_macs get_model_macs(model, dummy_input)
print(table_template.format(MACs (M),round(original_macs / 1e6),round(pruned_macs / 1e6),round(original_macs / pruned_macs, 1)))# 3. 测量模型大小参数量
original_param get_num_parameters(origin_model, count_nonzero_onlyTrue).item()
pruned_param get_num_parameters(model, count_nonzero_onlyTrue).item()
print(table_template.format(Param (M),round(original_param / 1e6, 2),round(pruned_param / 1e6, 2),round(original_param / pruned_param, 1))) Original Pruned Reduction Ratio
Latency (ms) 3.8 4.7 0.8
MACs (M) 18 18 1.0
Param (M) 0.04 0.01 3.8 五、PyTorch模型剪枝教程
Pytorch在1.4.0版本开始加入了剪枝操作在torch.nn.utils.prune模块中主要有以下剪枝方法
剪枝类型子类型剪枝方法局部剪枝结构化剪枝随机结构化剪枝 (random_structured)范数结构化剪枝 (ln_structured)非结构化剪枝随机非结构化剪枝 (random_unstructured)范数非结构化剪枝 (ln_unstructured)全局剪枝非结构化剪枝全局非结构化剪枝 (global_unstructured)自定义剪枝自定义剪枝 (Custom Pruning)
除此之外模块中还有一些其它方法
方法描述prune.remove(module, name)剪枝永久化prune.apply使用指定的剪枝方法对模块进行剪枝。prune.is_pruned(module)检查给定模块的某个参数是否已被剪枝。prune.custom_from_mask(module, name, mask)基于自定义的掩码进行剪枝用于定义更加细粒度的剪枝策略。
剪枝效果 参数变化 剪枝前weight 是模型的一个参数意味着它是模型训练时优化的对象可以通过梯度更新通过 optimizer.step() 来更新它的值。剪枝过程中原始权重被保存到新的变量 weight_orig中便于后续访问原始权重。剪枝后weight是剪枝后的权重值通过原始权重和剪枝掩码计算得出但此时不再是参数而是模型的属性一个普通的变量。 掩码存储生成一个名为 weight_mask的剪枝掩码会被保存为模块的一个缓冲区buffer。 前向传递PyTorch 使用 forward_pre_hooks 来确保每次前向传递时都会应用剪枝处理。每个被剪枝的参数都会在模块中添加一个钩子来实现这一操作。
剪枝测试总结
对weight进行剪枝效果已介绍。对weight进行迭代剪枝相当于把多个剪枝核mask序列化成一个剪枝核 最终只有一个weight_orig和weight_maskhook也被更新。对weight剪枝后再对bias进行剪枝weight_orig和weight_mask不变新增bias_orig和bias_mask新增bias hook。可以对多个模块同时进行剪枝最后使用remove进行剪枝永久化 使用remove函数后 weight_orig 和 bias_orig 被移除剪枝后的weight 和 bias 成为标准的模型参数。经过 remove 操作后剪枝永久化生效。此时剪枝掩码weight_mask 和 hook不再需要named_buffers和_forward_pre_hooks 都被清空。局部剪枝需要根据自己的经验来决定对某一层网络进行剪枝需要对模型有深入了解所以全局剪枝跨不同参数更通用即从整体网络的角度进行剪枝。采用全局剪枝时不同的层被剪掉的百分比可能不同。
parameters_to_prune ((model.conv1, weight),(model.conv2, weight),(model.fc1, weight),(model.fc2, weight))# 应用20%全局剪枝
prune.global_unstructured(parameters_to_prune, pruning_methodprune.L1Unstructured, amount0.2)最终各层剪枝比例为随机的
Sparsity in conv1.weight: 5.33%
Sparsity in conv2.weight: 17.25%
Sparsity in fc1.weight: 22.03%
Sparsity in fc2.weight: 14.67%
Global sparsity: 20.00%自定义剪枝需要通过继承class BasePruningMethod()来定义,其内部有若干方法: call, apply_mask, apply, prune, remove。其中必须实现__init__和compute_mask两个函数才能完成自定义的剪枝规则设定。此外您必须指定要实现的修剪类型 global, structured, and unstructured。
完整章节内容见《datawhale11月组队学习 模型压缩技术2PyTorch模型剪枝教程》。