1. 线性回归:深度学习的最小训练闭环
线性回归作为机器学习中最基础的模型,却是理解深度学习训练流程的最佳切入点。很多人在学习深度学习时直接跳过了这一基础环节,导致后续对复杂模型的理解始终存在盲区。今天我们就从零开始,用PyTorch手写一个完整的线性回归训练流程,不借助任何高级封装,彻底搞明白参数究竟是如何"动起来"的。
为什么说线性回归是深度学习的最小训练闭环?因为它包含了神经网络训练的四大核心要素:
- 模型结构:线性变换 y = Xw + b
- 损失函数:衡量预测值与真实值的差距(这里用MSE)
- 梯度计算:通过自动微分得到参数更新方向
- 参数更新:使用SGD等优化算法调整参数
这个闭环正是所有深度学习模型训练的通用范式,无论是后面的CNN、RNN还是Transformer,本质上都是在模型结构部分变得更加复杂而已。
2. 数据准备:生成可控的合成数据
2.1 为什么要使用合成数据?
在实际项目中,我们通常使用真实数据集。但在学习阶段,使用已知真实参数的合成数据有几个独特优势:
- 可以精确控制数据分布特性
- 已知最优解(真实w和b),便于验证模型正确性
- 排除数据质量问题对学习过程的干扰
python复制import torch
from torch.utils import data
torch.manual_seed(0) # 固定随机种子保证可复现性
def synthetic_data(w, b, num_examples):
"""生成 y = Xw + b + noise"""
X = torch.randn(num_examples, len(w)) # 从标准正态分布采样特征
y = X @ w + b # 线性变换
y += torch.randn(num_examples, 1) * 0.01 # 添加高斯噪声
return X, y
true_w = torch.tensor([2.0, -3.4]) # 真实的权重参数
true_b = 4.2 # 真实的偏置参数
features, labels = synthetic_data(true_w, true_b, 1000)
print(features.shape, labels.shape) # 输出:(1000,2) (1000,1)
这里我们设置了真实参数w=[2.0, -3.4]和b=4.2,之后训练的目标就是看模型能否从数据中学习到接近这些真实值的参数。
2.2 数据加载与批处理
虽然我们坚持"从零实现",但数据加载这种工程性工作可以合理使用PyTorch提供的工具:
python复制def load_array(data_arrays, batch_size, is_train=True):
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 32
data_iter = load_array((features, labels), batch_size)
# 检查一个batch的形状
X_batch, y_batch = next(iter(data_iter))
print(X_batch.shape, y_batch.shape) # 输出:(32,2) (32,1)
注意:DataLoader只是帮我们实现了数据分批和打乱,不涉及任何模型封装,因此不违背"从零实现"的原则。在工业级实现中,数据加载往往比模型实现更复杂,合理使用框架工具是明智的选择。
3. 参数初始化与模型定义
3.1 初始化可学习参数
在深度学习中,参数初始化对训练效果有重要影响。对于线性回归这种简单模型,我们采用以下策略:
python复制w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
关键点说明:
- w从均值为0、标准差为0.01的正态分布采样,这种小随机数初始化是深度学习中的常见做法
- b初始化为0,这是线性回归偏置项的常规初始化方式
- requires_grad=True告诉PyTorch这些参数需要计算梯度,这是自动微分的前提
3.2 定义模型结构
线性回归模型就是简单的线性变换:
python复制def linreg(X, w, b):
"""线性回归模型"""
return X @ w + b # @表示矩阵乘法
这个看似简单的公式实际上包含了深度学习模型的两个基本操作:
- 矩阵乘法(X @ w):特征与权重的线性组合
- 加法(+ b):添加偏置项
3.3 定义损失函数
我们使用均方误差(MSE)作为损失函数,但实现时有些技巧:
python复制def squared_loss(y_hat, y):
"""平方损失函数"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
为什么除以2?这是一个实用技巧,因为当我们对平方项求导时,系数2会被1/2抵消,使得梯度表达式更简洁。这在数学上不影响优化结果,因为常数系数可以通过学习率调整来补偿。
4. 优化器实现:手动SGD
4.1 随机梯度下降原理
随机梯度下降(SGD)是最基础的优化算法,其参数更新公式为:
θ = θ - η·∇θ
其中:
- θ表示模型参数(w和b)
- η是学习率
- ∇θ是损失函数对参数的梯度
4.2 手动实现SGD
python复制def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
with torch.no_grad(): # 更新时不构建计算图
for param in params:
param -= lr * param.grad / batch_size # 梯度更新
param.grad.zero_() # 梯度清零
关键细节解析:
- torch.no_grad()上下文管理器:防止参数更新操作被记录到计算图中,避免不必要的内存消耗
- 除以batch_size:因为我们计算的是batch内样本损失的和,梯度也相应是batch样本梯度的和,除以batch_size得到平均梯度
- zero_():PyTorch的梯度是累加的,必须手动清零,否则会导致梯度错误
5. 训练循环实现
5.1 完整的训练流程
现在我们将所有组件组合起来,实现完整的训练循环:
python复制lr = 0.03 # 学习率
num_epochs = 3 # 训练轮数
net = linreg # 模型
loss = squared_loss # 损失函数
for epoch in range(num_epochs):
for X, y in data_iter: # 遍历数据批次
y_hat = net(X, w, b) # 前向传播
l = loss(y_hat, y) # 计算损失
l.sum().backward() # 反向传播
sgd([w, b], lr, batch_size) # 参数更新
# 每个epoch结束后评估整体损失
with torch.no_grad():
train_l = loss(net(features, w, b), labels).mean()
print(f"epoch {epoch+1}, loss {train_l.item():.6f}")
# 输出训练结果
print("w error:", true_w - w.reshape(true_w.shape))
print("b error:", true_b - b)
print("learned w:", w.reshape(-1).tolist(), " learned b:", b.item())
5.2 训练过程解析
- 前向传播:计算当前参数下的模型预测值
- 损失计算:评估预测值与真实值的差距
- 反向传播:自动计算损失对参数的梯度
- 参数更新:根据梯度调整参数值
- 周期评估:每个epoch结束后计算在整个数据集上的平均损失
5.3 关键实现细节
为什么需要l.sum().backward()?
- l是一个形状为(batch_size, 1)的张量,包含batch中每个样本的损失
- PyTorch的backward()通常需要在一个标量上调用
- sum()将batch内所有样本的损失相加得到一个标量,然后进行反向传播
梯度清零的重要性
PyTorch的设计中,梯度是累加的。如果不手动清零,每次backward()计算的梯度会与之前计算的梯度相加,导致参数更新方向错误。这是初学者常犯的错误之一。
6. 结果分析与模型评估
训练完成后,我们可以检查模型学到的参数与真实参数的接近程度:
code复制epoch 1, loss 0.000050
epoch 2, loss 0.000050
epoch 3, loss 0.000050
w error: tensor([ 0.0003, -0.0004], grad_fn=<SubBackward0>)
b error: tensor([-0.0003], grad_fn=<RsubBackward1>)
learned w: [1.9996984004974365, -3.3995954990386963] learned b: 4.200299739837646
可以看到:
- 损失值迅速下降并稳定在一个很小的值
- 学到的w和b与真实值非常接近(误差在0.0005以内)
- 验证了我们实现的正确性
7. 深度学习训练的通用模式
通过这个简单的线性回归实现,我们已经掌握了深度学习训练的通用模式:
-
前向传播:计算模型输出
- 线性回归:y = Xw + b
- 复杂模型:可能是多层神经网络、注意力机制等
-
损失计算:量化预测误差
- 回归问题:MSE
- 分类问题:交叉熵
- 其他任务:设计相应的损失函数
-
反向传播:自动计算梯度
- 无论模型多复杂,PyTorch的autograd都能自动处理
- 理解计算图的概念很重要
-
参数更新:优化算法调整参数
- SGD是最基础的优化器
- 实际中常用Adam等更复杂的优化器
这个模式适用于几乎所有的深度学习模型,区别仅在于模型结构的复杂度和损失函数的设计。
8. 常见问题与调试技巧
8.1 梯度爆炸/消失
现象:损失值变成NaN或变得异常大
解决方法:
- 调整学习率(通常是降低)
- 检查参数初始化方式
- 添加梯度裁剪(gradient clipping)
8.2 模型不收敛
现象:损失值波动大或持续不下降
可能原因:
- 学习率设置不当
- 数据预处理有问题(如特征尺度差异大)
- 模型实现存在bug
8.3 过拟合
现象:训练损失低但验证损失高
解决方法:
- 增加训练数据
- 使用正则化(如L2正则化)
- 简化模型结构
9. 扩展思考
9.1 为什么不用解析解?
线性回归实际上有解析解(正规方程),为什么我们要用梯度下降?
- 解析解需要计算矩阵逆,当特征维度高时计算代价大(O(n³)复杂度)
- 梯度下降更适合大规模数据,可以分批处理
- 梯度下降的思维方式可以推广到更复杂的模型
9.2 如何扩展到更复杂模型?
理解了线性回归的训练流程后,扩展到更复杂模型只需要:
- 替换模型结构(如前向传播函数)
- 根据任务选择合适的损失函数
- 可能需要调整优化器
例如,要实现一个神经网络:
- 前向传播变为多个线性变换加激活函数
- 损失函数可能变为交叉熵
- 优化器可以选择Adam
10. 工程实践建议
- 日志记录:训练过程中记录损失、准确率等指标,便于分析
- 可视化:绘制损失曲线、参数分布等,直观理解训练过程
- 参数保存:定期保存模型参数,防止训练中断丢失结果
- 超参数调优:系统性地调整学习率、batch size等超参数
- 代码模块化:将模型、数据加载、训练循环等组件分离,提高可维护性
通过这个从零实现的线性回归示例,我们不仅理解了深度学习训练的基本原理,还掌握了PyTorch的核心使用方法。这些知识将为学习更复杂的深度学习模型打下坚实基础。