前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >逃不过呀!不论是训练还是部署都会让你踩坑的Batch Normalization

逃不过呀!不论是训练还是部署都会让你踩坑的Batch Normalization

原创
作者头像
老潘
修改2021-08-09 11:03:48
2K0
修改2021-08-09 11:03:48
举报

看完本文想与我交流请点这里~

简单的Batch Normalization

BN、Batch Normalization、批处理化层。

想必大家都不陌生。

BN是2015年论文Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift提出的一种**数据归一化方法**。现在也是大多数神经网络结构的**标配**,我们可能已经**熟悉的不能再熟悉了**。

简单回归一下BN层的作用:

BN层往往用在深度神经网络的卷积层之后、激活层之前。其作用可以**加快模型训练时的收敛速度**,使得模型训练过程**更加稳定**,避免梯度爆炸或者梯度消失。并且起到一定的**正则化**作用,几乎代替了Dropout。

借一下Pytorch官方文档中的BN公式,我们来回顾一下:

BatchNorm
BatchNorm

上述的式子很简单,无非就是减均值除方差(其实是标准差),然后乘以一个权重加上一个系数,其中权重和系数是**可以学习**的,在模型forward和backward的时候会进行更新。是不是很简单?

但BN层的作用和内部原理可能远远不止于此。可研究点还有很多,前一阵子facebook新出的论文Rethinking “Batch” in Batch Norm对BN层进行了一次新的解释。

BN存在信息泄露的问题
BN存在信息泄露的问题

借用陀飞轮兄的回答:

BN效果好是因为BN的存在会引入mini-batch内其他样本的信息,就会导致预测一个独立样本时,其他样本信息相当于正则项,使得loss曲面变得更加平滑,更容易找到最优解。相当于一次独立样本预测可以看多个样本,学到的特征泛化性更强,更加general。

BN层仍然有一些我们未知的特性待我们去发掘,不过**BatchNormalization的简单介绍先到这里**,接下来我们讨论下BN的细节以及会遇到的坑,不论是训练还是部署,如果对BN不熟悉,说不定哪天就会踩到坑里~

你懂得
你懂得

BN层都在哪里

因为BN层太**常见**了,以至于我们以为每个神经网络中可能都有BN层,但事实肯定不是这样。

除了BN层,还有GN(Group Normalization)、LN(Layer Normalization、IN(Instance Normalization)这些个标准化方法,每个标注化方法都适用于不同的任务。

各种标准化
各种标准化

举几个简单的应用场景:

  • ResNet、Resnext、Mobilenet等常见的backbone,使用的就是**BN**
  • Swin Transformer,使用了**Layer Normalization**
  • **Group Normalization**有时候会代替BN用在我们常见的网络中
  • **Instance Normalization**在Gan、风格迁移类模型中经常用到

上述是老潘见到过的一些例子,也算是抛砖引玉。这些不同的标准化方法,说白了就是**不同维度**的标准化,有的时候稍微改变一下代码就可以互相混用,不过本文的重点不在这里。

BN层都在这里

我们翻一翻常见的backbone的结构。可以看到在官方Pytorch的resnet.pyclass BasicBlock中,forward时的基本结构是Conv+BN+Relu:

代码语言:txt
复制
# 省略了一些地方

class BasicBlock(nn.Module):

    def \_\_init\_\_(self,...) -> None:

        ...

        self.conv1 = conv3x3(inplanes, planes, stride)

        self.bn1 = norm\_layer(planes)

        self.relu = nn.ReLU(inplace=True)

        self.conv2 = conv3x3(planes, planes)

        self.bn2 = norm\_layer(planes)

        self.downsample = downsample

        self.stride = stride



    def forward(self, x: Tensor) -> Tensor:

        identity = x

        # 常见的Conv+BN+Relu

        out = self.conv1(x)

        out = self.bn1(out)

        out = self.relu(out)

        # 又是Conv+BN+relu

        out = self.conv2(out)

        out = self.bn2(out)

        if self.downsample is not None:

            identity = self.downsample(x)

        out += identity

        out = self.relu(out)



        return out

resnet作为我们常见的**万年青backbone**不是没有理由的,**效果好速度快方便部署**。当然还有很多其他优秀的backbone,这些backbone的内部结构也多为Conv+BN+Relu或者Conv+BN的结构。

这种结构是很常见的。

常见的Conv+BN+Relu融合

既然这个融合已经很常见了,现在也基本是标配。一般网络中往往也有很多这样的结构,如果可以优化的话,岂不是可以实现加速?当然是可以的。

我们在训练模型的时候,网络结构都是按照Conv+BN+Relu这样的顺序搭建的,我们的数据也会一层一层从卷积到批处理化、从批处理化到激活层。嗯,这种很显而易见嘛。

但我们都知道BN层在**推理的时候**也只需要之前训练好**固定的参数**:均值${\hat{\sigma}}$、方差$\sigma_{2}$、权重$\gamma$以及偏置$\beta$:

那么,有没有办法将BN层的参数和前一层的卷积合并,这样BN层就可以功成名就了,之后的模型推理也就不再需要它了。

当然是可以的,假设上一层卷积的输出:$w*x+b$。

而BN层的输出公式可以转化为以下形式:

我们对于最后的w、x和偏置b,发现只需要将**卷积权重缩放一定倍数,并对偏置进行一定变化**,就可以将BN层的参数融合进Conv中了。这就相当于**两次线性变化**,两个线性变化是可以叠加融合的。

融合后的Conv+BN就相当于一个Conv了,因为大部分网络结构中Conv+BN这样的组合很多,所以一般来说仅仅是这个融合操作就可以使模型加速10%左右。

TensorRT中的融合

这种基本的优化方式TensorRT肯定是不会放过的,我们来看看TensorRT对BN层的处理:

TensorRT中CBR的融合
TensorRT中CBR的融合

这张图是官方介绍TensorRT优化的例子,很显然上述的3x3 conv + bias + relu被合并成了一个3x3 CBR,其中bias可以相当于bn(之后会介绍),我们简单看下Pytorch中inception的结构:

代码语言:txt
复制
def \_forward(self, x: Tensor) -> List[Tensor]:

    branch1x1 = self.branch1x1(x)



    branch5x5 = self.branch5x5\_1(x)

    branch5x5 = self.branch5x5\_2(branch5x5)



    branch3x3dbl = self.branch3x3dbl\_1(x)

    branch3x3dbl = self.branch3x3dbl\_2(branch3x3dbl)

    branch3x3dbl = self.branch3x3dbl\_3(branch3x3dbl)



    branch\_pool = F.avg\_pool2d(x, kernel\_size=3, stride=1, padding=1)

    branch\_pool = self.branch\_pool(branch\_pool)

    # 就是这里需要cat的四个分支

    outputs = [branch1x1, branch5x5, branch3x3dbl, branch\_pool]

    return outputs

其中每个最基本的CONV模块就是,显然还是一个CONV+BN的结构:

代码语言:txt
复制
class BasicConv2d(nn.Module):



  def \_\_init\_\_(

      self,

      in\_channels: int,

      out\_channels: int,

      \*\*kwargs: Any

  ) -> None:

      super(BasicConv2d, self).\_\_init\_\_()

      self.conv = nn.Conv2d(in\_channels, out\_channels, bias=False, \*\*kwargs)

      self.bn = nn.BatchNorm2d(out\_channels, eps=0.001)



  def forward(self, x: Tensor) -> Tensor:

      x = self.conv(x)

      x = self.bn(x)

      return F.relu(x, inplace=True)

在TensorRT中BN层相当于Scale级别的变化,为什么,回顾一下老潘介绍过的公式:

我们在利用TensorRT进行模型解析时,比如从ONNX中解析成TensorRT的网络结构,我们会提前对BN层的一些操作进行合并和融合。来看看ONNX-TensorRT是怎么做的吧:

代码语言:txt
复制
DEFINE\_BUILTIN\_OP\_IMPORTER(BatchNormalization)

{

    // ...省略部分代码

    // 从ONNX中BN层中会取到四个参数,分别是权重、偏置、mean和va

    const auto scale = inputs.at(1).weights();

    const auto bias = inputs.at(2).weights();

    const auto mean = inputs.at(3).weights();

    const auto variance = inputs.at(4).weights();

    // ...



    OnnxAttrs attrs(node, ctx);

    float eps = attrs.get<float>("epsilon", 1e-5f);



    // 在这里将以上四个参数合并为 最终的权重和偏置

    const int32\_t nbChannels = scale.shape.d[0];

    auto combinedScale = ctx->createTempWeights(scale.type, scale.shape);

    auto combinedBias = ctx->createTempWeights(bias.type, bias.shape);

    for (int32\_t i = 0; i < nbChannels; ++i)

    {

        combinedScale.at<float>(i) = scale.at<float>(i) / sqrtf(variance.at<float>(i) + eps);

        combinedBias.at<float>(i) = bias.at<float>(i) - mean.at<float>(i) \* combinedScale.at<float>(i);

    }

    // 这里将合并后的权重和偏置 组合为一个scale层

    return scaleHelper(ctx, node, \*tensorPtr, nvinfer1::ScaleMode::kCHANNEL, combinedBias, combinedScale,

        ShapedWeights::empty(scale.type), bias.getName(), scale.getName());

}

通过TensorRT的前端解释器解释后,TensorRT会将BN层视为一个简单的Scale层(通过addScaleNd构建),之后的优化中会根据情况与该层前面的CONV层合并:

Convolution and Scale

A Convolution layer followed by a Scale layer that is kUNIFORM or kCHANNEL can be fused into a single convolution by adjusting the convolution weights. This fusion is disabled if the scale has a non-constant power parameter.

RepVGG中融合方式

当然很多其他地方也可以实现相应的操作,最简单的我们可以直接在Pytorch模型中通过修改.py文件实现这样的操作,这样我们在推理的时候就会比在训练中快一些,在repvgg中也有类似的融合思想,这里就不详细描述了:

代码语言:txt
复制
# https://github.com/DingXiaoH/RepVGG/blob/main/repvgg.py

# repvgg中bn层的融合方式

def \_fuse\_bn\_tensor(self, branch):

    if branch is None:

        return 0, 0

    if isinstance(branch, nn.Sequential):

        kernel = branch.conv.weight

        running\_mean = branch.bn.running\_mean

        running\_var = branch.bn.running\_va

        gamma = branch.bn.weight

        beta = branch.bn.bias

        eps = branch.bn.eps

    else:

        assert isinstance(branch, nn.BatchNorm2d)

        if not hasattr(self, 'id\_tensor'):

            input\_dim = self.in\_channels // self.groups

            kernel\_value = np.zeros((self.in\_channels, input\_dim, 3, 3), dtype=np.float32)

            for i in range(self.in\_channels):

                kernel\_value[i, i % input\_dim, 1, 1] = 1

            self.id\_tensor = torch.from\_numpy(kernel\_value).to(branch.weight.device)

        kernel = self.id\_tenso

        running\_mean = branch.running\_mean

        running\_var = branch.running\_va

        gamma = branch.weight

        beta = branch.bias

        eps = branch.eps

    std = (running\_var + eps).sqrt()

    t = (gamma / std).reshape(-1, 1, 1, 1)

    return kernel \* t, beta - running\_mean \* gamma / std

有坑的优化

上述我们聊了BN层的基本优化策略,那么这种优化我们在任何时候都可以使用吗?当然不是!

大多数的CONV+BN优化都是无害的,既可以提升模型的速度也不会影响模型的**正常推理**。但有时候这个优化是有害的!在某些场景下**这样优化会严重影响模型的结果**。比如**Pix2Pix-GAN**这类模型,也就是风格迁移或者GAN等图像生成任务场景下,如果无脑使用了这种优化,可能会使模型产出错误的结果。

当然其他场景下也可能有问题,这种问题更容易出现在像素级别预测的模型(分割、GAN、风格迁移之类),相信也有很多的同学遇到过这样的问题,在Pytorch中,会发现model.eval()model.train()的结果差异很大,这是为什么呢?

有一种原因可能是因为我们在训练时候和推理时候,数据的均值和方差差异较大。或者说你训练的时候batchsize比较小,无法较好统计整体训练数据的整体mean和std,具体原因老潘也不确定,有相同遭遇的同学们不妨说下~

回到BN,刚才介绍的BN中的四个参数均值${\hat{\sigma}}$、方差$\sigma_{2}$、权重$\gamma$以及偏置$\beta$,其中**均值和方差**在推理过程中可以是动态也可以是固定。

如果是动态的,也就是我们在推理的时候也会实时计算当前输入batch(推理的时候batch往往为1)数据的均值方差,然后执行BN操作;如果是固定的,则会使用训练过程中更新好的均值方差进行计算,此时均值方差是固定参数不会变。

而我们在优化BN的时候,通常就是将固定好的这四个参数与上一层卷积融合,这样就相当于将**BN层置于推理模式**。mean和std当前是固定死了,这时候就会出现上述的问题。

那么怎么解决呢?在解决之前,我想分析一下Pytorch关于BN的源码,如果不想看源码分析的**直接看最后的结论**就好。

探索一下Pytorch中BN层源码

就这个问题来说,为什么train和eval会对模型性能产生差异,我们看Pytorch的BN层是怎么实现的。注意~这部分在面试中**要考**。

首先看一下Pytorch中的\_NormBase实现,之后Pytorch的具体BN层是继承这个类的。

我们可以看到默认affinetrack\_running\_stats都是开启的,也就是我们平时在使用BN层的时候,权重、偏置、running\_meanrunning\_var都是随着模型训练时候随时更新的。其中权重偏置是随着训练反向梯度的时候会进行更新,而running\_meanrunning\_var则是buffer类型数据,可以在模型推理的时候设置是否需要更新。

代码语言:txt
复制
class \_NormBase(Module):

    def \_\_init\_\_(

        self,

        num\_features: int,

        eps: float = 1e-5,

        momentum: float = 0.1,

        affine: bool = True,

        track\_running\_stats: bool = True

    ) -> None:

        ...

        if self.affine:

            self.weight = Parameter(torch.Tensor(num\_features))

            self.bias = Parameter(torch.Tensor(num\_features))

        else:

            self.register\_parameter('weight', None)

            self.register\_parameter('bias', None)

        # 如果关闭self.track\_running\_stats选项,就不会储存并更新running\_mean和running\_var这两个参数

        if self.track\_running\_stats:

            self.register\_buffer('running\_mean', torch.zeros(num\_features))

            self.register\_buffer('running\_var', torch.ones(num\_features))

            self.register\_buffer('num\_batches\_tracked', torch.tensor(0, dtype=torch.long))

        else:

            self.register\_parameter('running\_mean', None)

            self.register\_parameter('running\_var', None)

            self.register\_parameter('num\_batches\_tracked', None)

        self.reset\_parameters()

再看\_BatchNorm的实现,而我们在平常搭建网络时使用的BatchNorm2d就是继承了它。Pytorch的Python端BN层核心的实现都在\_BatchNorm这里了,BatchNorm2d仅仅是做了一下接口检查。

代码语言:txt
复制
class \_BatchNorm(\_NormBase):



    def \_\_init\_\_(self, num\_features, eps=1e-5, momentum=0.1, affine=True,

                 track\_running\_stats=True):

        super(\_BatchNorm, self).\_\_init\_\_(

            num\_features, eps, momentum, affine, track\_running\_stats)



    def forward(self, input: Tensor) -> Tensor:

        self.\_check\_input\_dim(input)

        ... 



        if self.training and self.track\_running\_stats:

            # TODO: if statement only here to tell the jit to skip emitting this when it is None

            if self.num\_batches\_tracked is not None:  # type: ignore

                self.num\_batches\_tracked = self.num\_batches\_tracked + 1  # type: ignore

                if self.momentum is None:  # use cumulative moving average

                    exponential\_average\_factor = 1.0 / float(self.num\_batches\_tracked)

                else:  # use exponential moving average

                    exponential\_average\_factor = self.momentum

        # 需要注意这里,bn\_training这个参数会传递到底层的C++实现端,通过这个参数在C++端决定是否更新mean和std

        if self.training:

            bn\_training = True

        else:

            bn\_training = (self.running\_mean is None) and (self.running\_var is None)



        r"""

        Buffers are only updated if they are to be tracked and we are in training mode. Thus they only need to be

        passed when the update should occur (i.e. in training mode when they are tracked), or when buffer stats are

        used for normalization (i.e. in eval mode when buffers are not None).

        """

        assert self.running\_mean is None or isinstance(self.running\_mean, torch.Tensor)

        assert self.running\_var is None or isinstance(self.running\_var, torch.Tensor)

        return F.batch\_norm(

            input,

            # If buffers are not to be tracked, ensure that they won't be updated

            self.running\_mean if not self.training or self.track\_running\_stats else None,

            self.running\_var if not self.training or self.track\_running\_stats else None,

            self.weight, self.bias, bn\_training, exponential\_average\_factor, self.eps)

需要注意的,上述Pytorch前端BN代码中的bn\_training会传递到C++的底层实现,而BN的C++底层实现会根据这个布尔变量决定是否实时计算mean和std。

bn\_training这个参数并不是一定由**模型在训练状态决定**的参数,如果BN层中没有初始化self.running\_meanself.running\_var,也就是我们一开始初始化BN层的时候,关闭了track\_running\_stats这个参数,那么这个BN层是不会在训练过程中记录self.running\_meanself.running\_var,而是实时计算。

再看Pytorch的C++源码

Pytorch中底层C++BN层的具体实现代码在/pytorch/aten/src/ATen/native/Normalization.cpp中,这里不涉及到BN的反向传播,我们先看BN的前向处理过程。

为了方便理解,我们阅读的是**CPU版本**的实现(GPU版本与CPU的原理是相同的)。

代码语言:txt
复制
std::tuple<Tensor, Tensor, Tensor> batch\_norm\_cpu(const Tensor& self, const c10::optional<Tensor>& weight\_opt, const c10::optional<Tensor>& bias\_opt, const c10::optional<Tensor>& running\_mean\_opt, const c10::optional<Tensor>& running\_var\_opt,

                                                  bool train, double momentum, double eps) {

  // See [Note: hacky wrapper removal for optional tensor]

  const Tensor& weight = c10::value\_or\_else(weight\_opt, [] {return Tensor();});

  const Tensor& bias = c10::value\_or\_else(bias\_opt, [] {return Tensor();});

  const Tensor& running\_mean = c10::value\_or\_else(running\_mean\_opt, [] {return Tensor();});

  const Tensor& running\_var = c10::value\_or\_else(running\_var\_opt, [] {return Tensor();});



  checkBackend("batch\_norm\_cpu", {self, weight, bias, running\_mean, running\_var}, Backend::CPU);



  return AT\_DISPATCH\_FLOATING\_TYPES(self.scalar\_type(), "batch\_norm", [&] {

      if (!train) {

        return batch\_norm\_cpu\_transform\_input\_template<scalar\_t>(self, weight, bias, {}, {}, running\_mean, running\_var, train, eps);

      } else {

        // 可以看到这里,如果是传递过来的train是true的话,首先会根据当前数据动态更新一波`running\_mean`和`running\_var`

        auto save\_stats = batch\_norm\_cpu\_update\_stats\_template<scalar\_t, InvStd>(self, running\_mean, running\_var, momentum, eps);

        return batch\_norm\_cpu\_transform\_input\_template<scalar\_t>(self, weight, bias, std::get<0>(save\_stats), std::get<1>(save\_stats), running\_mean, running\_var, train, eps);

      }

    });

}

batch\_norm\_cpu\_update\_stats\_template做了啥?就是更新一下当前输入batch数据的均值和方差。

代码语言:txt
复制
template<typename scalar\_t, template<typename T> class VarTransform>

std::tuple<Tensor,Tensor> batch\_norm\_cpu\_update\_stats\_template(

    const Tensor& input, const Tensor& running\_mean, const Tensor& running\_var,

    double momentum, double eps) {



  using accscalar\_t = at::acc\_type<scalar\_t, false>;

  // 计算channel维度

  int64\_t n\_input = input.size(1);

  int64\_t n = input.numel() / n\_input;



  Tensor save\_mean = at::empty({n\_input}, input.options());

  Tensor save\_var\_transform = at::empty({n\_input}, input.options());

  auto save\_mean\_a = save\_mean.accessor<scalar\_t, 1>();

  auto save\_var\_transform\_a = save\_var\_transform.accessor<scalar\_t, 1>();

  // 得到running\_mean\_a 和 running\_var\_a

  auto running\_mean\_a = conditional\_accessor\_1d<scalar\_t>(running\_mean);

  auto running\_var\_a = conditional\_accessor\_1d<scalar\_t>(running\_var);



  parallel\_for(0, n\_input, 1, [&](int64\_t b\_begin, int64\_t b\_end) {

    for (int64\_t f = b\_begin; f < b\_end; ++f) {

      Tensor in = input.select(1, f);



      // compute mean per input

      auto iter = TensorIteratorConfig()

        .add\_input(in)

        .build();

      accscalar\_t sum = 0;

      cpu\_serial\_kernel(iter, [&](const scalar\_t i) -> void {

        sum += i;

      });

      scalar\_t mean = sum / n;

      save\_mean\_a[f] = mean;



      // compute variance per input

      accscalar\_t var\_sum = 0;

      iter = TensorIteratorConfig()

        .add\_input(in)

        .build();

      cpu\_serial\_kernel(iter, [&](const scalar\_t i) -> void {

        var\_sum += (i - mean) \* (i - mean);

      });

      save\_var\_transform\_a[f] = VarTransform<accscalar\_t>{}(var\_sum / n, eps);

      // 更新运行中的mean和var 状态

      if (running\_mean.defined()) {

        running\_mean\_a[f] = momentum \* mean + (1 - momentum) \* running\_mean\_a[f];

      }

      if (running\_var.defined()) {

        accscalar\_t unbiased\_var = var\_sum / (n - 1);

        running\_var\_a[f] = momentum \* unbiased\_var + (1 - momentum) \* running\_var\_a[f];

      }

    }

  });

  return std::make\_tuple(save\_mean, save\_var\_transform);

}

batch\_norm\_cpu\_transform\_input\_template就是具体的BN层实现:

代码语言:txt
复制
template<typename scalar\_t>

std::tuple<Tensor,Tensor,Tensor> batch\_norm\_cpu\_transform\_input\_template(

    const Tensor& input, const Tensor& weight, const Tensor& bias,

    const Tensor& save\_mean /\* optional \*/, const Tensor& save\_invstd /\* optional \*/,

    const Tensor& running\_mean /\* optional \*/, const Tensor& running\_var /\* optional \*/,

    bool train, double eps) {

  ...

  Tensor output = at::empty\_like(input, LEGACY\_CONTIGUOUS\_MEMORY\_FORMAT);



  int64\_t n\_input = input.size(1);



  auto save\_mean\_a = conditional\_accessor\_1d<scalar\_t>(save\_mean);

  auto save\_invstd\_a = conditional\_accessor\_1d<scalar\_t>(save\_invstd);



  auto running\_mean\_a = conditional\_accessor\_1d<scalar\_t>(running\_mean);

  auto running\_var\_a = conditional\_accessor\_1d<scalar\_t>(running\_var);



  parallel\_for(0, n\_input, 1, [&](int64\_t b\_begin, int64\_t b\_end) {

    for (int64\_t f = b\_begin; f < b\_end; ++f) {

      Tensor in = input.select(1, f);

      Tensor out = output.select(1, f);



      scalar\_t mean, invstd;

      // 根据是否是训练模式,决定是否使用实时更新的mean和std

      if (train) {

        mean = save\_mean\_a[f];

        invstd = save\_invstd\_a[f];

      } else {

        mean = running\_mean\_a[f];

        invstd = 1 / std::sqrt(running\_var\_a[f] + eps);

      }



      // compute output

      scalar\_t w = weight.defined() ? weight.data\_ptr<scalar\_t>()[f \* weight.stride(0)] : 1;

      scalar\_t b = bias.defined() ? bias.data\_ptr<scalar\_t>()[f \* bias.stride(0)] : 0;



      auto iter = TensorIterator::unary\_op(out, in);

      cpu\_serial\_kernel(iter, [=](const scalar\_t i) -> scalar\_t {

        return ((i - mean) \* invstd) \* w + b;

      });

    }

  });

  return std::make\_tuple(output, save\_mean, save\_invstd);

}

总结下Pytorch中的BN行为

总结一下,Pytorch中的BN有两种处理running\_meanrunning\_var的方式:

  • 默认情况下是开启的,这两个参数会注册存在,也可以被更新,当train设置为false则不会更新
  • 可以初始化的时候关闭,此时不管是推理还是训练,都会重新计算一遍mean和std

而Pytorch中BN的train与eval的区别,则是train是否设置为True,传入C++中即bn\_training是否为True,这个参数会决定BN层是否实时更新mean和std。

最终解决方案

知道问题出在哪里了,那我们该怎么办呢?

如果模型只是需要运行在Pytorch端,那么只需要在模型推理时候加上model.train()即可,但如果该模型需要**转化为TensorRT**或者其他推理框架,我们该怎么办?这里有两种方法:

  • 一种方法是重新训练模型,可以在训练的时候冻住BN层,防止其更新mean和std,强制模型使用固定的mean和std进行训练;
  • 另一种当然是修改转换端了,我们这里修改下TensorRT的BN实现

既然通过**ONNX-TensorRT**的方式走不通,我们可以换另一种转换方式:

Torch2trt是通过映射**Pytorch的op到TensorRT**中,这样我们就可以实现一个推理版的TrainBatchNorm2d,然后让这个解释器按照相应的op来转换到TensorRT端:

代码语言:txt
复制
# 这里我们实现了一个纯python版的BN

class TrainBatchNorm2d(torch.nn.BatchNorm2d):

    def \_\_init\_\_(self, num\_features, eps=1e-5, momentum=0.1,

                 affine=True, track\_running\_stats=True):

        super(TrainBatchNorm2d, self).\_\_init\_\_(

            num\_features, eps, momentum, affine, track\_running\_stats)



    def forward(self, input):



        exponential\_average\_factor = self.momentum

        # 注意,这里的mean和var是实时计算的

        mean = input.mean([0, 2, 3])

        var = input.std([0, 2, 3], unbiased=False)

        var = torch.pow(var,2)

        n = input.numel() / input.size(1)

        input = (input - mean[None, :, None, None]) / (torch.sqrt(var[None, :, None, None] + self.eps))

        if self.affine:

            input = input \* self.weight[None, :, None, None] + self.bias[None, :, None, None]



        return input

这样写好之后,模型顺利转换为TensorRT而且结果也正常了,但是速度缺很慢...dump了一下每层的耗时,发现耗时卡在mean = input.mean([0, 2, 3])这一步,网上简单搜了一下有没有和我们遇到相同问题的,还真有:https://forums.developer.nvidia.com/t/tensorrt-custom-roialign-plugin-is-very-slow/113150/5

这貌似是TensorRT的一个bug,在某种情况下,这种计算方式会导致GPU和CPU中的数据传输很慢很慢,具体原因这里先不展开(其实我也不知道为啥,但就是不行)。我们先尝试下有没有其他路子。

究极无敌替换大法,我们可以将mean = input.mean([0, 2, 3])等价替换为mean = input.mean(3).mean(2).mean(0)这样可以避免多维度的同时处理。至于std的话,我们不能直接std = input.std(3).std(2).std(0),但是因为BN只是对H、W和N维度上进行均值方差,我们可以这样:

  • 首先将其以channel打成二维input\_hw\_flatten = input.view(input.size(1),-1)
  • 然后对二维input\_hw\_flatten的第二维进行方差计算

即可,整体来说就是个等价替换:

代码语言:txt
复制
class TrainedBatchNorm2d(torch.nn.BatchNorm2d):

    def \_\_init\_\_(self, num\_features, eps=1e-5, momentum=0.1,

                 affine=True, track\_running\_stats=True):

        super(TrainBatchNorm2d, self).\_\_init\_\_(

            num\_features, eps, momentum, affine, track\_running\_stats)



    def forward(self, input):



        mean = input.mean(3).mean(2).mean(0)

        input\_hw\_flatten = input.view(input.size(1),-1)

        var = input\_hw\_flatten.std(1, unbiased=False)



        input = input - mean[None, :, None, None]

        input = input / (var[None, :, None, None] + self.eps)

        

        if self.affine:

            input = input \* self.weight[None, :, None, None] + self.bias[None, :, None, None]



        return input

咦,这样貌似就没问题了,模型转换成功,BN层不再依赖固定的meanstd,而且速度也正常了。

解决!

后记

BN层很简单,但是其中的一些隐含的信息却不得不值得我们去重视。毕竟细节决定成败呀。

老潘来北京也快一年了,想想时间过得真是快,上半年也很快过去了,下半年也即将开始。回顾之前,自己不足的地方仍有很多,想做的事情也有很多,想再提升、充实下自己,也希望和各位共勉。

我是老潘,我们下期见~

参考资料

https://blog.csdn.net/weixin_39580564/article/details/110518533

https://blog.csdn.net/Cxiazaiyu/article/details/81838306#commentBox

https://discuss.pytorch.org/t/what-num-batches-tracked-in-the-new-bn-is-for/27097

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简单的Batch Normalization
  • BN层都在哪里
    • BN层都在这里
    • 常见的Conv+BN+Relu融合
      • TensorRT中的融合
        • RepVGG中融合方式
        • 有坑的优化
        • 探索一下Pytorch中BN层源码
          • 再看Pytorch的C++源码
            • 总结下Pytorch中的BN行为
            • 最终解决方案
            • 后记
            • 参考资料
            相关产品与服务
            流计算 Oceanus
            流计算 Oceanus 是大数据产品生态体系的实时化分析利器,是基于 Apache Flink 构建的企业级实时大数据分析平台,具备一站开发、无缝连接、亚秒延时、低廉成本、安全稳定等特点。流计算 Oceanus 以实现企业数据价值最大化为目标,加速企业实时化数字化的建设进程。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档