1. Batch Normalization介绍和原理
1.1 Batch Normalization的介绍和作用
Batch Normalization 是由2015年 《Batch Normalization: Accelerating Deep Network Training by Reducing
Internal Covariate Shift》提出,从标题就知道Batch Normalization通过减少内部协变量偏移,以提高深度学习网络训练速度和稳定性。
内部协变量偏移是指在训练过程中,网络各层的输入数据的分布会随着前一层参数的更新而改变,数值上的波动会比较大,从而需要更小的学习率和仔细的参数初始化。 所以在数据输入到下一层卷积层之前先对数据进行归一化操作以维持数据分布相对稳定, 网络结构的设计就有如下两种方式:
方案一 | 方案二 |
---|---|
Conv2d | Conv2d |
BN | activation |
activation | BN |
Conv2d | Conv2d |
实际上比较常见的是方案一BN放在activation之前,也是原文中推荐的做法。主要原因是
- 将BN放在激活函数之前可以确保激活函数的输入是规范化的,有助于防止激活函数输出的过饱和,特别是sigmoid和Tanh这类过饱和的激活函数。
- 激活函数一般具有非线性特性如ReLu,如果先进行激活再进行BN, 非线性的变换可能会破坏通过BN实现的规范会效果。
Question 1: 什么是过饱和,会产生什么后果?
Answer 1: 过饱和是指在神经网络中,对于某些激活函数而言(如sigmoid, Tanh), 如果它们的输入非常大或者非常小,会导致输出值趋于一个常数,且其导数或者梯度趋近于零的现象,从而引发梯度消失的现象。BN能将激活函数的输入数据分布强行拉回正态分布,防止梯度消失,加快模型训练
Sigmoid | Tanh |
---|---|
σ ( x ) = 1 1 + e − 1 \sigma(x) = \frac{1}{1 + e^{-1}} σ(x)=1+e−11 | tanh ( x ) = e x − e − x e x + e − x \tanh(x) = \frac{e^x - e^{-x}} {e^x + e^{-x}} tanh(x)=ex+e−xex−e−x |
Question 2: 激活函数ReLu建议放在BN之前吗?
Answer 2: 不建议。ReLu对输入数据进行了非线性变化,将所有负值置为0,保留正值,改变了数据的分布。因为BN设计的目的是规范化前一层的输出,将输出规范到均值接近0,标准差接近1的分布,但由于非线性变换已经改变了数据的分布特性,BN 可能无法有效地将数据规范化到预期的分布。
BN 带来的好处有
- 防止过拟合:单个样本的输出依赖于整个 mini-batch,防止对某个样本过拟合;
- 加快收敛:梯度下降过程中,每一层的weight和bias都会不断变化,导致输出结果的分布在不断变化,后层网络就要不停地去适应这种分布变化。用BN 后,可以使每一层输入的分布近似不变。
- 防止梯度消失:使得激活函数的输入不会进行过饱和区。导致梯度很小
1.2 BatchNorm的原理
BatchNorm的本质是归一化操作,对除了channel维度其他所有维度的归一化,假设x的shape为[B, C, H, W]
BatchNorm 表示batch 方向做归一化,算 N ∗ H ∗ W 的均值
LayerNorm 表示Channel方向做归一化,算C * H * W的均值
InstanceNorm:一个 channel 内做归一化,算 H ∗ W 的均值
GroupNorm: 将Channel方向分group, 然后每个group做归一化,算(C / G)* H* W的均值
对于每个
x
i
x_i
xi, shape为[B, 1, H, W], BatchNorm的数学公式为:
y
i
=
x
i
−
E
[
x
i
]
V
a
r
[
x
i
]
+
ϵ
∗
γ
+
β
y_i = \frac{x_i - E[x_i]}{\sqrt{Var[x_i] + \epsilon}} * \gamma + \beta
yi=Var[xi]+ϵxi−E[xi]∗γ+β, 其中缩放因子
γ
\gamma
γ和平移因子
β
\beta
β均为引入的可学习参数,作者在文章中解释了它们的作用:
1. 直接Normalize到
N
(
u
,
σ
)
N(u, \sigma)
N(u,σ)会导致新的分布丧失了从前层传递过来的特征与知识
2. 以sigmoid为例,加入
γ
和
β
\gamma和\beta
γ和β可以防止大部分值落在近似线性的中间部分,导致无法利用非线性的部分
2. Batch Normalization代码解析
import torch
from torch import nn
# function used to caculate y, running_mean, running_var
def batch_norm(x, gamma, beta, running_mean, running_var, eps, momentum, is_training=True):
if not is_training:
x_hat = (x - running_mean) / torch.sqrt(running_var + eps)
else:
assert x.shape in (2, 4)
# 求 mean 和 var
if x.shape == 2: # 对应的是nn.BatchNorm1d
mean = x.mean(dim=0, keep_dim=True)
var = ((x - mean) ** 2).mean(dim=0,keep_dim=True)
# 对应的是nn.BatchNorm2d
else:
mean = x.mean(dim=0, keep_dim=True).mean(dim=2, keep_dim=True).mean(dim=3, keep_dim=True)
var = ((x - mean)**2).(dim=0, keep_dim=True).mean(dim=2, keep_dim=True).mean(dim=3, keep_dim=True)
# 更新 running_mean 和 running_var
running_mean = momentum * running_mean + (1 - mometum) * mean
running_var = momentum * running_var + (1 - momentum) * var
x_hat = (x - mean) / torch.sqrt(var + eps)
y = x_hat * gamma + beta
return y, running_mean, running_var
class BatchNorm(nn.module):
def __init__(self, num_features, num_dims):
super().__init__()
assert num_dims in (2, 4)
if num_dims == 2: # nn.BatchNorm1d
shape = (1, num_features)
else: # nn.BatchNorm2d
shape = (1, num_features, 1, 1)
# init learnable parameter gamma and beta; globel mean and var
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
self.running_mean = torch.zeros(shape)
self.running_var = torch.ones(shape)
def foward(self, x, is_training=True):
if self.running_mean.device != x.device:
self.running_mean = self.running_mean.to(x.device)
self.running_var = self.running_var.to(x.device)
y, self.running_mean, self.running_var = batch_norm(x, self.gamma, self.beta, self.running_mean, self.running_var, eps=1e-5, momentum=0.9, is_training=True)
return y
def demo():
x = torch.Tensor([[1, 2, 3], [4, 5, 6]])
batch_norm_1d = BatchNorm(3, 2)
y = batch_norm_1d(x)
return y
3. Synchronized Batch Normalization
上面的BN通常是针对单卡情况下的运算。我们可以知道BN的步骤是计算每个batch的均值和方差,然后进行规范化操作。意味着假设每个batch的数据分布近似于整个数据集的数据分布。在实际操作中,由于显卡显存的限制,我们并不能将batch size设置得很大,那么上面的假设就很可能不太成立,影响模型性能和模型收敛。
为了加快模型训练,我们通常会使用分布式训练DDP来进行模型训练。Synchornized Batch Normalization (SyncBN)是指在多卡的情况下,每个进程(通常是每张卡)都计算各自BN中的meaning和var,然后进行通信同步mean, var, 分别计算总体的mean和总体的var,然后再正则化当前卡的数据。相当于变相的通过多机多卡的方式来增大batch. 具体流程如下:
- 前向传播,在各个进程上计算各自的小batch mean和batch variance
- 各自的进程对各自的小batch mean和小batch variance进行all_gathter操作,每个进程都得到所有进程的mean和variance
- 每个进程通过所有进程的mean和variance,分别计算总体的mean和总体的variance
- 用总体的mean和variance来替代上面BN计算得到的小batch mean和小batch variance进行后续的计算。
- 后向传播
使用SyncBN通常需要注意以下两点:
- 当前PyTorch SyncBN只在DDP单进程单卡模式中支持, 以一个进程只能配一个GPU
- 由于SyncBN需要用到all_gather这个分布式接口,SyncBN需要在DDP环境初始化后初始化,但是要在DDP模型前就准备好。
import os
import torch
import torch.nn as nn
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
# Pytorch多机多卡分布式训练,参考: https://blog.csdn.net/qq_34826149/article/details/141033501
def dist_init():
rank, world_size, local_rank = 0, 1, 0
if "RANK" in os.environ and "WORLD_SIZE" in os.environ:
rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
local_rank = int(os.environ["LOCAL_RANK"])
dist_url = "env://"
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="nccl", init_method=dist_url, world_size=world_size, rank=rank)
return rank, world_size, local_rank
class MyModel(nn.Module):
def __init__(self):
super().__init__()
pass
def forward(self, input):
pass
rank, world_size, local_rank = dist_init()
my_model = MyModel()
my_model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).cuda(local_rank)
model = DDP(model, device_ids=[local_rank], output_device=local_rank)
待持续更新
Reference:
https://www.cnblogs.com/shuimuqingyang/p/14167465.html
https://blog.csdn.net/qq_42722197/article/details/126258664