☰
PyTorch 进阶指南:从张量操作到模型训练全流程
2026/9/28 21:08:50 网站建设 项目流程

昨天我们入门了深度学习的核心概念和 PyTorch 的 Tensor 基础操作。但如果只停留在创建张量、做做加减乘除,离真正训练一个模型还差得远。今天我们继续深入——掌握张量的形状操作与拼接,理解自动微分的原理,走通模型训练的五步循环,最后用一个线性回归案例把所有知识串起来。

这篇文章是从"会用 PyTorch"到"能训练模型"的关键一跃。


一、张量的形状操作

在深度学习中,数据在不同层之间流动时,形状经常需要变换。把一个图像张量从[批量, 通道, 高, 宽]变成[批量, 高×宽×通道],把一个特征矩阵转置——这些都需要形状操作。

查看形状

import torch t = torch.randn(2, 3, 4) print(t.shape) # torch.Size([2, 3, 4]) print(t.size()) # torch.Size([2, 3, 4])

.shape是属性,.size()是方法,两者结果完全一样。看到torch.Size([2, 3, 4])就知道这个张量有 3 个维度,分别有 2、3、4 个元素。

修改形状

t = torch.randn(2, 3, 4) # 24个元素 # reshape:改变形状,不改变数据 t1 = t.reshape(6, 4) # 变成 [6, 4] t2 = t.reshape(2, 12) # 变成 [2, 12] # view:同样改变形状,但要求内存连续 t3 = t.view(6, 4) # 变成 [6, 4]

reshape和view看起来一样,但有一个关键区别:view要求张量的物理内存是连续的。如果一个张量经过了维度交换(transpose)等操作,物理内存不再连续,view就会报错,而reshape仍然可以工作(它会自动处理内存问题)。

判断内存是否连续可以用.contiguous属性。如果需要让不连续的张量变连续,调用.contiguous()方法即可:

t = torch.randn(2, 3) t_t = t.transpose(0, 1) # 交换维度后内存不连续 # t_t.view(6) # 报错! t_t.contiguous().view(6) # 正常工作

实际项目中,推荐统一使用reshape,它更安全,不会因为内存不连续而报错。但如果你追求极致性能,view不涉及数据拷贝,比reshape更快——在确定内存连续的情况下用view是更好的选择。理解两者的底层差异,比记住"用哪个"更重要。

一个常见场景:CNN 的全连接层之前,需要把[batch, channels, height, width]的四维张量变成[batch, channels × height × width]的二维张量。这时就用reshape(batch, -1)——-1表示自动推断这一维的大小,非常方便。

增删维度

# unsqueeze:增加一个大小为1的维度 t = torch.randn(3, 4) t1 = t.unsqueeze(0) # [1, 3, 4] 在第0维增加 t2 = t.unsqueeze(2) # [3, 1, 4] 在第2维增加 # squeeze:删除大小为1的维度 t3 = t1.squeeze(0) # [3, 4] 删除第0维 t4 = t1.squeeze() # [3, 4] 删除所有大小为1的维度

unsqueeze在深度学习中极为常用——比如把一个样本[3, 224, 224]变成[1, 3, 224, 224]加上 batch 维度,就是用unsqueeze(0)完成的。squeeze则是反向操作,去掉多余的维度。

调整维度顺序

t = torch.randn(2, 3, 4) # transpose:交换两个维度 t1 = t.transpose(1, 2) # [2, 4, 3] 交换第1和第2维 # permute:批量交换所有维度 t2 = t.permute(2, 0, 1) # [4, 2, 3] 按指定顺序重排所有维度

transpose只能交换两个维度,permute可以一次性重排所有维度。在图像处理中,permute特别常用——PyTorch 默认图片格式是[C, H, W](通道、高、宽),但很多数据加载器给出的是[H, W, C],就需要用permute(2, 0, 1)来转换。

形状操作在深度学习中的使用频率非常高。一个典型的 CNN 数据流是这样的:输入图片[B, 3, 224, 224]经过卷积层变成[B, 64, 56, 56],进入全连接层前用reshape展平为[B, 64×56×56],输出后再用reshape恢复形状。如果形状搞错了,PyTorch 报错信息通常很清晰——它会告诉你期望的形状和实际的形状,根据报错信息调整即可。


二、张量的拼接

深度学习中经常需要把多个张量拼到一起——比如把多个 batch 的特征拼接,或者把不同层的输出合并。

cat:同维度拼接

a = torch.randn(2, 3) b = torch.randn(2, 3) # dim=0:上下拼接(沿行方向),不产生新维度 c1 = torch.cat([a, b], dim=0) # [4, 3] # dim=1:左右拼接(沿列方向),不产生新维度 c2 = torch.cat([a, b], dim=1) # [2, 6]

cat的特点是不会产生新的维度——两个[2, 3]的张量沿 dim=0 拼接,结果是[4, 3],维度数不变,还是二维。

stack:新维度拼接

a = torch.randn(2, 3) b = torch.randn(2, 3) # stack:产生一个新的维度 c = torch.stack([a, b], dim=0) # [2, 2, 3] 新增了第0维

stack和cat的区别在于:stack会产生一个新的维度。两个[2, 3]的张量用stack拼接,结果是[2, 2, 3]——多了一个维度。这就像把两张照片叠成一摞,多出了"第几张"这个维度。

简单记忆:cat是拼接,stack是堆叠。不想增加维度用cat,想增加维度用stack。

选择cat还是stack取决于你的需求。比如你有 4 张图片[3, 224, 224],想组成一个 batch——那就用stack把它们堆叠成[4, 3, 224, 224],新增了 batch 维度。但如果你已经有两个 batch[32, 3, 224, 224]和[16, 3, 224, 224],想合成一个[48, 3, 224, 224]的大 batch——那就用cat沿 dim=0 拼接,不增加维度。理解了"是否需要新维度"这个判断标准,选 cat 还是 stack 就不会纠结了。


三、张量的自动微分

自动微分是 PyTorch 的灵魂功能,也是深度学习能"自动训练"的核心。理解了它,你就理解了 PyTorch 为什么能自动更新模型参数。

梯度是什么

梯度是多个参数的偏导数组成的一个向量。对于损失函数L = f(w₁, w₂, ..., wₙ),梯度就是[∂L/∂w₁, ∂L/∂w₂, ..., ∂L/∂wₙ]。梯度的方向是损失增加最快的方向,所以参数更新时要沿着梯度的反方向走——这就是梯度下降。

自动微分的四步流程

import torch # 第一步:在张量中设置 requires_grad=True w = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) # 第二步:前向传播,构建计算图(PyTorch自动记录运算过程) y = w * 2 + 1 loss = y.sum() # 第三步:反向传播,自动计算梯度 loss.backward() # 第四步:获取梯度结果 print(w.grad) # tensor([2., 2., 2.])

设置requires_grad=True后,PyTorch 会自动跟踪这个张量的所有运算,构建一个计算图——记录每一步操作的函数和依赖关系。调用.backward()时,PyTorch 沿着计算图反向追溯,用链式法则自动算出每个参数的梯度,结果存在.grad属性中。

上面例子中,y = w * 2 + 1,loss = y.sum(),所以∂loss/∂w = 2——梯度确实是[2, 2, 2]。

一个关键陷阱:梯度清零

# 梯度会累加!必须每次反向传播前清零 w = torch.tensor([1.0], requires_grad=True) # 第一轮 y1 = (w * 3).sum() y1.backward() print(w.grad) # tensor([3.]) # 第二轮——不清零的话梯度会累加 y2 = (w * 3).sum() y2.backward() print(w.grad) # tensor([6.]) 错误!是3+3=6

梯度每轮训练会自动累加——这是 PyTorch 的设计,在某些场景(如梯度累积,用小 batch 模拟大 batch)有用,但在常规训练中会导致梯度越来越大,必须每次训练前清零:

w.grad.zero_() # 手动清零 # 或用优化器清零(更常用) optimizer.zero_grad()

这个陷阱几乎每个 PyTorch 初学者都踩过——训练几轮发现 loss 越来越大,模型预测越来越离谱,debug 半天最后发现是梯度没清零。养成习惯:写训练循环的第一行永远是optimizer.zero_grad(),就像出门前先检查钥匙一样自然。

还有一个细节值得注意:.backward()默认只能对标量(只有一个元素的张量)调用。如果你的 loss 是一个向量,需要传入梯度参数:loss.backward(gradient=torch.ones_like(loss))。但在实际项目中,损失函数通常输出标量,所以这个问题不常见。


四、模型训练的五步循环

有了前面的基础,现在来看 PyTorch 模型训练的核心流程。不管网络多复杂,训练过程都遵循这五步循环:

# === 训练循环 === for epoch in range(epochs): for x, y in dataloader: # 1. 梯度清零 optimizer.zero_grad() # 2. 前向传播 output = model(x) # 3. 计算损失 loss = criterion(output, y) # 4. 反向传播(自动计算梯度) loss.backward() # 5. 参数更新(沿梯度反方向更新参数) optimizer.step()

五步的含义:

梯度清零(zero_grad):清空上一轮累积的梯度。这是最容易被遗忘但最重要的一步——忘了清零,梯度就会像滚雪球一样越滚越大,模型直接崩掉。

前向传播(model(x)):把数据送入模型,得到预测结果。这一步是"用当前参数算一遍预测"。

计算损失(criterion(output, y)):用损失函数衡量预测值和真实值的差距。分类问题常用交叉熵损失,回归问题常用均方误差。

反向传播(loss.backward()):PyTorch 自动从 loss 出发,沿计算图反向计算每个参数的梯度。你不需要手动求导,框架全部搞定。

参数更新(optimizer.step()):优化器根据梯度更新参数,沿着梯度的反方向走一步。学习率决定了步子大小。

这五步构成了深度学习训练的最小完整循环。理解了它,你看任何 PyTorch 训练代码都不会迷路——不管多复杂的模型,核心都是这个循环。

还有一个概念需要注意:epoch vs batch vs iteration。一个 epoch 是把所有训练数据跑完一遍;一个 batch 是一次送入模型的一批数据(如 32 条);一个 iteration 是一次前向+反向传播的过程。如果 1000 条数据、batch_size=32,一个 epoch 大约需要 32 个 iteration。五步循环的每一轮就是一个 iteration。

另外,训练循环中经常需要加上model.train()和model.eval()的切换。model.train()告诉模型处于训练模式,Dropout 会随机丢弃神经元、BatchNorm 会更新运行统计量;model.eval()告诉模型处于评估模式,Dropout 关闭、BatchNorm 使用固定统计量。训练时调用model.train(),验证或测试时调用model.eval(),这也是一个容易遗漏的细节。


五、实战案例:线性回归

理论讲完了,用一个完整的线性回归案例把所有知识串起来。这个案例涵盖了从数据创建到模型保存的完整流程。

完整流程

import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from sklearn.datasets import make_regression from torch.utils.tensorboard import SummaryWriter # 1. 数据创建 X, y = make_regression(n_samples=500, n_features=5, noise=10, random_state=42) X = torch.FloatTensor(X) y = torch.FloatTensor(y).unsqueeze(1) # 增加维度 [500] → [500, 1] # 2. TensorBoard 可视化 writer = SummaryWriter('runs/linear_regression') # 3. 构建 DataLoader,设置批次 dataset = TensorDataset(X, y) dataloader = DataLoader(dataset, batch_size=32, shuffle=True) # 4. 创建模型 model = nn.Linear(5, 1) # 5个特征 → 1个输出 # 5. 创建损失函数 criterion = nn.MSELoss() # 6. 创建优化器 optimizer = torch.optim.SGD(model.parameters(), lr=0.001) # 7. 模型训练 for epoch in range(100): total_loss = 0 for batch_x, batch_y in dataloader: optimizer.zero_grad() # 梯度清零 output = model(batch_x) # 前向传播 loss = criterion(output, batch_y) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 参数更新 total_loss += loss.item() writer.add_scalar('Loss', total_loss / len(dataloader), epoch) if (epoch + 1) % 20 == 0: print(f"Epoch {epoch+1}, Loss: {total_loss/len(dataloader):.4f}") # 8. 保存模型 torch.save(model.state_dict(), 'linear_model.pth') # 9. 加载模型 loaded_model = nn.Linear(5, 1) loaded_model.load_state_dict(torch.load('linear_model.pth')) loaded_model.eval() # 10. 结果预测 with torch.no_grad(): # 预测时不需要计算梯度 test_x = torch.FloatTensor([[1.0, 2.0, 3.0, 4.0, 5.0]]) prediction = loaded_model(test_x) print(f"预测结果: {prediction.item():.4f}")

流程解读

这个案例覆盖了十个步骤,每一步都有讲究:

数据创建用make_regression生成回归数据,这是 sklearn 提供的便捷函数。注意unsqueeze(1)把标签从一维变成二维——这是 PyTorch 的要求,nn.Linear的输入输出都需要是二维的[batch_size, features]。

DataLoader负责把数据分批次送入模型。batch_size=32表示每次送 32 条样本,shuffle=True表示每个 epoch 打乱数据顺序。分批训练既节省内存又能让梯度更有随机性。

模型创建用nn.Linear(5, 1)创建一个线性层——5 个输入特征,1 个输出值,这就是最简单的神经网络。复杂的模型也只是把多个层组合起来。

优化器用 SGD(随机梯度下降),学习率 0.001。学习率的选择和前面线性回归中学到的一样——太大震荡,太小太慢。除了 SGD,PyTorch 还提供了 Adam、RMSprop 等优化器,其中 Adam 是最常用的"万能"优化器,通常不需要太多调参就能取得不错的效果。

模型保存与加载用state_dict()保存模型参数(不是保存整个模型),加载时先创建相同结构的模型再加载参数。这是 PyTorch 推荐的保存方式——更灵活、更安全。保存整个模型(torch.save(model, ...))虽然在代码上更简洁,但它依赖于 Python 的 pickle 序列化,加载时必须保证模型类的定义在同一个路径下,迁移性差。而state_dict只保存参数数值,不依赖类定义,更适合实际部署。

预测时用torch.no_grad()包裹——告诉 PyTorch 不需要构建计算图、不需要计算梯度,节省内存和计算。model.eval()把模型切换到评估模式(影响 Dropout 和 BatchNorm 的行为)。这两个在预测时缺一不可——no_grad()省内存,eval()保证行为正确。

TensorBoard在案例中承担了"训练监控"的角色。通过writer.add_scalar('Loss', loss_value, epoch)把每轮的 loss 写入日志,启动 TensorBoard 后就能在浏览器中看到 loss 的下降曲线。如果曲线平滑下降,说明训练正常;如果曲线剧烈震荡或停滞,就需要排查问题。这种"可视化训练过程"的能力在调参时极其重要。

TensorBoard 可视化

案例中用到了TensorBoard,它是 TensorFlow 生态中诞生但已被 PyTorch 广泛采用的可视化工具。通过SummaryWriter把训练过程中的 loss 写入日志,然后在终端启动 TensorBoard 就能看到 loss 曲线的变化趋势——是下降还是震荡,有没有收敛。

tensorboard --logdir=runs/linear_regression

打开浏览器就能看到实时更新的训练曲线。在调参时,TensorBoard 是你的"仪表盘"——学习率合不合适、模型有没有过拟合,看一眼曲线就知道了。


六、学习心得与建议

第一,形状操作是 PyTorch 的基本功。深度学习代码中一半的 bug 都和形状有关——维度对不上、batch 维度忘了加、通道顺序搞反了。把 reshape、unsqueeze、permute 这些操作练到肌肉记忆,调试效率会大幅提升。

第二,自动微分是 PyTorch 的灵魂。你不需要手动推导梯度、不需要实现反向传播——requires_grad=True+backward()搞定一切。但你要理解它的工作原理:计算图、链式法则、梯度累加。理解了这些,遇到梯度爆炸、梯度消失等问题时才知道怎么排查。

第三,五步训练循环要背下来。zero_grad → forward → loss → backward → step,这五步是所有 PyTorch 训练代码的骨架。不管模型多复杂,核心都是这个循环。把它刻在脑子里,读任何训练代码都能快速抓住主线。

第四,线性回归案例是最好的起点。虽然它简单,但包含了完整的训练流程——数据加载、模型定义、损失函数、优化器、训练循环、模型保存加载、预测。后面学 CNN、RNN、Transformer,变化的只是模型结构,外围的训练流程几乎一模一样。把这个案例吃透,后面学任何模型都是"换汤不换药"。

第五,善用 TensorBoard。很多初学者训练完才知道效果不好,却不知道问题出在哪。TensorBoard 能让你实时看到 loss 曲线的变化——如果 loss 一直不降,可能是学习率太小或模型太简单;如果 loss 突然飙升,可能是梯度爆炸或学习率太大;如果 loss 降到一定程度就不动了,可能是陷入了局部最优。训练不盲目,先让过程可视化。


写在最后

从昨天的 Tensor 基础到今天的模型训练全流程,我们完成了 PyTorch 的核心入门。张量的形状操作和拼接是工具,自动微分是引擎,五步训练循环是骨架,线性回归案例是验证——四者合在一起,就是一个完整的 PyTorch 模型训练知识体系。

回顾这一路:从机器学习概述到线性回归,从 KNN 到决策树与集成学习,从聚类到深度学习概念,再到今天的 PyTorch 模型训练——我们走过了一条从浅到深、从理论到实践的完整学习路径。每一篇文章都不是孤立的,它们层层递进,前一篇为后一篇铺路,后一篇在前一篇的基础上深入。

深度学习的大门已经打开,接下来的路——CNN 处理图像、RNN 处理文本、Transformer 大杀四方——都是在今天这个基础上搭建更复杂的模型结构。骨架已经搭好,剩下的就是往上面添砖加瓦。


如果这篇文章对你有帮助,欢迎点赞收藏。下一篇我们将用 PyTorch 搭建更复杂的神经网络模型,敬请关注。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询