☰
PyTorch 2实操:从环境搭建到ONNX导出全流程
2026/10/28 16:53:41 网站建设 项目流程

Pytorch-2(实操版)这个项目,我一看到就明白了,目标很聚焦:不是讲30页PPT的原理,而是把安装pytorch、环境搭建、基础框架、数据加载、训练、保存、导出onnx这条链路从头到尾亲手跑一遍。很多人学深度学习卡住,不是卡在概念上,而是卡在“装完环境之后不知道下一步点什么”,这套实操版的价值就在这里:它把你在终端里会遇到的原生错误、在GPU环境下会踩的坑、在模型转换时会报的错,全部摆到明面上,然后一个一个解决掉。

我见过太多从TensorFlow转PyTorch的人,装好库、跑通一个LeNet就觉得自己入门了,结果一换到自己的数据集就原形毕露。深度学习不是“图灵完备的API背诵”,它是一套需要动手调、动手看、动手改的工程流程。Pytorch-2(实操版)要做的,就是用“第二遍”的姿态带你把这些流程补完:第一遍你可能照着教程敲,第二遍你才真正知道为什么要这样写。下面我把这个实操过程完整拆开,从环境开始,一直讲到模型落地。

1. 项目概述与整体思路

1.1 为什么要叫“2”

先说这个“2”。它有两层含义。第一层是PyTorch 2.x这个版本代际。从2.0开始,PyTorch引入了torch.compile、动态Shape友好化、CPU/GPU融合执行等一堆底层优化,很多老博客里的代码还是1.x时代的老写法,拿过来有时候跑得通,但性能未必发挥得出来。第二层含义是“第二阶段”,就是你至少应该有一遍基础,不管是学校课程、网课还是自己瞎折腾,哪怕只知道tensor是什么,都算第一遍完成。实操版就是帮你从“懂个大概”变成“能上手交付”,属于进阶但又是必需的那一层。

我见过有人把PyTorch当NumPy用,全用一个for循环在CPU上推,根本没用到张量并行和自动求导的特性。也有人反过来,一步到位直接上分布式,结果单卡还没跑明白就去折腾DistributedDataParallel,最后被一堆环境变量搞到怀疑人生。这个项目名里的“2”更像是一种提醒:先掌握基础框架,再去够更高处的工具。

1.2 适合谁看,需要什么基础

如果你是这三类人,这套实操版的方向很对:

  • 刚学完Python,想把深度学习从“看过理论”变成“跑过代码”的初学者。
  • 已经在用TensorFlow,但因为调研、课程或部署需求,需要快速迁移到PyTorch的工程师。
  • 已经能跑通简单模型,但面对ONNX导出、模型部署、GPU环境配置时一头雾水的开发者。

前置基础不需要太高。Python语法只要会写函数就够,深度学习理论知识只需要知道“梯度下降”和“前向传播”的大意即可。真正的门槛在实操里,你不需要背住API,你只需要知道一个模型从数据到损失函数到梯度更新是怎么流转的。项目的前几步其实就是为了把这条流转线捋直,捋顺了再往里加花活,就不会乱。

1.3 这条实操路线是怎么设计的

我的习惯是:先环境、再张量、再模型、再训练、再部署,五个节点顺序不能乱。环境不干净,后面所有报错都会伪装成“代码错了”,实际上是在资源冲突里打转。张量不熟悉,后面写view、permute、unsqueeze这些变换时会反复查文档。模型不会定义,训练循环写得再漂亮也没有意义。所以实操版的整体设计就是一条线性主线,中途每个节点配一到两个可复跑的例子,最后落点在ONNX导出这种真实部署需求上,而不是糊一个漂亮精度就结束。

2. 环境搭建:从零开始装好PyTorch

2.1 用Anaconda隔离环境,别把系统搞乱

安装pytorch这件事,最忌讳的就是直接在系统Python里pip install torch。因为你今天可能装一个项目依赖,明天又装另一个,时间一长,包版本之间互相打架,连卸载都找不到罪魁祸首。我的做法是先建一个独立的conda环境,环境名用项目名,比如pytorch2,Python版本固定成3.9或者3.10都可以。

conda create -n pytorch2 python=3.9 -y conda activate pytorch2

为什么推荐3.9?倒不是说新版3.11、3.12不好,而是很多深度学习依赖的编译产物,在3.9上兼容性最稳。你会少遇到类似“编译Python包时找不到Python.h”这种破事。如果你手上有conda,也可以用conda install pytorch直接装,但我更建议用pip,因为PyPI上的轮子一般来说更新更快,而且不会把 conda 的依赖树搅乱。

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

这条命令里的cu121是CUDA 12.1的意思。写这篇实操的时候,PyTorch 2.x 开始默认带CUDA 12.1的轮子,和以前还要单独选cu118不一样了。如果你知道自己显卡驱动支持更高版本,也可以去PyTorch官网首页复制对应命令,但要记住一个底层逻辑:torch的CUDA版本和驱动兼容,不是看驱动版本越高越好,而是看工具包和显卡驱动之间有没有覆盖关系。

2.2 CUDA、cuDNN和PyTorch版本怎么匹配

这是全网提问最多的点,也是环境搭建里最容易翻车的地方。很多教程告诉你第一步先装CUDA Toolkit,第二步装cuDNN,第三步装PyTorch,结果你真的照着做,常常把自己的驱动搞坏。我告诉你一个更稳妥的顺序:

  1. 先看显卡支持哪个CUDA版本,在终端输入nvidia-smi,看右上角的“CUDA Version”。
  2. 根据这个版本来选PyTorch的编译版本,不一定要完全相等,但要保证PyTorch要求的CUDA版本小于等于驱动最高支持的版本。
  3. 不要在系统里单独装另一套CUDA Toolkit,除非你有编译自定义算子的需求。PyTorch的pip包已经内置了CUDA运行时,普通用户直接装就行。

举个例子,驱动支持CUDA 12.4,那么装cu121或cu124版本的PyTorch都没问题。如果你驱动只支持11.8,硬装一个cu121的PyTorch,大概率在torch.cuda.is_available()那一步返回False。出现这种情况,别慌,把当前torch卸载重装一个适配版本就行,不用重装系统。

下面是常见版本的匹配参考表:

PyTorch版本默认CUDA版本常见轮子标记适合的驱动CUDA
2.0.x11.7cu117大于等于11.7
2.1.x11.8cu118大于等于11.8
2.2.x12.1cu121大于等于12.1
2.5.x12.4cu124大于等于12.4

如果你用的是30系、40系显卡,直接选12.x系列没问题;如果你还在用10系、20系显卡,最好确认一下是否还受支持。旧卡不是不能跑,但新版PyTorch可能逐步降低支持权重。

2.3 Ubuntu和WSL下的安装要点

Linux环境比Windows干净,但也不是完全省心。Ubuntu下安装pytorch,核心问题是Python环境和图形驱动。如果是纯服务器,没有桌面,就在SSH里用上面那条pip命令;如果是有桌面环境的Ubuntu,注意别把OpenGL之类的图形库和CUDA驱动搞混,NVIDIA驱动一旦出故障,重启后可能直接卡在登录界面。

我目前最推荐的Windows开发方案是WSL2。你在Windows里装上WSL2和Ubuntu 22.04,然后在WSL内部装PyTorch,外部直接用VSCode连接。好处很明显:Windows真机上的NVIDIA驱动会自动映射到WSL里,你不需要在Linux里额外装驱动,而且文件系统两边共存,数据交换也方便。

wsl --install -d Ubuntu-22.04

装完进入WSL,先跑一下:

nvidia-smi

如果你能看到GPU信息,说明驱动映射正常。看不到也没关系,很大概率是Windows侧驱动太旧,去NVIDIA官网更新一下驱动再来。接下来就在WSL内部按普通的Linux流程创建conda环境就行。

2.4 安装完成后必须做的三行验证

环境到底装好没有?不要只看“pip install成功”就高兴。运行下面的三行代码:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

如果torch.cuda.is_available()返回True,而且能看到显卡名字,说明安装pytorch这一步完全OK。如果返回False,依次排查:先看nvidia-smi是否存在,再看你安装的PyTorch版本是不是带CUDA的轮子,最后看版本兼容表。

注意:torch.__version__如果是2.5.1+cu121这种带有+cuXXX后缀的,说明装的是对应CUDA版本;如果只是纯2.5.1,那是CPU版本,GPU训练跑不起来,你需要卸载后从官网拿CUDA版命令重装。

3. 核心框架快速上手

3.1 张量创建与设备迁移

环境搭好以后,第一件事不是建模型,而是把张量给我玩熟。PyTorch的核心就是张量,你把它想象成带GPU加速和自动求导功能的NumPy数组。很多初学者最容易忽略的是“创建张量之后,它在哪块设备上”——是在CPU上还是在GPU上。

import torch x_cpu = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32) print(x_cpu.device) # cpu x_gpu = x_cpu.to("cuda") print(x_gpu.device) # cuda:0 x_back = x_gpu.cpu()

为什么反复强调设备迁移?因为模型在GPU上算得快,但如果你某个输入忘了迁移到GPU,代码马上报“Expected all tensors to be on the same device”之类的错。这个错误的本质是Tensor和Tensor做乘法时,设备不一致。所以我在所有训练代码里都习惯先定义设备:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) data = data.to(device)

另外还要注意数据类型。PyTorch里默认的浮点是float32,但在混合精度训练里会用到float16。保存模型和加载模型时,推荐只保留float32的权重,推理时再转半精度,这样能兼顾兼容性和推理速度。

3.2 用nn.Module搭建第一个模型

有了张量基础,就可以开始搭建基础框架了。PyTorch中所有模型都继承自nn.Module,你只需要重写__init__和forward两个方法。我建议新手先别急着用nn.Sequential把层堆到底,而是手写一个类出来,因为实际项目里你总会遇到要修改中间特征的情况。

import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(3, 16, 3, padding=1) self.conv2 = nn.Conv2d(16, 32, 3, padding=1) self.fc = nn.Linear(32 * 8 * 8, 10) def forward(self, x): x = F.relu(self.conv1(x)) x = F.max_pool2d(x, 2) x = F.relu(self.conv2(x)) x = F.max_pool2d(x, 2) x = x.view(x.size(0), -1) return self.fc(x)

这个网络输入是一张32×32的RGB图,输出是一个10维向量。view操作的作用就是把卷积特征展平成全连接层能吃的向量,很多新手忘掉这一步,结果全连接层拿不到匹配维度的输入。forward方法里的顺序就代表数据在模型里的流式处理顺序,Pytorch的灵活之处恰恰在于这一步可以写任意张量操作,而不只是简单的层拼接。

3.3 优化器、损失函数与L2正则化

模型定义好以后,需要选定损失函数和优化器。分类任务用交叉熵,回归任务用MSE,这没什么好争的。优化器的话,Adam是实践里最省心的选择,SGD则需要更多调参经验。

criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)

这里出现了一个高频关键词:weight_decay。它就是深度学习里的L2正则化在PyTorch中的实现入口。很多人只看参数名,不懂它到底做了什么。L2正则化的本质,是在损失函数后面加一项权重的平方和除以2,再乘上一个系数λ,然后反向传播时,就会额外产生一个“权重衰减”项,迫使大权重变小,从而降低过拟合。

如果你是一个初学者,想看到L2正则化的实际代码,最直观的写法是手动计算:

l2_reg = 0.0 for param in model.parameters(): l2_reg += torch.norm(param, 2) ** 2 loss = criterion(outputs, labels) + 1e-4 * l2_reg

但日常训练里我不会手动加,因为优化器自带weight_decay参数已经完成了这件事,而且实现方式更高效。你只要记住:lambda越大,正则化越强,模型权重越趋向扁平;但lambda太大会把模型压成一个近似常数,精度反而崩。实操时从1e-4或1e-5开始试。

4. 模型训练实战全流程

4.1 自定义Dataset与数据预处理

模型和数据是双胞胎,数据加载不顺畅,后面全是填不完的坑。PyTorch里标准做法是继承Dataset类,实现__len__和__getitem__两个方法。拿图片分类举例:

import os import torch from torch.utils.data import Dataset from PIL import Image class MyImageDataset(Dataset): def __init__(self, img_dir, labels, transform=None): self.img_dir = img_dir self.labels = labels self.transform = transform self.img_names = sorted(os.listdir(img_dir)) def __len__(self): return len(self.img_names) def __getitem__(self, idx): img_path = os.path.join(self.img_dir, self.img_names[idx]) image = Image.open(img_path).convert("RGB") label = self.labels[idx] if self.transform: image = self.transform(image) return image, label

这个类不复杂,但“懒加载”思想很重要:整个数据集不会一次性读进内存,而是每次通过索引读取一张图。这样做的好处是能处理上万张图片的目录,不至于把内存打爆。

数据预处理通常交给torchvision.transforms。它的本质是一个可组合的变换流水线,比如先缩放、再转张量、再标准化:

from torchvision import transforms transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

Normalize里的均值和标准差都是ImageNet统计出来的,如果你用自己的数据集,想更严谨就自己算,不想太讲究就用这套默认值也可以,反正大多数模型的输入分布就是这么调的。

4.2 DataLoader的参数你真的调对了吗

数据类写好之后,还要用DataLoader包一层,它负责自动打乱、分批、多进程预读。这里有几个参数值得你仔细定:

from torch.utils.data import DataLoader train_loader = DataLoader( dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True, )

batch_size决定每轮迭代喂给模型多少样本。batch这个值不是越大越好,太大占显存、收敛也未必更快;太小会引入更多噪声,收敛稳定性差。我一般先从32或64起步,如果训练掉点再往下调,如果显存还有富余就往上加。

shuffle=True只对训练集开启,验证集和测试集不需要打乱,因为验证结果不依赖顺序。num_workers是并行读取数据的进程数,在Windows上如果设得较大容易出“DataLoader worker死掉”的问题,建议不超过CPU核心数,先设2或4比较稳妥。pin_memory设为True后,GPU训练时能把数据从分页内存搬到固定内存,小幅提升传输效率。

还有一个新手容易忽略的问题:如果你的数据加载代码写在Jupyter Notebook里,且用了num_workers > 0,跨平台调用时可能会在Windows下报错。最省事的做法是把整个训练脚本写成.py文件,或者把入口代码包在if __name__ == "__main__":里面。

4.3 训练循环:前向、反向、更新

有了数据,有了模型,训练循环就是把这些要素串起来。这个循环你会在无数个项目里重复写,所以最好一次就写熟、写顺。

def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss = 0.0 for imgs, labels in loader: imgs = imgs.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) return total_loss / len(loader.dataset)

每个步骤的分工是这样的:zero_grad()是把上一次迭代保留的梯度清零,否则梯度会在反向传播时累加;backward()会从损失开始倒推,计算出所有张量的梯度;step()让优化器根据梯度去更新模型参数。这里初学者最常犯的错是忘了zero_grad(),导致loss曲线波动很夸张,看起来像在抽风。

如果你遇到的是梯度爆炸或loss突然变成NaN,除了调学习率,还可以考虑梯度裁剪。我一般会在.step()之前加一行:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

这句话会把所有参数的梯度统一限制在范数1以内,防止过大梯度把权重直接推出稳定区域。它不是必须的,但在文本模型或深层网络里非常管用。

4.4 验证、保存与断点续训

训练完一个epoch之后,一定要评估,不能只看训练集loss。验证时记得切到model.eval()模式并用torch.no_grad()包住,这样能关闭Dropout和BatchNorm的统计更新,也不至于为验证计算梯度浪费显存。

def validate(model, loader, device): model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs = imgs.to(device) labels = labels.to(device) outputs = model(imgs) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return correct / total

模型保存这块,我强烈建议保存state_dict而不是整个模型对象。因为整个模型对象会序列化源码结构,换环境后很容易因为类路径不一致而加载失败。

torch.save(model.state_dict(), "model.ckpt")

加载的时候要先建一个同样结构的模型,再load_state_dict:

model = SimpleCNN() model.load_state_dict(torch.load("model.ckpt", map_location="cpu")) model = model.to(device)

map_location="cpu"这个参数非常有用:你在GPU上存的权重,到了没有GPU的机器上,也能正常加载到CPU。断点续训的话,最优做法是保存一个字典,里面带上epoch、model_state_dict、optimizer_state_dict和best_acc,这样中断了也能原地满血重启。

5. 模型导出与部署:PyTorch转ONNX

5.1 为什么要转ONNX

很多人的项目停留在“训练完,打印一下准确率”就结束了,但实际生产环境里,你经常要把模型交给另一个语言或框架去推理。ONNX(Open Neural Network Exchange)就是模型之间的“通用语言”。它把PyTorch模型的计算图导出成一种中立的图格式,然后可以用ONNX Runtime、TensorRT或者其他推理引擎加载。

我们训练时用的是PyTorch动态图,方便调试,但推理部署时更希望静态化、压缩计算图、搞量化或者层融合。ONNX正好提供了这个中间层。你可以把pytorch转onnx理解为:把一张“动态图纸”固化成一版“施工蓝图”,之后任何能读懂ONNX的工具都能按蓝图施工。

5.2 导出ONNX的标准流程

导出的核心是torch.onnx.export,但它比想象中讲究。首先你得准备一个“标准输入”,也就是我们要传给模型的示例张量。这个张量的shape必须和真实推理时一致,因为ONNX会根据它固定计算图的结构。

import torch model = SimpleCNN() model.load_state_dict(torch.load("model.ckpt", map_location="cpu")) model.eval() dummy_input = torch.randn(1, 3, 32, 32) torch.onnx.export( model, dummy_input, "model.onnx", opset_version=17, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, )

这里opset_version是ONNX的算子集版本,设得太低,有些新算子不支持;设得太高,推理引擎版本太老又读不懂。我习惯先用17这个版本试试,如果安装的onnxruntime比较新,一般都能兼容。dynamic_axes的作用是把第0维设为动态批次维度,这样导出的模型可以接收不同batch的输入,不至于只支持batch=1。

导出后你会看到一个.onnx文件,它的大小和网络参数量成正比。这时候你可以用Netron这个可视化工具打开它,检查图结构有没有问题,算子是否都在预计位置。我每次导出完都习惯看一眼,比对着黑盒测试靠谱得多。

5.3 用ONNX Runtime验证导出结果

导出成ONNX不等于万事大吉,还要验证导出的模型和原PyTorch模型的输出是否一致。最通用的验证方式就是用onnxruntime跑一遍同样的输入:

import numpy as np import onnxruntime as ort session = ort.InferenceSession("model.onnx") x = np.random.randn(1, 3, 32, 32).astype(np.float32) onnx_output = session.run(output_names=["output"], input_feed={"input": x})[0]

然后把onnx_output和PyTorch模型的输出做一次对比,看最大误差是不是在1e-3以内:

with torch.no_grad(): torch_output = model(torch.from_numpy(x)).numpy() print("max diff:", np.abs(onnx_output - torch_output).max())

误差通常来自浮点运算顺序,几e-7到几e-5都很正常;如果差距到了0.1以上,基本上就是导出过程中某个算子解析错了,或者层顺序被改变,要回查模型结构。

5.4 ONNX导出的几个实战大坑

第一个坑是“动态维度理解错误”。dynamic_axes如果你只设了batch维度,那其他维度比如宽、高都是固定死的。你的部署服务如果会收到不同尺寸的图片,就必须在导出前把Resize做在模型内部,或者在导出时把所有相关维度都设为“dynamic”。不过设动态维度太多,输出性能会受影响,通用做法是先统一resize到固定尺寸再进模型。

第二个坑是“某些算子在低版本opset下无法导出”。最常见的是torchvision里的RoIAlign、NMS这类检测用的算子。遇到不支持的情况,第一选择是升级opset,第二选择是换一种等价的PyTorch写法绕开这个算子,第三选择是在导出脚本里把不支持的子模块替换成手动函数。

第三个坑是“BatchNorm在eval和train模式下的差异”。导出前一定记得调用model.eval(),否则BatchNorm会把当前batch的统计量写进图里,导出的ONNX在推理时表现会莫名变差。这个问题隐蔽性极强,我见过不少人导完模型后精度对不上,结果就是忘了这行。

6. 常见问题与排查技巧实录

6.1 安装阶段高频故障速查

现象常见原因解决办法
ModuleNotFoundError: No module named 'torch'环境没激活或没安装成功先conda activate pytorch2,再重装
torch.cuda.is_available()返回False装成了CPU版,或CUDA版本不匹配查看torch.__version__是否带cu后缀,卸载重装GPU轮子
pip下载慢或超时网络问题使用国内PyPI镜像:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple
conda创建环境一直卡在Solvingconda依赖解析慢换用conda create -n pytorch2 python=3.9 -y --no-default-packages
显卡驱动在WSL里无法识别Windows侧NVIDIA驱动过旧更新Windows驱动,确保能在Windows本机nvidia-smi正常输出

安装阶段最大的经验就是:不要迁移环境,不要复制site-packages。Python环境和优化器状态一样,干净才是王道。你与其花一个小时修复一个被弄脏的环境,不如花十分钟新建一个重装一遍。

6.2 训练阶段常见问题定位

训练中你会发现:

  • loss在某个值附近抖来抖去:先看学习率。1e-3对大多数小模型是合理的,如果数据量小,可以降到1e-4。
  • loss变成NaN:先看输入数据里有没有NaN;再看学习率是否过猛;最后检查混合精度是否正常。梯度裁剪是急救包。
  • CUDA out of memory:先把batch_size减半,再考虑torch.cuda.empty_cache();如果还是不够,用梯度累积模拟大batch。
  • DataLoader多进程卡死:在Windows下尤其常见,建议放弃Jupyter,写独立训练脚本,或把num_workers降到0测试。

直观来说,训练阶段最需要的是“把变量拆开看”。我每次遇到奇怪问题,都会临时写一段脚本,只保留那一个问题的最小复现路径。比如怀疑数据增强问题,就直接把transform去掉跑一百步;怀疑模型问题,就用一个随机输入测前向是否通。逻辑比文档好使。

6.3 模型转换阶段的排查要点

转换阶段如果你用的是自定义网络,报错大概率集中在“不支持某个op”或“输入shape mismatch”。这一阶段我的排查套路是三步走:

  1. 看报错定位到具体是哪一行。ONNX导出的报错信息往往直接告诉你哪个节点出了问题。
  2. 如果报错指向某个自定义函数,把它替换成原生nn.Module组合。
  3. 依旧失败就把opset_version调高或调低一格试试,同时检查PyTorch和onnxruntime版本。

这些坑都不是智商问题,纯粹是生态版本之间的兼容矩阵太大。碰上问题先去GitHub Issues搜索,基本都能找到同款报错。

最后再分享一个我自己的习惯:每次新建项目,我都会先写一个requirements.txt,把torch、torchvision、onnxruntime的版本固定好。模型训练完,我会用脚本同时输出PyTorch和ONNX的推理结果做对拍,哪怕只做一次,也能省掉部署现场半夜抓狂的麻烦。这个实操流程走到这里,你已经把从安装、训练到导出的完整闭环走完了,后面每一次踩坑,本质上都是对这个主线的查漏补缺。

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

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

立即咨询