news 2026/10/18 6:36:22

PyTorch入门实战:Tensor、自动求导与图像分类全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch入门实战:Tensor、自动求导与图像分类全流程

1. 先想明白:你究竟在跟 PyTorch 学什么

前阵子有个朋友问我,说他想学 PyTorch,但打开教程看到一堆nn.Module、DataLoader、backward(),整个人就懵了。他说自己不是程序员,只是做数据分析的,被领导安排要训练一个图像分类模型,问我应该怎么下手。

我特别理解这种感觉。PyTorch 现在已经是深度学习领域使用最广的框架之一,大量论文、开源项目、工业落地案例都跑在它上面。但大多数人第一次接触它,问题往往不是"PyTorch 怎么用",而是"我到底在学什么"。如果你把 PyTorch 当成一个普通的 Python 库去学,会越学越乱;如果你把它当成一个"自动求导机 + 张量计算器",思路就会清晰很多。

我习惯这样理解 PyTorch 的层级:

  • 最底层是 Tensor,负责存储数据和做各种数学运算,类似 NumPy 数组的加强版,但它能自动记录计算路径。
  • 第二层是autograd,负责自动求导。深度学习的核心就是梯度下降,而梯度就是损失函数对参数的导数,手动推导非常痛苦,PyTorch 帮你自动算了。
  • 第三层是torch.nn,提供了神经网络组件,比如卷积层、全连接层、损失函数。
  • 第四层是torch.optim,封装了各类优化器,比如 SGD、Adam 这样的参数更新算法。
  • 最上面才是DataLoader、训练循环、模型保存加载这些工程化内容。

很多人一上来就盯着第五层写代码,前四层靠复制粘贴。结果遇到问题就完全没法排查——因为你不知道数据流是怎么走的,也不知道梯度是什么时候算出来的。

这篇内容我打算从一个完整的图像分类项目切入,带你从零跑通 PyTorch 的核心流程。中途我会重点解释"为什么会这样设计",也会把我在实际项目中踩过的坑直接摆出来。适合谁看呢?两类人:一类是刚接触深度学习、连 Tensor 和torch.Tensor都分不清的纯新手;另一类是写了一段时间代码但还是靠拼凑的初学者,希望通过一次完整梳理把整个体系打通。

2. 装环境不劝退:版本、显卡和那些"缺一步就崩"的细节

很多人的 PyTorch 之旅不是倒在模型上,而是倒在装环境上。说实话,PyTorch 的安装已经比几年前友好太多了,但网上教程五花八门,新旧版本混杂,照着一套老教程装很容易翻车。

2.1 教你一个最稳的安装判断方法

先别急着pip install torch,先想清楚两个问题:你用不用 GPU?你的 CUDA 环境是什么情况?

打开终端,按顺序看这几项:

python --version pip --version nvidia-smi

重点说nvidia-smi这个命令。如果它提示找不到命令,说明你机器上要么没有 NVIDIA 显卡,要么驱动没装好。如果它正常输出了 GPU 信息,那么看右上角的 "CUDA Version",比如 12.1 或者 11.8,这个数字代表你当前驱动支持的 CUDA 最高版本。

然后去 PyTorch 官网找对应的安装命令。官网做的安装配置选择器已经非常直观了,你只要把操作系统、包管理器、CUDA 版本选好,它就会给出对应的pip命令。

我见过最典型的坑是:显卡驱动支持 CUDA 12.1,但很多人选了 PyTorch 编译时用的 CUDA 11.8 版本,结果也能跑,只是某些算子可能会警告或者性能差一点。其实在大多数场景下,用略低于驱动支持版本的 CUDA 轮子包问题不大,因为 PyTorch 的 CUDA 支持是向后兼容的。但如果你装的是 CPU 版本,然后发现torch.cuda.is_available()一直返回False,就说明你没看清安装命令里的cpu标志。

2.2 我推荐的环境管理方式

作为过来人我非常建议你用 Python 虚拟环境来装 PyTorch,不要让它在系统环境里裸奔。我见过太多人pip install torch之后,某天装其他包时不小心把 torch 升级到不兼容版本,整个项目直接跑不起来。

我习惯用conda建环境:

conda create -n torch_env python=3.10 conda activate torch_env

Python 版本不用太激进,3.9 或 3.10 都很稳。然后按照官网给出的pip命令装 torch。装完做个快速自检:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.device_count())

如果这三行都正常,环境就基本没问题。这里多说一句,很多人看到torch.cuda.is_available()返回False就很慌,但其实如果你没有 NVIDIA 显卡,这个False是正常的。CPU 也能跑 PyTorch,只是慢一些。我第一年做深度学习的时候就是用 CPU 训练小型网络,虽然慢,但理解原理完全够用。

还有一个很实际的建议:装特定版本的 PyTorch 时,尽量让 torch、torchvision、torchaudio 三个包保持同一个发布版本。比如你用2.1.0,那这三个最好都升到2.1.0,因为它们之间存在编译层面的对齐要求,版本不一致经常会报出特别奇怪的错误。

3. 拆掉 Tensor 和自动求导这堵墙

在正式写模型前,必须先打通 Tensor 和自动求导这层。这层不打通,后面所有代码对你来说都是玄学——能跑但不知道为什么跑。

3.1 Tensor 到底是什么

用最简单的话说,Tensor 是多维数组。标量是一个 0 维 Tensor,向量是 1 维,矩阵是 2 维,图像数据是 4 维——形状一般是(批次大小, 通道数, 高, 宽),比如一批 32 张图像,每张是 3 通道、224x224,那 Tensor 的形状就是(32, 3, 224, 224)。

你可以把它理解为带了很多额外功能的 NumPy 数组:它能自动放到 GPU 上运算,能记录操作历史从而自动求导。如果你会 NumPy,Tensor 上手很快:

import torch import numpy as np # 从列表创建 t1 = torch.tensor([1.0, 2.0, 3.0]) # 从 NumPy 数组创建 arr = np.array([1, 2, 3]) t2 = torch.from_numpy(arr) # 创建全零矩阵 t3 = torch.zeros(3, 4) print(t1.shape) # torch.Size([3]) print(t2.dtype) # torch.int64

很多新手在数据处理阶段特别容易栽在 dtype 上。比如从 NumPy 读进来的是float64,而 PyTorch 模型默认参数是float32,一旦混用往往在计算损失时突然报错。解决办法就是显式转换:

t = torch.from_numpy(arr).float()

或者用torch.as_tensor(arr, dtype=torch.float32)。类似这种坑没有任何技术含量,但如果你不知道根源,会白白排查很久。

3.2 自动求导为什么是 PyTorch 的立身之本

机器学习训练的核心套路是:前向传播算出预测值,计算损失,然后通过反向传播求出损失对每个参数的梯度,最后用优化器更新参数。

问题来了,求梯度这件事,对于简单函数你手动算算还行,对于动辄几十层、上百层的神经网络,手推是不可能的。而 PyTorch 的autograd机制,会在你进行 Tensor 运算时自动构建一张计算图,记住你是通过什么操作得到最终结果的。然后你调用.backward()时,它会沿着这张图反方向把梯度算出来,存到每个张量的.grad属性里。

看一下最简单的例子:

x = torch.tensor(2.0, requires_grad=True) y = x ** 2 y.backward() print(x.grad) # 输出 tensor(4.)

这里y = x * x,导数在x = 2处是 4。你看到的是 PyTorch 自动算出来了。你只需要在创建需要梯度变量的 Tensor 时标上requires_grad=True,或者用nn.Parameter包装。

这里有个关键认知:模型的权重参数是"需要梯度"的,输入数据一般是"不需要梯度"的。所以如果你手写代码时给输入数据也设置了requires_grad=True,虽然不报错,但计算图会变大,内存和显存消耗上升,训练变慢,属于典型的"不该做的动作"。

3.3 训练流程中梯度怎么一步步流动

一个完整的训练循环,我想先把全貌摆出来,再拆解每一步在干什么:

import torch import torch.nn as nn import torch.optim as optim for epoch in range(num_epochs): for batch_data, batch_label in dataloader: batch_data = batch_data.to(device) batch_label = batch_label.to(device) # 前向传播 outputs = model(batch_data) loss = criterion(outputs, batch_label) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step()

我第一次看到这段代码时非常不理解"为什么每轮要zero_grad()"。后来才明白,PyTorch 的梯度是累积的,如果你不清零,上次算出来的梯度会和这次梯度叠加。这意味着如果不zero_grad(),你的参数更新方向就是多次梯度的叠加,不是一个正确的单步梯度。这是新手最容易忽略、但影响极大的细节。

backward()是沿着计算图计算梯度。step()是让优化器拿着梯度去更新参数。这三种方法各干各的活,很多人只会复制调用顺序,却不知道去掉任何一个都会出问题。

4. 用三小时跑通一个像样的图像分类 Demo

光讲概念不讲实操就是耍流氓。这一章我带你手动搭一个用于图像分类的完整项目,从数据准备到模型训练再到预测,整个过程跟着敲一遍,你会对 PyTorch 的工程链路有一个整体手感。

4.1 数据怎么准备:从原始图片到 DataLoader

图像分类第一步是准备好数据。假设我们现在手里有一个"猫狗分类"的数据集,目录长这样:

data/ train/ cat/ 1000 张图片 dog/ 1000 张图片 val/ cat/ 200 张图片 dog/ 200 张图片

PyTorch 提供了一个非常常用的数据加载组合:torchvision.datasets.ImageFolder负责按目录结构读取图片并打标签,torchvision.transforms负责对图像做预处理,torch.utils.data.DataLoader负责把数据打包成批次并自动打乱顺序。

from torchvision import datasets, transforms from torch.utils.data import DataLoader transform = transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder(root='data/train', transform=transform) val_dataset = datasets.ImageFolder(root='data/val', transform=transform) 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) print(train_dataset.classes) # ['cat', 'dog']

这里每个环节都值得细说。Resize((128, 128))把图片统一到固定尺寸,因为模型要求固定尺寸输入。ToTensor()有两件事:一是把PIL.Image或numpy.ndarray转成 Tensor,二是把像素值从0~255缩放到0~1。Normalize用均值 0.485、标准差 0.229 这些数值,这几个数不是随便拍的,是 ImageNet 数据集的统计值。用它们在训练时能加快收敛,属于行业通用做法。

为什么我不在训练集里直接加随机裁剪、随机翻转这些数据增强?因为在搭第一个 Demo 时,数据增强会引入太多变量,你很难判断模型性能变化到底来自哪里。先把最朴素的流程跑通,后面再逐步加增强,效果会比一步到位好得多。

4.2 模型搭建:别一上来就上 ResNet

很多新手上来就想用 ResNet、EfficientNet 这种大网络,我觉得没必要。理解 PyTorch 的首要目标是理解数据流和训练循环。所以我建议先手搭一个两层卷积的小网络,既能看到nn.Conv2d的作用,又不会因为网络太复杂导致排查困难。

import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc = nn.Linear(32 * 16 * 16, num_classes) # 输入尺寸后续解释 def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(x.size(0), -1) x = self.fc(x) return x

输入是(32, 3, 128, 128)的批次。经过一次卷积加池化,变成(32, 16, 64, 64);再经过一次,变成(32, 32, 32, 32)。展平后每个样本是32 * 32 * 32 = 32768维,所以全连接层输入就是 32768。我在上面代码里写了32 * 16 * 16,那是我把输入尺寸改成 64x64 时的值,实战里你最好用公式推一遍,或者直接打印特征图形状。

nn.Module是 PyTorch 模型的基础类。你的模型必须继承它才能真正融入自动求导体系。在__init__里定义子层,在forward里定义前向计算逻辑。注意一个常见误解:__init__里用nn.Conv2d这样的层,因为它们是nn.Module的子类,模型会自动管理它们的参数。如果你用普通 Python 列表存层,参数不会被注册,优化器也看不到它们。正确做法是用nn.Sequential或nn.ModuleList。

4.3 训练模型的组装:损失函数、优化器、训练循环

网络定义好之后,还需要选一个损失函数和一个优化器。对于图像分类这种离散标签任务,最常用的是交叉熵损失nn.CrossEntropyLoss。这里有个特别容易搞错的设计:这个损失函数本身已经包含了softmax步骤,所以你不需要在模型的最后一层加 Softmax,直接输出原始分数(logits)就行。

model = SimpleCNN(num_classes=2) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device)

optimizer绑定的是model.parameters(),也就是说优化器只知道模型里被注册的参数,然后它会在每次step()里更新这些参数。lr=0.001是 Adam 优化器一个非常常用的默认值,大多数简单任务从 0.001 起步都比较稳妥。

训练循环我之前已经写过,这里我把关键部分再展开一下,加上打印日志和验证逻辑:

def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss = 0.0 correct = 0 total = 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() avg_loss = total_loss / total accuracy = correct / total return avg_loss, accuracy

model.train()表示进入训练模式,这个在简单网络里看起来没什么作用,但之后用 Dropout 或 BatchNorm 就会出现训练和验证行为不一致的问题。torch.max(outputs, 1)是找出每一行最大值的索引,也就是模型认为最可能的类别。.item()把 Tensor 转成 Python 数值,方便打印。

验证函数和训练函数几乎一样,唯一不同的是要加with torch.no_grad():,因为在验证阶段我们不需要计算梯度,这样省显存也更快。还要调用model.eval()切到验证模式。

4.4 跑起来看效果,然后判断该往哪调

如果上面代码都敲对了,训练几个 epoch 后你会发现损失下降、准确率上升。我的经验是,在猫狗这种简单二分类任务上,用这个小网络训练大概十个 epoch,验证准确率应该能到 90% 以上。

如果发现损失不降或者准确率很低,不要急着改网络结构,按顺序检查几件事:第一,标签是否张冠李戴,比如ImageFolder是按字母顺序分配类别的,cat是 0、dog是 1,确认没有反;第二,学习率是不是太大或太小,太大损失会震荡,太小收敛太慢;第三,数据预处理是不是出错了,比如Normalize参数写反。

跑通之后,你可以试着把SimpleCNN换成torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT),并修改最后全连接层输出来适配自己的类别数。迁移学习在这个体量的任务上,效果会显著好于自己从零训练的小网络。这也是行业里最常见的做法——大多数人不会从零训练整个大模型,而是用预训练模型做微调。

5. Debug 才是主菜:新手必掉的 5 个坑

我在帮别人排查 PyTorch 代码的时候,发现有些坑几乎每隔一段时间就会有人踩一次。下面这几类问题覆盖面很高,每次遇到它们,解决思路基本都一致。

5.1 维度不匹配:最常见的报错"Expected shape ... but found ..."

这类错误的典型场景是:把全连接层输入维度算错了。我把排查方法总结成一条:在forward里临时打印每一层的输出形状。

def forward(self, x): print("input:", x.shape) x = self.pool(F.relu(self.conv1(x))) print("after conv1:", x.shape) x = self.pool(F.relu(self.conv2(x))) print("after conv2:", x.shape) x = x.view(x.size(0), -1) print("after flatten:", x.shape) x = self.fc(x) return x

调试完之后记得删掉这些print。依赖打印调试虽然看起来土,但实话实说,它比盯着权重初始化代码推演要快得多。我一般先用很小的批次跑一次,确认数据流没问题再放开训练。

5.2 梯度爆炸与梯度消失:loss 变成 NaN

训练着训练着 loss 突然变成nan,这是很有冲击感的一类问题。最常见的原因有几个:学习率太大,导致梯度更新幅度过大;数据里混入了NaN值;网络初始化不良。

最简单的排查方案是调低学习率,比如从1e-3降到1e-4,如果恢复正常,说明大概率是学习率问题。另外可以在backward()后加一句torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),这在训练 RNN、Transformer 类模型时几乎是标配操作,可以防止梯度范数过大导致更新异常。

5.3 CPU 和 GPU 张量混用

PyTorch 的一个硬性要求是参与运算的张量必须在同一个设备上。最常见的报错信息是 "Expected all tensors to be on the same device"。原因很简单:模型被.to(device)放到了 GPU,但输入数据忘了.to(device)。

这个问题的解决办法其实是在代码里统一管理设备。我的习惯是尽早定义device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),然后模型和数据都在进入训练循环前调用.to(device)。

5.4 训练集和验证集的模型模式搞混

Dropout 和 BatchNorm 这两种层在训练和验证时行为不同。训练时 Dropout 会随机丢弃一部分神经元,验证时不会;BatchNorm 训练时使用当前批次的均值和方差,验证时使用历史统计量。如果你在验证时忘了model.eval(),验证结果会不稳定,时高时低。

我见过不少人训练完直接在训练脚本里顺手算了一个验证准确率,结果比自己预期低不少(可能是 Dropout 引入了随机性),然后就开始怀疑模型有问题。其实只要在验证前面加一行model.eval()就解决了。

5.5 过拟合:训练准确率高但验证准确率低

训练集准确率升到 100%,验证集只有 70%,这种状态叫过拟合,意思是模型死记硬背了训练样本,没有学到通用的规律。

最简单的应对方式有三板斧:加数据增强,比如随机水平翻转、随机裁剪、色彩抖动;加 Dropout;或者改用预训练模型做迁移学习。对于图像分类任务,在这三者里面优先尝试数据增强,成本最低、效果最明显。

这些小问题单独来看都不难,但它们组合起来的排查成本很高。如果你刚开始写 PyTorch,我可以给你一个非常实用的定位思路:如果把模型换成极小的网络,问题依然存在,那说明问题多半不在模型结构,而在数据处理或训练流程。

6. 从"能跑"到"跑得好":我的几个私人习惯

最后聊一些不常写进教程但实际很有用的经验。

我在调试所有深度学习项目时都会先做一个"单批次过拟合测试":只拿一个批次的数据,训练几十步,看模型能不能把这一批数据完全记住(损失降到极低)。能做到这一步,说明你的代码链路是通的;做不到,说明哪里有问题。这一步还能帮你快速发现损失是否下降、参数是否更新,是最推荐做的 sanity check。

然后是模型保存。PyTorch 保存模型通常有两种做法:torch.save(model.state_dict(), 'model.pth')只保存参数;torch.save(model, 'model.pth')保存整个模型。我强烈建议用第一种,因为它只保存张量数据,体积小,跨设备、跨版本兼容性更好。加载时先重新定义模型结构,再load_state_dict。

torch.save(model.state_dict(), 'model.pth') model = SimpleCNN(num_classes=2) model.load_state_dict(torch.load('model.pth', map_location=device)) model.eval()

map_location=device这个参数很关键。你在 GPU 上训练的模型,在 CPU 机器上加载时必须指定map_location='cpu',不然会报设备不匹配的错误。

还有关于随机种子的设置。如果你希望实验可复现,最好固定随机种子:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)

不过要说明的是,在 GPU 上做并行运算时,同样的种子也不能保证 100% 结果一致,因为 CUDA 的某些操作本身有随机性。所以不要追求绝对的复现,固定种子只是帮你把大多数变量控制住。

回看整个 PyTorch 的学习路径,你会发现它其实没有想象中那么难。核心只围绕一件事:张量计算、自动求导、参数更新。把这些环节拆开理解,再通过一个完整的小项目串起来,比起背 API 要高效得多。我现在做一个新任务时,也依然会按最朴素的流程先跑通,再逐步优化。这个习惯救过我非常多次。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 6:32:52

RK3562+RK817充电关机后无法开机:PMIC电源状态分支的排查与修复

前阵子调一块 RK3562 RK817 的带电池主板,遇到一个特别折磨人的现象:插着充电器的时候系统能正常关机,但关机后马上按电源键,整机就像彻底断电一样,串口一个字符都没有,U-Boot 完全没有跑起来。拔掉充电器…

作者头像 李华
网站建设 2026/10/18 6:32:39

手机SSH运维实战:uniTerm v1.10打造移动端AI终端新体验

手机能 SSH 运维这件事,我之前一直觉得是“应急凑合”,但 uniTerm v1.10 出来后,我的看法变了。这款开源 AI 终端原本在桌面端就挺好用的,这次安卓版发布,补齐了移动端缺口。手机上跑 SSH 不再只是拿个 Termux 敲两行命…

作者头像 李华
网站建设 2026/10/18 6:32:03

跨学科开题报告写作指南:AI工具如何助力研究方案生成

1. 跨学科开题报告的痛点:想法再多也碰壁1.1 为什么跨学科选题总被导师说“立不住”跨学科研究这几年热度确实高,从“智能医疗”“教育大数据”到“智慧城市治理”,凡是沾了不同学科交叉边的选题,天然就显得前沿、有想象力。但热度…

作者头像 李华
网站建设 2026/10/18 6:30:39

老系统重构实战:从数据库连接池打爆到模块化单体选型复盘

这期节目聊完,我在整理笔记的时候发现,Dirk和Linus两个人聊得最投入、最没有嘉宾包袱的一次,反而是这期讲"旧系统改造"的。原因很简单:他们俩都亲手拆过自己的烂摊子,也都被人用"别动,动了出…

作者头像 李华
网站建设 2026/10/18 6:29:35

自然语言编程实战:从需求描述到可落地代码的完整流程

前几年要在编辑器里让程序做个事,你得先把数据结构和调用关系在脑子里过一遍,再敲上几十行代码。现在圈子里讨论的方向已经变了,很多人开始直接用自然语言去描述"我想要什么效果",让工具去生成、修改甚至调试代码。这种…

作者头像 李华
网站建设 2026/10/18 6:26:15

CEF 3.2357 Windows32 实战指南:C++嵌入网页与NPAPI插件集成

简介:本资源为CEF(Chromium Embedded Framework)3.2357.1271.g8e0674e Windows 32位官方二进制发布包的完整存档,专为C桌面应用开发者、浏览器嵌入式方案研究者及需要NPAPI插件支持的遗留系统维护人员提供。该版本是CEF官方明确终…

作者头像 李华