我写这篇笔记的起因很简单:身边总有朋友纠结,深度学习框架到底选哪个,担心跟错“方向”。我自己的答案是,先把 pytorch 彻底用熟,其他框架的选择自然会有判断力。这几年我在不少实际项目里用 pytorch 搭过模型、调过参数、踩过坑,这篇东西不是官方教程的复述,而是一份“过来人”的实战手记——从环境搭建、核心概念,到一个可以照跑的图像分类流程,再到训练过程中那些让人头大的玄学问题,我会把值得注意的细节和背后的逻辑一并讲清楚。无论你是刚接触深度学习,还是已经会写一点模型但没系统性用过 pytorch,相信都能从这里拿走一些直接能用的东西。
1. 为什么我把PyTorch当成主力框架来用
1.1 动态图带给开发体验的质变
我第一次从静态图框架转过来的时候,最强烈的感受是:模型终于能像普通 Python 代码一样被调试了。静态图框架的思路,是先描述一张完整的计算图,再把它交给运行时去执行,这样编译器可以拿到全局信息做优化,性能上限更高。代价是开发体验很别扭:想打印中间某个张量,得在图上挂节点;想按条件分支,很多时候要套框架提供的控制流 API;一旦报错,堆栈信息常常和业务逻辑隔得很远,排查成本很高。
pytorch 走的是动态图路线,本质上是“执行到哪行,计算图就建到哪行”。你在模型的 forward 函数里随便写一个 print,跑一个 batch 就能立刻看到中间张量的形状和值;想加断点、想写 if-else、想用 dict 存状态,都和写普通程序没区别。这种“写代码即建图”的方式,看起来只是一个工程选择,实际上极大降低了试错成本,尤其是做科研、做算法验证的时候,思路可以快速切换,不用为框架表达力受限而分心。这也是 pytorch 在学术界快速普及的底层原因。
1.2 像写普通Python类一样定义模型
pytorch 里定义一个网络,本质上就是继承nn.Module写一个 Python 类。这个设计我非常喜欢,因为纸上的网络结构几乎可以和代码一一对应:__init__里摆放卷积、池化、全连接这些“积木”,forward里写明数据怎么一层层流动。看别人的代码时,网络的层级结构清晰可见,数据流也不会被框架的魔法藏起来。
我举一个很小的例子,定义一个两层的全连接分类器,只需要这样:
import torch.nn as nn class MyModel(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 256) self.fc2 = nn.Linear(256, 10) def forward(self, x): x = torch.relu(self.fc1(x)) return self.fc2(x)没有复杂配置,没有额外注册,类写完了,整个模型就可以用model.parameters()去拿所有可训练参数。配合社区里大量的预训练模型,视觉任务直接加载骨干网络、替换分类头,文本任务也有对应的迁移方案,几行代码就能完成一个基础 baseline。我在做迁移学习的时候,基本不会自己从零搭一个深层特征提取器,而是用现成预训练权重起步,这在实际项目里非常省时间。
1.3 框架选型要看的是这四个问题
有人问我该选哪个框架,我一般不会直接给答案,而是建议先回答四个实际问题:第一,团队里其他人熟不熟悉这个框架,出了问题能不能在社区或内部快速找到解法;第二,目标部署平台对这种框架的导出格式支持得好不好,比如 ONNX 或者对应的推理引擎;第三,社区活跃度和文档完整度,冷门报错能不能搜到答案,更新节奏是不是稳定;第四,实验迭代的速度能不能匹配项目周期,因为模型改一版、跑一轮训练的成本,最终会折算成整个项目的进度。
用这四个标准去衡量,pytorch 在很多场景下都会排在前面。但我也要说明白,这不等于其他框架没有价值。框架永远只是手段,你愿意花几个月把一个框架吃透,这件事本身比它在一张榜单上多跑几分重要得多。后面的章节我会不断回到这个观点:真正重要的是理解数据、模型、训练、评估这条主链路,框架只是承载主链路的工具。
2. 环境搭建:版本匹配是第一个真正的坑
2.1 conda隔离环境:先给项目一个干净的Python
很多人一开始就栽在环境上。系统里装一个 Python,为了跑别的项目又改了包版本,结果 pytorch 装完,要么 import 报错,要么 CPU 版和 GPU 版混在一起,非常折磨。我现在的习惯是每个项目都新建一个独立的 conda 环境,让项目依赖互不干扰。
conda create -n torch_env python=3.10 conda activate torch_env用 3.10 或 3.11 都行,关键是别和系统 Python 混用。环境建好之后,再往里装 pytorch 相关的东西。这一步看着琐碎,但它能帮你避开后面 80% 的 “版本冲突” 问题。如果你已经因为环境混乱而遭遇过ModuleNotFoundError或者ImportError指向同一个包的不同版本,你就明白隔离环境有多重要了。
2.2 显卡驱动、CUDA和PyTorch版本的三方关系
这是整个环境搭建里最容易让人懵的地方。简单说,有三层东西:最底层的显卡驱动负责操作系统和显卡硬件之间的通信;中间层是 CUDA 工具包,提供开发需要的并行计算接口;最上层是 pytorch 的 GPU 版本安装包,它内部已经带好了运行时需要的 CUDA 依赖。
为什么你经常听说“不需要单独装 CUDA”?原因就在这里:pytorch 的 GPU 版安装包已经把与它对接的那部分 CUDA 运行时打包进去了。只要显卡驱动版本足够新,pytorch 就能直接调起来。真正要匹配的是两件事:你的显卡驱动支持的最低 CUDA 版本,以及你选择安装的 pytorch 版本对应的 CUDA 编译版本。
驱动版本太低,torch.cuda.is_available()会返回False,看起来像是装了 GPU 版,实际却在用 CPU 算。版本高一般没问题,向下兼容是默认行为。所以遇到问题,先看驱动版本,再查 pytorch 安装时用的 CUDA 版本标签,这两者对照起来,很多环境问题就清晰了。
2.3 安装GPU版PyTorch的具体操作
新建完环境之后,安装命令的核心是:不要只敲pip install torch。默认的 pip 源给到的是 CPU 版本,很多同学装完发现训练极慢,还以为是自己代码写得有问题,其实是用 CPU 跑了很久。
正确的做法是去官方安装页,根据系统、包管理工具和需要的 CUDA 版本,找对应的安装命令。一个常见的示例是这样的:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121这里的cu121表示这个包是针对 CUDA 12.1 编译的。你需要根据自己的驱动版本选择cu118、cu121、cu124等标签。注意,这个 tag 是编译时使用的 CUDA 版本,不是让你手动安装一个对应版本 CUDA Toolkit。如果驱动支持,装高一点的 tag 通常没问题;如果驱动太老,就只能选相对保守的版本。如果你工作时绝大多数时间都在 CPU 上,或者只做数据预处理、看模型结构,CPU 版也能跑,但做训练还是建议配好 GPU 环境。
2.4 验证环境:不只是打个“True”
装完之后,我建议第一时间验证一遍,不要等训练到一半才发现问题。打开 Python 终端,运行下面这段代码:
import torch print(torch.__version__) print(torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0))第一次看到torch.cuda.is_available()返回True的时候,确实有种“这下对了”的踏实感。但这里还有一个隐藏坑:True只代表 pytorch 能看到显卡,并不代表你的张量运算都在 GPU 上发生。默认创建的 tensor 是在 CPU 上的,需要显式移动到 GPU:
x = torch.randn(3, 3) x = x.to('cuda') print(x.device)记住这个device概念,后面的代码会反复用到。很多项目的性能问题并不是因为框架没用好,而是因为训练过程中张量一直在 CPU 和 GPU 之间来回搬运。在 Mac 的 M 系列芯片上工作的话,也可以用 MPS 后端,把.to('cuda')换成.to('mps'),不过要留意不同算子对 MPS 的支持情况,最好按官方文档确认。
3. 三个核心概念:Tensor、autograd、nn.Module
3.1 Tensor:数据流动的载体
pytorch 里的 Tensor 可以理解成“带计算能力的多维数组”,它和 numpy 的 ndarray 长得很像,差别在于 Tensor 能被框架的自动求导机制跟踪。一个 Tensor 有三个属性需要重点关注:形状 shape、数据类型 dtype、所在设备 device。这三个属性不匹配,经常是各种报错的源头。
比如模型输入要求float32,你喂进去一个float64的张量,可能报类型错误;比如模型在 GPU 上,数据还在 CPU 上,会报 device 不匹配;再比如卷积网络期望的形状是(batch, channel, height, width),你给的却是(height, width, channel),维度对不上也会直接报错。数据进模型之前,先统一检查这三项,是节省时间的好习惯。
和 numpy 互转也很常用:torch.from_numpy()能从 numpy 数组建 Tensor,tensor.numpy()可以转回去。但要注意,默认情况下它们可能共享内存,改了一个会影响另一个,如果你不想让它们互相干扰,记得先.clone()或者用.detach().cpu().numpy()。
3.2 autograd:反向传播怎么自动化
autograd 是 pytorch 最核心的机制之一。以前写神经网络,反向传播的梯度要自己按链式法则一层层推导、实现,步骤繁琐不说,还容易出错。pytorch 的做法是:当你把某个张量的requires_grad设为True,框架会记录这个张量参与过的运算,形成一张计算图;调用loss.backward()时,梯度从损失开始沿计算图逐层反传,每个张量的梯度会存放在它的.grad属性里。
用一个最简例子来说明:
import torch x = torch.tensor([2.0], requires_grad=True) y = x ** 2 y.backward() print(x.grad)这里x的梯度是 4.0,恰好等于2 * x,这就是链式法则被自动执行的结果。实际训练中,模型参数的requires_grad默认就是True,所以调用loss.backward()之后,所有参数都会获得梯度,优化器再用这次梯度去更新模型。优化器更新的代码是optimizer.step(),但有一个细节很多人一开始会漏掉:每次更新前要调用optimizer.zero_grad()把上次的梯度清零。因为 pytorch 的梯度是累加的,不手动清零,梯度就会跨 batch 累积,训练很容易发散。虽然可以自定义 zero_grad 的方式,但标准流程一定是一套四连:清空梯度、前向计算、计算损失、反向传播、更新参数。
3.3 nn.Module:如何组织模型代码
nn.Module是 pytorch 组织模型的基类。所有层、所有自定义模型,本质都是一个 Module,大 Module 里可以套小 Module,形成一个树状结构。这种设计带来的好处是:调用model.parameters()时,会递归把树里所有可训练参数都找出来;调用model.to(device)时,树里所有参数会统一搬到目标设备。
此外,训练和评估时要切换模型的运行模式。model.train()会让 Dropout 等层进入训练状态,model.eval()会切换到推理状态。很多人在评估时忘了model.eval(),结果 Dropout 还在随机丢弃节点,验证集准确率忽高忽低,看起来像“模型不稳定”,其实就是模式没切换对。而with torch.no_grad():则是另一个常用工具,它告诉 autograd 不需要记录梯度,推理和验证时能省不少内存和计算时间。
3.4 初学者最容易混的三个梯度相关操作
和梯度相关,有三个术语经常混在一起:torch.no_grad()、tensor.detach()、optimizer.zero_grad()。它们名字都有点像,实际用途完全不同。
| 操作 | 作用 | 典型场景 |
|---|---|---|
torch.no_grad() | 上下文管理器,临时关闭梯度记录 | 验证、推理时 |
.detach() | 返回一个新的 Tensor,和当前计算图断开 | 想把中间张量取出看值、或避免梯度传到某条路径 |
optimizer.zero_grad() | 将所有参数的梯度清零 | 每次参数更新前 |
我自己理解这三个操作的记忆方式:no_grad就是“这段时间不记账”,detach是“这一笔账和我无关”,zero_grad是“之前的账页清空再记新账”。训练里最常见的是第 1 和第 3 个,第 2 个在写复杂的多分支网络时很有用。下次再看到代码里这三样东西,你应该一眼能分清它们分别在想干什么。
4. 从零训练一个图像分类模型
4.1 数据准备:目录结构、增强与DataLoader
有了环境、理解了基本概念,接下来要解决的就是“数据怎么喂给模型”。我拿图像分类举例,因为它的流程最直观。最省事的方式是把自己的图片数据整理成ImageFolder能读的标准目录结构:
data/ ├── train/ │ ├── class_a/ │ ├── class_b/ │ └── class_c/ └── val/ ├── class_a/ ├── class_b/ └── class_c/每个类别一个子目录,ImageFolder会自动把子目录名映射成类别标签。然后用transforms做预处理和增强:
from torchvision import datasets, transforms transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) transform_val = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) train_dataset = datasets.ImageFolder(root='data/train', transform=transform_train) val_dataset = datasets.ImageFolder(root='data/val', transform=transform_val)RandomHorizontalFlip是做数据增强,随机左右翻转图片,能给训练集增加一些多样性;验证集通常不做这种随机变换。数据增强的本质是“让模型见过更多变体”,减少过拟合。transforms.Resize会把图片统一缩放到网络要求的尺寸,Normalize用 mean 和 std 做标准化,能帮助训练更稳定。
下一步是把数据集包装进DataLoader:
from torch.utils.data import DataLoader train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)shuffle=True用于训练集,让每个 epoch 的数据顺序都不一样,防止模型“背顺序”;验证集不需要shuffle,这样可以保证每次评估的顺序一致。num_workers是并行加载数据的进程数,Windows 上如果遇到多进程相关的报错,可以先把它设成 0,优先保证流程跑通。
4.2 模型定义:先跑通再优化
分类模型我建议从简单结构开始。这一步的目标不是卷参数,而是先把数据到梯度更新这条链路完整走通。给一个用nn.Sequential堆叠的小网络:
import torch.nn as nn model = nn.Sequential( nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(32 * 56 * 56, 128), nn.ReLU(), nn.Linear(128, 3) )输入图片是 224x224 的 RGB 图像,经过两次池化,空间尺寸变成 56x56,所以全连接层的输入维度是32 * 56 * 56。这个维度很容易算错,我的建议是先不确定就写nn.AdaptiveAvgPool2d((1, 1))把特征压缩成固定大小,再接全连接,这样就不用手动算中间尺寸了。模型定义篇幅越短,越容易保持头脑清醒。
这里顺带说一句:nn.Sequential适合层与层线性堆叠的简单网络;如果模型有分支结构、需要跳跃连接,就该用自定义nn.Module子类。判断标准很简单:代码表达能力不够用时,就升级到类的方式。
4.3 训练循环:每个对象在做什么
训练循环看起来像模板代码,但其实每一行都有明确职责。标准的 pytorch 训练循环大致是这个样子:
import torch import torch.nn as nn import torch.optim as optim device = 'cuda' if torch.cuda.is_available() else 'cpu' model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) epochs = 10 for epoch in range(epochs): model.train() total_loss = 0.0 correct = 0 total = 0 for batch_x, batch_y in train_loader: batch_x = batch_x.to(device) batch_y = batch_y.to(device) optimizer.zero_grad() output = model(batch_x) loss = criterion(output, batch_y) loss.backward() optimizer.step() total_loss += loss.item() predicted = output.argmax(dim=1) correct += (predicted == batch_y).sum().item() total += batch_y.size(0) train_acc = correct / total avg_loss = total_loss / len(train_loader) print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}, Acc: {train_acc:.4f}")逐一拆解:model.train()让网络进入训练状态;batch_x, batch_y是一次拿出的小批量数据和标签;optimizer.zero_grad()清空上一步梯度;model(batch_x)是前向计算,得到预测;criterion计算损失;loss.backward()反传梯度;optimizer.step()用梯度更新参数。loss.item()把只含一个值的 loss 张量转成普通的 Python 数字,用于打印观察。
如果你想看训练过程中到底发生了什么,可以在每个 epoch 打印平均损失和准确率。损失不下降,说明学习率太小或模型没在收敛;损失变成 NaN,优先怀疑学习率过大或数据里有异常。这些后面会专门展开。
4.4 评估与保存:别只会保存整个模型
每个 epoch 跑完之后,要在验证集上看看真实表现。这里有一个评估模板,和训练略有不同:
model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x = batch_x.to(device) batch_y = batch_y.to(device) output = model(batch_x) predicted = output.argmax(dim=1) val_correct += (predicted == batch_y).sum().item() val_total += batch_y.size(0) val_acc = val_correct / val_total print(f"Val Acc: {val_acc:.4f}")model.eval()和torch.no_grad()组合使用,是为了让评估过程中不产生梯度记录,也不受 Dropout 随机性的影响。模型保存我强烈建议保存state_dict,而不是整个模型对象:
torch.save(model.state_dict(), 'model_weights.pt')加载时,先定义好和原来一模一样的模型结构,再读权重:
model = nn.Sequential(...) # 结构必须与保存时一致 model.load_state_dict(torch.load('model_weights.pt'))保存整个模型看起来很方便,但只要代码里网络结构定义有一点改动,加载就可能失败。保存state_dict反而干净,因为权重是纯数据,和机器、Python 版本无关。如果要恢复训练中间状态,我习惯保存一个字典,同时带上 optimizer 状态、epoch 等关键信息:
checkpoint = { 'state_dict': model.state_dict(), 'optimizer_state': optimizer.state_dict(), 'epoch': epoch } torch.save(checkpoint, 'checkpoint.pt')训练中断时,这个 checkpoint 能让你从断点接着跑,而不是推倒重来。
5. 训练路上那些“看起来玄学”的坑
5.1 loss变成NaN:先从学习率查起
训练过程中最吓人的画面之一,就是好好的 loss 突然变成nan。我第一次遇到时以为是数据有问题,把数据集翻了个底朝天,后来才发现只是学习率设大了,模型权重在几次更新后溢出。
排查顺序建议是这样:第一,先看输入数据里有没有 NaN,用torch.isnan(batch_x).any()检查即可;第二,再把学习率调小一个数量级,比如从1e-3调到1e-4;第三,如果模型里有复杂的深度网络,给优化器加梯度裁剪,限制梯度的最大范数;第四,检查是否用了混合精度训练,float16在极端情况下容易溢出。记住一个原则:loss 出现 NaN,优先怀疑学习率,而不是怀疑模型结构。
5.2 过拟合:训练很好,验证崩了
过拟合是图像分类里最普遍的现象:训练集准确率已经很高,验证集却停滞不前甚至在倒退。说白了,模型把训练数据的细节背下来了,而没有学到真正通用的规律。应对策略是一个组合拳,不是靠单点用力。
数据增强是最先想到的手段,随机裁剪、旋转、翻转都能让模型被迫学会更鲁棒的特征。第二是模型侧加 Dropout,它在每次训练时随机丢弃一部分节点,相当于训练了很多个“子模型”再融合,能明显抑制过拟合。第三是权重衰减,它让参数别长得太大,约束模型复杂度。第四是早停,也就是监控验证集指标,一旦连续多个 epoch 不提升,就中断训练,用之前的模型。最后还要检查模型复杂度是不是远超问题难度,比如只有几千张图却上一个超深网络,大概率会过拟合。
5.3 显存不足:不只是调小batch_size
CUDA out of memory听着像硬件极限问题,但很多时候是代码层面的浪费。最常见的原因有三个:batch size 太大,单次前向+反向的中间激活值太多;输入图片分辨率太高,中间特征图占显存成倍上升;模型和数据同时在显存里放了好几份,忘了释放。
显存不足的标准解法是减小 batch size。但要注意,调小 batch 之后,梯度基于更少的样本做估计,噪声变大,通常要把学习率也跟着调小,或者用梯度累积来模拟原来的 batch:
# 假设原 batch_size=32,显存只能承受 8 for batch_x, batch_y in train_loader: loss = criterion(model(batch_x), batch_y) loss = loss / 4 # 累积4次 loss.backward() if (i + 1) % 4 == 0: optimizer.step() optimizer.zero_grad()这个技巧叫梯度累积,把 4 个小 batch 的梯度加到一起,再更新一次参数,效果和用大 batch 近似,但显存占用小得多。另外,训练完的临时张量如果不小心没有释放,可以用del和torch.cuda.empty_cache()回收。不过别迷信empty_cache(),它清的是缓存,不是真正释放被张量占用的显存,清理临时变量才是关键。
5.4 结果不可复现:随机种子为什么重要
很多项目会碰到一个尴尬情况:同一个模型、同一份代码,跑两次结果不一样。这不是“运气”,而是深度学习训练里充满了随机性。权重初始化是随机的,DataLoader 的shuffle是随机的,Dropout 是随机的,GPU 上很多算子的并行执行顺序也可能不确定。结果不一致带来最直接的问题:实验没法对照,调参也很难判断是“改对了”还是“运气好”。
设置随机种子能缓解这个问题。在脚本开头,固定几类随机源:
import random import numpy as np import torch random.seed(0) np.random.seed(0) torch.manual_seed(0) torch.cuda.manual_seed_all(0) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic = True让卷积算法选择变成确定性的,benchmark = False关闭运行时自动调优,因为调优过程会引入不确定性。需要说明的是,在 GPU 上做到 100% 的逐位复现依然很难,不同显卡、不同驱动都可能造成微小差异。但设置种子之后,同一台机器上基本可以复现,这就足够支撑日常实验了。
5.5 类别不均衡:准确率会骗人
如果你在做多分类,发现训练集准确率到了 95%,别急着开心。先看看每个类别的样本数。当一类样本占绝对多数时,模型完全可以“躺平”,把所有样本都预测成多数类,准确率照样很高,但少数类一个也没学会。这就是类别不均衡的陷阱。
解决的思路有几个方向。数据层面,可以用WeightedRandomSampler对样本做重采样,让模型每个 epoch 看到各个类别的概率相近。损失函数层面,很多框架的交叉熵实现里提供了weight参数,给样本数量少的类别更大的损失权重,迫使模型更关注它们。评估层面,换成宏平均 F1、召回率、混淆矩阵这些指标,能更真实地暴露模型在少数类上的短板。我自己做分类项目时,不管是不是有均衡问题,都会额外看一眼每类别的准确率和召回率,因为综合准确率太容易掩盖问题了。
6. 框架选择的背后逻辑与后续路线
6.1 从项目场景反推框架选择
前面说过框架选型要看团队、部署、社区、迭代速度,展开来说,项目阶段不同,结论也不同。做快速验证的算法原型,最怕框架拖后腿,动态图、丰富预训练模型、方便的调试,这类需求 pytorch 很合适。做已经定型的大规模服务,则要更多考虑推理性能、服务化成本、跨语言调用,在这个环节,单纯的训练框架选择已经不够,最终可能都要转到 ONNX 这类中间表示或专门的推理引擎上。
换句话说,框架不是产品的终点。你花在 pytorch 上的时间,并不会因为以后换了部署链路而浪费,因为模型的层设计、训练流程、调参经验都是通用的。真正扎实的能力是:能看懂网络结构,能把数据管线搭起来,能判断训练是否收敛,出了问题能定位。这些能力在任何框架里都成立。
6.2 为什么从PyTorch入门依然是稳妥路线
如果要给一个“从零开始学深度学习框架”的建议,我还是会推荐从 pytorch 入手。理由不是因为它在所有榜单上无敌,而是三个可感知的优势:资料覆盖面大,从入门到进阶的教程、博客、开源项目数量都很可观,遇到问题搜一下,大概率能找到别人踩坑的记录;生态围绕它长得很完整,预训练模型、数据处理库、可视化工具链,基本都能即装即用;学习曲线相对平滑,动态图和 Python 原生的表达方式,让人可以把注意力放在深度学习的核心方法上,而不是陷在框架的复杂度里。
尤其对于还在学校或者刚开始工作的同学,先把 pytorch 用熟练,意味着你能快速看懂、复现很多开源工作里的核心代码。这一点在求职和实际项目里都很实用。
6.3 跑通基础之后还该补什么
把这篇笔记里的流程跑通,算是走出了第一步。之后如果你要继续深入,我建议按这个顺序补充:首先是部署相关,学会把模型导出为 ONNX 或 TorchScript,搞清楚离线推理和服务化部署的差别;然后是训练加速,包括混合精度、分布式数据并行,这是扩大实验规模绕不开的一环;接着是工程化习惯,比如用实验管理工具记录每次运行的超参数、指标和权重文件,用可视化的方式跟踪训练曲线。这些东西不需要一口气都学会,但越早建立这样的意识,后面项目变大时越省心。
说到底,框架学习没有一个“通关”时刻,它更像是一边做项目、一边补短板的循环。这篇笔记覆盖的是一条能让你快速跑起来的完整链路:环境、概念、训练、排坑。把我踩过的那几个坑记住,至少能帮你省下不少到处搜报错的时间。