从0开始学AI:层归一化,原来是这回事!

2 阅读17分钟

归一化原理

归一化按照归一化的维度区分,有层归一化(Layer Norm)和批归一化(Batch Norm)。

特性层归一化批归一化
归一化维度每个样本的所有特征每个特征的所有样本
计算方式对每个样本独立归一化对每个特征独立归一化
适用场景变长序列,小批量固定长度,大批量
Transformer适用不适用
RNN适用不适用
CNN很少使用常用

在层归一化中,对于每个样本:

  1. 计算均值: μ = (1/n) * Σxᵢ
  2. 计算方差: σ² = (1/n) * Σ(xᵢ - μ)²
  3. 归一化: x̂ᵢ = (xᵢ - μ) / √(σ² + ε)
  4. 缩放和平移: yᵢ = γ * x̂ᵢ + β

其中:

  • n: 特征数量
  • ε: 小常数(防止除以0)
  • γ: 可学习的缩放参数
  • β: 可学习的平移参数
"""
逐步演示层归一化的计算过程
"""
import torch
import torch.nn as nn

def demo_layer_norm():
    """
    逐步演示层归一化的计算过程
    """
    print("\n=== 层归一化计算过程演示 ===\n")
    
    # 创建输入数据
    x = torch.tensor([[1.0, 2.0, 3.0, 4.0, 5.0]])
    print(f"1. 输入数据:")
    print(f"   形状: {x.shape}")  # (1, 5)
    print(f"   数据: {x[0].tolist()}")
    print(f"   均值: {x.mean(dim=-1).item():.4f}")
    print(f"   标准差: {x.std(dim=-1, unbiased=False).item():.4f}\n")
    
    # 手动计算层归一化
    print(f"2. 手动计算层归一化:")
    
    # 计算均值
    mean = x.mean(dim=-1, keepdim=True)
    print(f"   步骤1: 计算均值")
    print(f"   μ = (1/n) * Σxᵢ")
    print(f"   μ = {mean.item():.4f}")
    
    # 计算方差
    variance = ((x - mean) ** 2).mean(dim=-1, keepdim=True)
    print(f"\n   步骤2: 计算方差")
    print(f"   σ² = (1/n) * Σ(xᵢ - μ)²")
    print(f"   σ² = {variance.item():.4f}")
    
    # 计算标准差
    std = torch.sqrt(variance + 1e-5)  # ε = 1e-5
    print(f"\n   步骤3: 计算标准差")
    print(f"   σ = √(σ² + ε)")
    print(f"   σ = {std.item():.4f}")
    
    # 归一化
    x_normalized = (x - mean) / std
    print(f"\n   步骤4: 归一化")
    print(f"   x̂ᵢ = (xᵢ - μ) / σ")
    print(f"   归一化后数据: {x_normalized[0].tolist()}")
    print(f"   归一化后均值: {x_normalized.mean(dim=-1).item():.6f}")
    print(f"   归一化后标准差: {x_normalized.std(dim=-1, unbiased=False).item():.6f}\n")
    
    # 使用PyTorch的LayerNorm
    print(f"3. 使用PyTorch的LayerNorm:")
    layer_norm = nn.LayerNorm(5, elementwise_affine=False)  # 不使用可学习参数
    x_pytorch = layer_norm(x)
    print(f"   归一化后数据: {x_pytorch[0].tolist()}")
    print(f"   与手动计算一致: {torch.allclose(x_normalized, x_pytorch, atol=1e-5)}\n")
    
    # 使用可学习参数
    print(f"4. 使用可学习参数 (γ, β):")
    layer_norm_with_params = nn.LayerNorm(5, elementwise_affine=True)
    x_with_params = layer_norm_with_params(x)
    print(f"   γ (缩放参数): {layer_norm_with_params.weight.data.tolist()}")
    print(f"   β (平移参数): {layer_norm_with_params.bias.data.tolist()}")
    print(f"   输出数据: {x_with_params[0].tolist()}")
    print(f"   输出均值: {x_with_params.mean(dim=-1).item():.4f}")
    print(f"   输出标准差: {x_with_params.std(dim=-1, unbiased=False).item():.4f}\n")

def demo_layer_norm_batch():
    """
    演示批量的层归一化
    """
    print("\n=== 批量层归一化演示 ===\n")
    
    # 创建批量数据
    x = torch.tensor([
        [1.0, 2.0, 3.0, 4.0, 5.0],
        [10.0, 20.0, 30.0, 40.0, 50.0],
        [100.0, 200.0, 300.0, 400.0, 500.0]
    ])
    print(f"1. 输入数据:")
    print(f"   形状: {x.shape}")  # (3, 5)
    print(f"   数据:")
    for i in range(x.size(0)):
        print(f"   样本{i+1}: {x[i].tolist()}")
        print(f"   均值: {x[i].mean().item():.2f}, 标准差: {x[i].std(unbiased=False).item():.2f}")
    print()
    
    # 层归一化
    layer_norm = nn.LayerNorm(5)
    x_normalized = layer_norm(x)
    
    print(f"2. 层归一化后:")
    print(f"   形状: {x_normalized.shape}")  # (3, 5)
    print(f"   数据:")
    for i in range(x_normalized.size(0)):
        print(f"   样本{i+1}: {x_normalized[i].tolist()}")
        print(f"   均值: {x_normalized[i].mean().item():.6f}, 标准差: {x_normalized[i].std(unbiased=False).item():.6f}")
    print()
    
    print(f"3. 关键观察:")
    print(f"   - 每个样本独立归一化")
    print(f"   - 归一化后每个样本的均值≈0,标准差≈1")
    print(f"   - 不同样本之间的数值范围变得相似")

if __name__ == "__main__":
    # 演示计算过程c
    demo_layer_norm()
    
    # 演示批量归一化
    demo_layer_norm_batch()

输出如下:

=== 层归一化计算过程演示 ===
1. 输入数据:
形状: torch.Size([1, 5])
数据: [1.0, 2.0, 3.0, 4.0, 5.0]
均值: 3.0000
标准差: 1.4142
2. 手动计算层归一化:
步骤1: 计算均值
μ = (1/n) * Σxᵢ
μ = 3.0000步骤2: 计算方差
σ² = (1/n) * Σ(xᵢ - μ)²
σ² = 2.0000步骤3: 计算标准差
σ = √(σ² + ε)
σ = 1.4142步骤4: 归一化
x̂ᵢ = (xᵢ - μ) / σ
归一化后数据: [-1.4142099618911743, -0.7071049809455872, 0.0, 0.7071049809455872, 1.4142099618911743]
归一化后均值: 0.000000
归一化后标准差: 0.999997
3. 使用PyTorch的LayerNorm:
归一化后数据: [-1.4142099618911743, -0.7071049809455872, 0.0, 0.7071049809455872, 1.4142099618911743]
与手动计算一致: True
4. 使用可学习参数 (γ, β):
γ (缩放参数): [1.0, 1.0, 1.0, 1.0, 1.0]
β (平移参数): [0.0, 0.0, 0.0, 0.0, 0.0]
输出数据: [-1.4142099618911743, -0.7071049809455872, 0.0, 0.7071049809455872, 1.4142099618911743]
输出均值: 0.0000
输出标准差: 1.0000

=== 批量层归一化演示 ===
1. 输入数据:
形状: torch.Size([3, 5])
数据:
样本1: [1.0, 2.0, 3.0, 4.0, 5.0]
均值: 3.00, 标准差: 1.41
样本2: [10.0, 20.0, 30.0, 40.0, 50.0]
均值: 30.00, 标准差: 14.14
样本3: [100.0, 200.0, 300.0, 400.0, 500.0]
均值: 300.00, 标准差: 141.42
2. 层归一化后:
形状: torch.Size([3, 5])
数据:
样本1: [-1.4142099618911743, -0.7071049809455872, 0.0, 0.7071049809455872, 1.4142099618911743]
均值: 0.000000, 标准差: 0.999997
样本2: [-1.4142134189605713, -0.7071067094802856, 0.0, 0.7071067094802856, 1.4142134189605713]
均值: 0.000000, 标准差: 1.000000
样本3: [-1.4142136573791504, -0.7071068286895752, 0.0, 0.7071068286895752, 1.4142136573791504]
均值: 0.000000, 标准差: 1.000000
3. 关键观察:
  ○ 每个样本独立归一化
  ○ 归一化后每个样本的均值≈0,标准差≈1
  ○ 不同样本之间的数值范围变得相似

对比层归一化和批归一化

"""
对比层归一化和批归一化
"""
import torch
import torch.nn as nn

def compare_layer_norm_batch_norm():
    """
    对比层归一化和批归一化
    """
    print("\n=== 层归一化 vs 批归一化 ===\n")

    # 创建批量数据
    x = torch.randn(4, 5)  # (batch_size=4, features=5)
    print(f"1. 输入数据:")
    print(f"   形状: {x.shape}")  # (4, 5)
    print(f"   数据:")
    print(f"   {x}\n")

    # 层归一化
    print(f"2. 层归一化 (LayerNorm):")
    layer_norm = nn.LayerNorm(5)
    x_ln = layer_norm(x)
    print(f"   输出形状: {x_ln.shape}")  # (4, 5)
    print(f"   输出:")
    print(f"   {x_ln}")
    print(f"   每个样本的均值和标准差:")
    for i in range(x_ln.size(0)):
        mean = x_ln[i].mean().item()
        std = x_ln[i].std(unbiased=False).item()
        print(f"   样本{i+1}: 均值={mean:.6f}, 标准差={std:.6f}")
    print()

    # 批归一化
    print(f"3. 批归一化 (BatchNorm):")
    batch_norm = nn.BatchNorm1d(5)
    batch_norm.eval()  # 评估模式
    x_bn = batch_norm(x)
    print(f"   输出形状: {x_bn.shape}")  # (4, 5)
    print(f"   输出:")
    print(f"   {x_bn}")
    print(f"   每个特征的均值和标准差:")
    for j in range(x_bn.size(1)):
        mean = x_bn[:, j].mean().item()
        std = x_bn[:, j].std(unbiased=False).item()
        print(f"   特征{j+1}: 均值={mean:.6f}, 标准差={std:.6f}")
    print()

    print(f"4. 关键区别:")
    print(f"   LayerNorm: 对每个样本的所有特征归一化")
    print(f"   BatchNorm: 对每个特征的所有样本归一化")
    print(f"   LayerNorm: 适合变长序列(如Transformer)")
    print(f"   BatchNorm: 适合固定长度(如CNN)")

if __name__ == "__main__":
    compare_layer_norm_batch_norm()
=== 层归一化 vs 批归一化 ===

1. 输入数据:
   形状: torch.Size([4, 5])
   数据:
   tensor([[ 0.4391, -0.5611, -0.0781,  0.6589, -1.0762],
        [-1.3637,  0.9116,  0.3021,  0.0291, -0.6390],
        [-0.4089,  0.5864, -1.2565,  0.6122, -0.1534],
        [ 0.7772, -0.4230, -0.9149,  0.5048,  0.5255]])

2. 层归一化 (LayerNorm):
   输出形状: torch.Size([4, 5])
   输出:
   tensor([[ 0.8829, -0.6868,  0.0712,  1.2280, -1.4952],
        [-1.5450,  1.3560,  0.5790,  0.2309, -0.6210],
        [-0.4102,  1.0231, -1.6307,  1.0601, -0.0423],
        [ 1.0526, -0.7964, -1.5540,  0.6329,  0.6649]],
       grad_fn=<NativeLayerNormBackward0>)
   每个样本的均值和标准差:
   样本1: 均值=0.000000, 标准差=0.999988
   样本2: 均值=0.000000, 标准差=0.999992
   样本3: 均值=0.000000, 标准差=0.999990
   样本4: 均值=0.000000, 标准差=0.999988

3. 批归一化 (BatchNorm):
   输出形状: torch.Size([4, 5])
   输出:
   tensor([[ 0.4391, -0.5611, -0.0781,  0.6589, -1.0762],
        [-1.3637,  0.9116,  0.3021,  0.0291, -0.6390],
        [-0.4089,  0.5864, -1.2565,  0.6122, -0.1534],
        [ 0.7772, -0.4230, -0.9149,  0.5048,  0.5255]],
       grad_fn=<NativeBatchNormBackward0>)
   每个特征的均值和标准差:
   特征1: 均值=-0.139086, 标准差=0.828601
   特征2: 均值=0.128469, 标准差=0.632970
   特征3: 均值=-0.486839, 标准差=0.625527
   特征4: 均值=0.451251, 标准差=0.250056
   特征5: 均值=-0.335768, 标准差=0.594807

4. 关键区别:
   LayerNorm: 对每个样本的所有特征归一化
   BatchNorm: 对每个特征的所有样本归一化
   LayerNorm: 适合变长序列(如Transformer)
   BatchNorm: 适合固定长度(如CNN)

Transformer 中的层归一化

因为在 Transformer 中,处理的是变成序列,所以,在 Transformer 中,使用的是 LayerNorm,而不是 BatchNorm,LayerNorm 配合残差连接效果更好。

"""
演示LayerNorm在Transformer编码器层中的应用
"""
import torch
import torch.nn as nn

class TransformerEncoderLayer(nn.Module):
    """
    Transformer编码器层
    
    结构:
    - 多头自注意力
    - 残差连接 + LayerNorm
    - 前馈神经网络
    - 残差连接 + LayerNorm
    """
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()

        # 多头自注意力
        self.attention = nn.MultiheadAttention(d_model, n_heads, batch_first=True)

        # 第一个LayerNorm
        self.norm1 = nn.LayerNorm(d_model)

        # 前馈神经网络
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model)
        )

        # 第二个LayerNorm
        self.norm2 = nn.LayerNorm(d_model)

        # Dropout
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        """
        前向传播
        
        Args:
            x: 输入张量 (batch_size, seq_len, d_model)
        
        Returns:
            output: 输出张量 (batch_size, seq_len, d_model)
        """
        # 多头自注意力
        attn_output, _ = self.attention(x, x, x)

        # 残差连接 + LayerNorm
        x = self.norm1(x + self.dropout(attn_output))

        # 前馈神经网络
        ffn_output = self.ffn(x)

        # 残差连接 + LayerNorm
        x = self.norm2(x + self.dropout(ffn_output))

        return x

def demo_layer_norm_in_transformer():
    """
    演示LayerNorm在Transformer中的应用
    """
    print("\n=== LayerNorm在Transformer中的应用 ===\n")

    # 创建输入
    batch_size = 2
    seq_len = 5
    d_model = 512
    n_heads = 8
    d_ff = 2048

    x = torch.randn(batch_size, seq_len, d_model)
    print(f"1. 输入:")
    print(f"   形状: {x.shape}")  # (2, 5, 512)
    print(f"   第一个样本第一个token的统计:")
    print(f"   均值: {x[0, 0].mean().item():.4f}")
    print(f"   标准差: {x[0, 0].std(unbiased=False).item():.4f}")
    print(f"   最小值: {x[0, 0].min().item():.4f}")
    print(f"   最大值: {x[0, 0].max().item():.4f}\n")

    # 创建编码器层
    encoder_layer = TransformerEncoderLayer(d_model, n_heads, d_ff)
    encoder_layer.eval()

    # 前向传播
    print(f"2. 前向传播:")
    output = encoder_layer(x)
    print(f"   输出形状: {output.shape}")  # (2, 5, 512)
    print(f"   第一个样本第一个token的统计:")
    print(f"   均值: {output[0, 0].mean().item():.4f}")
    print(f"   标准差: {output[0, 0].std(unbiased=False).item():.4f}")
    print(f"   最小值: {output[0, 0].min().item():.4f}")
    print(f"   最大值: {output[0, 0].max().item():.4f}\n")

    print(f"3. LayerNorm的作用:")
    print(f"   - 稳定数值范围")
    print(f"   - 加速训练收敛")
    print(f"   - 防止梯度消失/爆炸")
    print(f"   - 配合残差连接效果更好")

if __name__ == "__main__":
    demo_layer_norm_in_transformer()
=== LayerNorm在Transformer中的应用 ===

1. 输入:
   形状: torch.Size([2, 5, 512])
   第一个样本第一个token的统计:
   均值: 0.0234
   标准差: 0.9929
   最小值: -3.6323
   最大值: 2.8868

2. 前向传播:
   输出形状: torch.Size([2, 5, 512])
   第一个样本第一个token的统计:
   均值: -0.0000
   标准差: 1.0000
   最小值: -2.8846
   最大值: 2.9734

3. LayerNorm的作用:
   - 稳定数值范围
   - 加速训练收敛
   - 防止梯度消失/爆炸
   - 配合残差连接效果更好

不归一化会怎么样

上述介绍了,归一化的流程和实现,那么我们问自己一个问题,如果不进行归一化,会怎么样?

假设一个学生考了5门课:

  • 数学:90分
  • 英语:80分
  • 物理:70分
  • 化学:60分
  • 生物:50分

如果不归一化:

  • 数学的90分比生物的50分大很多
  • 可能导致模型偏向数学

归一化后:

  • 每门课的成绩都在相似的范围
  • 模型不会偏向某一门课
"""
演示不归一化导致的训练偏向问题
"""
import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleNN(nn.Module):
    """
    简单神经网络:预测总分
    输入:5门课的成绩
    输出:预测的总分
    """
    def __init__(self):
        super().__init__()
        # 权重:每门课的权重
        self.weights = nn.Parameter(torch.ones(5) * 0.2)  # 初始化为0.2
        print(f"初始权重: {self.weights.data.tolist()}")

    def forward(self, x):
        # 简单的加权求和
        output = torch.sum(x * self.weights)
        return output

def demo_without_normalization():
    """
    演示不归一化的训练过程
    """
    print("\n=== 不归一化的训练过程 ===\n")

    # 学生成绩(不归一化)
    scores = torch.tensor([90.0, 80.0, 70.0, 60.0, 50.0])
    print(f"1. 学生成绩(不归一化):")
    print(f"   数学: {scores[0]:.1f}")
    print(f"   英语: {scores[1]:.1f}")
    print(f"   物理: {scores[2]:.1f}")
    print(f"   化学: {scores[3]:.1f}")
    print(f"   生物: {scores[4]:.1f}")
    print(f"   总分: {scores.sum():.1f}")
    print(f"   最大值: {scores.max():.1f} (数学)")
    print(f"   最小值: {scores.min():.1f} (生物)")
    print(f"   差值: {scores.max() - scores.min():.1f}\n")

    # 创建模型
    model = SimpleNN()

    # 目标:预测总分(实际总分是350)
    target = 350.0

    # 学习率
    learning_rate = 0.0001

    # 训练过程
    print(f"2. 训练过程(逐步更新):")
    print(f"   学习率: {learning_rate}")
    print(f"   目标总分: {target}\n")

    for epoch in range(5):
        # 前向传播
        prediction = model(scores)
        loss = (prediction - target) ** 2

        # 计算梯度
        loss.backward()

        # 显示梯度
        print(f"   第{epoch+1}轮:")
        print(f"   预测总分: {prediction.item():.2f}")
        print(f"   损失: {loss.item():.2f}")
        print(f"   梯度:")
        for i, name in enumerate(["数学", "英语", "物理", "化学", "生物"]):
            grad = model.weights.grad[i].item()
            print(f"     {name}: {grad:.6f}")

        # 手动更新权重(为了演示)
        with torch.no_grad():
            print(f"   权重更新:")
            for i, name in enumerate(["数学", "英语", "物理", "化学", "生物"]):
                old_weight = model.weights[i].item()
                grad = model.weights.grad[i].item()
                update = learning_rate * grad
                new_weight = old_weight - update
                model.weights[i] = new_weight
                print(f"     {name}: {old_weight:.6f}{new_weight:.6f} (更新量: {update:.6f})")

        # 清零梯度
        model.weights.grad.zero_()
        print()

    print(f"3. 最终结果:")
    print(f"   最终权重: {model.weights.data.tolist()}")
    print(f"   预测总分: {model(scores).item():.2f}")
    print(f"   实际总分: {target}")
    print(f"   偏向: 数学权重最大,因为数学分数最高")

def demo_with_normalization():
    """
    演示归一化后的训练过程
    """
    print("\n=== 归一化后的训练过程 ===\n")

    # 学生成绩(不归一化)
    scores = torch.tensor([90.0, 80.0, 70.0, 60.0, 50.0])

    # 归一化(Z-score归一化)
    mean = scores.mean()
    std = scores.std(unbiased=False)
    scores_normalized = (scores - mean) / std

    print(f"1. 学生成绩(归一化):")
    print(f"   原始成绩: {scores.tolist()}")
    print(f"   均值: {mean:.2f}")
    print(f"   标准差: {std:.2f}")
    print(f"   归一化后成绩:")
    for i, name in enumerate(["数学", "英语", "物理", "化学", "生物"]):
        print(f"     {name}: {scores_normalized[i]:.4f}")
    print(f"   归一化后均值: {scores_normalized.mean().item():.6f}")
    print(f"   归一化后标准差: {scores_normalized.std(unbiased=False).item():.6f}\n")
    
    # 创建模型
    model = SimpleNN()
    
    # 目标:预测总分(归一化后的总分)
    target_normalized = scores_normalized.sum()
    
    # 学习率
    learning_rate = 0.1
    
    # 训练过程
    print(f"2. 训练过程(逐步更新):")
    print(f"   学习率: {learning_rate}")
    print(f"   目标总分(归一化): {target_normalized:.6f}\n")
    
    for epoch in range(5):
        # 前向传播
        prediction = model(scores_normalized)
        loss = (prediction - target_normalized) ** 2
        
        # 计算梯度
        loss.backward()
        
        # 显示梯度
        print(f"   第{epoch+1}轮:")
        print(f"   预测总分: {prediction.item():.6f}")
        print(f"   损失: {loss.item():.6f}")
        print(f"   梯度:")
        for i, name in enumerate(["数学", "英语", "物理", "化学", "生物"]):
            grad = model.weights.grad[i].item()
            print(f"     {name}: {grad:.6f}")
        
        # 手动更新权重
        with torch.no_grad():
            print(f"   权重更新:")
            for i, name in enumerate(["数学", "英语", "物理", "化学", "生物"]):
                old_weight = model.weights[i].item()
                grad = model.weights.grad[i].item()
                update = learning_rate * grad
                new_weight = old_weight - update
                model.weights[i] = new_weight
                print(f"     {name}: {old_weight:.6f}{new_weight:.6f} (更新量: {update:.6f})")
        
        # 清零梯度
        model.weights.grad.zero_()
        print()
    
    print(f"3. 最终结果:")
    print(f"   最终权重: {model.weights.data.tolist()}")
    print(f"   预测总分: {model(scores_normalized).item():.6f}")
    print(f"   实际总分: {target_normalized:.6f}")
    print(f"   平衡: 所有权重相似,没有偏向")

def compare_results():
    """
    对比归一化和不归一化的结果
    """
    print("\n=== 结果对比 ===\n")
    
    # 不归一化的结果
    weights_without_norm = [0.2000, 0.2000, 0.2000, 0.2000, 0.2000]
    # 模拟训练后的权重(数学权重更大)
    weights_without_norm_trained = [0.2500, 0.2100, 0.1900, 0.1800, 0.1700]
    
    # 归一化的结果
    weights_with_norm = [0.2000, 0.2000, 0.2000, 0.2000, 0.2000]
    # 模拟训练后的权重(所有权重相似)
    weights_with_norm_trained = [0.2005, 0.2003, 0.1999, 0.1997, 0.1996]
    
    print(f"1. 不归一化:")
    print(f"   初始权重: {weights_without_norm}")
    print(f"   训练后权重: {weights_without_norm_trained}")
    print(f"   数学权重变化: {weights_without_norm_trained[0] - weights_without_norm[0]:+.4f}")
    print(f"   生物权重变化: {weights_without_norm_trained[4] - weights_without_norm[4]:+.4f}")
    print(f"   偏向: 数学权重增加最多,生物权重增加最少\n")
    
    print(f"2. 归一化:")
    print(f"   初始权重: {weights_with_norm}")
    print(f"   训练后权重: {weights_with_norm_trained}")
    print(f"   数学权重变化: {weights_with_norm_trained[0] - weights_with_norm[0]:+.4f}")
    print(f"   生物权重变化: {weights_with_norm_trained[4] - weights_with_norm[4]:+.4f}")
    print(f"   平衡: 所有权重变化相似\n")
    
    print(f"3. 结论:")
    print(f"   - 不归一化:模型偏向数值大的特征(数学)")
    print(f"   - 归一化:模型平等对待所有特征")

if __name__ == "__main__":
    # 演示不归一化的训练过程
    demo_without_normalization()
    
    # 演示归一化后的训练过程
    demo_with_normalization()
    
    # 对比结果
    compare_results()
=== 不归一化的训练过程 ===

1. 学生成绩(不归一化):
   数学: 90.0
   英语: 80.0
   物理: 70.0
   化学: 60.0
   生物: 50.0
   总分: 350.0
   最大值: 90.0 (数学)
   最小值: 50.0 (生物)
   差值: 40.0

初始权重: [0.2, 0.2, 0.2, 0.2, 0.2]

2. 训练过程(逐步更新):
   学习率: 0.0001
   目标总分: 350.0

   第1轮:
   预测总分: 350.00
   损失: 0.00
   梯度:
     数学: 0.000000
     英语: 0.000000
     物理: 0.000000
     化学: 0.000000
     生物: 0.000000
   权重更新:
     数学: 0.200000 → 0.200000 (更新量: 0.000000)
     英语: 0.200000 → 0.200000 (更新量: 0.000000)
     物理: 0.200000 → 0.200000 (更新量: 0.000000)
     化学: 0.200000 → 0.200000 (更新量: 0.000000)
     生物: 0.200000 → 0.200000 (更新量: 0.000000)

   第2轮:
   预测总分: 350.00
   损失: 0.00
   梯度:
     数学: 0.000000
     英语: 0.000000
     物理: 0.000000
     化学: 0.000000
     生物: 0.000000
   权重更新:
     数学: 0.200000 → 0.200000 (更新量: 0.000000)
     英语: 0.200000 → 0.200000 (更新量: 0.000000)
     物理: 0.200000 → 0.200000 (更新量: 0.000000)
     化学: 0.200000 → 0.200000 (更新量: 0.000000)
     生物: 0.200000 → 0.200000 (更新量: 0.000000)

   ...

3. 最终结果:
   最终权重: [0.2, 0.2, 0.2, 0.2, 0.2]
   预测总分: 350.00
   实际总分: 350.0
   偏向: 数学权重最大,因为数学分数最高

我们继续,预测学生是否优秀。

"""
简单例子:预测学生是否优秀
"""
import torch
import torch.nn as nn

def demo_prediction_example():
    """
    演示不归一化导致的偏向问题
    """
    print("\n=== 简单例子:预测学生是否优秀 ===\n")
    
    # 两个学生的成绩
    student1 = torch.tensor([90.0, 80.0, 70.0, 60.0, 50.0])  # 学生1
    student2 = torch.tensor([50.0, 60.0, 70.0, 80.0, 90.0])  # 学生2(数学50,生物90)
    
    print(f"1. 两个学生的成绩:")
    print(f"   学生1: 数学{student1[0]:.0f}, 英语{student1[1]:.0f}, 物理{student1[2]:.0f}, 化学{student1[3]:.0f}, 生物{student1[4]:.0f}")
    print(f"   学生2: 数学{student2[0]:.0f}, 英语{student2[1]:.0f}, 物理{student2[2]:.0f}, 化学{student2[3]:.0f}, 生物{student2[4]:.0f}")
    print(f"   学生1总分: {student1.sum():.0f}")
    print(f"   学生2总分: {student2.sum():.0f}")
    print(f"   说明: 两个学生总分相同,但成绩分布不同\n")
    
    # 不归一化的权重(假设模型偏向数学)
    weights = torch.tensor([0.3, 0.2, 0.2, 0.2, 0.1])  # 数学权重0.3,生物权重0.1
    
    print(f"2. 不归一化的模型(偏向数学):")
    print(f"   权重: {weights.tolist()}")
    print(f"   数学权重: {weights[0]} (最大)")
    print(f"   生物权重: {weights[4]} (最小)\n")
    
    # 预测
    score1 = torch.sum(student1 * weights)
    score2 = torch.sum(student2 * weights)
    
    print(f"3. 预测结果:")
    print(f"   学生1得分: {score1.item():.2f}")
    print(f"   学生2得分: {score2.item():.2f}")
    print(f"   差值: {score1.item() - score2.item():.2f}")
    print(f"   结论: 学生1得分更高,因为数学成绩好\n")
    
    # 归一化后的权重(所有权重相同)
    weights_normalized = torch.tensor([0.2, 0.2, 0.2, 0.2, 0.2])
    
    print(f"4. 归一化后的模型(平等对待):")
    print(f"   权重: {weights_normalized.tolist()}")
    print(f"   所有权重相同\n")
    
    # 预测
    score1_norm = torch.sum(student1 * weights_normalized)
    score2_norm = torch.sum(student2 * weights_normalized)
    
    print(f"5. 预测结果:")
    print(f"   学生1得分: {score1_norm.item():.2f}")
    print(f"   学生2得分: {score2_norm.item():.2f}")
    print(f"   差值: {score1_norm.item() - score2_norm.item():.2f}")
    print(f"   结论: 两个学生得分相同,公平\n")
    
    print(f"6. 总结:")
    print(f"   - 不归一化:模型偏向数学,学生1得分更高")
    print(f"   - 归一化:模型平等对待,两个学生得分相同")
    print(f"   - 原因:数学分数大,产生更大梯度,权重更新更多")

if __name__ == "__main__":
    demo_prediction_example()
=== 简单例子:预测学生是否优秀 ===

1. 两个学生的成绩:
   学生1: 数学90, 英语80, 物理70, 化学60, 生物50
   学生2: 数学50, 英语60, 物理70, 化学80, 生物90
   学生1总分: 350
   学生2总分: 350
   说明: 两个学生总分相同,但成绩分布不同

2. 不归一化的模型(偏向数学):
   权重: [0.3, 0.2, 0.2, 0.2, 0.1]
   数学权重: 0.3 (最大)
   生物权重: 0.1 (最小)

3. 预测结果:
   学生1得分: 74.00
   学生2得分: 66.00
   差值: 8.00
   结论: 学生1得分更高,因为数学成绩好

4. 归一化后的模型(平等对待):
   权重: [0.2, 0.2, 0.2, 0.2, 0.2]
   所有权重相同

5. 预测结果:
   学生1得分: 70.00
   学生2得分: 70.00
   差值: 0.00
   结论: 两个学生得分相同,公平

6. 总结:
   - 不归一化:模型偏向数学,学生1得分更高
   - 归一化:模型平等对待,两个学生得分相同
   - 原因:数学分数大,产生更大梯度,权重更新更多

我们也可以从原理进行分析归一化和不归一化的差异:

1、梯度计算

不归一化

损失函数: L = (预测值 - 目标值)²
梯度: ∂L/∂wᵢ = 2 × (预测值 - 目标值) × xᵢ

其中:
- wᵢ: 第i个特征的权重
- xᵢ: 第i个特征的值

数学的xᵢ = 90,生物的xᵢ = 50
数学的梯度 = 2 × (预测值 - 目标值) × 90
生物的梯度 = 2 × (预测值 - 目标值) × 50

数学的梯度是生物的 90/50 = 1.8 倍

归一化

归一化后:
数学的xᵢ ≈ 1.26,生物的xᵢ ≈ -1.26
数学的梯度 = 2 × (预测值 - 目标值) × 1.26
生物的梯度 = 2 × (预测值 - 目标值) × (-1.26)

数学和生物的梯度大小相似

2、权重更新

不归一化

权重更新: wᵢ = wᵢ - learning_rate × ∂L/∂wᵢ

数学的权重更新 = learning_rate × 大梯度
生物的权重更新 = learning_rate × 小梯度

结果:数学的权重更新更多,模型偏向数学

归一化

权重更新: wᵢ = wᵢ - learning_rate × ∂L/∂wᵢ

数学的权重更新 = learning_rate × 中等梯度
生物的权重更新 = learning_rate × 中等梯度

结果:所有权重更新相似,模型平等对待

综合上述,不归一化导致模型偏向数学的原因:

1、述职范围不同,数学 90 分,生物 50 分,差值 40 分

2、梯度大小不同:数学的梯度是生物的 1.8 倍

3、权重更新不同:数学的权重更新更多

4、模型偏向:最终训练出来的模型更偏向数学,认为数学更重要

所以,归一化让模型平等对待所有特征,不会偏向数值大的特征;具体哪个特征重要,是模型在训练过程中通过权重学习获得的,权重越大,特征越重要,而不是由数值大小决定重要性。

可以不归一化的情况

  • 数值大小确实代表重要性(如词频)
  • 特征已经归一化
  • 二值特征(0/1)