第一章:神经网络原理
本章学习目标
- 理解神经网络的基本组成单元------神经元与感知机的工作原理
- 掌握主流激活函数的数学定义、特性及适用场景
- 深入理解前向传播与反向传播算法的数学推导与数据流转过程
- 掌握经典优化算法(SGD、Adam、AdamW)的核心思想与实现细节
- 了解深度学习核心组件(损失函数、正则化、归一化)的设计哲学
- 理解神经网络架构从MLP到Transformer的演进脉络
1.1 神经网络基础
1.1.1 神经元与感知机模型
概念引入:从生物神经元到人工神经元
人类大脑约由860亿个神经元组成,每个神经元通过突触与其他神经元相连,形成复杂的神经网络。人工神经网络(Artificial Neural Network, ANN)正是受此启发而设计。
类比理解:想象一个会议室决策场景------每个神经元是一位参会者,他们接收来自其他参会者的信息(输入信号),经过自己的判断(加权求和+激活函数),然后输出自己的意见(输出信号)。多位参会者相互连接,最终形成集体决策。
感知机模型(Perceptron)
感知机是神经网络的最基本单元,由Frank Rosenblatt于1957年提出,本质上是一个二分类线性分类器。
数学定义:
给定一个输入向量 x=x1,x2,...,xnT\mathbf{x} = x_1, x_2, \\dots, x_n^Tx=x1,x2,...,xnT,感知机的输出为:
y=f(wTx+b)=f(∑i=1nwixi+b) y = f(\mathbf{w}^T \mathbf{x} + b) = f\left(\sum_{i=1}^{n} w_i x_i + b\right) y=f(wTx+b)=f(i=1∑nwixi+b)
其中:
- w=w1,w2,...,wnT\mathbf{w} = w_1, w_2, \\dots, w_n^Tw=w1,w2,...,wnT 为权重向量,表示每个输入特征的重要程度
- bbb 为偏置项(bias),用于调整决策边界的偏移
- f(⋅)f(\cdot)f(⋅) 为阶跃函数(早期感知机使用):
f(z)={1,z≥00,z<0 f(z) = \begin{cases} 1, & z \geq 0 \\ 0, & z < 0 \end{cases} f(z)={1,0,z≥0z<0
数据流转过程:
输入层 → 加权求和 → 加偏置 → 激活函数 → 输出
[x1, x2, ..., xn] → [w1*x1+...+wn*xn] → [+b] → [f(·)] → y
现代神经元模型
现代神经网络中的神经元在感知机基础上进行了扩展,主要区别在于使用可微的激活函数(如Sigmoid、ReLU等),使得可以通过梯度下降进行训练。
python
import torch
import torch.nn as nn
# ===== 代码实战1.1:实现一个简单的神经元 =====
class Neuron(nn.Module):
"""
单个神经元实现
对应公式: y = activation(w^T * x + b)
"""
def __init__(self, input_dim, activation='relu'):
"""
初始化神经元
Args:
input_dim: 输入特征维度
activation: 激活函数类型,可选 'relu', 'sigmoid', 'tanh'
"""
super(Neuron, self).__init__()
# 权重参数,使用正态分布初始化
self.weights = nn.Parameter(torch.randn(input_dim, 1) * 0.01)
# 偏置参数,初始化为0
self.bias = nn.Parameter(torch.zeros(1))
# 激活函数
if activation == 'relu':
self.activation = nn.ReLU()
elif activation == 'sigmoid':
self.activation = nn.Sigmoid()
elif activation == 'tanh':
self.activation = nn.Tanh()
else:
raise ValueError(f"不支持的激活函数: {activation}")
def forward(self, x):
"""
前向传播
Args:
x: 输入张量,形状为 (batch_size, input_dim)
Returns:
神经元的输出,形状为 (batch_size, 1)
"""
# 加权求和: w^T * x + b
# x @ self.weights: (batch_size, input_dim) @ (input_dim, 1) -> (batch_size, 1)
linear_output = torch.matmul(x, self.weights) + self.bias
# 激活函数
output = self.activation(linear_output)
return output
# 测试代码
if __name__ == "__main__":
# 创建一个输入维度为3的ReLU神经元
neuron = Neuron(input_dim=3, activation='relu')
# 构造一批输入数据 (batch_size=2, input_dim=3)
x = torch.tensor([[1.0, 2.0, 3.0],
[0.5, -1.0, 1.5]])
# 前向传播
output = neuron(x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
print(f"输出值:\n{output.detach().numpy()}")
输出解读:
输入形状: torch.Size([2, 3])
输出形状: torch.Size([2, 1])
输出值:
[[0.234]
[0. ]] # 负值被ReLU置为0
多层感知机(MLP)
将多个神经元分层排列,就构成了多层感知机 (Multi-Layer Perceptron, MLP),也称为全连接神经网络(Fully Connected Neural Network)。
输入层 (Input Layer)
↓
隐藏层1 (Hidden Layer 1) ------ 每个神经元与上一层所有神经元相连
↓
隐藏层2 (Hidden Layer 2)
↓
输出层 (Output Layer)
关键洞察 :单层感知机只能解决线性可分 问题(如AND、OR逻辑),而多层感知机通过引入隐藏层和非线性激活函数,理论上可以逼近任意连续函数(万能逼近定理,Universal Approximation Theorem)。
1.1.2 激活函数详解
为什么需要激活函数?
如果没有激活函数,无论神经网络有多少层,最终都等价于一个线性变换:
y=Wn(⋯(W2(W1x+b1)+b2)⋯ )+bn=Weqx+beq \mathbf{y} = W_n(\cdots(W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}2)\cdots) + \mathbf{b}n = W{eq}\mathbf{x} + \mathbf{b}{eq} y=Wn(⋯(W2(W1x+b1)+b2)⋯)+bn=Weqx+beq
激活函数引入非线性,使得神经网络能够学习和表示复杂的非线性映射关系。
主流激活函数对比
| 激活函数 | 数学公式 | 输出范围 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| Sigmoid | σ(x)=11+e−x\sigma(x) = \frac{1}{1+e^{-x}}σ(x)=1+e−x1 | (0, 1) | 平滑可导,可解释为概率 | 梯度消失,输出非零中心 | 二分类输出层(历史用法) |
| Tanh | tanh(x)=ex−e−xex+e−x\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}tanh(x)=ex+e−xex−e−x | (-1, 1) | 零中心,收敛快于Sigmoid | 仍存在梯度消失 | RNN隐藏层(历史用法) |
| ReLU | ReLU(x)=max(0,x)\text{ReLU}(x) = \max(0, x)ReLU(x)=max(0,x) | [0,+∞)[0, +\infty)[0,+∞) | 计算简单,缓解梯度消失 | 死亡ReLU问题 | CNN隐藏层(最常用) |
| Leaky ReLU | max(αx,x),α=0.01\max(\alpha x, x), \alpha=0.01max(αx,x),α=0.01 | (−∞,+∞)(-\infty, +\infty)(−∞,+∞) | 解决死亡ReLU问题 | 负值区域梯度小 | ReLU的替代方案 |
| GELU | xΦ(x)x\Phi(x)xΦ(x) 或近似公式 | 约(-0.17, +∞) | 平滑非线性,性能优异 | 计算稍复杂 | Transformer(GPT、BERT) |
重点详解:ReLU与GELU
ReLU(Rectified Linear Unit)
python
import matplotlib.pyplot as plt
import numpy as np
# ===== 代码实战1.2:可视化主流激活函数 =====
def plot_activation_functions():
"""绘制并对比主流激活函数的曲线"""
x = np.linspace(-5, 5, 1000)
# 定义各激活函数
sigmoid = 1 / (1 + np.exp(-x))
tanh = np.tanh(x)
relu = np.maximum(0, x)
leaky_relu = np.where(x > 0, x, 0.01 * x)
# GELU近似公式: 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
gelu = 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))
# 绘制
plt.figure(figsize=(12, 8))
plt.plot(x, sigmoid, label='Sigmoid', linewidth=2)
plt.plot(x, tanh, label='Tanh', linewidth=2)
plt.plot(x, relu, label='ReLU', linewidth=2)
plt.plot(x, leaky_relu, label='Leaky ReLU (α=0.01)', linewidth=2)
plt.plot(x, gelu, label='GELU', linewidth=2)
plt.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
plt.grid(True, alpha=0.3)
plt.legend(fontsize=12)
plt.xlabel('输入 x', fontsize=12)
plt.ylabel('输出 f(x)', fontsize=12)
plt.title('主流激活函数对比', fontsize=14)
plt.tight_layout()
plt.savefig('activation_functions.png', dpi=150)
plt.show()
plot_activation_functions()
GELU(Gaussian Error Linear Unit)
GELU是Transformer架构(包括BERT、GPT系列)的默认激活函数,其思想是将输入乘以一个依赖于输入值的随机正则化项。
精确公式 (计算成本高):
GELU(x)=x⋅Φ(x)=x⋅121+erf(x2) \text{GELU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}1 + \\text{erf}(\\frac{x}{\\sqrt{2}}) GELU(x)=x⋅Φ(x)=x⋅211+erf(2 x)
其中 Φ(x)\Phi(x)Φ(x) 是标准正态分布的累积分布函数。
常用近似公式 (实际实现使用):
GELU(x)≈0.5x(1+tanh2/π(x+0.044715x3)) \text{GELU}(x) \approx 0.5x(1 + \tanh\\sqrt{2/\\pi}(x + 0.044715x\^3)) GELU(x)≈0.5x(1+tanh2/π (x+0.044715x3))
或更快的近似:
GELU(x)≈x⋅σ(1.702x) \text{GELU}(x) \approx x \cdot \sigma(1.702x) GELU(x)≈x⋅σ(1.702x)
其中 σ\sigmaσ 是Sigmoid函数。
python
# ===== 代码实战1.3:PyTorch中激活函数的正确使用 =====
import torch
import torch.nn as nn
class MLPWithVariousActivations(nn.Module):
"""
对比不同激活函数在MLP中的效果
"""
def __init__(self, input_dim=784, hidden_dims=[256, 128], activation='relu'):
super(MLPWithVariousActivations, self).__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.append(nn.Linear(prev_dim, hidden_dim))
# 根据选择添加激活函数
if activation == 'relu':
layers.append(nn.ReLU())
elif activation == 'leaky_relu':
layers.append(nn.LeakyReLU(negative_slope=0.01))
elif activation == 'gelu':
layers.append(nn.GELU())
elif activation == 'tanh':
layers.append(nn.Tanh())
elif activation == 'sigmoid':
layers.append(nn.Sigmoid())
else:
raise ValueError(f"不支持的激活函数: {activation}")
prev_dim = hidden_dim
# 输出层(无激活函数,用于回归任务)
layers.append(nn.Linear(prev_dim, 10)) # 假设10分类任务
self.network = nn.Sequential(*layers)
def forward(self, x):
# 将输入展平 (batch_size, features)
x = x.view(x.size(0), -1)
return self.network(x)
# 验证GELU与ReLU的差异
def compare_gelu_relu():
"""对比GELU和ReLU的梯度特性"""
x = torch.linspace(-3, 3, 100, requires_grad=True)
relu = nn.ReLU()
gelu = nn.GELU()
y_relu = relu(x)
y_gelu = gelu(x)
# 计算梯度(对输入x的梯度)
y_relu.sum().backward(retain_graph=True)
grad_relu = x.grad.clone()
x.grad = None # 清空梯度
y_gelu.sum().backward()
grad_gelu = x.grad.clone()
print("GELU vs ReLU 梯度对比(部分值):")
for i in [0, 25, 50, 75, 99]:
print(f" x={x[i].item():.2f}: ReLU梯度={grad_relu[i].item():.2f}, "
f"GELU梯度={grad_gelu[i].item():.2f}")
compare_gelu_relu()
1.1.3 前向传播与反向传播算法
前向传播(Forward Propagation)
前向传播是指数据从输入层经过隐藏层,最终到达输出层的过程。
以两层神经网络为例:
给定输入 x∈Rn\mathbf{x} \in \mathbb{R}^{n}x∈Rn,隐藏层维度 hhh,输出维度 mmm:
第一层(输入层→隐藏层) :
z(1)=W(1)x+b(1),a(1)=f(1)(z(1)) \mathbf{z}^{(1)} = W^{(1)}\mathbf{x} + \mathbf{b}^{(1)}, \quad \mathbf{a}^{(1)} = f^{(1)}(\mathbf{z}^{(1)}) z(1)=W(1)x+b(1),a(1)=f(1)(z(1))
第二层(隐藏层→输出层) :
z(2)=W(2)a(1)+b(2),a(2)=f(2)(z(2))=y^ \mathbf{z}^{(2)} = W^{(2)}\mathbf{a}^{(1)} + \mathbf{b}^{(2)}, \quad \mathbf{a}^{(2)} = f^{(2)}(\mathbf{z}^{(2)}) = \hat{\mathbf{y}} z(2)=W(2)a(1)+b(2),a(2)=f(2)(z(2))=y^
其中 f(1)f^{(1)}f(1) 和 f(2)f^{(2)}f(2) 分别为隐藏层和输出层的激活函数。
数据流转过程示意图:
输入 x ──→ [线性变换 W1*x+b1] ──→ [激活函数 f1] ──→ 隐藏表示 a1
│
↓
[线性变换 W2*a1+b2] ──→ [激活函数 f2] ──→ 预测输出 ŷ
反向传播(Backpropagation)
反向传播是训练神经网络的核心算法,由Rumelhart、Hinton等人于1986年提出,基于链式法则(Chain Rule)计算损失函数对每一层参数的梯度。
核心思想:从输出层向输入层反向计算梯度,每一层的梯度计算都依赖于后一层的梯度结果。
数学推导(以均方误差损失为例):
对于单层网络,损失函数为:
L=12(y^−y)2 \mathcal{L} = \frac{1}{2}(\hat{y} - y)^2 L=21(y^−y)2
其中 y^=f(wTx+b)\hat{y} = f(\mathbf{w}^T\mathbf{x} + b)y^=f(wTx+b)。
根据链式法则:
∂L∂wi=∂L∂y^⋅∂y^∂z⋅∂z∂wi=(y^−y)⋅f′(z)⋅xi \frac{\partial \mathcal{L}}{\partial w_i} = \frac{\partial \mathcal{L}}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w_i} = (\hat{y} - y) \cdot f'(z) \cdot x_i ∂wi∂L=∂y^∂L⋅∂z∂y^⋅∂wi∂z=(y^−y)⋅f′(z)⋅xi
∂L∂b=(y^−y)⋅f′(z) \frac{\partial \mathcal{L}}{\partial b} = (\hat{y} - y) \cdot f'(z) ∂b∂L=(y^−y)⋅f′(z)
对于多层网络 ,设损失函数为 L\mathcal{L}L,则第 lll 层的参数更新公式为:
∂L∂W(l)=∂L∂z(l)⋅∂z(l)∂W(l)=δ(l)⋅(a(l−1))T \frac{\partial \mathcal{L}}{\partial W^{(l)}} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}} \cdot \frac{\partial \mathbf{z}^{(l)}}{\partial W^{(l)}} = \delta^{(l)} \cdot (\mathbf{a}^{(l-1)})^T ∂W(l)∂L=∂z(l)∂L⋅∂W(l)∂z(l)=δ(l)⋅(a(l−1))T
δ(l)={∇a(L)L⊙f′(z(L)),l=L (输出层)(W(l+1))Tδ(l+1)⊙f′(z(l)),l<L (隐藏层) \delta^{(l)} = \begin{cases} \nabla_{\mathbf{a}^{(L)}}\mathcal{L} \odot f'(\mathbf{z}^{(L)}), & l = L \text{ (输出层)} \\ (W^{(l+1)})^T \delta^{(l+1)} \odot f'(\mathbf{z}^{(l)}), & l < L \text{ (隐藏层)} \end{cases} δ(l)={∇a(L)L⊙f′(z(L)),(W(l+1))Tδ(l+1)⊙f′(z(l)),l=L (输出层)l<L (隐藏层)
其中 δ(l)\delta^{(l)}δ(l) 称为误差项 (Error Term),⊙\odot⊙ 表示逐元素乘法(Hadamard积)。
python
# ===== 代码实战1.4:从零实现前向传播与反向传播 =====
import torch
import torch.nn as nn
class TwoLayerNetFromScratch:
"""
从零实现两层神经网络(不使用PyTorch自动求导)
用于理解前向传播和反向传播的底层原理
"""
def __init__(self, input_dim, hidden_dim, output_dim):
"""
初始化网络参数
Args:
input_dim: 输入维度
hidden_dim: 隐藏层维度
output_dim: 输出维度
"""
# 使用Xavier初始化(后面会详细介绍)
self.W1 = torch.randn(input_dim, hidden_dim) * np.sqrt(2.0 / (input_dim + hidden_dim))
self.b1 = torch.zeros(hidden_dim)
self.W2 = torch.randn(hidden_dim, output_dim) * np.sqrt(2.0 / (hidden_dim + output_dim))
self.b2 = torch.zeros(output_dim)
def relu(self, x):
"""ReLU激活函数"""
return torch.maximum(x, torch.zeros_like(x))
def relu_derivative(self, x):
"""ReLU的导数"""
return (x > 0).float()
def softmax(self, x):
"""Softmax函数(数值稳定版本)"""
exp_x = torch.exp(x - torch.max(x, dim=1, keepdim=True).values)
return exp_x / torch.sum(exp_x, dim=1, keepdim=True)
def forward(self, x):
"""
前向传播(保存中间结果用于反向传播)
Returns:
输出概率,以及中间结果缓存
"""
# 第一层线性变换 + ReLU
self.z1 = torch.matmul(x, self.W1) + self.b1 # (batch, hidden)
self.a1 = self.relu(self.z1) # (batch, hidden)
# 第二层线性变换 + Softmax
self.z2 = torch.matmul(self.a1, self.W2) + self.b2 # (batch, output)
self.a2 = self.softmax(self.z2) # (batch, output)
return self.a2
def backward(self, x, y_true, learning_rate=0.01):
"""
反向传播(手动计算梯度)
Args:
x: 输入数据 (batch, input_dim)
y_true: 真实标签 (batch,) 整数形式
learning_rate: 学习率
"""
batch_size = x.shape[0]
# 将真实标签转换为one-hot编码
y_one_hot = torch.zeros(batch_size, self.b2.shape[0])
y_one_hot[torch.arange(batch_size), y_true] = 1
# ===== 输出层梯度计算 =====
# δ2 = a2 - y (对于Softmax + 交叉熵损失)
delta2 = self.a2 - y_one_hot # (batch, output)
# ∂L/∂W2 = a1^T · δ2
dW2 = torch.matmul(self.a1.T, delta2) / batch_size # (hidden, output)
db2 = torch.sum(delta2, dim=0) / batch_size # (output,)
# ===== 隐藏层梯度计算 =====
# δ1 = (δ2 · W2^T) ⊙ ReLU'(z1)
delta1 = torch.matmul(delta2, self.W2.T) * self.relu_derivative(self.z1) # (batch, hidden)
# ∂L/∂W1 = x^T · δ1
dW1 = torch.matmul(x.T, delta1) / batch_size # (input, hidden)
db1 = torch.sum(delta1, dim=0) / batch_size # (hidden,)
# ===== 参数更新(梯度下降)=====
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
def compute_loss(self, y_pred, y_true):
"""计算交叉熵损失"""
# 避免log(0)
y_pred = torch.clamp(y_pred, min=1e-8, max=1.0)
batch_size = y_true.shape[0]
y_one_hot = torch.zeros(batch_size, y_pred.shape[1])
y_one_hot[torch.arange(batch_size), y_true] = 1
loss = -torch.sum(y_one_hot * torch.log(y_pred)) / batch_size
return loss
# 使用PyTorch自动求导验证手动实现的正确性
class TwoLayerNetPyTorch(nn.Module):
"""使用PyTorch实现的相同网络(用于对比验证)"""
def __init__(self, input_dim, hidden_dim, output_dim):
super(TwoLayerNetPyTorch, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
if __name__ == "__main__":
# 构造假数据
torch.manual_seed(42)
batch_size = 4
input_dim = 3
hidden_dim = 5
output_dim = 2
x = torch.randn(batch_size, input_dim)
y = torch.randint(0, output_dim, (batch_size,))
# 测试PyTorch版本
model = TwoLayerNetPyTorch(input_dim, hidden_dim, output_dim)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 前向传播
logits = model(x)
loss = criterion(logits, y)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"PyTorch版本 - 初始损失: {loss.item():.4f}")
print("PyTorch版本反向传播完成,参数已更新")
深度理解 :反向传播的效率来源于动态规划 思想------每一层的梯度计算只需一次前向传递的中间结果,避免了重复计算,使得训练深层网络成为可能。这也是为什么在代码实战1.4中,我们在
forward()方法中保存了z1, a1, z2, a2等中间结果。
1.1.4 梯度下降与优化算法
批量梯度下降(Batch Gradient Descent)
最基础的优化算法,每次迭代使用全部训练数据计算梯度:
wt+1=wt−η∇wL(wt) \mathbf{w}_{t+1} = \mathbf{w}t - \eta \nabla{\mathbf{w}}\mathcal{L}(\mathbf{w}_t) wt+1=wt−η∇wL(wt)
其中 η\etaη 为学习率(Learning Rate)。
缺点:
- 数据量大时计算开销巨大
- 无法在线更新(需要全部数据后才能更新)
随机梯度下降(Stochastic Gradient Descent, SGD)
每次迭代使用单个样本计算梯度:
wt+1=wt−η∇wL(wt;xi,yi) \mathbf{w}_{t+1} = \mathbf{w}t - \eta \nabla{\mathbf{w}}\mathcal{L}(\mathbf{w}_t; x_i, y_i) wt+1=wt−η∇wL(wt;xi,yi)
优点 :计算快,可在线学习
缺点:梯度估计噪声大,收敛路径震荡
小批量梯度下降(Mini-batch SGD)
实际工程中的标准做法,每次使用一小批数据(如32、64、128个样本):
wt+1=wt−η1m∑i=1m∇wL(wt;xi,yi) \mathbf{w}{t+1} = \mathbf{w}t - \eta \frac{1}{m}\sum{i=1}^{m}\nabla{\mathbf{w}}\mathcal{L}(\mathbf{w}_t; x_i, y_i) wt+1=wt−ηm1i=1∑m∇wL(wt;xi,yi)
其中 mmm 为批量大小(Batch Size)。
动量法(Momentum)
受物理学中动量概念启发,累积历史梯度方向以加速收敛并减少震荡:
vt=βvt−1+(1−β)∇wL(wt) \mathbf{v}t = \beta \mathbf{v}{t-1} + (1-\beta)\nabla_{\mathbf{w}}\mathcal{L}(\mathbf{w}_t) vt=βvt−1+(1−β)∇wL(wt)
wt+1=wt−ηvt \mathbf{w}_{t+1} = \mathbf{w}_t - \eta \mathbf{v}_t wt+1=wt−ηvt
其中 β\betaβ 为动量系数(通常取0.9)。
直观理解:想象一个小球在损失函数曲面上滚动,动量使得它能够越过局部最优的"小坑",继续向全局最优前进。
Adam优化器
Adam(Adaptive Moment Estimation)结合了动量法 和自适应学习率的优点,是深度学习中最常用的优化器之一。
算法步骤:
- 计算梯度:gt=∇wL(wt)g_t = \nabla_{\mathbf{w}}\mathcal{L}(\mathbf{w}_t)gt=∇wL(wt)
- 更新一阶矩估计(动量):mt=β1mt−1+(1−β1)gtm_t = \beta_1 m_{t-1} + (1-\beta_1)g_tmt=β1mt−1+(1−β1)gt
- 更新二阶矩估计(自适应学习率):vt=β2vt−1+(1−β2)gt2v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2vt=β2vt−1+(1−β2)gt2
- 偏差修正:m^t=mt1−β1t,v^t=vt1−β2t\hat{m}_t = \frac{m_t}{1-\beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1-\beta_2^t}m^t=1−β1tmt,v^t=1−β2tvt
- 参数更新:wt+1=wt−ηm^tv^t+ϵw_{t+1} = w_t - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}wt+1=wt−ηv^t +ϵm^t
默认超参数:
- β1=0.9\beta_1 = 0.9β1=0.9(一阶矩衰减率)
- β2=0.999\beta_2 = 0.999β2=0.999(二阶矩衰减率)
- ϵ=10−8\epsilon = 10^{-8}ϵ=10−8(数值稳定常数)
- η=10−3\eta = 10^{-3}η=10−3(学习率)
python
# ===== 代码实战1.5:不同优化器的收敛对比 =====
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
def compare_optimizers():
"""
对比SGD、SGD+Momentum、Adam在简单任务上的收敛速度
"""
torch.manual_seed(42)
# 构造一个简单的线性回归问题
# 真实参数: y = 2x1 - 3x2 + 1 + noise
n_samples = 1000
X = torch.randn(n_samples, 2)
true_w = torch.tensor([[2.0], [-3.0]])
true_b = torch.tensor([1.0])
y = X @ true_w + true_b + 0.1 * torch.randn(n_samples, 1)
# 定义相同的模型(简单的线性模型)
class LinearModel(nn.Module):
def __init__(self):
super(LinearModel, self).__init__()
self.linear = nn.Linear(2, 1)
def forward(self, x):
return self.linear(x)
# 测试不同的优化器
optimizers_config = {
'SGD': lambda model: torch.optim.SGD(model.parameters(), lr=0.01),
'SGD+Momentum': lambda model: torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9),
'Adam': lambda model: torch.optim.Adam(model.parameters(), lr=0.001),
}
loss_history = {}
for opt_name, opt_fn in optimizers_config.items():
model = LinearModel()
optimizer = opt_fn(model)
criterion = nn.MSELoss()
losses = []
for epoch in range(200):
# 前向传播
pred = model(X)
loss = criterion(pred, y)
losses.append(loss.item())
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_history[opt_name] = losses
print(f"{opt_name}: 最终损失 = {losses[-1]:.6f}")
# 绘制损失曲线
plt.figure(figsize=(10, 6))
for opt_name, losses in loss_history.items():
plt.plot(losses, label=opt_name, linewidth=2)
plt.xlabel('迭代次数', fontsize=12)
plt.ylabel('损失值', fontsize=12)
plt.title('不同优化器的收敛速度对比', fontsize=14)
plt.legend(fontsize=12)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('optimizer_comparison.png', dpi=150)
plt.show()
compare_optimizers()
AdamW:解耦权重衰减的Adam
在原始的Adam中,L2正则化(权重衰减)与梯度更新耦合,导致实际的正则化效果与预期不符。AdamW(Adam with Decoupled Weight Decay)将权重衰减与梯度更新解耦:
AdamW参数更新:{mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m^t=mt/(1−β1t),v^t=vt/(1−β2t)wt+1=wt−η(m^tv^t+ϵ+λwt) \text{AdamW参数更新}: \begin{cases} m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t \\ v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 \\ \hat{m}_t = m_t / (1-\beta_1^t), \quad \hat{v}t = v_t / (1-\beta_2^t) \\ w{t+1} = w_t - \eta(\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda w_t) \end{cases} AdamW参数更新:⎩ ⎨ ⎧mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m^t=mt/(1−β1t),v^t=vt/(1−β2t)wt+1=wt−η(v^t +ϵm^t+λwt)
其中 λ\lambdaλ 为权重衰减系数(通常为 0.010.010.01 或 0.10.10.1)。
工程实践 :在Transformer模型(如BERT、GPT)的预训练中,AdamW是标准配置。PyTorch中实现方式:
pythonoptimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)
1.2 深度学习核心组件
1.2.1 损失函数设计
损失函数(Loss Function)衡量模型预测值与真实值之间的差距,是模型训练的"指南针"。
回归任务常用损失函数
1. 均方误差(Mean Squared Error, MSE)
LMSE=1N∑i=1N(yi−y^i)2 \mathcal{L}{\text{MSE}} = \frac{1}{N}\sum{i=1}^{N}(y_i - \hat{y}_i)^2 LMSE=N1i=1∑N(yi−y^i)2
特点:对异常值敏感(平方放大了大误差的影响)。
2. 平均绝对误差(Mean Absolute Error, MAE)
LMAE=1N∑i=1N∣yi−y^i∣ \mathcal{L}{\text{MAE}} = \frac{1}{N}\sum{i=1}^{N}|y_i - \hat{y}_i| LMAE=N1i=1∑N∣yi−y^i∣
特点:对异常值鲁棒,但零点不可导。
3. Huber Loss(平滑MAE)
Lδ(a)={12a2,∣a∣≤δδ(∣a∣−12δ),∣a∣>δ \mathcal{L}_{\delta}(a) = \begin{cases} \frac{1}{2}a^2, & |a| \leq \delta \\ \delta(|a| - \frac{1}{2}\delta), & |a| > \delta \end{cases} Lδ(a)={21a2,δ(∣a∣−21δ),∣a∣≤δ∣a∣>δ
其中 a=y−y^a = y - \hat{y}a=y−y^,δ\deltaδ 为超参数(通常取1.0或1.5)。
选择建议:数据含异常值时用MAE或Huber Loss;数据干净时用MSE(梯度更平滑,收敛更稳定)。
分类任务常用损失函数
1. 交叉熵损失(Cross Entropy Loss)
对于多分类问题,设真实标签为one-hot向量 y\mathbf{y}y,模型预测概率为 y^\hat{\mathbf{y}}y^:
LCE=−∑i=1Cyilog(y^i) \mathcal{L}{\text{CE}} = -\sum{i=1}^{C} y_i \log(\hat{y}_i) LCE=−i=1∑Cyilog(y^i)
其中 CCC 为类别数。
2. 二元交叉熵损失(Binary Cross Entropy, BCE)
用于二分类问题:
LBCE=−1N∑i=1Nyilog(y\^i)+(1−yi)log(1−y\^i) \mathcal{L}{\text{BCE}} = -\frac{1}{N}\sum{i=1}^{N}y_i \\log(\\hat{y}_i) + (1-y_i)\\log(1-\\hat{y}_i) LBCE=−N1i=1∑Nyilog(y\^i)+(1−yi)log(1−y\^i)
3. Focal Loss(处理类别不平衡)
LFocal=−α(1−y^i)γyilog(y^i)−(1−α)y^iγ(1−yi)log(1−y^i) \mathcal{L}_{\text{Focal}} = -\alpha(1-\hat{y}_i)^\gamma y_i \log(\hat{y}_i) - (1-\alpha)\hat{y}_i^\gamma (1-y_i)\log(1-\hat{y}_i) LFocal=−α(1−y^i)γyilog(y^i)−(1−α)y^iγ(1−yi)log(1−y^i)
其中 α\alphaα 平衡正负样本权重,γ\gammaγ 降低易分类样本的损失贡献(通常 γ=2\gamma=2γ=2)。
企业级场景:在风控、推荐系统等正负样本极度不平衡的场景中,Focal Loss或加权交叉熵是必须的。
python
# ===== 代码实战1.6:常用损失函数的PyTorch实现 =====
import torch
import torch.nn as nn
import torch.nn.functional as F
class FocalLoss(nn.Module):
"""
Focal Loss实现
用于解决类别不平衡问题
"""
def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
"""
初始化Focal Loss
Args:
alpha: 平衡因子,用于平衡正负样本
gamma: 聚焦因子,降低简单样本的权重
reduction: 损失归约方式,'mean', 'sum', 或'none'
"""
super(FocalLoss, self).__init__()
self.alpha = alpha
self.gamma = gamma
self.reduction = reduction
def forward(self, inputs, targets):
"""
前向计算
Args:
inputs: 模型输出logits,形状为 (N, C)
targets: 真实标签,形状为 (N,)
Returns:
Focal Loss值
"""
# 计算交叉熵(不带reduction)
ce_loss = F.cross_entropy(inputs, targets, reduction='none')
# 计算概率
pt = torch.exp(-ce_loss)
# Focal Loss公式
focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
# 测试各类损失函数
def test_loss_functions():
"""测试并对比不同损失函数的数值特性"""
torch.manual_seed(42)
# 模拟模型输出(logits)和真实标签
logits = torch.tensor([[2.0, 1.0, 0.1], # 样本1:类别0概率高
[0.5, 2.0, 0.3], # 样本2:类别1概率高
[0.2, 0.3, 3.0]]) # 样本3:类别2概率高
targets = torch.tensor([0, 1, 2])
# CrossEntropyLoss
ce_loss = nn.CrossEntropyLoss()
loss_ce = ce_loss(logits, targets)
print(f"Cross Entropy Loss: {loss_ce.item():.4f}")
# Focal Loss
focal_loss = FocalLoss(alpha=0.25, gamma=2.0)
loss_focal = focal_loss(logits, targets)
print(f"Focal Loss (gamma=2.0): {loss_focal.item():.4f}")
# 模拟不平衡场景:一个难样本(logits接近随机)
hard_logits = torch.tensor([[0.8, 0.7, 0.9]]) # 模型预测不确定
hard_target = torch.tensor([0])
ce_hard = ce_loss(hard_logits, hard_target)
focal_hard = focal_loss(hard_logits, hard_target)
print(f"\n难样本 - CE: {ce_hard.item():.4f}, Focal: {focal_hard.item():.4f}")
print("说明: Focal Loss会降低易分类样本的贡献,聚焦难样本")
test_loss_functions()
1.2.2 正则化技术
正则化(Regularization)是一系列用于防止过拟合、提高模型泛化能力的技术。
L1/L2正则化
通过在损失函数中添加参数的范数惩罚项来实现:
L2正则化(Ridge Regression / Weight Decay):
Ltotal=Ldata+λ2∥w∥22=Ldata+λ2∑iwi2 \mathcal{L}{\text{total}} = \mathcal{L}{\text{data}} + \frac{\lambda}{2}\|\mathbf{w}\|2^2 = \mathcal{L}{\text{data}} + \frac{\lambda}{2}\sum_{i} w_i^2 Ltotal=Ldata+2λ∥w∥22=Ldata+2λi∑wi2
L1正则化(Lasso Regression):
Ltotal=Ldata+λ∥w∥1=Ldata+λ∑i∣wi∣ \mathcal{L}{\text{total}} = \mathcal{L}{\text{data}} + \lambda\|\mathbf{w}\|1 = \mathcal{L}{\text{data}} + \lambda\sum_{i} |w_i| Ltotal=Ldata+λ∥w∥1=Ldata+λi∑∣wi∣
| 特性 | L1正则化 | L2正则化 |
|---|---|---|
| 惩罚项 | ∣w∣1|\mathbf{w}|_1∣w∣1 | ∣w∣22|\mathbf{w}|_2^2∣w∣22 |
| 参数稀疏性 | 产生稀疏解(部分参数为0) | 参数趋近于0但不为0 |
| 特征选择 | 可进行特征选择 | 不适用特征选择 |
| 梯度特性 | 梯度为常数(需注意零点) | 梯度与参数成正比 |
工程选择:L1适用于特征选择场景(如高维稀疏数据);L2是深度学习中的默认选择(配合AdamW使用)。
Dropout
Dropout是一种在训练过程中随机丢弃(置零)一部分神经元的技术,由Hinton等人于2012年提出。
工作原理:
- 训练时:每个神经元以概率 ppp 被保留(或以概率 1−p1-p1−p 被丢弃)
- 测试时:所有神经元都被使用,但输出需要乘以 ppp 进行缩放(或使用倒置Dropout ,在训练时除以 ppp)
数学表达(倒置Dropout):
adropout=a⊙mp,m∼Bernoulli(p) \mathbf{a}_{\text{dropout}} = \frac{\mathbf{a} \odot \mathbf{m}}{p}, \quad \mathbf{m} \sim \text{Bernoulli}(p) adropout=pa⊙m,m∼Bernoulli(p)
python
# ===== 代码实战1.7:Dropout的正确使用方式 =====
import torch
import torch.nn as nn
class MLPWithDropout(nn.Module):
"""
带Dropout的MLP网络
展示训练模式和评估模式的差异
"""
def __init__(self, input_dim=784, hidden_dims=[512, 256], num_classes=10, dropout_rate=0.5):
super(MLPWithDropout, self).__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(nn.ReLU())
# Dropout层
layers.append(nn.Dropout(p=dropout_rate))
prev_dim = hidden_dim
layers.append(nn.Linear(prev_dim, num_classes))
self.network = nn.Sequential(*layers)
def forward(self, x):
x = x.view(x.size(0), -1) # 展平
return self.network(x)
def demonstrate_dropout_behavior():
"""演示Dropout在训练和评估模式下的不同行为"""
model = MLPWithDropout(dropout_rate=0.5)
# 构造固定输入
x = torch.ones(1, 784)
# 训练模式:Dropout激活,每次前向传播结果不同
model.train()
print("训练模式 (Dropout激活):")
for i in range(3):
output = model(x)
# 检查中间层的激活值
print(f" 前向传播 #{i+1}: 输出和 = {output.sum().item():.2f}")
# 评估模式:Dropout关闭,结果确定
model.eval()
print("\n评估模式 (Dropout关闭):")
with torch.no_grad(): # 评估时不需要计算梯度
for i in range(3):
output = model(x)
print(f" 前向传播 #{i+1}: 输出和 = {output.sum().item():.2f}")
demonstrate_dropout_behavior()
⚠️ 企业级避坑1:Dropout在Transformer中的使用
在Transformer架构中,不要在注意力层和归一化层之间使用Dropout,这会导致训练不稳定。现代大模型(如BERT、GPT)通常在以下位置使用Dropout:
- 嵌入层之后(Embedding Dropout)
- 注意力权重之后(Attention Dropout,比例通常很小如0.1)
- FFN的激活值之后(FFN Dropout)
另外,当使用大批量训练 时,应适当降低Dropout比例(如从0.5降至0.1),因为大批量本身已有正则化效果。
Batch Normalization(批归一化)
Batch Normalization(BN)由Sergey Ioffe和Christian Szegedy于2015年提出,通过归一化每一层的输入来加速训练并提高稳定性。
核心思想:
对于一批输入 x={x1,x2,...,xm}\mathbf{x} = \{x_1, x_2, \dots, x_m\}x={x1,x2,...,xm},BN对 each feature 进行归一化:
μB=1m∑i=1mxi \mu_B = \frac{1}{m}\sum_{i=1}^{m}x_i μB=m1i=1∑mxi
σB2=1m∑i=1m(xi−μB)2 \sigma_B^2 = \frac{1}{m}\sum_{i=1}^{m}(x_i - \mu_B)^2 σB2=m1i=1∑m(xi−μB)2
x^i=xi−μBσB2+ϵ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} x^i=σB2+ϵ xi−μB
yi=γx^i+β y_i = \gamma \hat{x}_i + \beta yi=γx^i+β
其中 γ\gammaγ 和 β\betaβ 是可学习的缩放和偏移参数,使得网络可以"恢复"原始表示(如果这样做是最优的)。
优点:
- 允许使用更大的学习率(加速收敛)
- 起到正则化效果(减少对Dropout的依赖)
- 减轻对权重初始化的依赖
缺点:
- 依赖于批量大小(小批量时统计量不准确)
- 在RNN等动态网络上难以应用
- 训练和推理时的行为不一致(需要使用滑动平均)
现代替代方案 :在计算机视觉中,BN仍广泛使用;但在NLP和Transformer中,Layer Normalization(将在3.2.4节详细介绍)是更常用的选择,因为它不依赖于批量统计量。
python
# ===== 代码实战1.8:BatchNorm与LayerNorm的对比 =====
import torch
import torch.nn as nn
def compare_norm_layers():
"""对比BatchNorm和LayerNorm的归一化维度"""
# 构造输入: (batch_size, seq_len, hidden_dim)
# 以NLP任务为例: batch_size=2, seq_len=3, hidden_dim=4
x = torch.tensor([
[[1.0, 2.0, 3.0, 4.0],
[2.0, 3.0, 4.0, 5.0],
[3.0, 4.0, 5.0, 6.0]],
[[4.0, 5.0, 6.0, 7.0],
[5.0, 6.0, 7.0, 8.0],
[6.0, 7.0, 8.0, 9.0]]
], dtype=torch.float32)
print(f"输入形状: {x.shape}")
print(f"输入:\n{x}")
# BatchNorm: 在 batch 和 seq_len 维度上归一化(对每个特征独立)
# 对于Conv网络,通常是对每个channel独立归一化
bn = nn.BatchNorm1d(4) # num_features=hidden_dim
# 注意: BatchNorm1d期望 (N, C, L) 格式
x_bn = x.transpose(1, 2) # (2, 4, 3)
out_bn = bn(x_bn)
print(f"\nBatchNorm输出形状: {out_bn.shape}")
# LayerNorm: 在最后一维(hidden_dim)上归一化(对每个样本独立)
ln = nn.LayerNorm(4)
out_ln = ln(x)
print(f"LayerNorm输出形状: {out_ln.shape}")
print(f"\nLayerNorm输出 (每个样本的特征被归一化到均值0方差1):")
print(out_ln)
compare_norm_layers()
1.2.3 过拟合与欠拟合的识别与处理
识别方法
| 现象 | 判断 | 原因 |
|---|---|---|
| 训练误差低,验证误差高 | 过拟合 | 模型过于复杂,记住了训练数据的噪声 |
| 训练误差高,验证误差也高 | 欠拟合 | 模型过于简单,无法捕捉数据规律 |
| 训练误差持续下降,验证误差先降后升 | 过拟合开始 | 需要从该epoch开始早停 |
学习曲线(Learning Curve)诊断法:
python
# ===== 代码实战1.9:绘制学习曲线诊断过拟合/欠拟合 =====
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
def generate_toy_data(n_samples=1000, n_features=20, noise=0.1):
"""生成一个简单的分类数据集"""
torch.manual_seed(42)
X = torch.randn(n_samples, n_features)
# 真实决策边界: 仅前5个特征有用
true_weights = torch.zeros(n_features)
true_weights[:5] = torch.tensor([2.0, -1.5, 1.0, -0.5, 0.8])
logits = X @ true_weights
y = (logits > 0).float()
return X, y
def train_and_plot_curves(model, X_train, y_train, X_val, y_val, epochs=200):
"""训练模型并绘制学习曲线"""
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.BCELoss()
train_losses = []
val_losses = []
for epoch in range(epochs):
# 训练模式
model.train()
optimizer.zero_grad()
pred_train = model(X_train).squeeze()
loss_train = criterion(pred_train, y_train)
loss_train.backward()
optimizer.step()
train_losses.append(loss_train.item())
# 评估模式
model.eval()
with torch.no_grad():
pred_val = model(X_val).squeeze()
loss_val = criterion(pred_val, y_val)
val_losses.append(loss_val.item())
if (epoch + 1) % 50 == 0:
print(f"Epoch {epoch+1}: Train Loss = {loss_train.item():.4f}, "
f"Val Loss = {loss_val.item():.4f}")
# 绘制学习曲线
plt.figure(figsize=(10, 6))
plt.plot(train_losses, label='训练损失', linewidth=2)
plt.plot(val_losses, label='验证损失', linewidth=2)
plt.xlabel('Epoch', fontsize=12)
plt.ylabel('Loss', fontsize=12)
plt.title('学习曲线(用于诊断过拟合/欠拟合)', fontsize=14)
plt.legend(fontsize=12)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('learning_curve.png', dpi=150)
plt.show()
# 主程序
if __name__ == "__main__":
# 生成数据
X, y = generate_toy_data(n_samples=2000, n_features=50)
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 构建一个容易过拟合的模型(参数远多于样本)
overparam_model = nn.Sequential(
nn.Linear(50, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, 1),
nn.Sigmoid()
)
print("=== 过拟合场景(大模型+小数据)===")
train_and_plot_curves(overparam_model, X_train, y_train, X_val, y_val, epochs=500)
处理策略总结
处理过拟合:
- 增加数据:数据增强、收集更多数据
- 正则化:L2正则化、Dropout、Early Stopping
- 简化模型:减少层数、减少每层神经元数量
- 数据清洗:去除异常值、噪声样本
处理欠拟合:
- 增加模型复杂度:增加层数、增加每层神经元数量
- 减少正则化:降低L2系数、关闭Dropout
- 增加训练时间:更多epoch、调整学习率
- 特征工程:增加更有意义的特征
1.2.4 超参数调优策略
重要超参数分类
| 类别 | 超参数 | 典型范围 | 影响程度 |
|---|---|---|---|
| 优化相关 | 学习率 η\etaη | 10−5∼10−110^{-5} \sim 10^{-1}10−5∼10−1 | ⭐⭐⭐ |
| 优化相关 | Batch Size | 32, 64, 128, 256 | ⭐⭐ |
| 架构相关 | 隐藏层数 | 2~24层 | ⭐⭐⭐ |
| 架构相关 | 每层神经元数量 | 64~4096 | ⭐⭐ |
| 正则化相关 | Dropout比例 | 0.1~0.5 | ⭐⭐ |
| 正则化相关 | Weight Decay | 10−5∼10−210^{-5} \sim 10^{-2}10−5∼10−2 | ⭐ |
调优方法论
1. 网格搜索(Grid Search)
遍历所有超参数组合,计算成本高,适用于少数量超参数。
2. 随机搜索(Random Search)
在超参数空间中随机采样,实践中比网格搜索更高效(因为有些超参数比其他更重要)。
3. 贝叶斯优化(Bayesian Optimization)
使用高斯过程建模超参数与验证集性能的关系,智能选择下一组超参数。
4. 学习率调度(Learning Rate Scheduling)
python
# ===== 代码实战1.10:学习率调度策略 =====
import torch
import torch.optim as optim
import matplotlib.pyplot as plt
def demonstrate_lr_schedules():
"""演示不同的学习率调度策略"""
model = nn.Linear(10, 1) # 简单模型用于演示
# 初始学习率
base_lr = 0.1
optimizer = optim.SGD(model.parameters(), lr=base_lr)
# 定义不同的调度器
schedulers = {
'StepLR (每30步衰减0.1)': optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1),
'ExponentialLR (每步衰减0.95)': optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95),
'CosineAnnealingLR': optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100),
}
# 记录学习率变化
lr_history = {name: [] for name in schedulers.keys()}
for name, scheduler in schedulers.items():
optimizer = optim.SGD(model.parameters(), lr=base_lr)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) if 'Step' in name else \
optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95) if 'Exponential' in name else \
optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
lrs = []
for epoch in range(100):
# 模拟训练步骤
lrs.append(optimizer.param_groups[0]['lr'])
scheduler.step()
lr_history[name] = lrs
# 绘制
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for idx, (name, lrs) in enumerate(lr_history.items()):
axes[idx].plot(lrs)
axes[idx].set_title(name, fontsize=10)
axes[idx].set_xlabel('Epoch')
axes[idx].set_ylabel('Learning Rate')
axes[idx].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('lr_schedules.png', dpi=150)
plt.show()
demonstrate_lr_schedules()
⚠️ 企业级避坑2:学习率warmup的重要性
在训练大模型(尤其是Transformer)时,不要从最大学习率开始训练 !应该使用学习率warmup策略:
python# PyTorch实现warmup from torch.optim.lr_scheduler import LambdaLR def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps): def lr_lambda(current_step): if current_step < num_warmup_steps: return float(current_step) / float(max(1, num_warmup_steps)) return max(0.0, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps))) return LambdaLR(optimizer, lr_lambda)原因:训练初期,模型参数是随机初始化的,如果立即使用大学习率,会导致训练不稳定甚至发散。Warmup让学习率从接近0逐渐增加到预设值,使训练更加稳定。
1.3 神经网络架构演进
1.3.1 从MLP到CNN:卷积神经网络原理
MLP的局限性
MLP(全连接网络)在处理图像等高维数据时有严重缺陷:
- 参数爆炸 :一张 224×224×3224 \times 224 \times 3224×224×3 的图片展平后有150,528个像素,如果第一层隐藏层有1000个神经元,则需要 1.5亿个参数!
- 忽略空间结构:图像中相邻像素的相关性被完全忽略。
- 平移不变性缺失:同一物体出现在图像不同位置,MLP需要重新学习。
卷积神经网络(CNN)的核心思想
CNN通过以下三个关键设计解决上述问题:
1. 局部感受野(Local Receptive Field)
每个神经元只连接输入图像的局部区域(如 3×33\times33×3 或 5×55\times55×5 的局部块),而非全连接。
2. 权值共享(Weight Sharing)
同一个卷积核(filter)在整张图像上滑动,大大减少参数数量。
3. 平移不变性(Translation Invariance)
通过池化层(Pooling Layer)或步长卷积,使得网络对物体的位置变化不敏感。
卷积操作详解
给定输入 X∈RH×W×Cin\mathbf{X} \in \mathbb{R}^{H \times W \times C_{in}}X∈RH×W×Cin 和卷积核 K∈Rkh×kw×Cin×Cout\mathbf{K} \in \mathbb{R}^{k_h \times k_w \times C_{in} \times C_{out}}K∈Rkh×kw×Cin×Cout,输出特征图为:
Yi,j,cout=∑cin=1Cin∑m=0kh−1∑n=0kw−1Xi⋅sh+m,j⋅sw+n,cin⋅Km,n,cin,cout+bcout Y_{i,j,c_{out}} = \sum_{c_{in}=1}^{C_{in}}\sum_{m=0}^{k_h-1}\sum_{n=0}^{k_w-1} X_{i\cdot s_h+m, j\cdot s_w+n, c_{in}} \cdot K_{m,n,c_{in},c_{out}} + b_{c_{out}} Yi,j,cout=cin=1∑Cinm=0∑kh−1n=0∑kw−1Xi⋅sh+m,j⋅sw+n,cin⋅Km,n,cin,cout+bcout
其中 sh,sws_h, s_wsh,sw 为垂直和水平方向的步长(stride)。
输出尺寸计算公式:
Hout=⌊Hin+2ph−khsh⌋+1 H_{out} = \left\lfloor\frac{H_{in} + 2p_h - k_h}{s_h}\right\rfloor + 1 Hout=⌊shHin+2ph−kh⌋+1
Wout=⌊Win+2pw−kwsw⌋+1 W_{out} = \left\lfloor\frac{W_{in} + 2p_w - k_w}{s_w}\right\rfloor + 1 Wout=⌊swWin+2pw−kw⌋+1
其中 ph,pwp_h, p_wph,pw 为填充(padding)大小。
python
# ===== 代码实战1.11:从零实现2D卷积操作 =====
import torch
import torch.nn as nn
import torch.nn.functional as F
def manual_conv2d(x, kernel, bias=None, stride=1, padding=0):
"""
手动实现2D卷积(用于理解底层原理)
Args:
x: 输入张量 (N, C_in, H, W)
kernel: 卷积核 (C_out, C_in, kH, kW)
bias: 偏置 (C_out,)
stride: 步长
padding: 填充大小
Returns:
卷积结果 (N, C_out, H_out, W_out)
"""
N, C_in, H, W = x.shape
C_out, _, kH, kW = kernel.shape
# 添加padding
if padding > 0:
x = F.pad(x, (padding, padding, padding, padding))
# 计算输出尺寸
H_out = (H + 2 * padding - kH) // stride + 1
W_out = (W + 2 * padding - kW) // stride + 1
# 初始化输出
output = torch.zeros(N, C_out, H_out, W_out)
# 滑动窗口卷积
for i in range(H_out):
for j in range(W_out):
# 当前窗口位置
h_start = i * stride
h_end = h_start + kH
w_start = j * stride
w_end = w_start + kW
# 提取窗口 (N, C_in, kH, kW)
window = x[:, :, h_start:h_end, w_start:w_end]
# 卷积计算: (N, C_out, C_in, kH, kW) -> (N, C_out)
for c_out in range(C_out):
# 逐通道卷积并求和
conv_result = (window * kernel[c_out:c_out+1, :, :, :]).sum(dim=(1, 2, 3))
if bias is not None:
conv_result += bias[c_out]
output[:, c_out, i, j] = conv_result
return output
def compare_with_pytorch():
"""对比手动实现与PyTorch官方实现的卷积结果"""
torch.manual_seed(42)
# 构造输入 (N=1, C_in=3, H=5, W=5)
x = torch.randn(1, 3, 5, 5)
# 构造卷积核 (C_out=2, C_in=3, kH=3, kW=3)
kernel = torch.randn(2, 3, 3, 3)
bias = torch.randn(2)
# 手动实现
output_manual = manual_conv2d(x, kernel, bias, stride=1, padding=1)
# PyTorch官方实现
conv = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, stride=1, padding=1, bias=True)
# 手动设置权重
conv.weight.data = kernel
conv.bias.data = bias
output_pytorch = conv(x)
# 对比
print(f"手动实现输出形状: {output_manual.shape}")
print(f"PyTorch输出形状: {output_pytorch.shape}")
print(f"最大差异: {torch.max(torch.abs(output_manual - output_pytorch)).item():.6f}")
print("说明: 差异应接近0(数值精度范围内一致)")
compare_with_pytorch()
经典CNN架构演进
| 架构 | 年份 | 核心创新 | 应用场景 |
|---|---|---|---|
| LeNet-5 | 1998 | 首个成功应用的CNN | 手写数字识别 |
| AlexNet | 2012 | ReLU+Dropout+GPU训练 | 图像分类(ImageNet) |
| VGGNet | 2014 | 小卷积核(3×3)堆叠 | 图像分类、特征提取 |
| ResNet | 2015 | 残差连接(Skip Connection) | 深度网络训练(152层+) |
| EfficientNet | 2019 | 复合缩放(深度/宽度/分辨率) | 高效图像分类 |
重点关注ResNet的残差连接 :
y=F(x)+x \mathbf{y} = \mathcal{F}(\mathbf{x}) + \mathbf{x} y=F(x)+x
这一简单设计解决了深度网络的梯度消失/爆炸问题 ,使得训练上百甚至上千层的网络成为可能。这也是为什么现代大模型(如Transformer)也广泛采用残差连接的原因。
1.3.2 RNN与LSTM:序列建模的突破
循环神经网络(RNN)原理
RNN通过在时间步之间共享参数,能够处理变长序列数据。
RNN单元的数学表达:
ht=tanh(Whht−1+Wxxt+b) \mathbf{h}t = \tanh(W_h\mathbf{h}{t-1} + W_x\mathbf{x}_t + \mathbf{b}) ht=tanh(Whht−1+Wxxt+b)
yt=Wyht+by \mathbf{y}_t = W_y\mathbf{h}_t + \mathbf{b}_y yt=Wyht+by
其中 ht\mathbf{h}_tht 为时刻 ttt 的隐藏状态,编码了序列的历史信息。
数据流转过程:
x1 → [RNN单元] → h1 → y1
↓
x2 → [RNN单元] → h2 → y2
↓
x3 → [RNN单元] → h3 → y3
↓
...
局限性 :RNN存在长期依赖问题(Long-Term Dependency)------随着序列长度增加,早期信息在传递过程中逐渐"消失"(梯度消失),导致无法捕捉长距离依赖关系。
LSTM(长短期记忆网络)
LSTM通过精心设计的门控机制解决RNN的长期依赖问题。
LSTM的核心组件(每个LSTM单元包含以下门):
-
遗忘门(Forget Gate) :决定丢弃哪些历史信息
ft=σ(Wfht−1,xt+bf) \mathbf{f}_t = \sigma(W_f\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_f) ft=σ(Wfht−1,xt+bf)
-
输入门(Input Gate) :决定写入哪些新信息
it=σ(Wiht−1,xt+bi) \mathbf{i}_t = \sigma(W_i\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_i) it=σ(Wiht−1,xt+bi)
C~t=tanh(WCht−1,xt+bC) \tilde{\mathbf{C}}_t = \tanh(W_C\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_C) C~t=tanh(WCht−1,xt+bC)
-
细胞状态更新(Cell State Update) :
Ct=ft⊙Ct−1+it⊙C~t \mathbf{C}_t = \mathbf{f}t \odot \mathbf{C}{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{C}}_t Ct=ft⊙Ct−1+it⊙C~t
-
输出门(Output Gate) :决定输出哪些信息
ot=σ(Woht−1,xt+bo) \mathbf{o}_t = \sigma(W_o\\mathbf{h}_{t-1}, \\mathbf{x}_t + \mathbf{b}_o) ot=σ(Woht−1,xt+bo)
ht=ot⊙tanh(Ct) \mathbf{h}_t = \mathbf{o}_t \odot \tanh(\mathbf{C}_t) ht=ot⊙tanh(Ct)
python
# ===== 代码实战1.12:从零实现LSTM单元 =====
import torch
import torch.nn as nn
class LSTMCellFromScratch(nn.Module):
"""
从零实现LSTM单元(用于理解门控机制)
"""
def __init__(self, input_dim, hidden_dim):
"""
初始化LSTM单元
Args:
input_dim: 输入特征维度
hidden_dim: 隐藏状态维度
"""
super(LSTMCellFromScratch, self).__init__()
# 将所有门的参数合并为一个大矩阵(提高效率)
# 输入: [h_{t-1}, x_t],维度为 (hidden_dim + input_dim)
# 输出: [i, f, g, o],维度为 (4 * hidden_dim)
self.x2h = nn.Linear(input_dim, 4 * hidden_dim, bias=False)
self.h2h = nn.Linear(hidden_dim, 4 * hidden_dim, bias=True)
def forward(self, x, hidden):
"""
前向传播
Args:
x: 当前时刻输入,形状为 (batch, input_dim)
hidden: (h_{t-1}, C_{t-1}),每个都是 (batch, hidden_dim)
Returns:
h_t, C_t: 更新后的隐藏状态和细胞状态
"""
h_prev, C_prev = hidden
# 合并计算所有门
# gates形状: (batch, 4 * hidden_dim)
gates = self.x2h(x) + self.h2h(h_prev)
# 分割为四个部分: i(输入门), f(遗忘门), g(候选值), o(输出门)
i_gate, f_gate, g_gate, o_gate = gates.chunk(4, dim=1)
# 应用激活函数
i_t = torch.sigmoid(i_gate) # 输入门
f_t = torch.sigmoid(f_gate) # 遗忘门
g_t = torch.tanh(g_gate) # 候选细胞状态
o_t = torch.sigmoid(o_gate) # 输出门
# 更新细胞状态
C_t = f_t * C_prev + i_t * g_t
# 更新隐藏状态
h_t = o_t * torch.tanh(C_t)
return h_t, C_t
class LSTMFromScratch(nn.Module):
"""多层LSTM网络(简化版,仅支持序列处理)"""
def __init__(self, input_dim, hidden_dim, num_layers=1):
super(LSTMFromScratch, self).__init__()
self.num_layers = num_layers
self.hidden_dim = hidden_dim
# 第一层
self.cells = nn.ModuleList()
self.cells.append(LSTMCellFromScratch(input_dim, hidden_dim))
# 后续层
for _ in range(1, num_layers):
self.cells.append(LSTMCellFromScratch(hidden_dim, hidden_dim))
def forward(self, x, hidden=None):
"""
处理整个序列
Args:
x: 输入序列,形状为 (batch, seq_len, input_dim)
hidden: 初始隐藏状态(可选)
Returns:
output: 所有时刻的输出,形状为 (batch, seq_len, hidden_dim)
(h_T, C_T): 最后时刻的隐藏状态和细胞状态
"""
batch_size, seq_len, _ = x.shape
if hidden is None:
h = [torch.zeros(batch_size, self.hidden_dim) for _ in range(self.num_layers)]
C = [torch.zeros(batch_size, self.hidden_dim) for _ in range(self.num_layers)]
else:
h, C = hidden
outputs = []
for t in range(seq_len):
x_t = x[:, t, :] # (batch, input_dim)
for layer in range(self.num_layers):
h[layer], C[layer] = self.cells[layer](x_t, (h[layer], C[layer]))
x_t = h[layer] # 下一层的输入是当前层的输出
outputs.append(h[-1]) # 取最后一层的输出
# 堆叠所有时刻的输出
output = torch.stack(outputs, dim=1) # (batch, seq_len, hidden_dim)
return output, (h, C)
# 测试LSTM实现
def test_lstm_implementation():
"""测试自定义LSTM与PyTorch官方LSTM的输出对比"""
torch.manual_seed(42)
batch_size = 2
seq_len = 3
input_dim = 5
hidden_dim = 8
# 输入数据
x = torch.randn(batch_size, seq_len, input_dim)
# 自定义LSTM
custom_lstm = LSTMFromScratch(input_dim, hidden_dim, num_layers=1)
output_custom, (h_custom, c_custom) = custom_lstm(x)
print(f"自定义LSTM输出形状: {output_custom.shape}")
print(f"自定义LSTM最后时刻隐藏状态形状: {h_custom.shape}")
# PyTorch官方LSTM
official_lstm = nn.LSTM(input_dim, hidden_dim, num_layers=1, batch_first=True)
output_official, (h_official, c_official) = official_lstm(x)
print(f"\nPyTorch LSTM输出形状: {output_official.shape}")
print("\n注: 由于权重初始化不同,输出数值不直接可比")
print(" 但门控机制和前向传播逻辑应完全一致")
test_lstm_implementation()
GRU(门控循环单元) :LSTM的简化版本,只有更新门 和重置门两个门,参数更少,计算更快,在某些任务上性能与LSTM相当。
1.3.3 注意力机制的诞生与演进
注意力机制的核心思想
传统的序列模型(如RNN/LSTM)将整个输入序列压缩为一个固定长度的向量 ,这成为性能瓶颈。注意力机制 (Attention Mechanism)允许模型在生成每个输出时,动态地关注输入序列的不同部分。
类比理解:想象你在阅读一篇长文章并需要回答一个问题。你不会一字不差地记住整篇文章,而是在回答每个具体问题时,有针对性地"回顾"文章中最相关的段落。这就是注意力机制的核心思想。
Bahdanau注意力(Additive Attention)
最早的注意力机制由Bahdanau等人于2014年在机器翻译中提出,用于解决Seq2Seq模型的瓶颈问题。
计算步骤:
-
计算注意力分数(Attention Scores) :
et,i=vaTtanh(Wast−1+Uahi) e_{t,i} = v_a^T \tanh(W_a \mathbf{s}_{t-1} + U_a \mathbf{h}_i) et,i=vaTtanh(Wast−1+Uahi)
其中 st−1\mathbf{s}_{t-1}st−1 为解码器上一时刻的隐藏状态,hi\mathbf{h}_ihi 为编码器第 iii 个时刻的隐藏状态。
-
归一化为注意力权重 :
αt,i=exp(et,i)∑j=1Txexp(et,j) \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{T_x}\exp(e_{t,j})} αt,i=∑j=1Txexp(et,j)exp(et,i)
-
计算上下文向量(Context Vector) :
ct=∑i=1Txαt,ihi \mathbf{c}t = \sum{i=1}^{T_x} \alpha_{t,i} \mathbf{h}_i ct=i=1∑Txαt,ihi
-
与解码器输入结合 :
st=f(st−1,yt−1,ct) \mathbf{s}t = f(\mathbf{s}{t-1}, \mathbf{y}_{t-1}, \mathbf{c}_t) st=f(st−1,yt−1,ct)
Luong注意力(Dot-Product Attention)
Luong等人于2015年提出了更简单的点积注意力:
et,i=st−1Thi e_{t,i} = \mathbf{s}_{t-1}^T \mathbf{h}_i et,i=st−1Thi
计算效率更高,成为后续Transformer架构的基础。
从注意力到Self-Attention
Self-Attention(自注意力)是注意力机制的一个特殊应用------在同一个序列内部计算注意力权重,使得序列中的每个位置都能"关注"到其他所有位置。
这一思想直接催生了Transformer架构(将在第三章详细介绍)。
python
# ===== 代码实战1.13:从零实现注意力机制 =====
import torch
import torch.nn as nn
import torch.nn.functional as F
class BahdanauAttention(nn.Module):
"""
Bahdanau注意力(Additive Attention)实现
用于Seq2Seq模型
"""
def __init__(self, hidden_dim):
"""
初始化注意力层
Args:
hidden_dim: 隐藏状态维度(编码器和解码器必须相同)
"""
super(BahdanauAttention, self).__init__()
self.W_a = nn.Linear(hidden_dim, hidden_dim)
self.U_a = nn.Linear(hidden_dim, hidden_dim)
self.v_a = nn.Linear(hidden_dim, 1)
def forward(self, decoder_hidden, encoder_outputs):
"""
计算注意力权重和上下文向量
Args:
decoder_hidden: 解码器上一时刻隐藏状态 (batch, hidden_dim)
encoder_outputs: 编码器所有时刻输出 (batch, src_len, hidden_dim)
Returns:
context: 上下文向量 (batch, hidden_dim)
attention_weights: 注意力权重 (batch, src_len)
"""
batch_size = decoder_hidden.size(0)
src_len = encoder_outputs.size(1)
# 扩展解码器隐藏状态以匹配编码器输出长度
# decoder_hidden: (batch, hidden_dim) -> (batch, src_len, hidden_dim)
decoder_hidden_expanded = decoder_hidden.unsqueeze(1).repeat(1, src_len, 1)
# 计算注意力分数 (Bahdanau公式)
# W_a(h_i) + U_a(s_{t-1})
scores = self.v_a(
torch.tanh(
self.W_a(encoder_outputs) + self.U_a(decoder_hidden_expanded)
)
) # (batch, src_len, 1)
scores = scores.squeeze(2) # (batch, src_len)
# 归一化为注意力权重
attention_weights = F.softmax(scores, dim=1) # (batch, src_len)
# 计算上下文向量: sum_i α_i * h_i
context = torch.bmm(
attention_weights.unsqueeze(1), # (batch, 1, src_len)
encoder_outputs # (batch, src_len, hidden_dim)
).squeeze(1) # (batch, hidden_dim)
return context, attention_weights
class DotProductAttention(nn.Module):
"""
点积注意力(Luong注意力)实现
计算效率更高,是现代Transformer的基础
"""
def __init__(self, hidden_dim):
super(DotProductAttention, self).__init__()
self.hidden_dim = hidden_dim
# 可选的缩放因子(当hidden_dim较大时建议使用)
self.scale = torch.sqrt(torch.tensor(hidden_dim, dtype=torch.float32))
def forward(self, query, key, value, mask=None):
"""
点积注意力前向传播
Args:
query: 查询向量 Q (batch, seq_len_q, hidden_dim)
key: 键向量 K (batch, seq_len_k, hidden_dim)
value: 值向量 V (batch, seq_len_k, hidden_dim)
mask: 可选掩码 (batch, seq_len_q, seq_len_k)
Returns:
output: 注意力输出 (batch, seq_len_q, hidden_dim)
attention_weights: 注意力权重 (batch, seq_len_q, seq_len_k)
"""
# 计算注意力分数: Q * K^T / sqrt(d_k)
scores = torch.bmm(query, key.transpose(1, 2)) / self.scale # (batch, seq_len_q, seq_len_k)
# 应用掩码(可选,用于padding或未来信息遮挡)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 归一化为注意力权重
attention_weights = F.softmax(scores, dim=-1) # (batch, seq_len_q, seq_len_k)
# 加权求和
output = torch.bmm(attention_weights, value) # (batch, seq_len_q, hidden_dim)
return output, attention_weights
def demonstrate_attention():
"""演示两种注意力机制的使用"""
torch.manual_seed(42)
batch_size = 2
src_len = 5
tgt_len = 3
hidden_dim = 8
# ===== Bahdanau注意力演示 =====
print("=== Bahdanau注意力 (用于Seq2Seq) ===")
bahdanau_attn = BahdanauAttention(hidden_dim)
# 模拟解码器隐藏状态 (通常是RNN/LSTM的隐藏状态)
decoder_hidden = torch.randn(batch_size, hidden_dim)
# 模拟编码器输出 (所有时刻的隐藏状态)
encoder_outputs = torch.randn(batch_size, src_len, hidden_dim)
context, attn_weights = bahdanau_attn(decoder_hidden, encoder_outputs)
print(f"上下文向量形状: {context.shape}")
print(f"注意力权重形状: {attn_weights.shape}")
print(f"注意力权重和: {attn_weights[0].sum().item():.4f} (应为1.0)")
# ===== 点积注意力演示 =====
print("\n=== 点积注意力 (Transformer基础) ===")
dot_attn = DotProductAttention(hidden_dim)
# 模拟Q, K, V (自注意力场景中它们来自同一输入)
query = torch.randn(batch_size, tgt_len, hidden_dim)
key = torch.randn(batch_size, src_len, hidden_dim)
value = key.clone() # 简化:K和V相同
output, attn_weights = dot_attn(query, key, value)
print(f"注意力输出形状: {output.shape}")
print(f"注意力权重形状: {attn_weights.shape}")
print(f"第一个样本的注意力权重:\n{attn_weights[0].detach().numpy()}")
demonstrate_attention()
1.3.4 神经网络在大模型时代的定位
从专用模型到基础模型(Foundation Models)
| 时代 | 代表技术 | 训练范式 | 特点 |
|---|---|---|---|
| 2012-2017 | CNN、RNN、LSTM | 监督学习(任务特定数据) | 专用模型,泛化能力有限 |
| 2018-2019 | BERT、GPT-1 | 预训练+微调 | 迁移学习,基础模型雏形 |
| 2020-2022 | GPT-3、CLIP | 大规模预训练+Prompt | 少样本/零样本学习 |
| 2023-至今 | GPT-4、LLaMA、Gemini | 预训练+RLHF/DPO | 多模态、推理能力、Agent |
大模型时代的神经网络核心组件
现代大模型(如GPT-4、LLaMA)虽然在架构上仍基于Transformer,但在以下方面进行了重要改进:
- 位置编码的演进:从绝对位置编码(Sinusoidal)到相对位置编码(RoPE,Rotary Position Embedding)
- 归一化层的位置:Post-Norm → Pre-Norm(稳定训练)
- 激活函数的选择:ReLU → GELU → SwiGLU(门控激活)
- 注意力机制的优化:Multi-Head → Multi-Query → Grouped-Query Attention
- 模型规模的扩展:从BERT-base的1.1亿参数到GPT-4的万亿级参数
关键洞察:神经网络的基本原理(反向传播、梯度下降、注意力机制)仍然是大模型训练的基石。理解这些基础原理,是深入掌握大模型技术的前提。
企业级考量与避坑指南
避坑1:梯度消失与梯度爆炸问题
问题描述 :
在深层网络中,反向传播时梯度需要经过多层连乘。如果每层的梯度都小于1(如Sigmoid的导数最大值为0.25),经过多层连乘后梯度会趋近于0(梯度消失 );反之,如果梯度都大于1,则会指数级增长(梯度爆炸)。
解决方案:
-
使用ReLU/GELU等现代激活函数(导数常为0或1,缓解梯度消失)
-
使用残差连接 (Residual Connection):y=f(x)+xy = f(x) + xy=f(x)+x,梯度可以直接通过skip connection传播
-
使用Batch/Layer Normalization:归一化层使得激活值保持在合理范围内
-
使用梯度裁剪 (Gradient Clipping):
pythontorch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) -
使用LSTM/GRU替代简单RNN(处理序列任务时)
避坑2:权重初始化不当导致训练不稳定
问题描述 :
如果权重初始化过小,信号在向前传播时会逐渐缩小;如果权重初始化过大,激活值会饱和(如Sigmoid)或导致梯度爆炸。
解决方案 :
使用合适的初始化策略:
| 激活函数 | 推荐初始化方法 | 公式 |
|---|---|---|
| Sigmoid/Tanh | Xavier初始化 | W∼U−6nin+nout,6nin+noutW \sim U-\\frac{\\sqrt{6}}{\\sqrt{n_{in}+n_{out}}}, \\frac{\\sqrt{6}}{\\sqrt{n_{in}+n_{out}}}W∼U−nin+nout 6 ,nin+nout 6 |
| ReLU/Leaky ReLU | Kaiming初始化 | W∼N0,2ninW \sim N0, \\sqrt{\\frac{2}{n_{in}}}W∼N0,nin2 |
| GELU | Kaiming初始化(He初始化) | 同上 |
python
# PyTorch中的正确初始化方式
def init_weights(m):
"""递归初始化模型权重"""
if isinstance(m, nn.Linear):
# 对于使用ReLU/GELU的层,使用Kaiming初始化
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
model = MyModel()
model.apply(init_weights) # 递归应用初始化
避坑3:Batch Size选择不当影响模型泛化
问题描述:
- Batch Size太小:梯度估计噪声大,训练不稳定,难以利用硬件加速
- Batch Size太大:泛化性能下降("Large Batch Size泛化差距"现象),显存不足
经验法则:
- 在显存允许的情况下,尽可能使用较大的Batch Size(充分利用GPU并行能力)
- 使用学习率warmup和衰减策略配合大Batch Size训练
- 当增加Batch Size时,等比例增加学习率 (Linear Scaling Rule):
KaTeX parse error: Expected 'EOF', got '' at position 65: ...rac{\text{batch_̲size}{\text{ne... - 如果Batch Size太大导致泛化性能下降 ,可以使用:
- 标签平滑(Label Smoothing)
- 知识蒸馏(Knowledge Distillation)
- 随机深度(Stochastic Depth,用于极深网络)
本章小结
核心Takeaways
-
神经网络的基础单元是神经元,通过加权求和、加偏置、激活函数三步完成一次前向传播。多层神经元堆叠形成深度网络。
-
激活函数引入非线性,是现代神经网络表达复杂函数的关键。ReLU因其简单高效成为CNN的标准选择,GELU则因平滑特性成为Transformer的首选。
-
反向传播算法基于链式法则,从输出层向输入层反向计算梯度,是训练神经网络的核心。理解其数据流转过程对于调试模型至关重要。
-
优化算法的选择直接影响收敛速度和模型性能。AdamW因解耦了权重衰减,成为训练Transformer模型的标准优化器。
-
正则化技术防止过拟合:Dropout、L1/L2正则化、Batch/Layer Normalization是深度学习的标配技术,但需要理解其适用场景。
-
神经网络架构经历了从MLP到CNN、RNN/LSTM,再到注意力机制和Transformer的演进。每一代架构都在解决上一代的局限性,最终催生了大模型时代。
-
企业级深度学习需要注意多个工程细节:梯度稳定性、权重初始化、学习率调度、Batch Size选择等,这些"小细节"往往决定了项目成败。
思考题
思考题1:为什么Transformer架构选择GELU而非ReLU作为激活函数?
参考答案 :
GELU相比ReLU有以下优势:
- 平滑性:GELU是平滑曲线(处处可导),而ReLU在零点不可导。平滑的激活函数有助于梯度传播,提高训练稳定性。
- 自适应保留概率:GELU可以理解为"随机正则化的期望",输入值越大,保留概率越高,这种自适应特性有助于模型学习。
- 实验性能:在BERT、GPT等Transformer模型中,GELU consistently outperforms ReLU。
- 理论支持:GELU基于高斯分布的累积分布函数,有更坚实的概率论基础。
思考题2:在训练深层网络时,为什么残差连接(Residual Connection)能有效缓解梯度消失问题?请结合反向传播的链式法则进行分析。
参考答案 :
对于普通网络,反向传播时梯度需要经过每一层的权重矩阵连乘:
∂L∂x=∂L∂y⋅WT⋅f′(⋅) \frac{\partial \mathcal{L}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot W^T \cdot f'(\cdot) ∂x∂L=∂y∂L⋅WT⋅f′(⋅)
当网络很深时,连乘效应导致梯度趋近于0。
而对于残差连接 y=f(x)+xy = f(x) + xy=f(x)+x,反向传播时:
∂L∂x=∂L∂y⋅(∂f(x)∂x+I) \frac{\partial \mathcal{L}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot \left(\frac{\partial f(\mathbf{x})}{\partial \mathbf{x}} + I\right) ∂x∂L=∂y∂L⋅(∂x∂f(x)+I)
其中 III 为单位矩阵。这意味着梯度可以直接通过skip connection传播 (∂L∂y⋅I\frac{\partial \mathcal{L}}{\partial \mathbf{y}} \cdot I∂y∂L⋅I 项),不受权重矩阵和激活函数导数的影响,从而有效缓解梯度消失问题。这也是ResNet能够成功训练上千层网络的关键原因。
参考文献
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.
- LeCun, Y., Bengio, Y., & Hinton, G. (2015). Deep learning. Nature, 521(7553), 436-444.
- Vaswani, A., et al. (2017). Attention is all you need. NeurIPS 2017.
- He, K., et al. (2016). Deep residual learning for image recognition. CVPR 2016.
- Ioffe, S., & Szegedy, C. (2015). Batch normalization: Accelerating deep network training by reducing internal covariate shift. ICML 2015.
- Kingma, D. P., & Ba, J. (2015). Adam: A method for stochastic optimization. ICLR 2015.
- Loshchilov, I., & Hutter, F. (2019). Decoupled weight decay regularization. ICLR 2019. (AdamW)