RepVgg实战:训练多分支推理单路的图像分类模型重参数化方法
发布时间:2026/9/28 14:44:43来源:尧图网络
简介一份面向图像分类实战的RepVgg配套资源包适合深度学习初学者与计算机视觉爱好者。内容紧扣VGG式网络的三个核心特点无分支的plain架构、仅用3x3卷积、仅用ReLU激活并围绕这些设计思路提供了从数据准备到模型训练的完整代码便于理解训练与推理结构转换。资源共2000个文件包含约2435张png图片构成用于分类任务的数据集可直观观察各类别样本12个py脚本覆盖数据加载、模型搭建、训练与预测等环节便于逐段学习2个json文件保存类别映射和训练参数2个pth权重文件可直接加载使用1个txt说明帮助快速浏览目录与文件用途。压缩包整体约986.61MB已有990人下载学习。通过这套资料读者既能获得一套可运行的RepVgg图像分类实现又能对照大量图片数据观察训练过程是扎实掌握卷积网络基础、开展分类实验的实用工具。1. RepVgg是什么图像分类模型里那个“训练多分支、推理变单路”的异类做图像分类时模型选型经常陷入两难想要ResNet这种精调过的老牌结构又嫌它推理时每一层都在算多余的残差加法和BN想换Transformer图像分类模型小数据集上又容易过拟合。RepVgg恰好卡在这个尴尬中间——它在训练时用类似残差的多分支结构把精度堆上去训练完通过重参数化“变形”成一条干净的3x3卷积堆叠推理时没有分支、没有残差加速度直追VGG精度又能和ResNet掰手腕。这篇实战笔记把RepVgg从原理到训练、转换、避坑讲透适合已经有PyTorch基础、想把分类模型真正部署到CPU或边缘设备上的工程师。2. 重参数化的数学底子Conv-BN融合和三分支相加怎么算2.1 为什么训练时要多分支推理时却要拆掉ResNet当年证明残差分支有用是因为它给梯度提供了一条“近道”让深层网络更容易优化。RepVgg把这件事做到了极致每个基本块同时跑3x3卷积、1x1卷积和恒等映射三条分支梯度从三条路回传训练时特征表达更丰富收敛也更稳。但多分支是有代价的。推理时每张图要经过三个卷积核再加上两次BN的逐通道计算访存和算子调度开销成倍增加。残差结构还会让显存占用变高因为要缓存前一层的输出等加法。RepVgg的思路是训练归训练推理归推理用结构重参数化把三条分支的权重合并成一条3x3卷积让部署模型回到VGG那种“一条道走到黑”的单路结构。2.2 Conv-BN融合把每一段的归一化消进卷积权重分支再复杂拆开看每一路都是“卷积BN”的组合。BN在推理时的计算是固定公式可以整体并进卷积里。设卷积权重为W、偏置为bRepVgg里卷积默认biasFalse但公式里先带上BN的均值、方差、缩放、偏移分别为mean、var、gamma、betaeps是防止除零的小量。推理时BN的输出是 y gamma * (x - mean) / sqrt(var eps) beta把它套在卷积输出后面整理一下就能把gamma和beta吸进卷积的权重和偏置里 W W * gamma / sqrt(var eps) b (b - mean) * gamma / sqrt(var eps) beta注意这里的除法、乘法和加法全是逐通道的gamma和beta形状是(cout,)对权重W要按输出通道维度广播。我在代码里通常写成下面这个函数输入是Conv2d和BatchNorm2d模块def fuse_bn_to_weight(conv_w, conv_b, bn): mean bn.running_mean var bn.running_var gamma bn.weight beta bn.bias scale gamma / torch.sqrt(var bn.eps) w_fused conv_w * scale.view(-1, 1, 1, 1) b_fused (conv_b if conv_b is not None else 0.0) (beta - mean * scale) return w_fused, b_fused逻辑说明scale先按输出通道归一化然后view成(cout,1,1,1)去和卷积核逐通道相乘。偏置融合时如果原卷积没有bias就当作0处理。这个函数在第3章的模型转换里会被调用三次三个分支各调一次。参数说明bn.eps默认是1e-5训练时一般不改如果训练时用了混合精度或SyncBN融合后需要拿验证集重新校准一下输出后面避坑章节会细说。2.3 三分支合并1x1和恒等分支怎么“长”成3x3Conv-BN融合只是第一步真正的关键是让三个不同尺寸的卷积核能直接相加。卷积核能相加的前提是输出通道数、输入通道数、卷积核空间尺寸完全一致。3x3分支融合后得到的是3x3核1x1分支融合后得到1x1核用F.pad在四周各补一圈01x1核正好落在3x3核的中心位置参与计算时其余位置乘0效果完全等价w2 F.pad(w2, (1, 1, 1, 1)) # 左右各1列0上下各1行0 w2 w2.view(...) # 保持形状为(cout, cin, 3, 3)恒等分支更特殊它的“卷积”就是一个单位映射。变成3x3核时让每个输出通道对应的输入通道在核中心位置为1其余位置为0w3 torch.zeros(out_c, in_c, 3, 3) for i in range(min(out_c, in_c)): w3[i, i, 1, 1] 1.0这个w3还要经过BN的融合公式。三条分支都变成(cout, cin, 3, 3)的核和对应偏置后直接逐元素相加W W1 W2 W3 b b1 b2 b3只保留这一个3x3卷积推理时所有分支和BN全部消失。这里有个隐含条件只有stride1且输入输出通道一致时恒等分支才存在下采样块里恒等分支自动关闭合并时就只有3x3和1x1两路。2.4 RepVgg的模型配置从A0到B3怎么选RepVgg按深度和宽度分了几档结构上都是四个stage区别在每个stage的block数和通道倍率。我常用的是A0和B2两档A0轻量适合边缘设备和快速验证B2容量大适合精度优先的场景。A0的block分布是[2, 4, 14, 1]四个stage的通道数在stem之后分别是64、128、256、512。B系列会在这基础上把每个stage的block数拉长通道倍率也更大。实际选型时不用背参数记住这条经验你的数据集越小、类别越少越往A系列靠数据量大、类别几百上千才需要B1以上。训练成本上A0在单张消费级显卡上跑224x224、100个epoch完全没有压力。3. 动手前的准备RepVgg模型定义、数据集组织与转换函数3.1 环境与工程结构我一般用一个干净的目录装这类实验不依赖任何第三方库的高级API只用PyTorch和torchvision。版本上PyTorch 1.10以上就行新版2.x完全兼容ONNX导出章节才需要额外装onnx和onnxruntime。repgvg_cls/ ├── data/ # 数据集直接放训练和验证子目录 │ ├── train/ │ └── val/ ├── repvgg.py # 模型定义和转换函数 ├── train.py # 训练脚本 └── predict.py # 推理脚本3.2 用ImageFolder组织自己的图像分类数据集RepVgg吃的是ImageFolder格式目录名就是类别名。以森林图像分类为例train和val下各放四个子目录分别装针叶林、阔叶林、混交林、采伐迹地四类图片。目录名用英文拼音或数字都行但一定不要有中文和空格ImageFolder读目录名时会把它们直接当作标签字符串。from torchvision import datasets train_ds datasets.ImageFolder(data/train) val_ds datasets.ImageFolder(data/val) print(train_ds.classes) # [broadleaf, conifer, harvested, mixed] print(train_ds.class_to_idx) # 每个类名对应的数字标签逻辑说明ImageFolder会自动扫描子目录把文件夹名按字典序映射为0开始的整数标签。这里的class_to_idx后面传给模型做类别输出时要用别漏了。参数说明图片格式支持jpg、png、bmp等常见格式如果你的图片后缀不标准ImageFolder会跳过训练出来的类别数就会对不上先打印class_to_idx确认是很多人的习惯。3.3 定义训练态RepVgg模型这是整个实验的核心代码要注意forward里对“是否已经转换”的判断我用的是有没有rbr_reparam属性而不是单独存一个deploy标志位这样从训练态到推理态切换不用改类定义。import torch import torch.nn as nn import torch.nn.functional as F class RepVGGBlock(nn.Module): def __init__(self, in_c, out_c, stride1, groups1): super().__init__() self.out_c out_c self.in_c in_c self.groups groups self.rbr_identity nn.BatchNorm2d(in_c) if out_c in_c and stride 1 else None self.rbr_dense self._conv_bn(in_c, out_c, 3, stride, 1, groups) self.rbr_1x1 self._conv_bn(in_c, out_c, 1, stride, 0, groups) self.rbr_reparam None # 转换后挂这里 def _conv_bn(self, in_c, out_c, k, s, p, g): return nn.Sequential( nn.Conv2d(in_c, out_c, k, s, p, groupsg, biasFalse), nn.BatchNorm2d(out_c) ) def forward(self, x): if self.rbr_reparam is not None: return self.rbr_reparam(x) if self.rbr_identity is None: y torch.zeros(x.shape[0], self.in_c, x.shape[2], x.shape[3]) else: y self.rbr_identity(x) return self.rbr_dense(x) self.rbr_1x1(x) y class RepVGG(nn.Module): def __init__(self, num_blocks, num_classes, widths(64, 128, 256, 512), groups1): super().__init__() self.stage0 nn.Sequential( nn.Conv2d(3, widths[0], 3, 2, 1, biasFalse), nn.BatchNorm2d(widths[0]) ) self.in_c widths[0] self.stage1 self._make_stage(widths[0], num_blocks[0], 1, groups) self.stage2 self._make_stage(widths[1], num_blocks[1], 2, groups) self.stage3 self._make_stage(widths[2], num_blocks[2], 2, groups) self.stage4 self._make_stage(widths[3], num_blocks[3], 2, groups) self.gap nn.AdaptiveAvgPool2d(1) self.linear nn.Linear(widths[3], num_classes) def _make_stage(self, out_c, n, stride, groups): blocks [RepVGGBlock(self.in_c, out_c, stride, groups)] self.in_c out_c for _ in range(n - 1): blocks.append(RepVGGBlock(self.in_c, out_c, 1, groups)) return nn.Sequential(*blocks) def forward(self, x): x self.stage0(x) x self.stage1(x) x self.stage2(x) x self.stage3(x) x self.stage4(x) x self.gap(x) return self.linear(x.flatten(1)) def repvgg_a0(num_classes, **kwargs): return RepVGG(num_blocks[2, 4, 14, 1], num_classesnum_classes, **kwargs)逻辑说明RepVGGBlock的forward在推理态会走rbr_reparam单路恒等分支关闭时我直接构造一个零张量参与加法这段在转换后永远不会执行所以性能无影响。RepVGG类里stage0是stem只用普通3x3卷积后面四个stage按论文配置堆叠下采样发生在每个stage的第一个block。参数说明stem用stride2把224x224降成112x112如果你的训练图是32x32这种小尺寸需要把stem的stride改成1否则特征图缩得太快这是很多人训练CIFAR翻车的地方。3.4 提前写好重参数化转换函数转换函数放在模型定义文件里训练结束之后调用。它遍历模型所有模块凡是带rbr_dense属性的块就把三个分支融合成一个新卷积挂到rbr_reparam上。def fuse_bn_to_weight(conv_w, conv_b, bn): scale bn.weight / torch.sqrt(bn.running_var bn.eps) w_fused conv_w * scale.view(-1, 1, 1, 1) b_fused (conv_b if conv_b is not None else 0.0) (bn.bias - bn.running_mean * scale) return w_fused, b_fused def repvgg_model_convert(model): model.eval() for m in model.modules(): if not hasattr(m, rbr_dense): continue # 3x3分支 w1, b1 fuse_bn_to_weight(m.rbr_dense[0].weight, None, m.rbr_dense[1]) # 1x1分支补零成3x3 w2, b2 fuse_bn_to_weight(m.rbr_1x1[0].weight, None, m.rbr_1x1[1]) w2 F.pad(w2, (1, 1, 1, 1)) # 恒等分支 if m.rbr_identity is not None: w3 torch.zeros(m.out_c, m.in_c, 3, 3) for i in range(min(m.out_c, m.in_c)): w3[i, i, 1, 1] 1.0 w3, b3 fuse_bn_to_weight(w3, None, m.rbr_identity) else: w3, b3 0.0, 0.0 fused_w w1 w2 w3 fused_b b1 b2 b3 new_conv nn.Conv2d(m.in_c, m.out_c, 3, m.rbr_dense[0].stride, 1, groupsm.groups, biasTrue) new_conv.weight.data fused_w new_conv.bias.data fused_b m.rbr_reparam new_conv m.rbr_dense None m.rbr_1x1 None m.rbr_identity None return model逻辑说明新卷积的stride直接取原来3x3分支的stridepadding固定为1因为融合后核就是3x3。恒等分支的单位核我用了循环赋值只适配groups1的情况想支持组卷积需要把单位核按分组重复本节代码不展开。参数说明函数末尾把原分支置为None一是省内存二是后面导出ONNX时计算图里不会残留多分支节点。转换后rbr_reparam仍然放在原模块里forward通过hasattr判断自然走新路径不需要额外改forward。4. 森林图像分类实战从零训练一个四类RepVgg分类器4.1 数据增强与加载训练集和验证集为什么不一样图像分类的训练集和验证集增强策略历来不同。训练集用RandomResizedCrop把目标随机裁剪到224x224同时加水平翻转和颜色抖动让模型看到更多形态的森林图像验证集只用Resize到256再CenterCrop到224保证每张图经过固定的几何变换这样不同模型之间的指标才可对比。from torchvision import transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.4, 0.4, 0.4), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])逻辑说明RandomResizedCrop的默认scale范围是0.08到1.0意味着裁剪后目标在原图里可能只占8%这对森林这类大场景图像反而合适ColorJitter的四个参数同时扰动亮度、对比度、饱和度和色相能压住光照变化带来的过拟合。参数说明Normalize用的均值和标准差是ImageNet统计值RepVgg各家预训练权重都按这个值训练的如果从零训练自己的数据建议花几分钟跑一遍数据集的mean/std替换成你自己的尤其是森林图像整体偏绿用ImageNet均值初始化会有一小段自适应期。加载时用DataLoaderbatch size按显存调。224分辨率下A0模型在12G显存跑batch 128没问题8G卡就降到64。from torch.utils.data import DataLoader train_loader DataLoader(train_ds, batch_size128, shuffleTrue, num_workers8, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size128, shuffleFalse, num_workers8, pin_memoryTrue)逻辑说明shuffle只在训练集开验证集固定顺序保证每次评估结果可比num_workers按CPU核数给8到12都可以太大会在window上频繁报错。4.2 训练循环与超参设置SGD、cosine、标签平滑RepVgg论文用的优化器是SGD加Nesterov动量学习率0.1配合consine退火这个组合在ImageNet上验证过。我第一次直接用Adam跑RepVggacc比SGD低两个多点后来想通了一点多分支结构本身做了隐式集成SGD的噪声反而帮助探索更平坦的极小值。import torch.nn as nn from torch.optim import SGD, lr_scheduler model repvgg_a0(num_classes4) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer SGD(model.parameters(), lr0.1, momentum0.9, nesterovTrue, weight_decay1e-4) scheduler lr_scheduler.CosineAnnealingLR(optimizer, T_max120) for epoch in range(120): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) scheduler.step() print(fepoch {epoch1} loss {running_loss / len(train_ds):.4f} lr {scheduler.get_last_lr()[0]:.4f})逻辑说明CrossEntropyLoss的label_smoothing参数把硬标签向均匀分布拉一点四个类别的森林分类数据量不大平滑一下能明显减少过拟合风险。CosineAnnealingLR的T_max设为120正好和训练轮数一致学习率从0.1余弦下降到接近0。参数说明weight_decay用1e-4而不是默认的0RepVgg的恒等分支和1x1分支里BN参数多不配weight_decay会在后期过拟合时刹不住。如果显存不够把batch调到64同时学习率也应当按比例降到0.05左右这个线性缩放规则经常被忽略。训练结束后在验证集上跑一轮记录top1准确率和每个类别的准确率后面转换后要拿同样数据做一致性校验。4.3 训练完之后先切换结构再保存权重训练态权重只用来接着训练部署和迁移学习都应该用推理态权重。我的习惯是训练结束先复制一份模型只对副本做重参数化原模型留着不动这样万一转换出错还有后悔药。from copy import deepcopy torch.save({model: model.state_dict(), classes: train_ds.classes}, repvgg_a0_train.pth) deploy_model deepcopy(model) repvgg_model_convert(deploy_model) with torch.no_grad(): sample next(iter(val_loader))[0].cuda() out_train model(sample) out_deploy deploy_model(sample) max_diff (out_train - out_deploy).abs().max().item() print(fmax output diff: {max_diff:.6f}) torch.save({model: deploy_model.state_dict(), classes: train_ds.classes}, repvgg_a0_deploy.pth)逻辑说明复用验证集的一批数据把转换前后的logits逐元素比较最大误差只要在1e-3级别就说明融合数值正确。这里的关键是必须先把原模型切到eval模式再做转换否则BN的running_mean和running_var还在按训练逻辑更新融合出的权重就会带偏差。参数说明deepcopy会把所有参数完整复制转换发生在副本上所以原模型state_dict仍保持多分支结构保存推理态时classes信息要跟着存否则推理时不知道预测的索引对应哪一类树。4.4 一键推理脚本加载推理态模型做单张预测推理脚本的思路是先用deployFalse构建同结构模型加载权重后立刻调用repvgg_model_convert再走预测流程。加载推理态权重时一定要用普通结构构建模型再转换不要直接构造一个“已经带rbr_reparam”的模型因为参数名对不上。from PIL import Image model repvgg_a0(num_classes4).cuda() ckpt torch.load(repvgg_a0_deploy.pth) model.load_state_dict(ckpt[model]) model.eval() img Image.open(test_broadleaf.jpg).convert(RGB) x val_tf(img).unsqueeze(0).cuda() with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1) top1 prob.argmax(dim1).item() print(fpred: {ckpt[classes][top1]} conf: {prob[0, top1].item():.3f})逻辑说明val_tf里包含Normalize所以推理输入的预处理和验证集完全一致。torch.softmax在dim1上把logits转成概率argmax拿最高概率对应索引再从classes列表还原成可读的类别名。参数说明模型.forward走的是rbr_reparam单路卷积运行时的显存占用和计算量都比训练态小这也是部署用推理态的核心原因。单张预测如果想更快可以把模型转到CPU用torch.jit.trace固化下一章会说。5. RepVgg实战避坑五个让人翻车的细节与排查5.1 现象eval模式直接导出线上推理比预期慢一半一开始我在训练脚本里eval之后就保存没调用转换函数直接拿去部署。结果onnxruntime里跑出来的耗时和ResNet差不多完全没有单路模型该有的速度。查计算图才发现每个block里仍然是三个卷积分支加两个加号部署框架照单全收一个block算三遍。原因是保存的是训练态结构权重和结构一起打包了。解决办法是保存前必须走repvgg_model_convert并且转换后再导出ONNX。我在工程里约定凡是文件名带_deploy的必然是转换后的推理态带_train的只用于继续训练。这个习惯避免了后续所有误用。5.2 现象转换后精度暴跌输出和原模型对不上转换后验证集准确率从85%掉到30%属于直接崩掉而不是正常波动。逐层对照发现融合后的权重和原分支的计算结果差了很大一截问题出在BN的统计量上。融合时用了bn.running_mean和bn.running_var这两个值只在eval模式下才稳定。如果模型还在train模式就调用转换函数BN层会根据当前batch的输入重新更新running统计量导致融合的scale全部错位。解决办法是repvgg_model_convert函数开头强制model.eval()这条我已经写进了3.4的代码里如果你的代码是在训练循环内部调转换记得先退出训练模式再转换。5.3 现象1x1分支补零位置写错最大误差在0.1级别有次自己写转换把1x1核补成3x3时用了不对称padding只补了右边和下边结果是卷积核中心和左上角偏移了一位融合后的输出虽然看起来差不多但逐元素误差跑到了1e-1。原因是对F.pad的四参数顺序理解错了它的顺序对2D张量是(left, right, top, bottom)。让1x1核居中必须四个参数全是(1,1,1,1)写成(0,1,0,1)就会整体偏移。排查方法是拿一批固定输入转换前后输出相减看最大误差误差大于1e-3就不用往下看了。这里还有一个坑F.pad的padding参数如果是二维张量实际顺序是从最后一维开始匹配的建议写代码时加注释标明左右上下。5.4 现象加载pretrained模型转换后误差在1e-2级别用别人转换好的RepVgg权重时转换函数跑完最大误差在1e-2量级虽然能用但总觉得不干净。查下来是对方训练时把BN的eps改成了1e-4我融合时用了默认的1e-5。这个坑的本质是eps必须和训练时完全一致。卷积融合公式里的sqrt(var eps)eps差一个数量级在方差接近0的通道上会产生可感知的数值差。解决办法是融合前打印bn.eps确认训练代码里如果改动过eps转换函数里也要对应改。5.5 现象batch size调不上去训练loss震荡RepVgg的多分支结构对BN极其敏感。我用batch 16训练时loss不仅不降还来回跳换ResNet同样的超参就正常。原因是三个分支都挂BN每个BN在小batch下统计量抖动大三条分支的噪声叠加起来梯度方向就乱了。解决方法是优先保证batch至少32实在显存不够就降resolution到160x160或者用梯度累积模拟大batch。从零训练时我建议batch 128起步预训练微调可以放宽到64。这条也解释了为什么RepVgg做迁移学习比从零训练更容易因为权重已经收敛BN统计量的扰动对整体影响被压缩了。6. 进阶验证如何确认你手里的RepVgg真的变成了单路模型6.1 用整批验证集校验融合正确性单批数据对比只是快速检查整批验证集对比才可信。我在转换后会把训练态和推理态模型在全部验证集上各跑一遍统计逐样本预测结果的一致性完全一致才是真的转换成功。def check_deploy_consistency(model, deploy_model, loader): model.eval() deploy_model.eval() wrong 0 total 0 with torch.no_grad(): for x, y in loader: x, y x.cuda(), y.cuda() p1 model(x).argmax(dim1) p2 deploy_model(x).argmax(dim1) wrong (p1 ! p2).sum().item() total x.size(0) print(fconsistency: {(1 - wrong / total) * 100:.4f}%) return wrong / total逻辑说明如果整批预测完全一致说明融合在数值上对最终决策没有任何影响。出现个别样本不一致时定位方式是把最大误差层的name打印出来检查是不是恒等分支或组卷积处理有问题。参数说明这个校验只用CPU都可以跑模型不大验证集几百张图片就够了。记得加载推理态权重时也要走一次repvgg_model_convert让两侧结构都处于推理态而不是拿多分支的原始forward做对比。6.2 用ONNX导出看计算图是否只剩单路3x3torch.onnx.export把推理态模型导出后拿netron打开期待中的计算图应该是一串Conv2d节点穿插ReLU和池化中间没有任何Add和BatchNormalization。如果看到Add节点说明转换函数根本没有把分支去掉。import torch deploy_model repvgg_a0(num_classes4).cuda() ckpt torch.load(repvgg_a0_deploy.pth) deploy_model.load_state_dict(ckpt[model]) deploy_model.eval() x torch.randn(1, 3, 224, 224).cuda() torch.onnx.export( deploy_model, x, repvgg_a0_deploy.onnx, input_names[input], output_names[output], opset_version13 )逻辑说明opset_version选13及以上新版本的ONNX运行时不需要额外的兼容补丁。导出用随机输入即可ONNX模型只是固化计算图结构不依赖输入内容。参数说明如果netron图里还有BN节点说明你导出的是训练态模型。ONNX Runtime推理时BN会变成独立算子拖慢速度且增加量化难度这正是当初想做RepVgg要避免的。导出后跑一次onnxruntime比对PyTorch输出误差在1e-4以内才算通过。6.3 推理态模型的量化与部署建议单路3x3卷积结构对INT8量化极其友好。多分支模型量化时每个分支的激活分布不同合并后再量化要重新收集统计量误差容易被放大RepVgg推理态只剩一条卷积链量化校准的统计非常干净。我在实际项目里INT8量化后top1只掉了0.3到0.5个点比ResNet量化少掉一半。做法是先转推理态再导出ONNX然后用onnxruntime的QDQ或TensorRT做INT8校准。量化校准集建议用验证集的子集200张左右就够了覆盖每个类别。注意校准集不要和测试集重叠否则量化后的指标会虚高。另外一个小技巧是如果目标部署平台对3x3卷积有特殊的算子优化比如Intel的oneDNN或NVIDIA的TensorRTRepVgg这种全3x3单路结构能吃到最大的优化红利。对比一下转换前后的延迟通常在CPU上能快30%到50%这个差距在边缘设备上更明显。我现在每次训练完RepVgg都把“eval、转换、整批校验、导出ONNX”这一套流程当固定动作来做已经成了肌肉记忆。第一次做的时候偷懒跳过整批校验结果线上有个别样本预测漂移查了一整天才发现是恒等分支在某个特定通道上的数值误差被放大了。从那以后所有重参数化模型我都会坚持做整批一致性校验宁可多跑两分钟不给自己留半夜查线的机会。希望这篇笔记能帮你绕开这些坑把RepVgg稳稳地用到自己的图像分类项目里希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网