深度学习:归一化技术
在深度学习中,归一化技术是提高模型训练效率和性能的重要手段。归一化通过调整输入数据的分布,使得模型在训练过程中更易于收敛,减少过拟合的风险。本文将介绍几种常见的归一化技术,包括特征归一化、批归一化、层归一化和实例归一化。
一、特征归一化(Feature Normalization)
在深度学习和机器学习中,特征之间的量纲差异可能会对模型的训练和性能产生显著影响。当特征的量纲差异较大时,某些特征可能会在模型训练中占据主导地位,从而导致模型对这些特征的过度依赖,而忽略其他特征。这可能导致模型无法有效学习到数据的整体结构。假设我们有一个数据集,其中包含两个特征:
- 特征 A:房屋面积(单位:平方英尺),取值范围为 [500, 5000]
- 特征 B:房屋价格(单位:美元),取值范围为 [50,000, 500,000]
在这种情况下,特征 A 的值相对较小,而特征 B 的值相对较大。如果不进行归一化,模型在训练时可能会更关注特征 B,因为它的数值范围更大。这可能导致模型对房屋价格的预测过于敏感,而忽略了房屋面积的重要性。
特征归一化是将每个特征的值缩放到一个特定的范围,通常是 [0, 1] 或 [-1, 1]。这种方法可以消除特征之间的量纲差异,使得模型在训练时更快收敛。特征归一化通常用于输入数据的预处理阶段,尤其是在使用基于梯度的优化算法时。
对于特征xxx,特征归一化的公式如下:
x′=x−min(x)max(x)−min(x)x' = \frac{x - \text{min}(x)}{\text{max}(x) - \text{min}(x)}x′=max(x)−min(x)x−min(x)
import numpy as np# 创建示例数据
data = np.array([[10, 200, 30],[20, 150, 40],[30, 300, 50],[40, 250, 60],[50, 100, 70]])print("原始数据:")
print(data)# 特征归一化函数
def feature_normalization(data):# 计算每个特征的最小值和最大值min_vals = np.min(data, axis=0)max_vals = np.max(data, axis=0)# 应用归一化公式normalized_data = (data - min_vals) / (max_vals - min_vals)return normalized_data# 进行特征归一化
normalized_data = feature_normalization(data)print("\n归一化后的数据:")
print(normalized_data)
原始数据:
[[ 10 200 30][ 20 150 40][ 30 300 50][ 40 250 60][ 50 100 70]]归一化后的数据:
[[0. 0.5 0. ][0.25 0.25 0.25][0.5 1. 0.5 ][0.75 0.75 0.75][1. 0. 1. ]]
二、批归一化(Batch Normalization)
批归一化(Batch Normalization)是一种在神经网络中广泛使用的技术,旨在提高模型的训练速度和稳定性。它通过对每一层的输入进行归一化,使得输入的均值为 0,方差为 1,从而减少内部协变量偏移(Internal Covariate Shift)。
批归一化的公式如下:
x^=x−μBσB2+ϵ\hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}x^=σB2+ϵx−μB
其中:
- x^\hat{x}x^是归一化后的输出。
- xxx是当前批次的输入。
- μB\mu_BμB是当前批次的均值,计算公式为:
μB=1m∑i=1mxi\mu_B = \frac{1}{m} \sum_{i=1}^{m} x_iμB=m1i=1∑mxi
其中mmm是当前批次的样本数量。 - σB2\sigma_B^2σB2是当前批次的方差,计算公式为:
σB2=1m∑i=1m(xi−μB)2\sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2σB2=m1i=1∑m(xi−μB)2 - ϵ\epsilonϵ是一个小常数,通常设置为 1e−51e-51e−5或 1e−81e-81e−8,用于防止除零错误。
批归一化的主要步骤如下:
-
计算均值和方差:
- 对于每个批次,计算当前批次的均值μB\mu_BμB和方差σB2\sigma_B^2σB2。
-
归一化:
- 使用计算得到的均值和方差对输入进行归一化。
-
缩放和偏移:
- 在归一化后,批归一化还引入了两个可学习的参数γ\gammaγ和β\betaβ,用于缩放和偏移:
y=γx^+βy = \gamma \hat{x} + \betay=γx^+β
其中yyy是最终的输出,γ\gammaγ和β\betaβ是在训练过程中学习到的参数。
- 在归一化后,批归一化还引入了两个可学习的参数γ\gammaγ和β\betaβ,用于缩放和偏移:
在训练阶段,批归一化会使用当前批次的均值和方差进行归一化。每个批次的均值和方差是动态计算的,这使得模型能够适应训练数据的变化。具体步骤如下:
- 计算当前批次的均值和方差。
- 使用这些统计量对输入进行归一化。
- 更新移动均值和方差,以便在推理阶段使用:
μmoving=(1−α)⋅μB+α⋅μmoving\mu_{\text{moving}} = (1-\alpha) \cdot \mu_B + \alpha \cdot \mu_{\text{moving}}μmoving=(1−α)⋅μB+α⋅μmoving
σmoving2=(1−α)⋅σB2+α⋅σmoving2\sigma_{\text{moving}}^2 = (1-\alpha) \cdot \sigma_B^2 + \alpha \cdot \sigma_{\text{moving}}^2σmoving2=(1−α)⋅σB2+α⋅σmoving2
其中α\alphaα是滑动平均系数,通常设置为接近 1(如 0.9 或 0.99)。
在推理阶段,批归一化的处理方式与训练阶段有所不同。推理阶段不再使用当前批次的均值和方差,而是使用在训练阶段计算得到的移动均值和方差。具体步骤如下:
- 使用训练阶段计算的移动均值和方差进行归一化:
x^=x−μmovingσmoving2+ϵ \hat{x} = \frac{x - \mu_{\text{moving}}}{\sqrt{\sigma_{\text{moving}}^2 + \epsilon}}x^=σmoving2+ϵx−μmoving - 应用缩放和偏移:
y=γx^+βy = \gamma \hat{x} + \betay=γx^+β
下面代码展示了,手动计算的,批归一化,移动均值和方差与模型自动计算的结果一致性。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset# 定义简单的全连接神经网络模型
class SimpleFC(nn.Module):def __init__(self):super(SimpleFC, self).__init__()self.fc1 = nn.Linear(20, 1) # 从 1 个输入特征到 1 个输出self.bn1 = nn.BatchNorm1d(1) # 批归一化层def forward(self, x):fc_output = self.fc1(x) # 全连接层x = self.bn1(fc_output) # 批归一化return x, fc_output# 创建模型实例
model = SimpleFC()# 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam 优化器# 生成随机正态分布数据
num_samples = 10000 # 样本数量
X = torch.randn(num_samples, 20) # 1 个特征
y = torch.randn(num_samples, 1) # 目标值# 创建数据集和数据加载器
dataset = TensorDataset(X, y)
batch_size = 5000 # 每个批次的样本数量
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)# 训练模型
num_epochs = 1 # 训练轮数
alpha = 0.9 # 滑动平均系数
moving_mean = torch.zeros(1) # 初始化移动均值
moving_var = torch.ones(1) # 初始化移动方差for epoch in range(num_epochs):model.train() # 设置模型为训练模式for batch_idx, (images, labels) in enumerate(train_loader):optimizer.zero_grad() # 清零梯度outputs, fc_output = model(images) # 前向传播loss = criterion(outputs, labels) # 计算损失loss.backward() # 反向传播optimizer.step() # 更新参数# 获取当前批次的均值和方差batch_mean = fc_output.mean(dim=0) # 当前批次的均值batch_var = fc_output.var(dim=0) # 当前批次的方差# 获取模型的批归一化层的移动均值和方差model_moving_mean = model.bn1.running_meanmodel_moving_var = model.bn1.running_var# 手动更新移动均值和方差moving_mean = alpha * moving_mean + (1 - alpha) * batch_meanmoving_var = alpha * moving_var + (1 - alpha) * batch_var# 打印当前批次的均值和方差print(f'Batch {batch_idx + 1}, Batch Mean: {batch_mean.data.numpy()}, Batch Var: {batch_var.data.numpy()}')# 打印模型自动计算的移动均值和方差print(f'Batch {batch_idx + 1}, Model Moving Mean: {model_moving_mean.data.numpy()}, Model Moving Var: {model_moving_var.data.numpy()}')# 打印手动计算的移动均值和方差print(f'Batch {batch_idx + 1}, Manual Moving Mean: {moving_mean.data.numpy()}, Manual Moving Var: {moving_var.data.numpy()}')# 验证手动计算的移动均值和方差与模型的自动计算结果一致assert torch.allclose(moving_mean, model_moving_mean), "Manual moving mean does not match model moving mean!"assert torch.allclose(moving_var,model_moving_var), "Manual moving variance does not match model moving variance!"# 测试模型
model.eval() # 设置模型为评估模式
with torch.no_grad(): # 不计算梯度test_outputs, fc_output = model(X) # 前向传播test_loss = criterion(test_outputs, y) # 计算测试损失# 打印推理阶段的均值和方差
print(f'Test Loss: {test_loss.item()}')
print(f'Model Moving Mean: {model.bn1.running_mean.data.numpy()}')
print(f'Model Moving Var: {model.bn1.running_var.data.numpy()}')
Batch 1, Batch Mean: [0.07945078], Batch Var: [0.0101127]
Batch 1, Model Moving Mean: [0.00794508], Model Moving Var: [0.9010112]
Batch 1, Manual Moving Mean: [0.00794508], Manual Moving Var: [0.9010112]
Batch 2, Batch Mean: [0.07626408], Batch Var: [0.00997146]
Batch 2, Model Moving Mean: [0.01477698], Model Moving Var: [0.81190723]
Batch 2, Manual Moving Mean: [0.01477698], Manual Moving Var: [0.81190723]
Test Loss: 0.9921324253082275
Model Moving Mean: [0.01477698]
Model Moving Var: [0.81190723]
三、层归一化(Layer Normalization)
层归一化(Layer Normalization)是一种归一化技术,主要用于深度学习模型,特别是在处理序列数据(如循环神经网络 RNN 和 Transformer)时表现良好。与批归一化不同,层归一化是对每个样本的所有特征进行归一化,而不是对整个批次进行归一化。
在层归一化中,“层”指的是神经网络中的一层,通常是一个全连接层或卷积层。层归一化的目标是对该层的输出进行归一化,以提高模型的训练效率和稳定性。
“样本特征”是指输入数据中每个样本的特征向量。在层归一化中,每个样本的特征向量包含多个特征值,这些特征值可以是不同的输入变量。例如,在图像分类任务中,一个样本的特征可能是图像的像素值;在文本处理任务中,一个样本的特征可能是词嵌入向量。
在层归一化中,我们会对每个样本的所有特征进行归一化处理。
x^=x−μσ2+ϵ\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}x^=σ2+ϵx−μ
其中:
- x^\hat{x}x^是归一化后的输出。
- xxx是当前样本的输入特征向量。
- μ\muμ是当前样本的均值,计算公式为:
μ=1d∑i=1dxi\mu = \frac{1}{d} \sum_{i=1}^{d} x_iμ=d1i=1∑dxi
其中ddd是特征的维度。 - σ2\sigma^2σ2是当前样本的方差,计算公式为:
σ2=1d∑i=1d(xi−μ)2\sigma^2 = \frac{1}{d} \sum_{i=1}^{d} (x_i - \mu)^2σ2=d1i=1∑d(xi−μ)2 - ϵ\epsilonϵ是一个小常数,通常设置为1e−51e-51e−5或1e−81e-81e−8,用于防止除零错误。
层归一化的主要步骤如下:
-
计算均值和方差:
- 对于每个样本,计算该样本全部特征的均值μ\muμ和方差σ2\sigma^2σ2。
-
归一化:
- 使用计算得到的均值和方差对输入特征进行归一化。
-
缩放和偏移:
- 在归一化后,层归一化还引入了两个可学习的参数γ\gammaγ和 β\betaβ,用于缩放和偏移:
y=γx^+β y = \gamma \hat{x} + \betay=γx^+β
其中yyy是最终的输出,γ\gammaγ和β\betaβ是在训练过程中学习到的参数。
- 在归一化后,层归一化还引入了两个可学习的参数γ\gammaγ和 β\betaβ,用于缩放和偏移:
在层归一化中,训练和推理阶段的处理方式是相同的。与批归一化不同,层归一化不依赖于批次的统计量,而是对每个样本的特征进行归一化。因此,无论是在训练阶段还是推理阶段,层归一化都使用当前样本的均值和方差进行归一化。这使得层归一化在处理小批量数据或单个样本时表现良好。
import torchdef layer_normalization(X, epsilon=1e-5):"""计算层归一化:param X: 输入特征矩阵,形状为 (num_samples, num_features):param epsilon: 防止除零错误的小常数:return: 归一化后的特征矩阵"""# 计算均值和方差mu = X.mean(dim=1, keepdim=True) # 每个样本的均值sigma_squared = X.var(dim=1, keepdim=True) # 每个样本的方差# 进行归一化X_normalized = (X - mu) / torch.sqrt(sigma_squared + epsilon)return X_normalized# 随机生成样本
num_samples = 5 # 样本数量
num_features = 4 # 每个样本的特征数量
X = torch.randn(num_samples, num_features) # 生成随机正态分布数据print("原始特征矩阵:")
print(X)# 计算层归一化
X_normalized = layer_normalization(X)print("\n层归一化后的特征矩阵:")
print(X_normalized)
原始特征矩阵:
tensor([[-0.4186, 1.8211, -0.6178, -2.0494],[-0.3354, -1.9183, -0.5551, 0.7775],[ 0.0546, 0.5884, -0.8421, -0.2335],[ 0.3276, -0.5106, -0.0648, 0.0211],[ 0.6945, -0.8199, 0.5595, -2.3835]])层归一化后的特征矩阵:
tensor([[-0.0640, 1.3364, -0.1886, -1.0837],[ 0.1558, -1.2746, -0.0427, 1.1615],[ 0.2730, 1.1685, -1.2312, -0.2102],[ 1.1095, -1.3107, -0.0234, 0.2246],[ 0.8222, -0.2314, 0.7283, -1.3191]])