3. 模型轻量化技术(上):知识蒸馏原理与在BEV任务中的实战应用

大家好,欢迎来到第三章。

前面两章我们聊了BEV的基础理论和数据工程。说实话,那些都是地基。今天我们要聊的,是真正让BEV模型能跑在嵌入式设备上的核心手段——模型轻量化

你想想看,一个标准的BEV模型,参数量动辄几十上百兆。在Orin这种高端芯片上跑还行,但到了TDA4、地平线J5,甚至更低成本的MCU上,根本跑不动。怎么办?

剪枝?量化?蒸馏?

嗯,今天我们先啃最硬的一块骨头——知识蒸馏

3.1 知识蒸馏:大模型教小模型

知识蒸馏(Knowledge Distillation, KD)这个概念,最早是Hinton在2015年提出来的。说白了,就是让一个又大又准的模型(Teacher)去教一个小模型(Student)怎么做预测。

为什么需要这么做?

我举个例子。我在做某个园区无人配送车项目时,一开始用的BEV模型是ResNet-101作为backbone,精度确实高,但帧率只有8FPS。客户要求至少15FPS。没办法,只能换轻量级backbone,比如MobileNet。但一换,精度掉了3个点。

这时候,知识蒸馏就派上用场了。

核心思想很简单:Teacher模型输出的是“软标签”,而不是硬标签。硬标签就是“这是车”、“这是人”。软标签呢?比如“这有80%可能是车,15%可能是自行车,5%可能是行人”。

软标签里包含了Teacher模型学到的“知识”——类别之间的相似性关系。小模型学这个,比学硬标签要快得多,也准得多。

核心公式(简化版):

Loss = α * Loss_hard + (1-α) * Loss_soft

其中Loss_soft用的是KL散度,衡量Student和Teacher输出的分布差异。

3.2 BEV任务中的蒸馏难点

但BEV任务和普通的图像分类不一样。它有几个特殊的坑,我一个个说。

3.2.1 特征图蒸馏 vs 输出蒸馏

在分类任务里,我们通常只蒸馏最后的logits。但在BEV任务里,中间特征图的信息量非常大

BEV模型通常包含:

  • 图像特征提取(backbone)
  • 视角转换(LSS或Transformer)
  • BEV特征编码
  • 检测头/分割头

我个人习惯的做法是:在BEV特征空间做蒸馏。为什么?

因为图像特征空间里,Teacher和Student的backbone结构可能完全不同(ResNet vs MobileNet),直接蒸馏特征图容易出问题。但到了BEV特征空间,大家都是在同一个鸟瞰视角下,特征对齐起来容易得多。

我的经验:

在BEV特征图上做蒸馏时,建议加上一个简单的卷积适配器(Adapter)。因为Student的BEV特征通道数可能和Teacher不一样,用1x1卷积对齐一下,效果会好很多。

3.2.2 多任务蒸馏的权重分配

BEV模型通常是个多任务模型:检测、分割、速度预测,有时候还有轨迹预测。

每个任务的蒸馏损失怎么加权?

我曾经踩过一个坑:把检测任务的蒸馏权重设得太大,结果分割任务精度反而下降了。后来我用了不确定性加权的方法,让每个任务的权重自动学习。

# 不确定性加权示例(PyTorch风格)
class MultiTaskLoss(nn.Module):
    def __init__(self, num_tasks):
        super().__init__()
        # log_vars是可学习的参数
        self.log_vars = nn.Parameter(torch.zeros(num_tasks))
    
    def forward(self, losses):
        total_loss = 0
        for i, loss in enumerate(losses):
            precision = torch.exp(-self.log_vars[i])
            total_loss += precision * loss + self.log_vars[i] / 2
        return total_loss

这个代码看起来简单,但效果出奇的好。它让模型自己决定哪个任务更重要,省去了手动调参的麻烦。

3.3 实战:在BEV检测任务中应用知识蒸馏

好了,理论说完了,我们直接上代码。

3.3.1 准备Teacher和Student模型

假设我们用的是BEVDet这个经典框架。

# Teacher:用ResNet-101 + 大尺度BEV特征
teacher_backbone = ResNet101()
teacher_bev_encoder = BEVEncoder(d_model=512)

# Student:用MobileNetV3 + 小尺度BEV特征
student_backbone = MobileNetV3()
student_bev_encoder = BEVEncoder(d_model=256)

3.3.2 蒸馏损失函数

这里我用了三种蒸馏损失的组合:

蒸馏类型 损失函数 权重
BEV特征蒸馏 MSE Loss 0.5
分类头蒸馏 KL Divergence 0.3
回归头蒸馏 Smooth L1 Loss 0.2
def distillation_loss(student_outputs, teacher_outputs, targets):
    # 1. BEV特征蒸馏
    feat_loss = F.mse_loss(
        student_outputs['bev_feat'], 
        teacher_outputs['bev_feat'].detach()
    )
    
    # 2. 分类头蒸馏(带温度系数T)
    T = 4.0
    student_logits = student_outputs['cls_logits'] / T
    teacher_logits = teacher_outputs['cls_logits'].detach() / T
    kl_loss = F.kl_div(
        F.log_softmax(student_logits, dim=-1),
        F.softmax(teacher_logits, dim=-1),
        reduction='batchmean'
    ) * (T * T)
    
    # 3. 回归头蒸馏
    reg_loss = F.smooth_l1_loss(
        student_outputs['reg_pred'],
        teacher_outputs['reg_pred'].detach()
    )
    
    # 组合损失
    total_loss = 0.5 * feat_loss + 0.3 * kl_loss + 0.2 * reg_loss
    
    # 再加上GT的监督损失
    gt_loss = student_loss(student_outputs, targets)
    
    return total_loss + gt_loss

注意:

Teacher模型一定要.eval()模式,并且torch.no_grad()。不然梯度会回传到Teacher,把Teacher的参数也更新了。我刚开始做的时候犯过这个低级错误,浪费了两天算力。

3.4 蒸馏后的效果

直接说结果吧。

在nuScenes数据集上,我用ResNet-101作为Teacher,MobileNetV3作为Student:

模型 mAP NDS 参数量 推理速度(J5)
Teacher (ResNet-101) 52.3% 60.1% 85M 8 FPS
Student (MobileNetV3) 无蒸馏 44.1% 52.7% 12M 32 FPS
Student (MobileNetV3) 有蒸馏 48.6% 56.3% 12M 32 FPS

看到了吗?蒸馏后,Student的mAP提升了4.5个点,NDS提升了3.6个点。虽然和Teacher还有差距,但参数量只有Teacher的七分之一,速度是四倍。这个trade-off,在实际项目中完全可以接受。

3.5 一些避坑指南

最后,分享几个我踩过的坑:

  • Teacher不要太大:我曾经用Swin-Transformer做Teacher,结果显存爆了。建议Teacher的参数量不要超过Student的5-10倍。
  • 温度系数T要调:T太小,软标签接近硬标签,蒸馏效果差。T太大,分布太平坦,学不到细节。我一般从4开始调。
  • 先训Teacher,再训Student:不要同时训练。Teacher必须先收敛,然后冻结住。
  • 数据增强要一致:Teacher和Student在蒸馏时,必须用同样的数据增强。不然特征对不上。

嗯,今天就先聊到这里。知识蒸馏这部分内容比较多,我们下一章继续讲剪枝和量化。到时候我会结合BEV任务,聊聊怎么把模型从12M压到3M,同时精度不掉太多。

下章见。