模型轻量化技术(上):模型剪枝、权重量化与知识蒸馏

各位同学,今天我们来聊聊嵌入式AI里最实在的话题——模型轻量化。

说实话,我刚开始做嵌入式AI那会儿,踩过不少坑。最典型的一次,我在服务器上训了一个ResNet-50,准确率92%,心里美滋滋。结果往STM32上一部署,好家伙,Flash直接爆了,RAM也不够用。那感觉,就像你做好了满汉全席,结果发现碗太小装不下。

从那以后,我学乖了。模型轻量化不是可选项,而是必选项。今天咱们就聊三个最核心的技术:模型剪枝、权重量化、知识蒸馏。这三板斧用好了,模型体积能缩到原来的十分之一,速度还能翻倍。

核心思路:在尽量不损失精度的前提下,把模型变小、变快、变省电。

模型轻量化技术 模型剪枝 结构化 / 非结构化 权重量化 INT8 / INT4 知识蒸馏 教师-学生网络 三者可组合使用,效果更佳

一、模型剪枝:给神经网络"瘦身"

模型剪枝,说白了就是去掉那些不重要的连接或神经元。你想想看,一个神经网络里,很多权重其实接近于零,它们对最终结果贡献很小。去掉它们,模型照样能工作。

我个人习惯把剪枝分成两类:非结构化和结构化。

1.1 非结构化剪枝

非结构化剪枝,就是逐个权重地剪。设定一个阈值,低于这个阈值的权重直接置零。好处是压缩率高,坏处是——嗯,剪完后的权重矩阵变得稀疏了,普通硬件跑起来反而不快。

我在项目中遇到过这种情况:用PyTorch做了非结构化剪枝,模型大小从50MB降到了15MB,心里挺高兴。结果部署到ARM Cortex-M上,推理速度反而慢了30%。为什么?因为稀疏矩阵在通用CPU上没法高效计算,你得用专门的稀疏计算库才行。

我的建议:非结构化剪枝适合有硬件加速的场景(比如NVIDIA GPU支持稀疏计算)。普通嵌入式MCU,还是老老实实用结构化剪枝吧。

1.2 结构化剪枝

结构化剪枝,剪的是整个通道、整个卷积核或者整个层。剪完之后网络结构还是规整的,硬件跑起来很友好。

举个例子,一个卷积层有64个输出通道,结构化剪枝可能只保留32个。这样一来,计算量直接减半,而且不需要特殊硬件支持。

# PyTorch结构化剪枝示例(剪掉BN层中gamma值小的通道)
import torch
import torch.nn.utils.prune as prune

model = torchvision.models.resnet18(pretrained=True)

# 对第一个卷积层做结构化剪枝,保留50%通道
prune.ln_structured(
    model.conv1, 
    name='weight', 
    amount=0.5, 
    n=2,  # L2范数
    dim=0  # 按输出通道剪
)

# 剪枝后需要重新打包参数
prune.remove(model.conv1, 'weight')
print(f"剪枝后卷积核形状: {model.conv1.weight.shape}")
# 输出: torch.Size([32, 3, 7, 7])  — 通道数从64减到32

注意:结构化剪枝后,下一层的输入通道数也变了,需要相应调整。我曾经因为忘了调整全连接层的输入维度,模型直接报错,排查了半天。

二、权重量化:用更少的比特表示权重

量化,说白了就是把模型里的浮点数(FP32)换成整数(INT8或INT4)。FP32一个数占4字节,INT8只占1字节,INT4更狠,半个字节。模型体积直接缩到原来的1/4甚至1/8。

但量化不是简单的四舍五入。你得考虑数值范围、精度损失、量化误差补偿等问题。

2.1 INT8量化

INT8量化是目前最成熟、应用最广的方案。它把浮点数映射到[-128, 127]的整数范围。映射方式有两种:对称量化和非对称量化。

量化类型 映射范围 适用场景 精度损失
对称量化 [-128, 127] 权重(通常对称分布) 较小
非对称量化 [0, 255] 激活值(通常非对称分布) 更小

我建议你直接用TensorRT或ONNX Runtime的量化工具,它们会自动选择最优的量化方案。自己手写量化校准,说实话,容易翻车。

# 使用ONNX Runtime做INT8量化
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType

model_fp32 = 'resnet18.onnx'
model_int8 = 'resnet18_int8.onnx'

# 动态量化:只量化权重,激活值还是浮点
quantize_dynamic(
    model_fp32,
    model_int8,
    weight_type=QuantType.QInt8
)

print("INT8量化完成!模型体积缩小约4倍")

2.2 INT4量化

INT4量化更激进,精度损失也更大。我一般只在资源极度受限的场景下用,比如Flash只有256KB的MCU。

INT4量化有个坑:很多硬件不支持INT4的矩阵乘法。你得用两个INT4拼成一个INT8来算,或者用查表法。嗯,这里要注意,查表法虽然快,但会占用额外的RAM。

避坑指南:我曾经在一个项目里用了INT4量化,模型体积确实小了,但推理结果全是NaN。查了半天发现是量化参数计算时溢出了。解决办法:先做INT8校准,再转INT4,并且把scale值限制在合理范围内。

三、知识蒸馏:大模型教小模型

知识蒸馏的思路很有意思——用一个大的、精度高的教师模型,去教一个小的、轻量的学生模型。学生模型学到的不是硬标签(比如"猫"或"狗"),而是教师模型输出的软概率分布。

为什么软标签更好?因为软标签包含了类别之间的相似性信息。比如教师模型看到一张图片,输出"猫"的概率是0.7,"狗"是0.2,"老虎"是0.1。这个分布告诉学生:猫和狗有点像,但猫和老虎更像。这些信息硬标签里是没有的。

3.1 蒸馏的损失函数

知识蒸馏的损失函数由两部分组成:

  • 硬损失:学生模型输出与真实标签的交叉熵
  • 软损失:学生模型输出与教师模型输出的KL散度

软损失里有个关键参数——温度T。温度越高,软标签分布越平滑,信息越丰富。我一般把T设在3-10之间,具体看任务。

# 知识蒸馏训练代码框架
import torch.nn.functional as F

def distillation_loss(student_output, teacher_output, labels, T=4, alpha=0.7):
    """
    student_output: 学生模型输出 (logits)
    teacher_output: 教师模型输出 (logits)
    labels: 真实标签
    T: 温度参数
    alpha: 软损失权重
    """
    # 硬损失
    hard_loss = F.cross_entropy(student_output, labels)
    
    # 软损失(用温度软化)
    soft_student = F.log_softmax(student_output / T, dim=1)
    soft_teacher = F.softmax(teacher_output / T, dim=1)
    soft_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (T ** 2)
    
    # 总损失
    total_loss = alpha * soft_loss + (1 - alpha) * hard_loss
    return total_loss

3.2 实战中的经验

我做知识蒸馏时,发现几个关键点:

  • 教师模型不能太强:如果教师模型准确率99%,学生模型学起来会很吃力。我一般选比学生强10-15个点的教师。
  • 温度要逐步降低:训练初期用高温度(T=10),让学生多学软信息;后期降低温度(T=2),让学生更关注硬标签。
  • 学生模型结构要合理:不是越小的模型越好。我试过把ResNet-50蒸馏到MobileNetV2,效果不错;但蒸馏到只有3层卷积的小网络,效果就很差。

总结一下:模型剪枝、量化和蒸馏,这三者不是互斥的。我通常的做法是:先做知识蒸馏,让学生模型学到教师的知识;再做结构化剪枝,去掉冗余通道;最后做INT8量化,把模型压缩到极致。三步走下来,模型体积能缩到原来的1/10,精度损失控制在1-2%以内。

好了,今天的内容就到这里。模型轻量化是个实践性很强的工作,光看理论没用,得动手试。我建议你找个现成的模型,按今天讲的流程走一遍,踩踩坑,收获会更大。

专注资料整理