返回学习路线
7模型进阶

第 7 周:深度学习基础理论

反向传播、损失函数、优化器原理

深度学习作为机器学习的重要分支,通过多层神经网络学习数据的层次化特征表示。本章从神经网络的基本原理出发,系统介绍深度学习的核心概念、训练方法和主流架构,为后续学习CNN、RNN和Transformer等高级模型奠定基础。

7.1 神经网络基础

7.1.1 感知机与多层感知机

感知机(Perceptron)是神经网络的基本单元,由Rosenblatt于1958年提出。感知机接收多个输入信号,通过加权求和并经过激活函数产生输出。数学表达式为:

y=f(i=1nwixi+b)=f(wTx+b)y = f\left(\sum_{i=1}^{n} w_i x_i + b\right) = f(\mathbf{w}^T \mathbf{x} + b)

其中,x=[x1,x2,...,xn]T\mathbf{x} = [x_1, x_2, ..., x_n]^T 是输入向量,w=[w1,w2,...,wn]T\mathbf{w} = [w_1, w_2, ..., w_n]^T 是权重向量,bb 是偏置项,f()f(\cdot) 是激活函数。

单层感知机只能解决线性可分问题。对于非线性问题,需要引入多层感知机(Multi-Layer Perceptron, MLP)。MLP由输入层、一个或多个隐藏层和输出层组成,相邻层之间全连接。设网络有 LL 层,第 ll 层的计算为:

z(l)=W(l)a(l1)+b(l)\mathbf{z}^{(l)} = \mathbf{W}^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)} a(l)=f(z(l))\mathbf{a}^{(l)} = f(\mathbf{z}^{(l)})

其中,W(l)Rnl×nl1\mathbf{W}^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}} 是权重矩阵,b(l)Rnl\mathbf{b}^{(l)} \in \mathbb{R}^{n_l} 是偏置向量,a(l)\mathbf{a}^{(l)} 是第 ll 层的激活值。

万能近似定理(Universal Approximation Theorem)指出:一个具有足够多隐藏神经元的前馈神经网络,配合非线性激活函数,可以以任意精度逼近任意连续函数。这从理论上保证了神经网络的表达能力。

7.1.2 激活函数与选择

激活函数为神经网络引入非线性,使其能够学习复杂的模式。没有激活函数,多层网络将退化为单层线性模型。以下是主流激活函数的分析:

Sigmoid函数

σ(x)=11+ex\sigma(x) = \frac{1}{1 + e^{-x}}

Sigmoid将输入映射到 (0,1)(0, 1) 区间,适合输出概率的场景。但其存在明显的梯度消失问题:当 x|x| 较大时,导数 σ(x)=σ(x)(1σ(x))\sigma'(x) = \sigma(x)(1-\sigma(x)) 趋近于0,导致深层网络训练困难。

Tanh函数

tanh(x)=exexex+ex\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}

Tanh将输入映射到 (1,1)(-1, 1),输出零中心化,收敛速度优于Sigmoid。但同样存在梯度消失问题。

ReLU(Rectified Linear Unit)

ReLU(x)=max(0,x)\text{ReLU}(x) = \max(0, x)

ReLU是深度学习中最常用的激活函数。其优势包括:

  • 计算简单,梯度不会饱和(正区间导数为1)
  • 引入稀疏性,部分神经元输出为0
  • 加速收敛,缓解梯度消失

但ReLU存在"神经元死亡"问题:当输入持续为负时,神经元永远不被激活,梯度始终为0。

Leaky ReLU

LeakyReLU(x)={xx>0αxx0\text{LeakyReLU}(x) = \begin{cases} x & x > 0 \\ \alpha x & x \leq 0 \end{cases}

通过给负区间一个小的斜率 α\alpha(通常0.01),解决神经元死亡问题。

GELU(Gaussian Error Linear Unit)

GELU(x)=xΦ(x)=x12(1+erf(x2))\text{GELU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}\left(1 + \text{erf}\left(\frac{x}{\sqrt{2}}\right)\right)

GELU被BERT、GPT等Transformer模型广泛采用。其特点包括:

  • 平滑且处处可导,有利于优化
  • 非单调性,能捕捉更复杂的模式
  • 输出近似零中心化

实际实现常用近似公式: GELU(x)0.5x(1+tanh(2π(x+0.044715x3)))\text{GELU}(x) \approx 0.5x\left(1 + \tanh\left(\sqrt{\frac{2}{\pi}}(x + 0.044715x^3)\right)\right)

SiLU(Swish)

SiLU(x)=xσ(x)=x1+ex\text{SiLU}(x) = x \cdot \sigma(x) = \frac{x}{1 + e^{-x}}

SiLU与GELU性质相似,都是平滑的非单调函数。研究表明,SiLU和GELU可以作为ReLU的上位替代,在多数任务上表现更优。

常见激活函数对比

图7-1 常见激活函数形状对比。从左到右、从上到下依次为Sigmoid、Tanh、ReLU、Leaky ReLU、GELU和SiLU。

激活函数与导数对比

图7-2 激活函数(左)及其导数(右)对比。Sigmoid和Tanh的导数在输入较大时迅速衰减,而ReLU、GELU和SiLU的梯度分布更均匀。

激活函数选择建议

激活函数优点缺点适用场景
ReLU计算快、缓解梯度消失神经元死亡隐藏层默认选择
Leaky ReLU解决神经元死亡需调参ReLU的改进版
GELU平滑、性能好计算稍复杂Transformer、BERT
SiLU平滑、非单调计算稍复杂现代CNN、LLM
Sigmoid输出概率梯度消失二分类输出层
Tanh零中心化梯度消失循环神经网络

7.1.3 损失函数设计

损失函数衡量模型预测与真实值之间的差距,指导网络参数优化。

回归损失函数

均方误差(MSE): LMSE=1Ni=1N(yiy^i)2\mathcal{L}_{\text{MSE}} = \frac{1}{N}\sum_{i=1}^{N}(y_i - \hat{y}_i)^2

MSE对异常值敏感,梯度随误差增大而线性增长。

平均绝对误差(MAE): LMAE=1Ni=1Nyiy^i\mathcal{L}_{\text{MAE}} = \frac{1}{N}\sum_{i=1}^{N}|y_i - \hat{y}_i|

MAE对异常值更鲁棒,但在零点不可导。

Huber损失结合两者优点: LHuber={12(yy^)2yy^δδyy^12δ2otherwise\mathcal{L}_{\text{Huber}} = \begin{cases} \frac{1}{2}(y - \hat{y})^2 & |y - \hat{y}| \leq \delta \\ \delta|y - \hat{y}| - \frac{1}{2}\delta^2 & \text{otherwise} \end{cases}

分类损失函数

交叉熵损失(Cross-Entropy): LCE=1Ni=1Nc=1Cyi,clog(y^i,c)\mathcal{L}_{\text{CE}} = -\frac{1}{N}\sum_{i=1}^{N}\sum_{c=1}^{C} y_{i,c} \log(\hat{y}_{i,c})

其中,CC 是类别数,yi,cy_{i,c} 是one-hot标签,y^i,c\hat{y}_{i,c} 是softmax输出的概率。

对于二分类问题,二元交叉熵(BCE)为: LBCE=1Ni=1N[yilog(y^i)+(1yi)log(1y^i)]\mathcal{L}_{\text{BCE}} = -\frac{1}{N}\sum_{i=1}^{N}\left[y_i \log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i)\right]

标签平滑(Label Smoothing)是一种正则化技术,将硬标签 [0,1][0, 1] 替换为软标签 [ϵ,1ϵ][\epsilon, 1-\epsilon],防止模型过度自信: y=(1ϵ)y+ϵ/Cy' = (1-\epsilon) \cdot y + \epsilon / C

7.1.4 网络初始化策略

良好的参数初始化对训练深度网络至关重要。不当的初始化可能导致梯度消失或爆炸。

Xavier初始化(Glorot初始化):

假设激活函数是线性的,Xavier初始化使每层输出的方差与输入相等。对于均匀分布: WU[6nin+nout,6nin+nout]W \sim U\left[-\sqrt{\frac{6}{n_{in} + n_{out}}}, \sqrt{\frac{6}{n_{in} + n_{out}}}\right]

对于正态分布: WN(0,2nin+nout)W \sim N\left(0, \sqrt{\frac{2}{n_{in} + n_{out}}}\right)

Xavier初始化适用于Sigmoid和Tanh等对称激活函数。

He初始化(Kaiming初始化):

针对ReLU激活函数设计,考虑ReLU将负值置零的特点: WN(0,2nin)W \sim N\left(0, \sqrt{\frac{2}{n_{in}}}\right)

He初始化是ReLU网络的默认选择,能有效缓解前向传播中方差衰减的问题。

PyTorch初始化示例

import torch
import torch.nn as nn

# Xavier初始化
layer = nn.Linear(784, 256)
nn.init.xavier_uniform_(layer.weight)
nn.init.zeros_(layer.bias)

# He初始化
layer = nn.Linear(784, 256)
nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')
nn.init.zeros_(layer.bias)

7.2 深度网络训练

7.2.1 训练流程与数据加载

深度学习训练遵循迭代优化范式:前向传播计算预测,反向传播更新参数。

数据加载:PyTorch提供 DatasetDataLoader 实现高效数据加载:

from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
    def __init__(self, data, labels, transform=None):
        self.data = data
        self.labels = labels
        self.transform = transform
    
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        sample = self.data[idx]
        label = self.labels[idx]
        if self.transform:
            sample = self.transform(sample)
        return sample, label

# 创建DataLoader
dataset = CustomDataset(data, labels)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)

训练循环基本结构

for epoch in range(num_epochs):
    model.train()
    for batch_idx, (data, target) in enumerate(dataloader):
        # 前向传播
        output = model(data)
        loss = criterion(output, target)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

7.2.2 学习率调度策略

学习率是最重要的超参数之一。固定学习率往往不够灵活,学习率调度策略能显著提升训练效果。

StepLR:每隔固定步数衰减学习率

ηnew=ηold×γepoch/step_size\eta_{new} = \eta_{old} \times \gamma^{\lfloor \text{epoch} / \text{step\_size} \rfloor}

CosineAnnealing:按余弦曲线衰减

ηt=ηmin+12(ηmaxηmin)(1+cos(tTmaxπ))\eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})\left(1 + \cos\left(\frac{t}{T_{max}}\pi\right)\right)

Warmup + Cosine:训练初期线性增加学习率,之后按余弦衰减

from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR

# StepLR
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)

# CosineAnnealing
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)

# Warmup + Cosine (自定义实现)
class WarmupCosineScheduler:
    def __init__(self, optimizer, warmup_epochs, total_epochs, base_lr, min_lr):
        self.optimizer = optimizer
        self.warmup_epochs = warmup_epochs
        self.total_epochs = total_epochs
        self.base_lr = base_lr
        self.min_lr = min_lr
    
    def step(self, epoch):
        if epoch < self.warmup_epochs:
            lr = self.base_lr * (epoch + 1) / self.warmup_epochs
        else:
            progress = (epoch - self.warmup_epochs) / (self.total_epochs - self.warmup_epochs)
            lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * (1 + np.cos(np.pi * progress))
        
        for param_group in self.optimizer.param_groups:
            param_group['lr'] = lr
        return lr

学习率调度策略

图7-3 常见学习率调度策略对比。StepLR阶梯式衰减简单直接;CosineAnnealing平滑衰减;Warmup+Cosine结合两者优点,适合大规模训练。

7.2.3 梯度问题与解决方案

梯度消失(Vanishing Gradient):在深层网络中,梯度通过链式法则逐层传播时呈指数级衰减,导致浅层参数难以更新。

原因分析:对于Sigmoid激活函数,最大导数为0.25。在 nn 层网络中,梯度最多衰减 0.25n0.25^n。当 n=10n=10 时,梯度衰减约 10610^{-6}

梯度爆炸(Exploding Gradient):与梯度消失相反,梯度在反向传播中指数级增长,导致参数更新过大,训练不稳定。

梯度问题可视化

图7-4 梯度消失(左)与梯度爆炸(右)问题可视化。Sigmoid在深层网络中梯度迅速衰减至数值精度极限;梯度裁剪能有效控制梯度爆炸。

解决方案

  1. 合适的激活函数:使用ReLU、GELU等梯度不会饱和的激活函数
  2. 批归一化(Batch Normalization):稳定每层的输入分布
  3. 残差连接(Residual Connection):提供梯度捷径
  4. 梯度裁剪(Gradient Clipping):限制梯度范数
  5. 更好的初始化:Xavier、He初始化
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 批归一化
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.bn1 = nn.BatchNorm1d(256)
        self.fc2 = nn.Linear(256, 10)
    
    def forward(self, x):
        x = self.fc1(x)
        x = self.bn1(x)
        x = F.relu(x)
        x = self.fc2(x)
        return x

7.2.4 训练监控与可视化

有效的训练监控能帮助诊断问题、优化超参数。

TensorBoard:PyTorch内置的TensorBoard支持:

from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter('runs/experiment_1')

# 记录标量
writer.add_scalar('Loss/train', loss.item(), epoch)
writer.add_scalar('Accuracy/val', acc, epoch)

# 记录直方图
writer.add_histogram('weights/layer1', model.fc1.weight, epoch)

# 记录图像
writer.add_image('input', img_grid, epoch)

writer.close()

Weights & Biases (WandB):更强大的实验跟踪工具:

import wandb

wandb.init(project="my-project", config={"lr": 0.001, "batch_size": 32})

# 记录指标
wandb.log({"loss": loss.item(), "accuracy": acc})

# 保存模型
wandb.save("model.pt")

训练过程可视化

图7-5 典型训练过程可视化。训练损失和验证损失同步下降表明模型正常学习;验证准确率稳步提升,最终达到95%以上。

7.3 卷积神经网络基础

7.3.1 卷积操作原理

卷积神经网络(CNN)是处理网格结构数据(如图像)的专用神经网络。卷积操作通过局部感受野和权重共享有效提取空间特征。

二维卷积定义: (IK)(i,j)=mnI(i+m,j+n)K(m,n)(I * K)(i, j) = \sum_{m}\sum_{n} I(i+m, j+n) \cdot K(m, n)

其中,II 是输入特征图,KK 是卷积核(滤波器)。

关键超参数

  • 卷积核大小(Kernel Size):通常为3×3或5×5
  • 步长(Stride):卷积核移动的步幅,控制输出尺寸
  • 填充(Padding):在输入边缘补零,控制输出尺寸

输出尺寸计算公式: Hout=Hin+2×paddingkernel_sizestride+1H_{out} = \left\lfloor \frac{H_{in} + 2 \times \text{padding} - \text{kernel\_size}}{\text{stride}} \right\rfloor + 1

卷积操作可视化

图7-6 卷积操作原理可视化。左上为5×5输入特征图,中上为3×3卷积核,右上显示卷积操作过程,左下为输出特征图,中下展示步长为2的下采样效果,右下展示填充为1的尺寸保持效果。

感受野(Receptive Field):输出特征图上某一点对应输入图像的区域大小。感受野计算公式: RFl=RFl1+(kl1)×i=1l1siRF_{l} = RF_{l-1} + (k_l - 1) \times \prod_{i=1}^{l-1} s_i

其中,klk_l 是第 ll 层的卷积核大小,sis_i 是第 ii 层的步长。

7.3.2 池化与下采样

池化操作降低特征图空间维度,减少参数量,提供平移不变性。

最大池化(Max Pooling):取池化窗口内的最大值 yi,j=maxm,nxis+m,js+ny_{i,j} = \max_{m,n} x_{i \cdot s + m, j \cdot s + n}

最大池化保留最强激活响应,对边缘和纹理敏感。

平均池化(Average Pooling):取池化窗口内的平均值 yi,j=1k2m=0k1n=0k1xis+m,js+ny_{i,j} = \frac{1}{k^2}\sum_{m=0}^{k-1}\sum_{n=0}^{k-1} x_{i \cdot s + m, j \cdot s + n}

平均池化平滑特征,保留背景信息。

池化操作可视化

图7-7 池化操作对比。左图为4×4原始特征图,中图为2×2最大池化结果(保留每个区域的最大值),右图为2×2平均池化结果(保留每个区域的平均值)。

全局平均池化(Global Average Pooling, GAP):对整个特征图做平均,将每个特征图转化为一个数值,直接用于分类,减少全连接层参数量。

7.3.3 经典CNN架构

LeNet-5(1998):最早的CNN架构之一,由LeCun等人提出。包含两个卷积层和三个全连接层,用于手写数字识别。

AlexNet(2012):ImageNet竞赛冠军,开启深度学习革命。创新包括:

  • ReLU激活函数
  • Dropout正则化
  • GPU并行训练
  • 数据增强

VGGNet(2014):使用非常小的3×3卷积核堆叠,证明网络深度的重要性。VGG-16和VGG-19成为后续研究的基准架构。

架构年份层数参数量关键创新
LeNet1998860K卷积+池化+全连接
AlexNet2012860MReLU、Dropout、GPU训练
VGG-16201416138M小卷积核(3×3)堆叠
ResNet-5020155025M残差连接

7.4 循环神经网络基础

7.4.1 RNN结构与展开

循环神经网络(RNN)专门处理序列数据,通过隐藏状态传递历史信息。RNN的核心是循环连接,当前时刻的输出依赖于当前输入和上一时刻的隐藏状态。

RNN基本公式ht=f(Whhht1+Wxhxt+bh)\mathbf{h}_t = f(\mathbf{W}_{hh}\mathbf{h}_{t-1} + \mathbf{W}_{xh}\mathbf{x}_t + \mathbf{b}_h) yt=g(Whyht+by)\mathbf{y}_t = g(\mathbf{W}_{hy}\mathbf{h}_t + \mathbf{b}_y)

其中,ht\mathbf{h}_t 是时刻 tt 的隐藏状态,xt\mathbf{x}_t 是输入,yt\mathbf{y}_t 是输出,ffgg 是激活函数。

RNN与LSTM结构

图7-8 RNN展开结构(左)与LSTM门控结构(右)。RNN通过隐藏状态在时序间传递信息;LSTM通过遗忘门、输入门、输出门和细胞状态实现更精细的信息控制。

BPTT算法(Backpropagation Through Time):RNN的反向传播算法。将RNN在时间上展开,然后应用标准反向传播。由于梯度需要沿时间步传播,深层时间序列容易遇到梯度消失或爆炸问题。

7.4.2 LSTM与GRU

长短期记忆网络(LSTM)通过门控机制解决RNN的长期依赖问题。

LSTM核心组件

  1. 遗忘门:决定丢弃多少历史信息 ft=σ(Wf[ht1,xt]+bf)f_t = \sigma(\mathbf{W}_f \cdot [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_f)

  2. 输入门:决定存储多少新信息 it=σ(Wi[ht1,xt]+bi)i_t = \sigma(\mathbf{W}_i \cdot [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_i) C~t=tanh(WC[ht1,xt]+bC)\tilde{\mathbf{C}}_t = \tanh(\mathbf{W}_C \cdot [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_C)

  3. 细胞状态更新Ct=ftCt1+itC~t\mathbf{C}_t = f_t \odot \mathbf{C}_{t-1} + i_t \odot \tilde{\mathbf{C}}_t

  4. 输出门:决定输出什么 ot=σ(Wo[ht1,xt]+bo)o_t = \sigma(\mathbf{W}_o \cdot [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_o) ht=ottanh(Ct)\mathbf{h}_t = o_t \odot \tanh(\mathbf{C}_t)

GRU(Gated Recurrent Unit)是LSTM的简化版本,合并细胞状态和隐藏状态,减少门控数量:

zt=σ(Wz[ht1,xt])z_t = \sigma(\mathbf{W}_z \cdot [\mathbf{h}_{t-1}, \mathbf{x}_t]) rt=σ(Wr[ht1,xt])r_t = \sigma(\mathbf{W}_r \cdot [\mathbf{h}_{t-1}, \mathbf{x}_t]) h~t=tanh(W[rtht1,xt])\tilde{\mathbf{h}}_t = \tanh(\mathbf{W} \cdot [r_t \odot \mathbf{h}_{t-1}, \mathbf{x}_t]) ht=(1zt)ht1+zth~t\mathbf{h}_t = (1 - z_t) \odot \mathbf{h}_{t-1} + z_t \odot \tilde{\mathbf{h}}_t

GRU参数更少,计算更快,在许多任务上与LSTM性能相当。

7.4.3 双向RNN与深层RNN

双向RNN(BiRNN):同时处理正向和反向序列,捕获双向上下文信息。

ht=f(Wxt+Uht1)\overrightarrow{\mathbf{h}}_t = f(\overrightarrow{\mathbf{W}}\mathbf{x}_t + \overrightarrow{\mathbf{U}}\overrightarrow{\mathbf{h}}_{t-1}) ht=f(Wxt+Uht+1)\overleftarrow{\mathbf{h}}_t = f(\overleftarrow{\mathbf{W}}\mathbf{x}_t + \overleftarrow{\mathbf{U}}\overleftarrow{\mathbf{h}}_{t+1}) ht=[ht;ht]\mathbf{h}_t = [\overrightarrow{\mathbf{h}}_t; \overleftarrow{\mathbf{h}}_t]

BiLSTM和BiGRU在自然语言处理任务中广泛应用。

深层RNN:堆叠多个RNN层,学习更复杂的时序模式。

# PyTorch实现
import torch.nn as nn

# 单层LSTM
lstm = nn.LSTM(input_size=128, hidden_size=256, num_layers=1, batch_first=True)

# 双向LSTM
bilstm = nn.LSTM(input_size=128, hidden_size=256, num_layers=2, 
                 batch_first=True, bidirectional=True)

# GRU
gru = nn.GRU(input_size=128, hidden_size=256, num_layers=2, batch_first=True)

7.5 深度学习框架实践

7.5.1 PyTorch核心概念

PyTorch是当前最流行的深度学习框架之一,以其动态计算图和直观的API设计著称。

Tensor:PyTorch的基本数据结构,支持GPU加速。

import torch

# 创建Tensor
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
x = torch.zeros(3, 3)
x = torch.randn(3, 3)  # 标准正态分布

# GPU加速
if torch.cuda.is_available():
    x = x.cuda()
    # 或
    device = torch.device('cuda')
    x = x.to(device)

Autograd:自动微分系统,自动计算梯度。

x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 + 3 * x + 1
y.backward()
print(x.grad)  # dy/dx = 2x + 3 = 7

计算图:PyTorch使用动态计算图,每次前向传播都重新构建图,便于调试和条件控制。

7.5.2 模型定义与训练

nn.Module:所有神经网络模块的基类。

import torch
import torch.nn as nn
import torch.nn.functional as F

class NeuralNetwork(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(NeuralNetwork, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.bn1 = nn.BatchNorm1d(hidden_size)
        self.dropout = nn.Dropout(0.3)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.bn2 = nn.BatchNorm1d(hidden_size)
        self.fc3 = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        x = self.fc1(x)
        x = self.bn1(x)
        x = F.relu(x)
        x = self.dropout(x)
        
        x = self.fc2(x)
        x = self.bn2(x)
        x = F.relu(x)
        x = self.dropout(x)
        
        x = self.fc3(x)
        return x

# 实例化模型
model = NeuralNetwork(784, 512, 10)

反向传播手动实现

为了深入理解反向传播,以下是纯NumPy实现的简单神经网络:

import numpy as np

class SimpleNN:
    def __init__(self, input_size, hidden_size, output_size):
        # Xavier初始化
        self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
        self.b2 = np.zeros((1, output_size))
    
    def relu(self, x):
        return np.maximum(0, x)
    
    def relu_derivative(self, x):
        return (x > 0).astype(float)
    
    def softmax(self, x):
        exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
        return exp_x / np.sum(exp_x, axis=1, keepdims=True)
    
    def forward(self, X):
        # 前向传播
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = self.relu(self.z1)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        self.a2 = self.softmax(self.z2)
        return self.a2
    
    def backward(self, X, y, learning_rate=0.01):
        m = X.shape[0]
        
        # 输出层梯度
        dz2 = self.a2 - y
        dW2 = np.dot(self.a1.T, dz2) / m
        db2 = np.sum(dz2, axis=0, keepdims=True) / m
        
        # 隐藏层梯度
        dz1 = np.dot(dz2, self.W2.T) * self.relu_derivative(self.z1)
        dW1 = np.dot(X.T, dz1) / m
        db1 = np.sum(dz1, axis=0, keepdims=True) / m
        
        # 参数更新
        self.W2 -= learning_rate * dW2
        self.b2 -= learning_rate * db2
        self.W1 -= learning_rate * dW1
        self.b1 -= learning_rate * db1
        
        return np.mean(np.sum(dz2**2))

# 使用示例
np.random.seed(42)
X = np.random.randn(100, 784)
y = np.eye(10)[np.random.randint(0, 10, 100)]

model = SimpleNN(784, 256, 10)
output = model.forward(X)
loss = model.backward(X, y)
print(f"Loss: {loss:.4f}")

7.5.3 模型保存与部署

模型保存与加载

# 保存整个模型
torch.save(model, 'model.pth')
model = torch.load('model.pth')

# 仅保存参数(推荐)
torch.save(model.state_dict(), 'model_weights.pth')
model.load_state_dict(torch.load('model_weights.pth'))

# 保存检查点(包含优化器状态)
checkpoint = {
    'epoch': epoch,
    'model_state_dict': model.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'loss': loss,
}
torch.save(checkpoint, 'checkpoint.pth')

# 加载检查点
checkpoint = torch.load('checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

ONNX导出:将PyTorch模型转换为ONNX格式,便于跨平台部署。

# 导出ONNX
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "model.onnx", 
                  input_names=['input'],
                  output_names=['output'],
                  dynamic_axes={'input': {0: 'batch_size'},
                               'output': {0: 'batch_size'}})

MNIST分类完整训练代码

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# 数据预处理
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# 加载MNIST数据集
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

# 定义模型
class MNISTNet(nn.Module):
    def __init__(self):
        super(MNISTNet, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(64)
        self.pool = nn.MaxPool2d(2, 2)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)
    
    def forward(self, x):
        x = self.pool(nn.functional.relu(self.bn1(self.conv1(x))))
        x = self.pool(nn.functional.relu(self.bn2(self.conv2(x))))
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = nn.functional.relu(self.fc1(x))
        x = self.dropout2(x)
        x = self.fc2(x)
        return nn.functional.log_softmax(x, dim=1)

# 训练设置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MNISTNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

# 训练循环
def train(epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        
        if batch_idx % 100 == 0:
            print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}'
                  f' ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')

# 测试
def test():
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += criterion(output, target).item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()
    
    test_loss /= len(test_loader)
    accuracy = 100. * correct / len(test_dataset)
    print(f'\nTest set: Average loss: {test_loss:.4f}, '
          f'Accuracy: {correct}/{len(test_dataset)} ({accuracy:.2f}%)\n')
    return accuracy

# 主训练循环
if __name__ == '__main__':
    best_acc = 0
    for epoch in range(1, 11):
        train(epoch)
        acc = test()
        scheduler.step()
        if acc > best_acc:
            best_acc = acc
            torch.save(model.state_dict(), 'best_mnist_model.pth')
    print(f'Best accuracy: {best_acc:.2f}%')

以上代码实现了一个完整的MNIST分类器,包含卷积层、批归一化、Dropout正则化和学习率调度。在标准设置下,该模型可以达到99%以上的测试准确率。