目标检测 YOLOv5 - 卷积层和BN层的融合

目标检测 YOLOv5 - 卷积层和BN层的融合

即Conv2d和 BatchNorm2d融合

flyfish

为了减少模型推理时间,YOLOv5源码中attempt_load已经包括两层的合并,主要用在推理和导出模型提供给其他平台推理使用时。

函数调用链条是

attempt_load -》fuse-》fuse_conv_and_bn

fuse_conv_and_bn函数就是Conv2d层和BatchNorm2d层的合并。

在模型训练完成后,YOLOv5在推理阶段和导出模型时,将卷积层和BN层进行融合。

融合过程

(1)卷积层公式简写如下:
x i = w ∗ x i − 1 + b x_{i} = w * x_{i-1} + b xi=wxi1+b

(2)Batch Normalization层公式简写如下
这里简单复述稍微详细一点看介绍BatchNorm2d
μ B ← 1 m ∑ i = 1 m x i \mu_{\mathcal{B}} \leftarrow \frac{1}{m} \sum_{i=1}^m x_i μBm1i=1mxi
σ B 2 ← 1 m ∑ i = 1 m ( x i − μ B ) 2 \sigma_{\mathcal{B}}^2 \leftarrow \frac{1}{m} \sum_{i=1}^m (x_i - \mu_{\mathcal{B}})^2 σB2m1i=1m(xiμB)2
x ^ i ← x i − μ B σ B 2 + ϵ \hat{x}_i \leftarrow \dfrac{x_i-\mu_{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}^2+\epsilon}} x^iσB2+ϵ xiμB
在这里插入图片描述

y i = x i − μ σ 2 + ϵ ⋅ γ + β y_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta yi=σ2+ϵ xiμγ+β

4个参数

γ \gamma γ β \beta β 在训练时,是可学习参数。
推理阶段均值和方差是整个训练集的均值和方差。假如硬件资源无限均值和方差训练时全部储存,最后计算推理时所用的均值和方差。硬件资源有限,训练时mini-batch的均值和方差不需要全部储存,太多了不好算,是通过滑动平均计算出整个训练集的均值和方差 。
训练完成后这四个值都是固定值即常量。
(3)将卷积层的式子带入到 BN 层的式子就是融合
y i = x i − μ σ 2 + ϵ ⋅ γ + β = w ∗ x i + b − μ σ 2 + ϵ ⋅ γ + β = w ∗ γ σ 2 + ϵ ⋅ x + ( b − μ σ 2 + ϵ ⋅ γ + β ) \begin{aligned} &y_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta \\ &= \frac{w * x_i + b - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta \\ &= \frac{w * \gamma}{\sqrt{\sigma^2 + \epsilon}} \cdot x + (\frac{b-\mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta) \end{aligned} yi=σ2+ϵ xiμγ+β=σ2+ϵ wxi+bμγ+β=σ2+ϵ wγx+(σ2+ϵ bμγ+β)

融合后

上述式子可以简单看成直线方程
y = k x + b y=kx+b y=kx+b为了防止跟上面式子字母重复换一个字母
y = k 1 x + k 2 y=k_1x+k_2 y=k1x+k2
k 1 = w ∗ γ σ 2 + ϵ k_1= \frac{w * \gamma}{\sqrt{\sigma^2 + \epsilon}} k1=σ2+ϵ wγ
k 2 = b − μ σ 2 + ϵ + β k_2= \frac{b-\mu}{\sqrt{\sigma^2 + \epsilon}} + \beta k2=σ2+ϵ bμ+β
w n e w = w ∗ γ σ 2 + ϵ b n e w = ( b − μ σ 2 + ϵ ⋅ γ + β ) \begin{aligned} & w_{new} = \frac{w * \gamma}{\sqrt{\sigma^2 + \epsilon}} \\ & b_{new} = (\frac{b-\mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta) \end{aligned} wnew=σ2+ϵ wγbnew=(σ2+ϵ bμγ+β)

YOLOv5版本的融合代码实现

import torch
import torchvision
def fuse_conv_and_bn(conv, bn):
	#
	# init
	fusedconv = torch.nn.Conv2d(
		conv.in_channels,
		conv.out_channels,
		kernel_size=conv.kernel_size,
		stride=conv.stride,
		padding=conv.padding,
		bias=True
	)
	#
	# prepare filters
	print("conv.out_channels:",conv.out_channels)
	w_conv = conv.weight.clone().view(conv.out_channels, -1)
	print("w_conv:",w_conv.shape)
	w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps+bn.running_var)))
	print("w_bn:", w_bn.shape)
	fusedconv.weight.copy_( torch.mm(w_bn, w_conv).view(fusedconv.weight.size()) )
	#
	# prepare spatial bias
	if conv.bias is not None:
		b_conv = conv.bias
	else:
		b_conv = torch.zeros( conv.weight.size(0) )
	b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))
	fusedconv.bias.copy_( b_conv + b_bn )
	#
	# we're done
	return fusedconv

#以下代码片段在ResNet18的前两层测试上述函数:

测试代码

torch.set_grad_enabled(False)
x = torch.randn(16, 3, 256, 256)
rn18 = torchvision.models.resnet18(pretrained=True)
rn18.eval()
net = torch.nn.Sequential(
	rn18.conv1,
	rn18.bn1
)
y1 = net.forward(x)
fusedconv = fuse_conv_and_bn(net[0], net[1])
y2 = fusedconv.forward(x)
d = (y1 - y2).norm().div(y1.norm()).item()
print("error: %.8f" % d)

测试结果

conv.out_channels: 64
w_conv: torch.Size([64, 147])
w_bn: torch.Size([64, 64])
error: 0.00000030

PyTorch版本的融合代码实现

import copy
import torch

def fuse_conv_bn_eval(conv, bn):
    assert(not (conv.training or bn.training)), "Fusion only for eval!"
    fused_conv = copy.deepcopy(conv)

    fused_conv.weight, fused_conv.bias = \
        fuse_conv_bn_weights(fused_conv.weight, fused_conv.bias,
                             bn.running_mean, bn.running_var, bn.eps, bn.weight, bn.bias)

    return fused_conv

def fuse_conv_bn_weights(conv_w, conv_b, bn_rm, bn_rv, bn_eps, bn_w, bn_b):
    if conv_b is None:
        conv_b = torch.zeros_like(bn_rm)
    if bn_w is None:
        bn_w = torch.ones_like(bn_rm)
    if bn_b is None:
        bn_b = torch.zeros_like(bn_rm)
    bn_var_rsqrt = torch.rsqrt(bn_rv + bn_eps)

    conv_w = conv_w * (bn_w * bn_var_rsqrt).reshape([-1] + [1] * (len(conv_w.shape) - 1))
    conv_b = (conv_b - bn_rm) * bn_var_rsqrt * bn_w + bn_b

    return torch.nn.Parameter(conv_w), torch.nn.Parameter(conv_b)

我们自己添加如下代码测试

torch.set_grad_enabled(False)
x = torch.randn(16, 3, 256, 256)
rn18 = torchvision.models.resnet18(pretrained=True)
rn18.eval()
net = torch.nn.Sequential(
	rn18.conv1,
	rn18.bn1
)
y1 = net.forward(x)
fusedconv = fuse_conv_bn_eval(net[0], net[1])
y2 = fusedconv.forward(x)
d = (y1 - y2).norm().div(y1.norm()).item()
print("error: %.8f" % d)

输出结果

error: 0.00000030

YOLOv5版本的两层融合与 PyTorch提供的两层融合结果是一模一样的。

阅读此文章的人还阅读了以下内容
目标检测 YOLOv5 - 卷积层和BN层的融合
目标检测 YOLOv5 - Sample Assignment
目标检测 YOLOv5 - 数据增强
目标检测 YOLOv5 - 学习率
目标检测 YOLOv5 - 多机多卡训练
目标检测 YOLOv5 - 浮点取模
目标检测 YOLOv5 - 在多类别中应用NMS(非极大值抑制)
目标检测 YOLOv5 - loss for objectness and classification
目标检测 YOLOv5 - loss for bounding box regression
目标检测 YOLOv5 - 指标计算
目标检测 YOLOv5 - anchor设置
目标检测 YOLOv5 - SPP模块
目标检测 YOLOv5 - 边框预测(bounding box prediction)
目标检测 YOLOv5 - 自定义网络结构(YOLOv5-ShuffleNetV2)
目标检测 YOLOv5 - 常见的边框(bounding box )坐标表示方法
目标检测 YOLOv5 - 图像大小与loss权重的关系
目标检测 YOLOv5 - 根据配置改变网络的深度和宽度
目标检测 YOLOv5 - 转ncnn移动端部署
目标检测 YOLOv5 - Backbone中的Focus
目标检测 YOLOv5 - 模型训练、推理、导出命令
目标检测 YOLOv5 - 人脸数据集widerface转YOLOv5格式
目标检测 YOLOv5 - 使用的数据集格式
目标检测 YOLOv5 - 中使用COCO数据集
目标检测 YOLOv5 - CrowdHuman数据集格式转YOLOv5格式

  • 17
    点赞
  • 134
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西笑生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值