归一化原理
归一化按照归一化的维度区分,有层归一化(Layer Norm)和批归一化(Batch Norm)。
| 特性 | 层归一化 | 批归一化 |
|---|---|---|
| 归一化维度 | 每个样本的所有特征 | 每个特征的所有样本 |
| 计算方式 | 对每个样本独立归一化 | 对每个特征独立归一化 |
| 适用场景 | 变长序列,小批量 | 固定长度,大批量 |
| Transformer | 适用 | 不适用 |
| RNN | 适用 | 不适用 |
| CNN | 很少使用 | 常用 |
在层归一化中,对于每个样本:
- 计算均值: μ = (1/n) * Σxᵢ
- 计算方差: σ² = (1/n) * Σ(xᵢ - μ)²
- 归一化: x̂ᵢ = (xᵢ - μ) / √(σ² + ε)
- 缩放和平移: 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)