7.5.BN
mean 用法
ref: https://blog.csdn.net/qq_40714949/article/details/115485140
dim 写第几维,第几维就会被求和,并消失。
x = torch.Tensor([1, 2, 3, 4, 5, 6]).view(2, 3)
y_0 = torch.mean(x, dim=0)
y_1 = torch.mean(x, dim=1)
print(x)
print(y_0)
print(y_1)
tensor([[1., 2., 3.],
[4., 5., 6.]])
tensor([2.5000, 3.5000, 4.5000])
tensor([2., 5.])
BN 层回忆提纲
$$\mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}\mathcal{B}}{\hat{\boldsymbol{\sigma}}\mathcal{B}} + \boldsymbol{\beta}$$
BN 层对上一层的输出进行规范化到 0 均值和 1 方差,同时学习一个缩放系数 gamma(初始为 1)和平移系数 beta(初始为 0)。符合直觉地,全连接层的每个元素对样本维求均值,mean.shape = (元素数)
。卷积层的每个通道对(样本,宽,高)求均值,mean.shape = (通道数)
。
下面 gamma 和 beta 是待学习参数,momentum
是上一次均值的权重(并不会直接采纳本次求得的均值,形同低通滤波)。
非训练模式时,直接使用先前计算出来的 moving_mean, moving_var
.
import torch
from torch import nn
from d2l import torch as d2l
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data
class BatchNorm(nn.Module):
# num_features:完全连接层的输出数量或卷积层的输出通道数。
# num_dims:2表示完全连接层,4表示卷积层
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 参与求梯度,拉伸和偏移参数,分别初始化成1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)
def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var
# 复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
nn.Linear(84, 10))
由上述示例知,BN 是放在激活函数和卷积或线性层之间。
- 为什么只检查 moving_mean 所在的显存,而不需要检查 gamma, beta 所在的显存?
chat: 在PyTorch中,nn.Parameter
对象自动与模块(nn.Module
)注册,并且会随模块一起被移动到指定的设备(CPU或GPU)上。当你调用.to(device)
、.cuda()
或.cpu()
方法在一个nn.Module
上时,它不仅会移动模块的参数(即由nn.Parameter
定义的参数)到指定的设备,还会移动缓冲区(如批量归一化层的运行时均值和方差)。然而,这里的关键点是self.moving_mean
和self.moving_var
并不是通过nn.Parameter
定义的,它们是作为普通的torch.Tensor
对象存在的,不会自动随着模块的.to(device)
调用而移动。
因此,在BatchNorm
类的forward
方法中,需要手动检查self.moving_mean
和self.moving_var
是否与输入X
在同一设备上。如果它们不在同一设备上,就需要手动将它们移动到X
所在的设备,以保证计算能够正常进行。这是为了确保所有用于计算的张量都在同一设备上,从而避免因设备不匹配导致的错误。
verified. loss 0.272, train acc 0.899, test acc 0.864
学习到的参数:
net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,))
(tensor([2.0412, 4.2506, 2.4837, 2.8996, 0.3911, 3.2350], device='cuda:0',
grad_fn=<ViewBackward0>),
tensor([-0.7424, 2.6919, 2.7948, -2.8892, -0.3490, 1.4538], device='cuda:0',
grad_fn=<ViewBackward0>))
pure pytorch
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
nn.Linear(84, 10))
loss 0.264, train acc 0.904, test acc 0.853
炼金术
从 BN 开始就很难解释网络层作用的原理了。
Exercise
-
在使用批量规范化之前,我们是否可以从全连接层或卷积层中删除偏置参数?为什么?
可以,BN 层会消除前一层 $b$ 的影响
-
比较LeNet在使用和不使用批量规范化情况下的学习率。
-
绘制训练和测试准确度的提高。
使用 (10 epochs):
loss 0.264, train acc 0.904, test acc 0.853
不使用 BN:
loss 0.502, train acc 0.807, test acc 0.804
-
学习率有多高?
带 BN 学习率 1.0,不带则 0.9
-
-
我们是否需要在每个层中进行批量规范化?尝试一下?
不删除BN层,则loss 0.268, train acc 0.901, test acc 0.861 只保留第二个和第四个BN层,则loss 0.289, train acc 0.894, test acc 0.787 只保留第一个和第三个BN层,则loss 0.286, train acc 0.893, test acc 0.820 只删除第一个BN层,则loss 0.272, train acc 0.899, test acc 0.769 只删除第二个BN层,则loss 0.269, train acc 0.901, test acc 0.774 只删除第三个BN层,则loss 0.272, train acc 0.901, test acc 0.857 只删除第四个BN层,则loss 0.283, train acc 0.895, test acc 0.857 可以看出删除全连接层后面的批量规范化对结果影响不大,因此实际上是可以删除的
-
可以通过批量规范化来替换暂退法吗?行为会如何改变?
似乎是可以的,BN层兼具稳定训练过程和正则化的效果,将全连接阶段的BN层全部替换为弃置概率为0.5的Dropout层, 则loss 0.486, train acc 0.827, test acc 0.763,对比而言精度不如使用BN层的情况,而且train acc与test acc的差距也更大,说明BN反而能够起到比dropout更好更稳定的正则化效果
-
确定参数beta和gamma,并观察和分析结果。
通过实验观察模型中gamma和beta的情况。从结果来看,随着网络加深,beta的值逐渐稳定到0附近,而gamma的值则逐步稳定到1到2之间,说明BN层确实有助于稳定中间结果的数据分布
-
查看高级API中有关BatchNorm的在线文档,以查看其他批量规范化的应用。
-
研究思路:可以应用的其他“规范化”转换?可以应用概率积分变换吗?全秩协方差估计可以么?