在PyTorch库中,nn.BatchNorm1d
是一个专为一维数据设计的批量归一化层。而nn.BatchNorm2d
与之非常类似,不过是输入数据的形状要求稍有不同。下面通过一些实例代码和其输出结果,让我们理解nn.BatchNorm1d
与nn.BatchNorm2d
的工作机制。
nn.BatchNorm1d使用示例
import torch
import torch.nn as nn
torch.manual_seed(42) # 固定随机种子
# 创建一个BatchNorm1d层,通道数为3
m = nn.BatchNorm1d(3) # d=3
print('m.weight:\n', m.weight) # (d,)
print('m.bias:\n', m.bias) # (d,)
# 准备一个批次大小为4,通道数为3的数据输入
x = torch.randn(4, 3) # (bs, d)
print('x:\n', x)
# 首先,我们计算原始输入数据每个通道的均值和方差
x_mean = x.mean(dim=0) # (d,)
x_var = x.var(dim=0, unbiased=False) # (d,)
print('x_mean:\n', x_mean)
print('x_var:\n', x_var)
# 将输入数据传递给BatchNorm1d层进行归一化处理
y = m(x) # (bs, d)
print('y:\n', y)
# 计算经过BatchNorm1d层后的输出数据在每个通道上的均值和方差
y_mean = y.mean(dim=0) # (d,)
y_var = y.var(dim=0, unbiased=False) # (d,)
print('y_mean:\n', y_mean)
print('y_var:\n', y_var)
运行这段代码后,我们得到以下输出:
m.weight:
Parameter containing:
tensor([1., 1., 1.], requires_grad=True)
m.bias:
Parameter containing:
tensor([0., 0., 0.], requires_grad=True)
x:
tensor([[ 0.3367, 0.1288, 0.2345],
[ 0.2303, -1.1229, -0.1863],
[ 2.2082, -0.6380, 0.4617],
[ 0.2674, 0.5349, 0.8094]])
x_mean:
tensor([ 0.7606, -0.2743, 0.3298])
x_var:
tensor([0.6999, 0.4174, 0.1307])
y:
tensor([[-0.5067, 0.6239, -0.2637],
[-0.6339, -1.3134, -1.4275],
[ 1.7302, -0.5630, 0.3647],
[-0.5896, 1.2525, 1.3264]], grad_fn=<NativeBatchNormBackward0>)
y_mean:
tensor([2.9802e-08, 0.0000e+00, 0.0000e+00], grad_fn=<MeanBackward1>)
y_var:
tensor([1.0000, 1.0000, 0.9999], grad_fn=<VarBackward0>)
从上述输出我们可以观察到:
-
nn.BatchNorm1d
初始化时,weight(缩放)参数默认为全1向量,bias(平移)参数默认为全0向量,并且它们都是可训练的(requires_grad=True)。 -
输入数据是一批具有4个样本(bs)和3个通道(d)的数据点。
-
在未经处理的输入数据上,我们分别计算了各个通道的均值和方差。
-
当我们将输入数据送入
nn.BatchNorm1d
层后,得到的输出数据经过了归一化处理,使得每个通道的均值接近于0(实际上由于浮点数精度问题,显示为接近于0的很小的数值),且方差近似为1。
nn.BatchNorm1d计算原理
import torch
import torch.nn as nn
def batch_norm_with_nn(x, num_features):
"""
使用 nn.BatchNorm1d 实现的批量归一化
x: (bs, d)
num_features: d
"""
m = nn.BatchNorm1d(num_features)
return m(x)
def batch_norm_manual(x, num_features):
"""
手动实现的批量归一化
x: (bs, d)
num_features: d
"""
weight = torch.ones(num_features) # (d,) 全1
bias = torch.zeros(num_features) # (d,) 全0
x_mean = x.mean(dim=0) # (d,) 均值
x_var = x.var(dim=0, unbiased=False) # (d,) 方差
normalized = (x - x_mean) / torch.sqrt(x_var + 1e-5) # (bs, d) 归一化
y_manual = weight * normalized + bias # (bs, d) 缩放和平移
# (d,)与(bs, d)计算时,(d,)会被广播为(bs, d)
return y_manual
# 设置随机种子
torch.manual_seed(42)
# 创建输入数据
batch_size = 4
num_features = 3
x = torch.randn(batch_size, num_features) # (bs, d)
y_nn = batch_norm_with_nn(x, num_features) # (bs, d) 直接调用 nn.BatchNorm1d
y_manual = batch_norm_manual(x, num_features) # (bs, d) 手动实现
print("Output using nn.BatchNorm1d:\n", y_nn)
print("Output using manual implementation:\n", y_manual)
运行这段代码后,我们得到以下输出:
Output using nn.BatchNorm1d:
tensor([[-0.5067, 0.6239, -0.2637],
[-0.6339, -1.3134, -1.4275],
[ 1.7302, -0.5630, 0.3647],
[-0.5896, 1.2525, 1.3264]], grad_fn=<NativeBatchNormBackward0>)
Output using manual implementation:
tensor([[-0.5067, 0.6239, -0.2637],
[-0.6339, -1.3134, -1.4275],
[ 1.7302, -0.5630, 0.3647],
[-0.5896, 1.2525, 1.3264]])
观察上述输出,我们可以看到,无论是使用nn.BatchNorm1d
还是手动实现批量归一化,两者对同一输入数据处理后得到的结果完全一致。这是因为它们遵循了相同的批量归一化计算流程:先计算每个通道的均值和方差,然后对原始数据进行标准化(减去均值并除以标准差),最后通过可学习的权重和偏置参数进行缩放和平移变换。
更多地,如果在上面的手动实现中, y_manual 直接等于 normalized 而不进行缩放(乘以权重)和平移(加上偏置),那么 y_manual 就是输入数据 x 经过零均值和单位方差的标准化处理。这意味着 y_manual(即 normalized)的每个通道将具有大致为 0 的均值和大致为 1 的方差。这是因为标准化过程是通过减去均值并除以方差的平方根来实现的。
nn.BatchNorm2d计算原理
import torch
import torch.nn as nn
def batch_norm_2d_with_nn(x, num_features):
"""
使用 nn.BatchNorm2d 实现的批量归一化
x: (N, C, H, W)
num_features: C (通道数)
"""
m = nn.BatchNorm2d(num_features)
print('m.weight:\n', m.weight)
print('m.bias:\n', m.bias)
return m(x)
def batch_norm_2d_manual(x, num_features):
"""
手动实现的批量归一化(适用于2D数据)
x: (N, C, H, W)
num_features: C (通道数)
"""
weight = torch.ones(num_features)[None, :, None, None] # (1, C, 1, 1) 全1
bias = torch.zeros(num_features)[None, :, None, None] # (1, C, 1, 1) 全0
x_mean = x.mean(dim=(0, 2, 3), keepdim=True) # (1, C, 1, 1) 均值
x_var = x.var(dim=(0, 2, 3), keepdim=True, unbiased=False) # (1, C, 1, 1) 方差
normalized = (x - x_mean) / torch.sqrt(x_var + 1e-5) # (N, C, H, W) 归一化
y_manual = weight * normalized + bias # (N, C, H, W) 缩放和平移
return y_manual
def batch_norm_2dto1d_manual(x, num_features):
"""
将 BatchNorm2d 转换为 BatchNorm1d 形式处理
x: (N, C, H, W)
num_features: C (通道数)
"""
N, C, H, W = x.shape
x_reshaped = x.permute(0, 2, 3, 1) # (N, H, W, C)
x_reshaped = x_reshaped.reshape(-1, C) # (N*H*W, C), 即(bs, d)
m = nn.BatchNorm1d(C)
y_reshaped = m(x_reshaped) # (N*H*W, C)
y = y_reshaped.reshape(N, H, W, C) # (N, H, W, C)
y = y.permute(0, 3, 1, 2) # (N, C, H, W)
return y
# 设置随机种子
torch.manual_seed(42)
# 创建输入数据
batch_size = 4
num_channels = 3
height = 2
width = 2
x = torch.randn(batch_size, num_channels, height, width) # (N, C, H, W)
y_nn = batch_norm_2d_with_nn(x, num_channels) # 使用 nn.BatchNorm2d
y_manual = batch_norm_2d_manual(x, num_channels) # 手动实现 BatchNorm2d
y_manual_2d_to_1d = batch_norm_2dto1d_manual(x, num_channels) # 将 BatchNorm2d 转换为 BatchNorm1d 处理
y_nn_y_manual = torch.allclose(y_nn, y_manual, rtol=1e-03, atol=1e-05) # 判断两个输出是否相等
y_nn_y_manual_2d_to_1d = torch.allclose(y_nn, y_manual_2d_to_1d, rtol=1e-03, atol=1e-05) # 判断两个输出是否相等
print("is y_nn equal to y_manual?", y_nn_y_manual)
print("is y_nn equal to y_manual_2d_to_1d?", y_nn_y_manual_2d_to_1d)
运行这段代码后,我们得到以下输出:
m.weight:
Parameter containing:
tensor([1., 1., 1.], requires_grad=True)
m.bias:
Parameter containing:
tensor([0., 0., 0.], requires_grad=True)
is y_nn equal to y_manual? True
is y_nn equal to y_manual_2d_to_1d? True
上面的代码演示了如何使用PyTorch中的nn.BatchNorm2d模块进行批量归一化处理,以及如何手动实现针对2D数据的批量归一化操作。同时,它还将2D批量归一化转换为1D形式进行处理,并验证了三种方法得到的结果一致。可以看到,batch_norm_2dto1d_manual
函数将 BatchNorm2d 转换为 BatchNorm1d 形式处理,并且得到的结果与使用 BatchNorm2d 直接实现的结果一致。
nn.BatchNorm1d和nn.BatchNorm2d总结
nn.BatchNorm1d
和nn.BatchNorm2d
两者处理的数据维度不同,但它们的核心参数和工作原理是一致的。它们的相同点在于 d (或着说,c)始终是独立的维度,即每个特征维度(d)被独立地归一化。在两种情况下,d 维度都是独立的,这意味着每个特征通道(无论是一维还是二维数据)都有其自己的归一化参数(均值和方差)。所以,不论是一维还是二维批量归一化层,它们的总参数量均为 2 * num_features
。而nn.BatchNorm2d
不过是将bs(批大小)、h(高度)、和h(宽度)的维度在操作中混合在一起,当作BatchNorm1d中的bs维度处理,正如batch_norm_2dto1d_manual
函数所演示的。