热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

RepVGG(ReparameterizationVGG):MakingVGGstyleConvNetsGreatAgain

参考:RepVGG网络简介_太阳花的小绿豆的博客-CSDN博客参考:Repvgg详解及其实现(pytorch)_一方热衷.

参考:RepVGG网络简介_太阳花的小绿豆的博客-CSDN博客

参考:Repvgg详解及其实现(pytorch)_一方热衷.的博客-CSDN博客

论文作者知乎:RepVGG:极简架构,SOTA性能,让VGG式模型再次伟大(CVPR-2021) - 知乎

官方开源代码:https://github.com/DingXiaoH/RepVGG

本文部分内容参考自其他作者博客,如有侵权请联系删除。



        尽管很多复杂的卷积神经网络模型比简单网络获得了更好的性能,但是这些复杂网络也有显著的缺点:


  1. 复杂的多分支网络结构设计(如ResNet的残差模块,Inception网络),导致模型能难实现,降低模型推理性能,增加显卡内存占用
  2. 一些轻量化的操作,如ShuffleNet中使用的通道shuffle,以及MobileNet中使用的深度可分离卷积操作,这些操作虽然可以降低模型的参数量,但是增加了访问内存的次数,并且这些操作不能很好的被一些设备支持(通常3x3卷积被优化和支持的最好)

MobileNetV1《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》_胖胖大海的博客-CSDN博客

《MobileNetV2: Inverted Residuals and Linear Bottlenecks》_胖胖大海的博客-CSDN博客_invertedresidual

        单纯基于模型的参数量,浮点计算量FLOPs来衡量模型的处理效率和推理速度是不准确的,比如MobileNet使用了深度可分离卷积,大幅降低了参数量和浮点计算量,但是增加了访问内存的次数,导致模型的推理速度提升并没有达到理想的程度。比如下图中EfficientNet的FLOPs计算量和参数量更小,但是处理速度确并不一定快。

        在学术论文中通常喜欢使用模型的参数量和浮点运算量FLOPs来衡量模型的大小以及处理速度,但是本文作者提出,参数量和FLOPs并不能真正反映出模型真实的推理速度。另外两个影响推理速度的重要因素分别是:


  1. 模型访问内存的次数Memory Access Cost(MAC),Multi-Branch模型每个分支都要访问内存,都要保存特征图,虽然有的分支参数量和计算量并不高(比如1x1卷积分支,Identity分支,分组卷积等),但是访问内存的次数以及占用的内存大小都增加了
  2. 模型的并行化程度,Multi-Branch模型不同branch的速度不同,但是需要等待其他分支,导致算力资源的浪费,并行度不高

RepVGG的优点:


  1. RepVGG的模型在推理阶段,是一个想VGG网络一样扁平化的网络,没有任何分支结构,这样的模型占用更少的显卡内存,访问内存的次数更少,计算并行度更高,所以计算效率就高
  2. RepVGG的模型在推理阶段网络中只包括3x3的卷积操作和ReLU激活操作,3x3的卷积操作执行效率很高的
  3. RepVGG网络模型没有经过特殊的设计,比如NAS搜索等

多分支Multi-branch模型为什么效果好?


  1. 从特征融合的层面理解,不同的分支学习到了不同的表征,融合之后的表征能力更强
  2. 从特征和梯度复用的层面理解,比如ResNeXt和DenseNet,多个分支之间可以进行特征和梯度的复用
  3. 从集成学习的层面理解,比如ResNet里面的short-cut连接,每遇到一个short-cut,模型就可能有两种可能,这样从头到尾模型就有2的N次方种可能,就像是将2的N次方个模型的结果进行综合集成

单分支扁平化的模型为什么快?


  1. 只有一个分支不存在特征复制,占用更少的显卡内存
  2. 由于不存在其他分支访问特征,访问内存的次数更少
  3. 由于不存在其他分支进行并行计算,所以不用等待其他分支处理完
  4. 扁平化的模型算子种类更单一,比如RepVGG里面只有3x3卷积和ReLU,执行效率更高

这里贴上论文作者知乎解答:

        鉴于多分支模型训练性能好,推理性能差,单分支扁平化模型训练性能差,推理性能好的情况,将二者综合,试图构建一种网络模型,在模型训练阶段使用多分支训练获得更好的训练性能,在模型推理阶段将训练好的多分支模型恒等转换为单分支的扁平化模型,推理阶段的网络模型中只有3x3的卷积和ReLU激活这两种操作。这其中的核心问题就是如何把多分支的模型转换为一个单分支的模型?RepVGG里面把这个过程叫做结构重参数化技术。

 

 

1、Conv3x3 + BN --> Conv3x3:

        BatchNorm在推理阶段的计算涉及4组参数,每组参数的数量和特征的维度相同,使用这4组参数计算BatchNorm。对于2D卷积的结果,特征的维度大小就是输出的feature map的通道数。将Conv3x3 + BN融合成为一步Conv3x3,重新设置Conv3x3中的权重和偏置参数。

2、Conv1x1 + BN -> Conv3x3:


  1. 将1x1卷积核补0变成3x3卷积核,为了保证卷积之后的输出特征图大小不变,给原始特征图的四周进行padding,padding的大小为1,把Conv1x1 + BN -> Conv3x3 + BN
  2. 使用第一步Conv3x3 + BN -> Conv3x3方法进行融合计算,得到Conv1x1 + BN -> Conv3x3 + BN -> Conv3x3

3、BN -> Conv3x3:


  1. 由于只有一个BN,没有卷积操作,先构建一个恒等的卷积操作,卷积核的大小为1x1,第n个卷积核的第n个通道权重为1,其余通道权重为0
  2. 然后使用与Conv1x1 + BN -> Conv3x3相同的方法,把1x1的卷积核补0扩展成3x3的卷积核,Conv1x1 + BN -> Conv3x3 + BN
  3. 然后使用第一步Conv3x3 + BN -> Conv3x3方法进行融合计算,得到BN -> Conv1x1 + BN -> Conv3x3 + BN -> Conv3x3

4、多分支Conv3x3融合成一个Conv3x3

        现在三个分支都转换成了Conv3x3操作,并且输出的特征图形状相同,由于卷积操作具有可加性,多分支的卷积结果相加,就等于多分支的卷积权重相加,偏置相加构成一个新的卷积操作,然后对输入特征图做一次卷积,这样就把三次卷积压缩成一次卷积了。

备注:RepVGG是为GPU和专用硬件设计的高效模型,追求高速度、省内存,较少关注参数量和理论计算量。在低算力设备上,可能不如MobileNet和ShuffleNet系列适用。

 

# coding:utf-8
from collections import OrderedDict
import numpy as np
import torch
import torch.nn as nn
def Conv3x3BNToConv3x3(g=2, in_channels=4, out_channels=4, tol=1e-4):
"""
Conv3x3 + BN -> Conv3x3
1、对于2D卷积的结果,特征的维度大小就是输出的feature map的通道数
2、BatchNorm在推理阶段的计算涉及4组参数,每组参数的数量和特征的维度相同,使用这4组参数计算BatchNorm
3、将Conv3x3 + BN融合成为一步Conv3x3,重新设置Conv3x3中的权重和偏置参数
:return:
"""
torch.random.manual_seed(0)
f1 = torch.randn(1, in_channels, 3, 3)
module = nn.Sequential(OrderedDict(
# 原始卷积不使用偏置参数
conv=nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=False, groups=g),
bn=nn.BatchNorm2d(num_features=out_channels)
))
# fuse conv + bn
# 获取原始卷积权重
kernel = module.conv.weight
# 获取BN的均值,nn.Buffer
running_mean = module.bn.running_mean
# 获取BN的方差
running_var = module.bn.running_var
# 获取BN学习的权重参数
gamma = module.bn.weight
# 获取BN学习的偏置参数
beta = module.bn.bias
# BN计算时为了防止除0异常使用的数值稳定参数
eps = module.bn.eps
# 计算BN的标准差
std = (running_var + eps).sqrt()
print("kernel: {}".format(kernel.shape))
print("running_mean: {}".format(running_mean.shape))
print("running_var: {}".format(running_var.shape))
print("gamma: {}".format(gamma.shape))
print("beta: {}".format(beta.shape))
print("eps: {}".format(eps))
print("std: {}".format(std.shape))
print(gamma, beta, std)
# 计算卷积和BN融合之后对原卷积权重的缩放系数
t = (gamma / std).reshape(-1, 1, 1, 1) # [ch] -> [ch, 1, 1, 1]
# 对原始卷积的权重进行缩放
kernel = kernel * t
# 计算卷积和BN融合之后卷积操作的偏置
bias = beta - running_mean * gamma / std
fused_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
# 将计算得到的卷积权重和偏置赋给新的卷积操作
fused_conv.load_state_dict(OrderedDict(weight=kernel, bias=bias))
module.eval()
fused_conv.eval()
with torch.no_grad():
out1 = module(f1).detach().cpu().numpy()
out2 = fused_conv(f1).detach().cpu().numpy()
print(out1)
print(out2)
print(np.allclose(out1, out2, rtol=tol, atol=tol))
def Conv1x1BNToConv3x3(g=2, in_channels=128, out_channels=128, tol=1e-4):
"""
Conv1x1 + BN -> Conv3x3 + BN -> Conv3x3
1、将1x1卷积核补0变成3x3卷积核,为了保证卷积之后的输出特征图大小不变,给原始特征图的四周进行padding,
padding的大小为1,把Conv1x1 + BN -> Conv3x3 + BN
2、使用Conv3x3BNToConv3x3方法进行融合计算,把Conv3x3 + BN -> Conv3x3
:return:
"""
torch.random.manual_seed(0)
f1 = torch.randn(1, in_channels, 3, 3)
module = nn.Sequential(OrderedDict(
# 原始卷积不使用偏置参数
conv=nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1, padding=0, bias=False, groups=g),
bn=nn.BatchNorm2d(num_features=out_channels)
))
# fuse conv + bn
# 获取原始1x1卷积权重
kernel = module.conv.weight
# 获取BN的均值,nn.Buffer
running_mean = module.bn.running_mean
# 获取BN的方差
running_var = module.bn.running_var
# 获取BN学习的权重参数
gamma = module.bn.weight
# 获取BN学习的偏置参数
beta = module.bn.bias
# BN计算时为了防止除0异常使用的数值稳定参数
eps = module.bn.eps
# 计算BN的标准差
std = (running_var + eps).sqrt()
# 初始化全为0的3x3卷积核
# 当使用分组卷积时,每个卷积核的通道数等于输入通道数除以分组数
weight = torch.zeros(out_channels, in_channels // g, 3, 3, dtype=torch.float)
# 将1x1卷积核放在3x3卷积核的中间,相当于对原始1x1卷积补0得到3x3卷积核
weight[:, :, 1:2, 1:2] = kernel.data
print("kernel: {}".format(kernel.data))
print("weight: {}".format(weight))
# 计算卷积和BN融合之后对原卷积权重的缩放系数
t = (gamma / std).reshape(-1, 1, 1, 1) # [ch] -> [ch, 1, 1, 1]
kernel_new = torch.nn.Parameter(weight * t, requires_grad=True)
print("kernel new: {}".format(kernel_new.data))
# 计算卷积和BN融合之后卷积操作的偏置
bias_new = beta - running_mean * gamma / std
fused_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
# 将计算得到的卷积权重和偏置赋给新的卷积操作
fused_conv.load_state_dict(OrderedDict(weight=kernel_new, bias=bias_new))
module.eval()
fused_conv.eval()
with torch.no_grad():
out1 = module(f1).detach().cpu().numpy()
out2 = fused_conv(f1).detach().cpu().numpy()
print(out1)
print(out2)
print(np.allclose(out1, out2, rtol=tol, atol=tol))
def BNToConv3x3Group(g=2, in_channels=4, out_channels=4, tol=1e-4):
"""
BN -> Conv1x1 + BN -> Conv3x3 + BN -> Conv3x3
1、由于只有一个BN,没有卷积操作,先构建一个恒等的卷积操作,卷积核的大小为1x1,第n个卷积核的第n个通道权重为1,其余通道权重为0
2、然后使用与Conv1x1BNToConv3x3相同的方法,把1x1的卷积核补0扩展成3x3的卷积核,Conv1x1 + BN -> Conv3x3 + BN
3、然后使用Conv3x3BNToConv3x3进行融合计算,把Conv3x3 + BN -> Conv3x3
:return:
"""
torch.random.manual_seed(0)
f1 = torch.randn(1, in_channels, 3, 3)
bn = nn.BatchNorm2d(num_features=out_channels)
# 获取BN的均值,nn.Buffer
running_mean = bn.running_mean
# 获取BN的方差
running_var = bn.running_var
# 获取BN学习的权重参数
gamma = bn.weight
# 获取BN学习的偏置参数
beta = bn.bias
# BN计算时为了防止除0异常使用的数值稳定参数
eps = bn.eps
# 计算BN的标准差
std = (running_var + eps).sqrt()
# 计算BN -> Conv3x3 + BN之后卷积权重的缩放系数
t = (gamma / std).reshape(-1, 1, 1, 1)
# 计算BN -> Conv3x3 + BN之后卷积的偏置
bias = beta - running_mean * gamma / std
# 设置卷积核,如果第n个卷积核的第n个通道的最中心一个元素权重为1,其余权重均为0
# 当使用分组卷积时,每个卷积核的通道数等于输入通道数除以分组数
# 同时要在分组卷积中保持恒等映射的效果,那么就要求在每个分组中,第n个卷积核的第n个通道的最中心一个元素权重为1,其余权重均为0
weight = torch.zeros(out_channels, in_channels // g, 3, 3, dtype=torch.float)
for i in range(in_channels):
# if g == in_channels:
# j = 0
# elif g == 1:
# j = i
# else:
# j = i % (in_channels // g)
j = i % (in_channels // g)
weight[i, j, 1:2, 1:2] = 1
kernel = torch.nn.Parameter(weight * t, requires_grad=True)
print("kernel: {}".format(kernel.data))
conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
conv.load_state_dict(OrderedDict(weight=kernel, bias=bias))
conv.eval()
bn.eval()
with torch.no_grad():
out1 = bn(f1).detach().cpu().numpy()
out2 = conv(f1).detach().cpu().numpy()
print(out1)
print(out2)
print(np.allclose(out1, out2, rtol=tol, atol=tol))
def FuseConv3x3(g=2, in_channels=4, out_channels=4, tol=1e-4):
"""
将多个Conv3x3卷积合并为一个Conv3x3卷积
将多个并行的Conv3x3卷积输出结果相加,等同于先将多个Conv3x3卷积的权重相加,偏置相加构成一个新的卷积操作,然后作用于输入特征图
:return:
"""
torch.random.manual_seed(0)
f1 = torch.randn(1, in_channels, 3, 3)
conv1 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
conv2 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
conv3 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
fuse_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1, bias=True, groups=g)
kernel = conv1.weight + conv2.weight + conv3.weight
bias = conv1.bias + conv2.bias + conv3.bias
fuse_conv.load_state_dict(OrderedDict(weight=kernel, bias=bias))
conv1.eval()
conv2.eval()
conv3.eval()
fuse_conv.eval()
with torch.no_grad():
out1 = conv1(f1).detach().cpu().numpy()
out2 = conv2(f1).detach().cpu().numpy()
out3 = conv3(f1).detach().cpu().numpy()
fuse_out = fuse_conv(f1).detach().cpu().numpy()
print(out1 + out2 + out3)
print(fuse_out)
print(np.allclose(out1 + out2 + out3, fuse_out, rtol=tol, atol=tol))
if __name__ == '__main__':
# Conv3x3BNToConv3x3(g=128, in_channels=128, out_channels=128, tol=1e-6)
# Conv1x1BNToConv3x3(g=1, in_channels=128, out_channels=128, tol=1e-6)
# BNToConv3x3Group(g=128, in_channels=128, out_channels=128, tol=1e-6)
FuseConv3x3(g=128, in_channels=128, out_channels=128, tol=1e-5)



推荐阅读
  • 由中科院自动化所、中科院大学及南昌大学联合研究提出了一种新颖的双路径生成对抗网络(TP-GAN),该技术能通过单一侧面照片生成逼真的正面人脸图像,显著提升了不同姿态下的人脸识别效果。 ... [详细]
  • 机器学习核心概念与技术
    本文系统梳理了机器学习的关键知识点,涵盖模型评估、正则化、线性模型、支持向量机、决策树及集成学习等内容,并深入探讨了各算法的原理和应用场景。 ... [详细]
  • Java实现文本到图片转换,支持自动换行、字体自定义及图像优化
    本文详细介绍了如何使用Java实现将文本转换为图片的功能,包括自动换行、自定义字体加载、抗锯齿优化以及图片压缩等技术细节。 ... [详细]
  • 2017年人工智能领域的十大里程碑事件回顾
    随着2018年的临近,我们一同回顾过去一年中人工智能领域的重要进展。这一年,无论是政策层面的支持,还是技术上的突破,都显示了人工智能发展的迅猛势头。以下是精选的2017年人工智能领域最具影响力的事件。 ... [详细]
  • 本文深入探讨了CART(分类与回归树)的基本原理及其在随机森林中的应用。重点介绍了CART的分裂准则、防止过拟合的方法、处理样本不平衡的策略以及其在回归问题中的应用。此外,还详细解释了随机森林的构建过程、样本均衡处理、OOB估计及特征重要性的计算。 ... [详细]
  • Coursera ML 机器学习
    2019独角兽企业重金招聘Python工程师标准线性回归算法计算过程CostFunction梯度下降算法多变量回归![选择特征](https:static.oschina.n ... [详细]
  • 深入理解Java字符串池机制
    本文详细解析了Java中的字符串池(String Pool)机制,探讨其工作原理、实现方式及其对性能的影响。通过具体的代码示例和分析,帮助读者更好地理解和应用这一重要特性。 ... [详细]
  • 本文介绍如何从字符串中移除大写、小写、特殊、数字和非数字字符,并提供了多种编程语言的实现示例。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 本文介绍了如何利用Python的高精度计算库mpmath实现π的100种不同计算方法。通过设置更高的精度和优化的数学函数,这些方法能够提供极其精确的结果。 ... [详细]
  • vivo Y5s配备了联发科Helio P65八核处理器,这款处理器采用12纳米工艺制造,具备两颗高性能Cortex-A75核心和六颗高效能Cortex-A55核心。此外,它还集成了先进的图像处理单元和语音唤醒功能,为用户提供卓越的性能体验。 ... [详细]
  • 探讨ChatGPT在法律和版权方面的潜在风险及影响,分析其作为内容创造工具的合法性和合规性。 ... [详细]
  • 本文介绍了如何利用TensorFlow框架构建一个简单的非线性回归模型。通过生成200个随机数据点进行训练,模型能够学习并预测这些数据点的非线性关系。 ... [详细]
  • Jenkins 是持续集成和持续交付(CI/CD)领域中的领先平台,在全球范围内拥有广泛的用户基础。本文将介绍 Jenkins 在中国市场的最新举措,以及为促进中文用户社区发展所采取的具体行动。 ... [详细]
  • PC时代的传奇人物
    回顾过去几十年,个人电脑(PC)的发展历程犹如一部英雄史诗。每一位杰出人物都在这一领域留下了不可磨灭的印记,他们的贡献不仅推动了技术的进步,也深刻影响了现代社会的发展。 ... [详细]
author-avatar
杜庆坤66
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有