ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RepVgg实战:利用结构重参数化实现图像分类模型的高效部署

RepVgg实战:利用结构重参数化实现图像分类模型的高效部署 简介一套面向图像分类实战场景的RepVgg完整资源包适用于已有一定深度学习基础、希望掌握无分支结构、卷积与激活函数组合使用方法的进阶学习者。压缩包内文件总数2000个主要包含大量图片、训练与测试脚本、配置、模型权重、缓存及说明文件整体大小约986.61MB内容覆盖从数据准备、模型训练、验证评估到结果导出的完整闭环。目前已有九百九十人学习下载。资源中图片数据按类别场景组织脚本模块清晰可直接运行复现RepVgg在图像分类任务上的实验同时pth权重可用于加载预训练模型进行微调或推理json文件记录了类别映射与训练指标方便二次开发。按目录结构快速检索即可定位所需数据或脚本适合作为论文研读后的动手实践、课程设计或竞赛方案的参考基底。1. RepVgg实战图像分类里那把“训练重、推理轻”的尺子做过图像分类的都知道模型选型就是在打架。VGG结构简单、部署友好但是精度上不去ResNet把残差分支一加精度上去了可每条推理路径上多了好几个卷积分支在CPU上跑得让人着急。RepVgg这个结构的思路很直接训练的时候让网络长成多分支的“胖”样子靠残差和1x1分支把精度拉高推理之前再用结构重参数化把三个分支合并成一个3x3卷积让模型在部署时变回一条VGG式的直筒路径。这等于把训练和部署当成两套模型来用精度上和残差网络能掰手腕推理速度贴着VGG走。对做森林图像分类这类数据量不大、又要在边缘设备上落地的场景RepVgg是替换backbone时最值得先试的选项之一。2. 理解RepVgg的核心训练时三分支推理时单卷积2.1 为什么VGG式结构反而能打结构重参数化的设计逻辑RepVgg借鉴了ResNet的“捷径连接”思想但它不是把残差当作恒等映射一直保留到推理阶段而是在训练完成后把残差路径“揉”进主卷积里。训练时每个RepVgg Block包含三个分支3x3卷积、1x1卷积、以及一个Identity恒等映射只在输入输出通道数相等时启用。三个分支的输出直接相加梯度可以从三条路径同时回传缓解了深层网络中梯度消失的问题训练精度自然比纯VGG高。到了推理阶段三个分支的线性变换可以合并成一个卷积核因为卷积本身是线性的加法满足分配律。合并后的网络没有任何分支就是一组3x3卷积堆叠。这个思路最妙的地方在于它没有引入任何推理期额外计算反而在FLOPs上比ResNet更便宜因为残差分支在训练时产生的额外开销只影响训练时间。从图像分类的选型角度看这一招把“精度”和“速度”两个曾经互相拉扯的指标解耦了。你用ResNet的思想去训最后部署的却是一个VGG式结构这在“最新的图像分类模型”普遍转向transformer的当下反而成了CNN阵营一个实用主义的选择。transformer图像分类模型在GPU上有优势但在CPU和边缘设备上3x3卷积的推理效率依然有很强的竞争力。2.2 BN融合与分支合并把三个卷积分支压成一个3x3合并的核心是两个数学操作BN融合和分支权重相加。先看BN融合。训练时卷积后面接BN推理时BN是一个逐通道的线性变换可以直接吸进卷积权重里。假设卷积原始权重为Wbias为b如果没有bias则视为0BN的均值为mean方差为var缩放系数为gamma偏移为betaeps为防止除零的小量那么融合后的权重W和bias分别为W W * gamma / sqrt(var eps) b (b - mean) * gamma / sqrt(var eps) beta这里要注意计算是按通道维度的乘除法都发生在输出通道上。PyTorch里实现时把gamma除以sqrt(vareps)的结果reshape成(C_out, 1, 1, 1)再跟卷积权重相乘。再看分支合并。三个分支各有一个融合了BN的卷积核但尺寸不同3x3分支是(C_out, C_in, 3, 3)1x1分支是(C_out, C_in, 1, 1)Identity分支是单位矩阵展开成的(C_out, C_in, 1, 1)卷积核。合并前需要把1x1和Identity的核pad成3x3即在核四周补零def pad_1x1_to_3x3(kernel1x1): # kernel1x1: (C_out, C_in, 1, 1) # 在最后两个维度各补一圈0变成 (C_out, C_in, 3, 3) return F.pad(kernel1x1, [1, 1, 1, 1]) def identity_kernel_to_3x3(in_channels): # 生成单位卷积核只有中心点为1其余为0 kernel torch.zeros(in_channels, in_channels, 3, 3) for i in range(in_channels): kernel[i, i, 1, 1] 1.0 return kernel三个分支都变成3x3核后直接逐元素相加bias也相加最终得到一个单独的3x3卷积层。这个操作没有任何精度损失因为每一步都是严格等价的线性代数变换。2.3 论文之外的细节分组卷积、padding和精度损耗实际复现时有个关键细节是padding。3x3分支的padding通常是1而1x1分支和Identity分支在原始结构中padding为0。两个路径的输出尺寸能对齐靠的是输入分辨率不变、卷积步长为1。合并时1x1内核pad成3x3后放在中心位置能自动等价于原1x1卷积的padding行为不需要额外处理。但有一个坑是如果你的输入经过了一个stride2的下采样RepVgg块分支之间的对齐方式就变了。此时Identity分支不可用1x1分支要配合padding03x3分支的padding也必须相应调整合并逻辑要单独处理。我一般会在代码里分别实现stride1和stride2两种Block避免在转换函数里做判断时搞混。另一个容易被忽略的参数是BN层的eps。PyTorch默认eps1e-5TensorFlow默认是1e-3。做模型转换时如果参考的预训练权重跟你复现的框架eps不一样融合出来的数值会有一点偏差可能在最后几层累积成几分之一的输出差异。所以复现别人权重前先确认训练时BN的eps是多少。3. 从数据集到训练用RepVgg跑通森林图像分类3.1 数据集准备与预处理图像分类数据集下载与划分先谈数据集。如果目标是森林图像分类公开数据集可以从常见图像分类数据集下载渠道拿到也可以自己从航拍素材里切图标注。这里不讨论数据来源只讲拿到数据后的标准流程。假设数据目录结构是forest_data/ train/ broadleaf/ # 阔叶林 conifer/ # 针叶林 mixed/ # 混交林 val/ broadleaf/ conifer/ mixed/用PyTorch的ImageFolder可以一步到位加载。但要注意ImageFolder会按文件夹名的字母序分配类别编号训练和验证的类别编号必须一致。我之前因为训练目录和验证目录里类名大小写不一致导致验证时标签错位loss看起来正常accuracy却一直不对。数据划分时我习惯把原始数据先全部放在一个目录里按stratified split分成train和val而不是直接移动文件。因为后续可能要反复调整比例写一个脚本管理划分更省事import os import shutil import random from collections import defaultdict # 把原始数据按类别收集路径 data_root raw_forest_images class_to_files defaultdict(list) for cls in os.listdir(data_root): cls_dir os.path.join(data_root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): class_to_files[cls].append(os.path.join(cls_dir, fname)) # 按7:3划分stratified保证每个类别比例一致 val_ratio 0.3 random.seed(42) for cls, paths in class_to_files.items(): random.shuffle(paths) val_count int(len(paths) * val_ratio) # 分别拷入train/val目录 for split, subset in [(train, paths[val_count:]), (val, paths[:val_count])]: os.makedirs(fforest_split/{split}/{cls}, exist_okTrue) for src in subset: shutil.copy(src, fforest_split/{split}/{cls}/ os.path.basename(src))这个脚本的关键参数是val_ratio和random.seed。seed固定为42保证每次划分结果一致便于复现。stratified划分很重要因为森林类型在自然采集中往往极不平衡比如阔叶林图片可能比针叶林多三倍不按比例划分会让验证集类别分布失真。3.2 构建RepVgg模型3x3分支、1x1分支与identity分支模型定义的核心是RepVggBlock。训练时forward走三分支相加同时提供一个get_equivalent_kernel方法供后续转换使用class RepVggBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.in_channels in_channels self.out_channels out_channels self.stride stride # 主分支3x3卷积 BN self.conv3x3 nn.Conv2d(in_channels, out_channels, 3, stride, padding1, biasFalse) self.bn3x3 nn.BatchNorm2d(out_channels) # 辅助分支1x1卷积 BN仅当stride1时可用 if stride 1: self.conv1x1 nn.Conv2d(in_channels, out_channels, 1, 1, padding0, biasFalse) self.bn1x1 nn.BatchNorm2d(out_channels) # identity分支要求输入输出通道一致 self.identity nn.BatchNorm2d(in_channels) if in_channels out_channels else None else: self.conv1x1 None self.bn1x1 None self.identity None def forward(self, x): if self.stride 1: out self.bn3x3(self.conv3x3(x)) self.bn1x1(self.conv1x1(x)) if self.identity is not None: out out self.identity(x) return out else: # stride2时没有identity和1x1分支只有3x3 return self.bn3x3(self.conv3x3(x))这里有几个设计取舍。conv后面的bias设为False因为BN自带平移参数beta加了bias会造成参数冗余且影响转换时的数值计算。identity分支用BN做单位映射相当于先过一遍BN再相加这也是论文里的做法。当stride2时1x1分支和identity分支都无法与3x3分支保持输出尺寸一致所以直接舍弃这个Block就是纯粹的3x3卷积加BN。整个RepVgg网络就是堆叠这样的Block通道数按阶段放大。以RepVgg-B0为例四个阶段的Block数量和通道配置是[2, 4, 14, 1]和[64, 128, 256, 512]中间穿插stride2的下采样Block。分类头用全局平均池化加全连接。3.3 训练脚本与参数配置优化器、学习率、数据增强训练配置我直接给出一个在ImageNet上验证过的参数组合缩放到森林图像分类这种小数据集上一样适用model RepVggB0(num_classes3) optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max120) # warmup前5个epoch线性从0.01 * 0.1升到0.01 warmup_epochs 5 for epoch in range(120): if epoch warmup_epochs: lr 0.01 * (0.1 0.9 * epoch / warmup_epochs) for g in optimizer.param_groups: g[lr] lr else: scheduler.step() # 正常训练循环...关键参数说明lr0.01是batch_size64时的经验值如果你把batch提到256lr要按线性缩放规则升到0.04左右。weight_decay设1e-4在RepVgg里不要设太大因为BN的gamma也会被weight_decay影响过大的weight_decay会让BN缩放系数萎缩推理时融合出来的权重数值不稳定。数据增强对森林图像分类影响很大。我用的组合是随机ResizedCrop(224) 随机水平翻转 颜色抖动验证集只用Resize到256后CenterCrop到224。森林图像的一个特点是颜色纹理高度相似所以颜色抖动强度我调得比ImageNet默认值大一些brightness0.3, contrast0.3, saturation0.3。这个增强组合大概能把top-1 accuracy提升2-3个百分点值得保留from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize参数用的是ImageNet统计值因为预训练权重是在ImageNet上训的输入分布要跟预训练阶段保持一致。如果你打算从零训练这个参数可以换成在你数据集上算出的mean和std但从零训练RepVgg在小数据集上效果不稳定我不推荐。训练中要关注两个指标loss和BN的running_mean的分布。我见过loss一直在降但验证集accuracy不动的情况后来发现是BN的running_mean在训练初期震荡太剧烈前10个epoch用了太高的学习率。RepVgg对BN统计量敏感因为推理时会直接使用running统计量做融合训练初期如果统计量没收敛后期精度天花板会被压低。4. 推理阶段的重参数化把模型换成“单分支VGG”来跑4.1 重参数化转换手动实现分支合并与BN融合训练完成后要做的是把每个RepVggBlock转换成单个Conv2d。转换前务必将模型设置为eval模式因为BN的running_mean和running_var在train模式下会被当前batch的统计量污染eval模式才会使用稳定的running统计量。转换流程分三步先对每个分支做BN融合再把1x1和Identity的核pad成3x3最后三个核直接相加。完整实现def fuse_bn_to_conv(conv, bn): 把convbn融合成一个带bias的conv 返回新权重 (C_out, C_in, k, k) 和 bias (C_out,) w conv.weight.data gamma bn.weight.data beta bn.bias.data mean bn.running_mean var bn.running_var eps bn.eps std torch.sqrt(var eps) scale gamma / std # (C_out,) # 权重逐通道缩放 w_fused w * scale.view(-1, 1, 1, 1) # 如果conv原本有bias要加入计算 b_conv conv.bias.data if conv.bias is not None else torch.zeros_like(mean) b_fused (b_conv - mean) * scale beta return w_fused, b_fused def repvgg_block_to_conv(block): 将RepVggBlock转换成单个3x3 Conv2d if block.stride 2: # 只有3x3分支做一次BN融合即可 w, b fuse_bn_to_conv(block.conv3x3, block.bn3x3) return torch.nn.Conv2d(block.in_channels, block.out_channels, 3, 2, 1, biasTrue) # 处理3x3分支 w3, b3 fuse_bn_to_conv(block.conv3x3, block.bn3x3) # 处理1x1分支pad成3x3后相加 w1, b1 fuse_bn_to_conv(block.conv1x1, block.bn1x1) w1_padded F.pad(w1, [1, 1, 1, 1]) # 处理identity分支 w_id torch.zeros_like(w3) for i in range(block.in_channels): w_id[i, i, 1, 1] 1.0 # identity分支的BN融合相当于以单位卷积为原始权重bias为0 scale block.identity.weight / torch.sqrt(block.identity.running_var block.identity.eps) w_id_fused w_id * scale.view(-1, 1, 1, 1) b_id_fused (0 - block.identity.running_mean) * scale block.identity.bias # 三个分支相加 w_final w3 w1_padded w_id_fused b_final b3 b1 b_id_fused fused_conv torch.nn.Conv2d(block.in_channels, block.out_channels, 3, stride1, padding1, biasTrue) fused_conv.weight.data w_final fused_conv.bias.data b_final return fused_conv这段代码里最需要注意的是融合顺序。我先对每个分支做BN融合再把1x1核pad成3x3最后相加。如果把padding放在BN融合之前做需要把BN的mean和var也pad数值上容易出错。另外Identity分支的“卷积权重”是单位矩阵融合BN时要按公式把单位卷积当普通权重处理不能直接跳过。4.2 转换后模型的正确打开方式推理代码与输入预处理对齐转换完成后整个模型变成了一条由Conv2dReLU组成的直筒网络。这时候可以把它重新组装成一个新模型对象而这个对象里不再有BN层也不再有分支。如果直接保存state_dict并加载用于部署需要保证模型结构定义和保存的键对应得上。更稳妥的做法是转换后直接把模型用TorchScript导出彻底脱离Python侧的模型定义# model 是已经完成重参数化转换、处于eval模式的RepVgg example_input torch.randn(1, 3, 224, 224) traced_model torch.jit.trace(model, example_input) traced_model.save(repvgg_b0_forest.pt) # 在纯推理环境加载不需要依赖原模型定义 loaded_model torch.jit.load(repvgg_b0_forest.pt) with torch.no_grad(): output loaded_model(example_input) pred output.argmax(dim1)TorchScript trace的一大优势是它会固化模型的计算图部署端不需要任何模型定义代码对C推理、libtorch、ONNX导出都友好。注意trace时要按真实输入尺寸来如果实际推理用不同分辨率需要重新trace。4.3 转换前后的性能对比精度、参数量和速度转换后必须做一次精度对比验证确认转换过程没有出错。方法是取一个batch的验证集图片分别用训练模型和转换模型跑推理比较输出torch.manual_seed(0) # 构建一个包含所有类别图片的batch用验证集前64张 data_iter iter(val_loader) images, labels next(data_iter) # 训练状态的模型先切到eval train_model.eval() with torch.no_grad(): out_train train_model(images) out_fused fused_model(images) # 输出应该逐元素一致误差在1e-4级别 diff (out_train - out_fused).abs().max().item() print(fmax abs diff: {diff:.2e}) assert diff 1e-3, 转换前后输出不一致请检查转换代码我在实测中转换前后的最大绝对差值通常在1e-5到1e-4之间。这个残差不是错误而是浮点运算顺序不同导致的累积误差只要在1e-3以下就是安全的不会影响argmax的最终结果。如果diff到了0.1级别基本可以断定融合代码有bug先排查padding方向和BN参数是否取错。速度方面转换后的模型在CPU上的提升非常明显。以RepVgg-B0为例输入224x224在普通笔记本CPU上单线程推理转换前因为三分支开销单帧约需要35ms转换后降到20ms左右提升了40%。参数量看起来没变但推理时的激活值内存占用和BN计算都省掉了对内存带宽受限的边缘设备尤其友好。5. RepVgg实战常见问题与排查从loss不降到输出全错5.1 训练时loss降不下去检查BN的momentum现象训练前几个epochloss下降正常大约20个epoch后loss开始震荡甚至上升验证集精度徘徊在50%左右。模型结构、学习率都查过没有明显问题。原因BN的momentum设得太小或太大导致running_mean更新跟不上batch分布的变化。RepVgg在训练时有三个分支的输出相加每个分支都带BN整体分布的方差比单分支网络更大BN统计量更容易震荡。PyTorch默认momentum0.1但如果batch size小于32这个值会导致running统计量更新过快loss出现周期性震荡。解决把BN的momentum调低到0.03或0.01同时用warmup让统计量平稳起步。修改方式是for module in model.modules(): if isinstance(module, torch.nn.BatchNorm2d): module.momentum 0.035.2 转换后精度暴跌问题多半在padding现象训练模型在验证集上top-1 accuracy有85%重参数化转换后同样跑验证集accuracy掉到了30%相当于随机猜。逐batch比较输出发现差异极大。原因最常见的原因是Identity分支或1x1分支在pad成3x3时padding方向写反了。PyTorch的F.pad对4维张量的padding参数顺序是(左, 右, 上, 下)也就是从最后一维往前填。写F.pad(kernel1x1, [1, 1, 1, 1])会把1x1核放在3x3的中心这是对的但如果写成[1, 1, 0, 0]核就被挤到左上角等价于把1x1卷积的输入整体向右下移了一个像素特征错位。解决转换后第一件事用一个小输入验证每个Block转换前后的输出一致。写一个单测函数随机生成一个Block、随机输入张量转换后对比输出。这一步能做掉90%的转换bug。5.3 模型文件size对不上多出来的BN层去哪了现象转换前模型的state_dict大小是342MB转换后只剩213MB差了100多MB。有同事担心是不是删错了层。原因不是删错而是BN层的参数被吸收进了卷积层。每个BN层有4个参数gamma、beta、running_mean、running_var每个形状都是(C_out,)。RepVgg每个Block有3个BN层整个网络几十个Block累积起来参数量不小。转换后这些参数都不需要了文件size当然会变小。另外转换后的Conv2d多了bias训练时biasFalse这部分补回了一些参数量但远小于BN层省下的。解决这是预期行为不是bug。如果对文件大小有要求可以进一步对转换后的模型做半精度保存用torch.jit.optimize_for_inference配合fp16size还能再压缩约一半traced_model torch.jit.trace(model, example_input) traced_model torch.jit.optimize_for_inference(traced_model) traced_model.save(repvgg_b0_forest_fp16.pt)5.4 推理时输出全错检查预处理是否复用训练时的归一化参数现象转换后的模型在电脑上用Python推理正确换成ONNX Runtime或TensorRT推理时所有图片都输出同一个类别。原因训练时用ImageFolder加载图片走的是ToTensor Normalize即把像素从0-255缩放到0-1再按ImageNet的mean和std做标准化。部署端如果用OpenCV读图得到的是BGR排列的0-255数组直接喂给模型输入的数值分布完全对不上模型输出自然全是乱套的。解决在部署代码里复刻训练时的预处理逐像素检查。一个容易忽略的细节是OpenCV读出来是BGR而训练用的ImageFolder读出来是RGB必须先转通道顺序import cv2 import numpy as np # 读图并转RGB img cv2.imread(forest_sample.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 缩放到224x224保持训练时的resizecenter crop逻辑 img cv2.resize(img, (256, 256)) h, w img.shape[:2] start_h, start_w (h - 224) // 2, (w - 224) // 2 img img[start_h:start_h224, start_w:start_w224] # 归一化并转CHW img img.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std img img.transpose(2, 0, 1) input_tensor torch.from_numpy(img).unsqueeze(0)5.5 自定义数据集上收敛慢预训练权重和类别数不匹配现象用RepVgg在森林数据集上训练一共3个类别但预训练模型的head是1000类。直接加载后替换fc层训练到50个epoch精度才勉强到70%远低于预期。原因预训练权重在ImageNet上训过浅中层的特征已经足够通用真正需要从头学的是分类头。但是直接把fc层替换成新的3类输出后等价于整个backbone最后一层特征到类别映射要从零开始如果学习率没有区分对待backbone微调太慢head震荡太大。解决给backbone和head设置不同的学习率backbone用主学习率的0.1倍head用全速学习率backbone_params [p for name, p in model.named_parameters() if fc not in name] head_params [p for name, p in model.named_parameters() if fc in name] optimizer torch.optim.SGD([ {params: backbone_params, lr: 0.001}, {params: head_params, lr: 0.01}, ], momentum0.9, weight_decay1e-4)另外替换fc层时不要把fc的权重随机初始化得太随意用kaiming_normal或xavier都行关键是head输出层的scale要小不然初始损失太大前几个epoch的梯度会把这层权重推得很偏后期要花很长时间拉回来。6. 进阶把RepVgg的“重参数化”用到剪枝和多尺度推理上6.1 基于BN scale的通道剪枝把RepVgg模型继续瘦身重参数化转换后的RepVgg是纯卷积结构没有分支特别适合做通道剪枝。一个实用的剪枝思路是利用训练时BN层的gamma值来判断通道重要性——gamma趋近0的通道对输出贡献小可以剪掉。操作分三步先训练一个完整RepVgg保存训练状态下的权重然后看每个BN层的gamma绝对值按比例剪掉最不重要的通道最后对剪枝后的模型做短周期微调约20个epochlr降到之前的0.1倍弥补信息损失。具体剪哪里要按RepVgg的阶段逐个处理。每个阶段内的通道是独立的剪枝时以卷积层的输出通道为单位。剪掉一个通道后下一层卷积的输入通道也要相应移除所以每层要记录一个channel index的映射表。这个步骤比剪枝本身更繁琐建议先把转换后的模型保存成state_dict再在torch里对各层做mask操作别直接在训练模型上剪训练阶段的BN参数一旦被修改后面再转换就会对不上。剪枝比例我一般控制在20%-40%。超过50%后即使微调精度也会掉3个点以上有点得不偿失。剪完后再做一次重参数化保存整个模型看起来就是一个“瘦版VGG”在边缘设备上非常实用。6.2 多尺度推理不修改模型的性能提升技巧RepVgg的推理结构是纯3x3卷积没有全局池化前的全连接层限制输入尺寸去掉全连接层或用全局平均池化后输入可以任意分辨率。所以一个实用的技巧是TTATest-Time Augmentation的多尺度推理对同一张图分别用224、256、288三个分辨率输入把三个输出logits取平均再softmax。因为RepVgg是卷积网络不同分辨率只是产生不同大小的特征图全局平均池化后都能得到相同维度的向量不需要改模型结构。这个技巧在森林图像分类里特别有效因为森林图像中同类物体的尺度差异很大近景的树干和远景的树冠在视觉上完全是两回事。多尺度推理能把这种尺度多样性吸收进来通常能稳定提升1-2个点。代价是推理时间翻三倍所以只在离线评测或对延迟不敏感的场景用。6.3 最后的验证习惯转换、剪枝、导出一条龙每次做RepVgg的部署流程我都习惯把“转换验证”放在一切优化之前。具体做法是写好一个验证脚本随机生成一批输入分别经过原始训练模型和转换模型断言输出一致。然后才敢做剪枝、量化、TorchScript导出这些后续操作。这已经成了我的一个条件反射——结构重参数化这类涉及权重搬移的操作最怕的就是“看起来没问题”。有一次我在剪枝后发现accuracy掉了8%到处找原因查了两天最后定位到是剪枝时把BN层的running_mean当成可训练参数一起剪掉了重新固定BN参数再微调效果才恢复正常。RepVgg这个方向的性价比在同代CNN分类模型里是数一数二的。训练代价比ResNet稍微高一点换来的是部署时白捡的40%左右推理加速而且转换逻辑就几十行代码出错也容易排查。如果你手头有旧的VGG式模型要升级或者新项目需要一个精度和速度均衡的分类backboneRepVgg值得作为首选方案先跑一版基线。希望帮到你。本文还有配套的精品资源点击获取
返回列表