深度学习:归一化技术

发布于:2025-09-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

在深度学习中,归一化技术是提高模型训练效率和性能的重要手段。归一化通过调整输入数据的分布,使得模型在训练过程中更易于收敛,减少过拟合的风险。本文将介绍几种常见的归一化技术,包括特征归一化、批归一化、层归一化和实例归一化。
在这里插入图片描述

一、特征归一化(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)xmin(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=1mxi
    其中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=1m(xiμB)2
  • ϵ\epsilonϵ是一个小常数,通常设置为 1e−51e-51e51e−81e-81e8,用于防止除零错误。

批归一化的主要步骤如下:

  1. 计算均值和方差

    • 对于每个批次,计算当前批次的均值μB\mu_BμB和方差σB2\sigma_B^2σB2
  2. 归一化

    • 使用计算得到的均值和方差对输入进行归一化。
  3. 缩放和偏移

    • 在归一化后,批归一化还引入了两个可学习的参数γ\gammaγβ\betaβ,用于缩放和偏移:
      y=γx^+βy = \gamma \hat{x} + \betay=γx^+β
      其中yyy是最终的输出,γ\gammaγβ\betaβ是在训练过程中学习到的参数。

在训练阶段,批归一化会使用当前批次的均值和方差进行归一化。每个批次的均值和方差是动态计算的,这使得模型能够适应训练数据的变化。具体步骤如下:

  1. 计算当前批次的均值和方差
  2. 使用这些统计量对输入进行归一化
  3. 更新移动均值和方差,以便在推理阶段使用:
    μ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)。

在推理阶段,批归一化的处理方式与训练阶段有所不同。推理阶段不再使用当前批次的均值和方差,而是使用在训练阶段计算得到的移动均值和方差。具体步骤如下:

  1. 使用训练阶段计算的移动均值和方差进行归一化
    x^=x−μmovingσmoving2+ϵ \hat{x} = \frac{x - \mu_{\text{moving}}}{\sqrt{\sigma_{\text{moving}}^2 + \epsilon}}x^=σmoving2+ϵ xμmoving
  2. 应用缩放和偏移
    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_mean
        model_moving_var = model.bn1.running_var

        # 手动更新移动均值和方差
        moving_mean = alpha * moving_mean + (1 - alpha) * batch_mean
        moving_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=1dxi
    其中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=1d(xiμ)2
  • ϵ\epsilonϵ是一个小常数,通常设置为1e−51e-51e51e−81e-81e8,用于防止除零错误。

层归一化的主要步骤如下:

  1. 计算均值和方差

    • 对于每个样本,计算该样本全部特征的均值μ\muμ和方差σ2\sigma^2σ2
  2. 归一化

    • 使用计算得到的均值和方差对输入特征进行归一化。
  3. 缩放和偏移

    • 在归一化后,层归一化还引入了两个可学习的参数γ\gammaγβ\betaβ,用于缩放和偏移:
      y=γx^+β y = \gamma \hat{x} + \betay=γx^+β
      其中yyy是最终的输出,γ\gammaγβ\betaβ是在训练过程中学习到的参数。

在层归一化中,训练和推理阶段的处理方式是相同的。与批归一化不同,层归一化不依赖于批次的统计量,而是对每个样本的特征进行归一化。因此,无论是在训练阶段还是推理阶段,层归一化都使用当前样本的均值和方差进行归一化。这使得层归一化在处理小批量数据或单个样本时表现良好。

import torch


def 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]])

网站公告

今日签到

点亮在社区的每一天
去签到