模型训练 loss 不降?神经网络基础组件调优指南
模型训练 loss 不降?神经网络基础组件调优指南
关键词:神经网络、激活函数、损失函数、梯度下降、反向传播、正则化、PyTorch
目录
- 一、神经网络到底在做什么?
- 二、激活函数:为什么非线性如此重要?
- 三、参数初始化:模型训练的起点决定终点
- 四、损失函数:模型优化的指南针
- 五、反向传播与优化算法
- 六、正则化:防止模型"死记硬背"
- 七、实战:电商商品定价区间预测
- 常见问题
- 和 AI 大模型开发的关系
- 总结
一、神经网络到底在做什么?
1.1 从生物神经元到人工神经元
人脑由约 860 亿个神经元组成,每个神经元通过树突接收信号,在细胞体中整合,当电位超过阈值时通过轴突输出电脉冲。人工神经网络(ANN)正是对这一机制的数学抽象:
- 输入:来自其他神经元或外部数据的信号 (x_i)
- 权重:每个输入的重要性系数 (w_i)
- 偏置:激活阈值的调整项 (b)
- 激活函数:决定是否"发放"信号的非线性变换 (f)
一个神经元的前向计算可以表示为:
内部状态值:z = w·x + b
激活值:a = f(z)
内部状态值是加权求和的结果,激活值则是经过非线性变换后的最终输出。这两个值在反向传播时都会产生对应的梯度,驱动参数更新。
1.2 全连接网络的结构与数据流动
将多个神经元按层组织,就构成了全连接神经网络(FCNN):
输入层 → 隐藏层1 → 隐藏层2 → ... → 输出层
核心特点:
- 同层神经元之间无连接
- 相邻层之间全连接(每个神经元与上一层所有神经元相连)
- 数据以二维张量形式逐层传递
- 信息单向流动,从输入到输出
数据在网络中的形状变化是一个容易被忽视但至关重要的细节。假设输入一个 batch 为 5、特征维度为 3 的样本,经过隐藏层(3→3→2)和输出层(2→2),最终得到 5×2 的预测结果。理解这种维度变换,是调试网络结构错误的基础。
二、激活函数:为什么非线性如此重要?
没有激活函数的神经网络,无论堆叠多少层,本质上等价于一个线性模型:(y = W_n … W_2 W_1 x),仍然是线性变换。激活函数为网络注入非线性,使其能够逼近任意复杂函数。
2.1 Sigmoid:经典但需谨慎使用
Sigmoid 将任意输入压缩到 (0, 1) 区间,形似"S"曲线:
import torch
import matplotlib.pyplot as plt
x = torch.linspace(-20, 20, 1000)
y = torch.sigmoid(x)
# 导数图像
x_grad = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.sigmoid(x_grad).sum().backward()
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(x.detach(), y.detach())
plt.title('Sigmoid 函数')
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(x_grad.detach(), x_grad.grad)
plt.title('Sigmoid 导数 (最大仅 0.25)')
plt.grid(True)
plt.show()
使用注意:
- 输入超出 [-6, 6] 时输出饱和,信息丢失
- 导数最大仅 0.25,多层连乘后梯度迅速衰减(梯度消失)
- 输出不以零为中心,导致梯度更新方向一致
- 仅推荐用于二分类输出层
2.2 Tanh:以零为中心的双曲正切
Tanh 将输入映射到 (-1, 1),以零为中心,收敛速度优于 Sigmoid:
x = torch.linspace(-20, 20, 1000)
y = torch.tanh(x)
虽然 Tanh 解决了零中心化问题,但两侧导数同样趋近于零,深层网络中仍会出现梯度消失。实践中可在隐藏层使用 Tanh,输出层配合 Sigmoid 处理二分类。
2.3 ReLU:深度网络的默认选择
ReLU(Rectified Linear Unit)的公式极其简单:(f(x) = max(0, x))
x = torch.linspace(-20, 20, 1000)
y = torch.relu(x)
为什么 ReLU 成为主流?
| 特性 | Sigmoid | ReLU |
|---|---|---|
| 计算复杂度 | 高(指数运算) | 低(比较操作) |
| 梯度消失 | 严重(导数<0.25) | 正区间梯度恒为 1 |
| 稀疏性 | 无 | 负区间输出为 0 |
| 零中心化 | 否 | 否 |
ReLU 的"神经元死亡"问题(负区间永久失活)可通过 Leaky ReLU 缓解:(f(x) = max(\alpha x, x)),其中 (\alpha) 通常取 0.01。
2.4 Softmax:多分类的概率归一化
Softmax 将网络输出的原始分数(logits)转换为概率分布:
scores = torch.tensor([0.2, 0.02, 0.15, 1.3, 3.75])
probabilities = torch.softmax(scores, dim=0)
print(probabilities)
# tensor([0.0212, 0.0177, 0.0202, 0.0638, 0.7392])
所有输出值落在 (0, 1) 之间且和为 1,天然符合概率定义。多分类任务的输出层标配。
激活函数选择速查:
- 隐藏层:优先 ReLU,效果不好时尝试 Leaky ReLU
- 二分类输出:Sigmoid
- 多分类输出:Softmax
- 回归输出:线性输出(无激活函数)
三、参数初始化:模型训练的起点决定终点
3.1 随机初始化与对称性破除
全零或全一初始化会导致所有神经元学习相同的特征,无法打破对称性。随机初始化是训练神经网络的第一步:
import torch.nn as nn
linear = nn.Linear(5, 3)
# 均匀分布随机初始化
nn.init.uniform_(linear.weight)
# 正态分布随机初始化
nn.init.normal_(linear.weight, mean=0, std=1)
但随机范围不当仍会引发问题:权重过大导致梯度爆炸,过小导致梯度消失。
3.2 Xavier 与 Kaiming:深度网络的标配
Xavier(Glorot)初始化:适用于 Sigmoid/Tanh 激活函数
# 正态分布 Xavier:std = sqrt(2 / (fan_in + fan_out))
nn.init.xavier_normal_(linear.weight)
# 均匀分布 Xavier:limit = sqrt(6 / (fan_in + fan_out))
nn.init.xavier_uniform_(linear.weight)
Kaiming(He)初始化:专为 ReLU 及其变体设计
# 正态分布 He:std = sqrt(2 / fan_in)
nn.init.kaiming_normal_(linear.weight, nonlinearity='relu')
# 均匀分布 He:limit = sqrt(6 / fan_in)
nn.init.kaiming_uniform_(linear.weight, nonlinearity='relu')
其中 fan_in 是输入神经元数量,fan_out 是输出神经元数量。深层网络(10 层以上)必须使用这两种初始化之一,否则训练难以收敛。
初始化选择策略:
- Sigmoid/Tanh → Xavier
- ReLU/Leaky ReLU → Kaiming
- 浅层网络(<5 层)→ 随机初始化即可
四、损失函数:模型优化的指南针
损失函数衡量预测值与真实值的差距,是梯度下降优化的直接目标。
4.1 分类任务:交叉熵损失
多分类交叉熵(Softmax + 交叉熵):
import torch
from torch import nn
# nn.CrossEntropyLoss 内部已包含 Softmax
y_true = torch.tensor([1, 2], dtype=torch.int64)
y_pred = torch.tensor([[0.2, 0.6, 0.2], [0.1, 0.8, 0.1]], dtype=torch.float32)
loss = nn.CrossEntropyLoss()
print(loss(y_pred, y_true)) # 标量损失值
注意 CrossEntropyLoss 的输入是原始 logits(未经 Softmax),内部会自动计算 Softmax 和交叉熵。
二分类交叉熵:
y_true = torch.tensor([0, 1, 0], dtype=torch.float32)
y_pred = torch.tensor([0.69, 0.55, 0.25], dtype=torch.float32)
loss = nn.BCELoss()
print(loss(y_pred, y_true))
此时输入必须是经过 Sigmoid 后的概率值。
4.2 回归任务:MAE、MSE 与 Smooth L1
| 损失函数 | 公式特点 | 适用场景 |
|---|---|---|
| MAE (L1) | 绝对误差 | 存在异常值,需鲁棒性 |
| MSE (L2) | 平方误差 | 标准回归,梯度敏感 |
| Smooth L1 | 分段函数:小误差用 L2,大误差用 L1 | 目标检测等,兼顾平滑与鲁棒 |
# MAE
mae = nn.L1Loss()
# MSE
mse = nn.MSELoss()
# Smooth L1
smooth_l1 = nn.SmoothL1Loss()
MSE 对离群点敏感(平方放大误差),MAE 在零点不可导,Smooth L1 结合两者优点,是目标检测等任务的首选。
五、反向传播与优化算法
5.1 链式法则与梯度回传
反向传播(Back Propagation)的核心是链式法则:从输出层开始,逐层向前计算损失函数对每个参数的偏导数。
假设一个简化网络:输入 → 隐藏层(Sigmoid)→ 输出层(Sigmoid),损失为均方误差。
前向传播计算预测值,反向传播则:
- 计算损失对输出的梯度
- 通过链式法则传播到输出层权重
- 继续向前传播到隐藏层权重
当网络存在多条路径(如隐藏层有多个神经元连接到输出),梯度会沿所有路径累加。
5.2 从 SGD 到 Adam:优化器演进
基础梯度下降直接使用当前梯度更新参数,容易在平缓区域、鞍点或峡谷处停滞。
Momentum:引入动量项,累积历史梯度
optimizer = torch.optim.SGD(params, lr=0.01, momentum=0.9)
动量相当于给梯度更新增加了"惯性",有助于跨越鞍点和加速收敛。
AdaGrad:为每个参数维护独立的学习率,梯度大的参数学习率自动减小
optimizer = torch.optim.Adagrad(params, lr=0.01)
缺点是学习率单调递减,后期可能过早停滞。
RMSProp:用指数加权平均替代 AdaGrad 的累积平方梯度
optimizer = torch.optim.RMSprop(params, lr=0.01, alpha=0.9)
解决了 AdaGrad 学习率过早衰减的问题。
Adam:Momentum + RMSProp 的结合
optimizer = torch.optim.Adam(params, lr=0.01, betas=(0.9, 0.999))
- 一阶矩估计(动量)修正梯度方向
- 二阶矩估计(梯度平方的指数平均)自适应调整学习率
- 偏差校正处理初始阶段的零偏问题
Adam 是大多数深度学习任务的默认选择,收敛快且对超参数不敏感。
5.3 学习率衰减:让收敛更稳定
固定学习率的问题:初期需要大步长快速接近最优解,后期需要小步长精细调整。
等间隔衰减:
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
# 每 50 个 epoch,学习率乘以 0.5
指定间隔衰减:
scheduler = torch.optim.lr_scheduler.MultiStepLR(
optimizer, milestones=[50, 125, 160], gamma=0.5
)
# 在第 50、125、160 个 epoch 衰减
指数衰减:
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)
# 每个 epoch,lr = lr * 0.95
学习率衰减策略的选择取决于任务特性:简单任务用 Step Decay,需要精细调优时用 MultiStep,希望平滑过渡时用 Exponential。
六、正则化:防止模型"死记硬背"
6.1 Dropout:随机失活的魔法
训练时以概率 (p) 随机将神经元输出置零,其余神经元输出缩放 (1/(1-p)):
dropout = nn.Dropout(p=0.4)
x = torch.randn(1, 128)
training_output = dropout(x) # 约 40% 的元素变为 0
为什么有效?
- 每次训练不同的"子网络",相当于集成学习
- 阻止神经元之间的共适应,强制学习更鲁棒的特征
- 测试时关闭 Dropout,使用全部神经元
典型配置:全连接层后接 Dropout,概率 0.2~0.5。深层网络可用更高概率。
6.2 Batch Normalization:批量归一化
对每个 batch 的数据进行标准化(均值为 0,方差为 1),再经过可学习的缩放和平移:
# 全连接层后使用
bn1d = nn.BatchNorm1d(num_features=128)
# 卷积层后使用
bn2d = nn.BatchNorm2d(num_features=64)
核心价值:
- 缓解内部协变量偏移,加速训练收敛
- 允许使用更大的学习率
- 具有一定的正则化效果,减少对 Dropout 的依赖
- 对初始化不那么敏感
使用位置:Linear/Conv → BatchNorm → ReLU(激活函数之前)
七、实战:电商商品定价区间预测
下面用一个完整的案例串联上述知识点:根据商品特征预测定价区间(多分类问题)。
数据准备:
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
import torch.optim as optim
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np
def create_dataset():
"""构建电商商品定价数据集"""
data = pd.read_csv('./data/product_pricing.csv')
x, y = data.iloc[:, :-1], data.iloc[:, -1]
# 数据标准化
scaler = StandardScaler()
x = scaler.fit_transform(x.astype(np.float32))
x_train, x_valid, y_train, y_valid = train_test_split(
x, y, train_size=0.8, random_state=42, stratify=y
)
train_dataset = TensorDataset(
torch.from_numpy(x_train), torch.tensor(y_train.values, dtype=torch.long)
)
valid_dataset = TensorDataset(
torch.from_numpy(x_valid), torch.tensor(y_valid.values, dtype=torch.long)
)
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))
模型定义:
class PricingModel(nn.Module):
"""商品定价区间预测模型"""
def __init__(self, input_dim, output_dim):
super(PricingModel, self).__init__()
self.linear1 = nn.Linear(input_dim, 128)
self.bn1 = nn.BatchNorm1d(128)
self.linear2 = nn.Linear(128, 256)
self.bn2 = nn.BatchNorm1d(256)
self.linear3 = nn.Linear(256, 128)
self.bn3 = nn.BatchNorm1d(128)
self.linear4 = nn.Linear(128, output_dim)
self.dropout = nn.Dropout(0.3)
def forward(self, x):
x = torch.relu(self.bn1(self.linear1(x)))
x = self.dropout(x)
x = torch.relu(self.bn2(self.linear2(x)))
x = self.dropout(x)
x = torch.relu(self.bn3(self.linear3(x)))
x = self.dropout(x)
output = self.linear4(x)
return output
训练流程:
def train_model(train_dataset, input_dim, class_num):
torch.manual_seed(42)
dataloader = DataLoader(train_dataset, shuffle=True, batch_size=32)
model = PricingModel(input_dim, class_num)
# 交叉熵损失(内部包含 Softmax)
criterion = nn.CrossEntropyLoss()
# Adam 优化器
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 学习率衰减
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5)
num_epochs = 100
for epoch in range(num_epochs):
model.train()
total_loss = 0.0
for x, y in dataloader:
optimizer.zero_grad()
output = model(x)
loss = criterion(output, y)
loss.backward()
optimizer.step()
total_loss += loss.item()
scheduler.step()
if (epoch + 1) % 20 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(dataloader):.4f}')
torch.save(model.state_dict(), './model/pricing_model.pth')
return model
评估:
def evaluate(model, valid_dataset):
model.eval()
dataloader = DataLoader(valid_dataset, batch_size=32, shuffle=False)
correct = 0
total = 0
with torch.no_grad():
for x, y in dataloader:
output = model(x)
_, predicted = torch.max(output, dim=1)
correct += (predicted == y).sum().item()
total += y.size(0)
accuracy = correct / total
print(f'验证集准确率: {accuracy:.4f}')
return accuracy
这个案例综合运用了:数据标准化、BatchNorm、Dropout、ReLU 激活、CrossEntropyLoss、Adam 优化器和学习率衰减。
常见问题
Q1:训练时 loss 完全不下降,可能是什么原因?
最常见的原因有:
- 学习率过大:导致参数在最优解附近震荡甚至发散,尝试减小到 1e-4 或 1e-5
- 梯度消失:深层网络使用 Sigmoid/Tanh 且未做合适的初始化,换 ReLU + Kaiming 初始化
- 数据未标准化:不同特征尺度差异大,某些特征主导梯度,使用 StandardScaler
- 标签格式错误:CrossEntropyLoss 要求标签是 LongTensor,且值为类别索引而非 one-hot
Q2:验证集准确率远低于训练集,怎么解决过拟合?
- 增加 Dropout 概率(如从 0.2 提高到 0.5)
- 添加 L2 正则化(weight_decay=1e-4)
- 增加训练数据或使用数据增强
- 减少网络层数或神经元数量
- 早停(Early Stopping):监控验证集 loss,不再下降时停止训练
Q3:BatchNorm 应该放在激活函数前还是后?
推荐顺序:Linear/Conv → BatchNorm → ReLU。BatchNorm 对输入进行标准化,使激活函数的输入分布更稳定。放在激活函数后可能导致 ReLU 的负值信息被永久截断。
Q4:为什么测试时必须调用 model.eval()?
model.eval() 会关闭 Dropout(使用全部神经元)和 BatchNorm(使用训练时统计的全局均值方差)。如果忘记调用,测试结果会不一致且通常更差。
和 AI 大模型开发的关系
神经网络基础组件在大模型开发中无处不在:
场景 1:LoRA 微调中的梯度计算
class LoRALayer(nn.Module):
"""低秩适配层,微调大模型时只训练少量参数"""
def __init__(self, in_features, out_features, rank=8):
super().__init__()
self.lora_A = nn.Parameter(torch.randn(in_features, rank))
self.lora_B = nn.Parameter(torch.zeros(rank, out_features))
# 使用 Kaiming 初始化 A,B 初始化为零保证训练开始时不影响原模型
nn.init.kaiming_uniform_(self.lora_A, a=5**0.5)
def forward(self, x, original_output):
# 低秩更新:W = W_0 + BA
lora_update = x @ self.lora_A @ self.lora_B
return original_output + lora_update
场景 2:Transformer 中的 LayerNorm 与注意力
class SimpleAttention(nn.Module):
"""简化版自注意力机制"""
def __init__(self, embed_dim):
super().__init__()
self.q_proj = nn.Linear(embed_dim, embed_dim)
self.k_proj = nn.Linear(embed_dim, embed_dim)
self.v_proj = nn.Linear(embed_dim, embed_dim)
self.out_proj = nn.Linear(embed_dim, embed_dim)
self.norm = nn.LayerNorm(embed_dim) # LayerNorm 是 BatchNorm 的变体
def forward(self, x):
# 预归一化(现代 Transformer 的标准做法)
x_norm = self.norm(x)
Q = self.q_proj(x_norm)
K = self.k_proj(x_norm)
V = self.v_proj(x_norm)
# 缩放点积注意力
scores = Q @ K.transpose(-2, -1) / (Q.size(-1) ** 0.5)
attn_weights = torch.softmax(scores, dim=-1)
out = attn_weights @ V
return self.out_proj(out) + x # 残差连接
场景 3:大模型推理时的 KV Cache 管理
class KVCache:
"""Transformer 推理时的键值缓存,避免重复计算"""
def __init__(self, max_batch_size, max_seq_len, num_heads, head_dim):
self.k_cache = torch.zeros(max_batch_size, num_heads, max_seq_len, head_dim)
self.v_cache = torch.zeros(max_batch_size, num_heads, max_seq_len, head_dim)
self.current_len = 0
def update(self, new_k, new_v):
"""追加新的 K/V,而非重新计算全部"""
self.k_cache[:, :, self.current_len, :] = new_k
self.v_cache[:, :, self.current_len, :] = new_v
self.current_len += 1
def get(self):
"""获取当前有效的 K/V"""
return self.k_cache[:, :, :self.current_len, :], \
self.v_cache[:, :, :self.current_len, :]
场景 4:RLHF 中的策略梯度优化
def ppo_loss(old_logprobs, new_logprobs, advantages, epsilon=0.2):
"""PPO 裁剪目标函数,稳定强化学习训练"""
ratio = torch.exp(new_logprobs - old_logprobs)
# 裁剪防止策略更新过大
clipped_ratio = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)
loss1 = ratio * advantages
loss2 = clipped_ratio * advantages
return -torch.min(loss1, loss2).mean() # 最大化奖励
总结
本文系统梳理了神经网络的核心基础组件:
- 激活函数为网络注入非线性,ReLU 是隐藏层的默认选择,Softmax/Sigmoid 用于输出层
- 参数初始化决定训练起点,深层网络必须使用 Xavier 或 Kaiming 初始化
- 损失函数是优化的目标,分类用交叉熵,回归用 MSE/Smooth L1
- 反向传播通过链式法则高效计算梯度,驱动参数更新
- 优化器从 SGD 演进到 Adam,自适应学习率让训练更稳定
- 正则化(Dropout、BatchNorm)防止过拟合,提升泛化能力
掌握这些基础后,你将能够理解从简单的全连接网络到复杂的 Transformer 大模型的共同底层逻辑。无论是微调 BERT 做文本分类,还是训练扩散模型生成图像,这些组件都是不可或缺的基石。
#神经网络 #深度学习 #PyTorch #激活函数 #损失函数 #梯度下降 #正则化 #机器学习
更多推荐


所有评论(0)