1. 为什么我建议你从PyTorch开始入门深度学习
如果你刚接触深度学习,面对TensorFlow、PyTorch、JAX、PaddlePaddle这些名字,大概率会陷入一种“选哪个”的纠结。我当年也是这么过来的,翻了几十篇对比文章,越看越晕。后来一个做CV的师兄跟我说了句大实话:“你纠结框架的时间,够你把PyTorch的官方教程跑完两遍了。”这句话点醒了我。
PyTorch这几年在学术圈和工业界的势头非常猛。根据我自己的观察,近两年顶会论文的开源代码里,PyTorch实现的比例已经占到了压倒性的多数。这意味着什么?意味着你遇到问题去搜解决方案,大概率能找到PyTorch版本的参考实现;意味着你读别人的论文想复现,大概率不用再费劲做框架迁移。对于初学者来说,这一点太重要了——生态好,学习成本就低。
这篇文章我想做的事情很简单:把PyTorch入门的路径给你捋清楚。不是那种“官方文档翻译”式的教程,而是从一个实际写过项目、踩过坑的人的角度,告诉你哪些概念必须吃透、哪些操作是高频使用的、哪些坑我替你踩过了。不管你是完全没碰过深度学习的小白,还是从其他框架转过来的老手,应该都能从里面找到对你有用的东西。
PyTorch本质上是一个基于动态计算图的深度学习框架。这句话听起来有点绕,我用大白话解释一下:它让你能用Python的方式去写神经网络,该调试调试、该打印打印,跟写普通Python程序没什么两样。这种“所见即所得”的体验,是它相比早期静态图框架最大的优势。你不需要先定义一个完整的计算图再运行,而是可以边写边跑,哪里不对立刻就能发现。
2. 入门之前必须搞清楚的几个核心概念
2.1 Tensor:一切数据的基本单位
PyTorch里所有的数据都是Tensor。你可以把它理解成NumPy的ndarray,但是多了两个关键能力:可以在GPU上运行,以及可以自动求梯度。这两点对于深度学习来说缺一不可。
我刚开始学的时候,觉得Tensor和NumPy数组差不多嘛,有什么好学的。后来发现,Tensor的维度操作、广播机制、设备切换这些细节,如果不熟悉,写模型的时候会非常痛苦。比如你从DataLoader里取出来的数据是[batch_size, channels, height, width],送进模型之前可能需要reshape、permute、归一化,每一步都涉及Tensor操作。
创建一个Tensor有很多种方式,我列几个最常用的:
import torch # 从Python列表创建 a = torch.tensor([[1, 2], [3, 4]]) # 创建全零/全一张量 b = torch.zeros(3, 4) c = torch.ones(2, 3, 5) # 创建随机张量 d = torch.randn(3, 3) # 标准正态分布 e = torch.rand(3, 3) # [0, 1)均匀分布 # 从NumPy数组转换 import numpy as np f = torch.from_numpy(np.array([1, 2, 3]))这里有个细节值得注意:torch.tensor()和torch.Tensor()是不一样的。前者会根据输入数据推断类型,后者默认创建float32类型的空张量。我建议统一用torch.tensor(),不容易出幺蛾子。
Tensor的设备切换也是高频操作。你写模型的时候,需要显式地把数据和模型都搬到GPU上:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") x = torch.randn(3, 3).to(device)注意:如果你在CPU上创建了Tensor,然后想和GPU上的Tensor做运算,PyTorch会直接报错。所以养成习惯,在训练循环开始之前就把模型和数据都
.to(device)。
2.2 Autograd:自动求导的魔法
深度学习的核心是反向传播,而反向传播的核心是求导。PyTorch的autograd机制让你不需要手动推导任何梯度公式,只要把前向计算写出来,调用.backward(),梯度就自动算好了。
这个机制的底层原理其实不复杂:PyTorch会记录你所有对Tensor的操作,构建一张计算图。当你调用.backward()的时候,它沿着这张图反向遍历,用链式法则计算每个参数的梯度。
x = torch.tensor([2.0], requires_grad=True) y = x ** 2 + 3 * x + 1 y.backward() print(x.grad) # 输出 dy/dx = 2x + 3 = 7requires_grad=True这个标志很关键。只有设置了它的Tensor,PyTorch才会追踪其上的操作。模型参数默认都是requires_grad=True,而输入数据通常是False,因为我们不需要对输入求梯度。
有一个坑我踩过:每次反向传播之前必须清零梯度。因为PyTorch默认会累积梯度,如果你不清零,第二次迭代的梯度就会加上第一次的,结果完全不对。
optimizer.zero_grad() # 这行不能省 loss.backward() optimizer.step()2.3 nn.Module:搭建网络的积木
PyTorch里所有的神经网络都继承自nn.Module。你只需要在__init__里定义层,在forward里定义数据怎么流,就这么简单。
import torch.nn as nn class SimpleNet(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 256) self.fc2 = nn.Linear(256, 10) self.relu = nn.ReLU() def forward(self, x): x = self.relu(self.fc1(x)) x = self.fc2(x) return x这个模式看起来简单,但里面有几个容易犯的错误。第一,忘记调用super().__init__(),这会导致PyTorch无法正确注册子模块。第二,在forward里用print调试的时候,如果模型在GPU上,打印出来的Tensor需要先.cpu()。第三,forward的输入输出维度要对齐,特别是全连接层,nn.Linear(in_features, out_features)的两个参数别搞反了。
2.4 动态图 vs 静态图:为什么PyTorch更“跟手”
这是PyTorch和其他框架最本质的区别。静态图框架(比如早期的TensorFlow)要求你先定义完整的计算图,然后再喂数据运行。这就像你先画好一张工厂流水线的图纸,然后才能开工。好处是优化空间大,坏处是调试极其痛苦——你没法在中间打断点看变量。
PyTorch的动态图是“边写边跑”,你写一行代码,它就执行一行。这意味着你可以用Python原生的调试工具,可以在forward里随意加print,可以根据条件动态改变网络结构。对于研究和实验来说,这种灵活性太重要了。
我举个实际例子:假设你想实现一个带条件分支的网络,某些输入走分支A,某些走分支B。在动态图里,你直接写if-else就行。在静态图里,你得用特殊的控制流算子,麻烦得多。
3. 从零搭建一个完整训练流程
3.1 数据准备:Dataset和DataLoader
实际项目中,数据很少能直接塞进模型。你需要处理加载、预处理、批量化、打乱顺序这些事情。PyTorch提供了Dataset和DataLoader两个工具来标准化这个流程。
Dataset负责定义“一条数据长什么样”和“怎么取一条数据”。你只需要继承它,实现__len__和__getitem__两个方法:
from torch.utils.data import Dataset class MyDataset(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): x = self.data[idx] y = self.labels[idx] if self.transform: x = self.transform(x) return x, yDataLoader则负责批量化、打乱、并行加载:
from torch.utils.data import DataLoader dataset = MyDataset(data, labels) loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)num_workers这个参数值得说一下。它控制用几个子进程来加载数据。设成0表示在主进程里加载,设成大于0的数可以加速数据读取。但也不是越大越好,我一般设成CPU核心数的一半左右。设太大了反而会因为进程间通信开销导致变慢。
实操心得:如果你在Windows上跑,
num_workers设大于0可能会遇到问题,建议先用0调试,确认流程通了再调大。
3.2 模型定义:层、激活、初始化
定义模型的时候,除了层的堆叠,还有几个细节会影响训练效果。
权重初始化。PyTorch的nn.Linear和nn.Conv2d有默认初始化,但有时候默认的并不适合你的任务。比如ReLU激活函数配合Kaiming初始化通常效果更好:
import torch.nn.init as init def init_weights(m): if isinstance(m, nn.Linear): init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') if m.bias is not None: init.zeros_(m.bias) model.apply(init_weights)激活函数的选择。ReLU是最常用的,计算简单、梯度不会饱和。但它有个“死亡ReLU”问题:如果某个神经元的输出一直是负的,梯度就永远是0,这个神经元就废了。LeakyReLU和ELU是常见的替代方案。我一般先用ReLU,如果发现训练不动再换。
BatchNorm和Dropout。这两个是防止过拟合的利器。BatchNorm放在线性层或卷积层之后、激活函数之前;Dropout一般放在全连接层之间。注意在推理阶段要调用model.eval(),这会关闭Dropout并让BatchNorm使用全局统计量。
3.3 损失函数与优化器:训练的两大件
损失函数衡量模型输出和真实标签的差距,优化器根据这个差距来更新参数。
分类任务最常用的是CrossEntropyLoss,它内部已经包含了Softmax,所以你的模型最后一层不要加Softmax:
criterion = nn.CrossEntropyLoss()回归任务用MSELoss或L1Loss。如果数据有异常值,L1比L2更鲁棒。
优化器方面,SGD和Adam是两个最主流的选择。SGD配合动量(momentum)在调参得当的情况下泛化性能往往更好,但收敛慢、对学习率敏感。Adam自适应调整学习率,收敛快,适合快速实验。
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)weight_decay是L2正则化系数,用来抑制过拟合。我一般从1e-4或1e-5开始试。
学习率是最重要的超参数,没有之一。设太大了loss会震荡甚至发散,设太小了收敛慢得让人抓狂。我常用的策略是先用一个中等偏小的学习率(比如1e-3)跑几个epoch,观察loss曲线,再决定调大还是调小。
3.4 训练循环:把上面所有东西串起来
训练循环的骨架是固定的:
for epoch in range(num_epochs): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y = batch_x.to(device), batch_y.to(device) optimizer.zero_grad() outputs = model(batch_x) loss = criterion(outputs, batch_y) loss.backward() optimizer.step() # 每个epoch结束后验证 model.eval() with torch.no_grad(): # 验证逻辑 pass这里有几个关键点。model.train()和model.eval()的切换不能忘,它们影响Dropout和BatchNorm的行为。torch.no_grad()在验证阶段使用,可以节省显存并加速计算,因为不需要构建计算图。
踩过的坑:有一次我忘了在验证时加
torch.no_grad(),结果显存爆了。因为验证数据虽然不更新参数,但PyTorch默认还是会记录计算图。数据量大的时候,这个开销很可观。
4. 那些让我头疼过的典型问题和排查思路
4.1 Loss不下降怎么办
这是新手最常见的问题。我按排查优先级列一下:
| 排查项 | 可能原因 | 解决方法 |
|---|---|---|
| 学习率 | 太大导致震荡,太小导致停滞 | 尝试1e-2到1e-5之间的几个值 |
| 数据标签 | 标签和输入没对齐 | 手动检查几条数据的标签是否正确 |
| 损失函数 | 用错了损失函数 | 分类用CrossEntropy,回归用MSE |
| 模型结构 | 层维度不匹配 | 打印每层输出的shape |
| 梯度 | 梯度消失或爆炸 | 检查梯度范数,考虑加BatchNorm |
我遇到最多的情况是学习率设大了。Loss曲线像心电图一样上下跳,这时候把学习率除以10通常就能解决。
4.2 过拟合怎么判断和处理
过拟合的标志很明确:训练集loss持续下降,验证集loss先降后升。这时候模型在“死记硬背”训练数据,而不是学习通用规律。
处理方法按优先级:
- 增加数据。这是最根本的解决办法,但往往不现实。
- 数据增强。对图像做随机裁剪、翻转、颜色抖动;对文本做同义词替换、随机删除。
- 加正则化。增大weight_decay,或者加Dropout层。
- 早停。验证集loss开始上升就停止训练。
- 简化模型。减少层数或每层的神经元数量。
我一般先加Dropout和数据增强,这两个见效最快。如果还不行,再考虑减小模型容量。
4.3 GPU显存不够用的几种解法
显存不够是绕不开的问题。我总结了几种实用的方法:
- 减小batch size。这是最直接的,但太小会影响BatchNorm的效果。
- 梯度累积。用小的batch size跑几次,累积梯度后再更新一次参数,等效于大batch。
- 混合精度训练。用
torch.cuda.amp,前向用float16,反向用float32,能省不少显存。 - 及时释放不需要的变量。用
del删掉中间变量,然后torch.cuda.empty_cache()。 - 检查是否有计算图泄漏。比如在验证时忘了
torch.no_grad()。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for batch_x, batch_y in loader: with autocast(): outputs = model(batch_x) loss = criterion(outputs, batch_y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()混合精度训练是我强烈推荐的一个技巧,基本上能省30%到50%的显存,速度也有提升,而且代码改动很小。
4.4 模型保存与加载的坑
保存模型有两种方式:保存整个模型和只保存参数。我强烈建议只保存参数:
# 保存 torch.save(model.state_dict(), 'model.pth') # 加载 model = SimpleNet() model.load_state_dict(torch.load('model.pth')) model.to(device)保存整个模型的问题是,它依赖于模型类的定义。如果你改了代码结构,加载旧模型就会报错。只保存参数就没这个问题,只要模型结构一致就能加载。
还有一个细节:加载参数时如果设备不匹配会报错。比如你在GPU上保存的,在CPU上加载。解决办法是用map_location:
model.load_state_dict(torch.load('model.pth', map_location='cpu'))5. 从入门到进阶的几条实用建议
5.1 调试技巧:让问题无处遁形
PyTorch调试有几个我常用的手段。第一,用print打印Tensor的shape和数值范围,这是最朴素但最有效的方法。第二,用torch.autograd.set_detect_anomaly(True)来定位梯度异常,它会告诉你哪个操作产生了NaN。第三,用TensorBoard可视化loss曲线和权重分布,直观程度远超看数字。
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('runs/experiment_1') for epoch in range(num_epochs): writer.add_scalar('Loss/train', train_loss, epoch) writer.add_scalar('Loss/val', val_loss, epoch) writer.close()5.2 学习路径:别在理论上死磕
我见过太多人卡在“要不要先学完吴恩达的课再动手”这个问题上。我的建议是:边做边学。先跑通一个MNIST分类,再跑一个CIFAR-10,然后找一个你感兴趣的任务复现。遇到不懂的概念再回去查,这样学得最快。
具体路径我建议这样:第一周把Tensor操作和autograd搞熟;第二周手写一个全连接网络做分类;第三周学CNN和DataLoader;第四周找一个开源项目复现。一个月下来,基本就能独立做小项目了。
5.3 版本兼容性:一个容易被忽视的坑
PyTorch的版本更新比较快,不同版本之间API可能有变化。我建议固定一个版本,不要频繁升级。如果你要复现别人的代码,先看它的requirements里写的PyTorch版本是多少,尽量保持一致。
另外,PyTorch和CUDA的版本要匹配。装之前先去官网查一下对应关系,别装完了发现GPU用不了。如果你用conda,它会自动处理这些依赖,省心很多。
5.4 代码组织:别把所有东西写在一个文件里
刚开始写demo的时候,一个.py文件从头写到尾没问题。但项目稍微大一点,就要学会拆分。我一般的组织方式是:
project/ ├── data/ │ └── dataset.py ├── models/ │ └── model.py ├── utils/ │ ├── train.py │ └── evaluate.py ├── configs/ │ └── config.yaml └── main.py这样拆的好处是,模型改了不影响数据加载,训练逻辑改了不影响模型定义。而且方便做实验对比,换个config就能跑不同的超参数组合。
5.5 性能优化:让训练快起来
训练速度直接影响你的实验效率。除了前面说的混合精度,还有几个立竿见影的优化点。第一,DataLoader的num_workers设对,别让数据加载成为瓶颈。第二,用pin_memory=True加速CPU到GPU的数据传输。第三,如果模型不大,可以试试torch.compile(),PyTorch 2.0引入的编译优化,有时候能提速不少。
model = torch.compile(model)这个功能还在演进中,不是所有模型都能完美支持,但值得一试。
5.6 社区资源:善用但别依赖
PyTorch的官方文档质量很高,教程也写得好。遇到问题先查官方文档,大部分基础问题都能解决。论坛和社区是第二选择,搜之前先想想关键词怎么描述。我自己的经验是,把报错信息直接复制去搜,命中率最高。
但要注意,社区里的代码质量参差不齐,有些是好几年前的版本,直接抄可能跑不通。看到解决方案先理解思路,再根据自己的环境调整。
6. 我个人的一些体会
写了这么多,最后说几句掏心窝子的话。PyTorch入门其实不难,难的是坚持把一个小项目从头到尾跑通。我见过太多人学了Tensor操作、学了模型定义,但从来没有完整地训练过一个模型。这就像学游泳,看再多教程不下水,永远学不会。
我的建议是,看完这篇文章之后,立刻去找一个你感兴趣的小数据集,哪怕就是MNIST或者Fashion-MNIST,把整个流程走一遍。数据加载、模型定义、训练循环、验证、保存模型,每一步都亲手敲一遍。遇到报错就查,查不到就简化问题再查。这个过程可能有点痛苦,但走完一遍之后,你对深度学习的理解会完全不一样。
另外,别追求一次就写出完美的代码。我现在的项目代码,也是迭代了好几个版本才稳定下来的。第一版能跑通就行,后面再慢慢优化。先完成,再完美。
PyTorch这个工具,用熟了之后你会发现它真的很“跟手”。动态图的灵活性让你可以快速验证想法,丰富的生态让你不用重复造轮子。入门阶段花的时间,后面都会加倍回报给你。