模型训练 loss 不降?神经网络基础组件调优指南

关键词:神经网络、激活函数、损失函数、梯度下降、反向传播、正则化、PyTorch


目录


一、神经网络到底在做什么?

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),损失为均方误差。

前向传播计算预测值,反向传播则:

  1. 计算损失对输出的梯度
  2. 通过链式法则传播到输出层权重
  3. 继续向前传播到隐藏层权重

当网络存在多条路径(如隐藏层有多个神经元连接到输出),梯度会沿所有路径累加。

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()  # 最大化奖励

总结

本文系统梳理了神经网络的核心基础组件:

  1. 激活函数为网络注入非线性,ReLU 是隐藏层的默认选择,Softmax/Sigmoid 用于输出层
  2. 参数初始化决定训练起点,深层网络必须使用 Xavier 或 Kaiming 初始化
  3. 损失函数是优化的目标,分类用交叉熵,回归用 MSE/Smooth L1
  4. 反向传播通过链式法则高效计算梯度,驱动参数更新
  5. 优化器从 SGD 演进到 Adam,自适应学习率让训练更稳定
  6. 正则化(Dropout、BatchNorm)防止过拟合,提升泛化能力

掌握这些基础后,你将能够理解从简单的全连接网络到复杂的 Transformer 大模型的共同底层逻辑。无论是微调 BERT 做文本分类,还是训练扩散模型生成图像,这些组件都是不可或缺的基石。


#神经网络 #深度学习 #PyTorch #激活函数 #损失函数 #梯度下降 #正则化 #机器学习

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐