1. 从零上手PyTorch:先搞清楚它到底在解决什么问题
很多人第一次接触PyTorch,是被“深度学习框架”这四个字吓住的,觉得必须先把神经网络理论啃完才能动手。我当初也是这么想的,结果拖了两个月没敢装环境。后来真正用起来才发现,PyTorch的核心价值其实非常朴素:它让你用Python写数学公式,同时自动帮你算导数,还能把计算丢到显卡上跑。就这三件事,撑起了它绝大部分的使用场景。
如果你正在犹豫要不要学PyTorch,或者装了之后不知道从哪下手,这篇内容就是写给你的。我不打算堆砌API文档,而是按照一个从业者真实的摸索路径,把环境搭建、张量操作、自动求导、模型训练这条主线串起来,中间穿插我自己踩过的坑和后来总结出来的省事做法。读完你至少能做到:独立配好环境、看懂一段PyTorch代码在干什么、自己写出一个能跑通的小训练循环。
PyTorch的定位可以用一句话概括:它是一个基于动态计算图的深度学习框架。动态图的意思是,你的计算过程是边跑边建的,不像早期某些框架要先定义好整个图再执行。这个特性带来的直接好处是调试极其方便——你可以像调试普通Python程序一样,在任意一行打印中间结果、加断点、改逻辑。对于研究和实验来说,这种灵活性比什么都重要。
另一个容易被忽略的点是,PyTorch和NumPy的关系非常近。如果你之前用过NumPy,那PyTorch的上手成本会低很多,因为它的张量(Tensor)操作和ndarray高度相似。区别在于Tensor可以放到GPU上,并且支持自动求导。所以我的建议是:把PyTorch当成“能上GPU、能自动求导的NumPy”来理解,心理门槛会瞬间降低不少。
下面我会按照实际使用的顺序,从环境准备讲到模型训练,每一块都给出可复现的操作和背后的理由。不管你是刚入门的学生,还是从其他框架转过来的开发者,都能找到对你有用的部分。
2. 环境搭建:版本选择与安装方式的取舍
2.1 为什么我不推荐一上来就装最新版
环境搭建是劝退新手的第一道坎,而这道坎里最大的坑就是盲目追新。我见过太多人直接pip install torch装了最新版,结果发现自己的显卡驱动不支持对应的CUDA版本,或者和项目里其他库冲突,折腾一整天跑不起来。
我的经验是:先确定你的硬件和用途,再倒推版本。如果你只是用CPU跑学习用的小模型,那版本选择很自由,装最新稳定版基本没问题。但如果你要用GPU训练,就必须让PyTorch的CUDA版本、你的显卡驱动版本、以及你系统里的CUDA Toolkit版本三者对齐。
这里有个关键概念要澄清:PyTorch官方提供的安装包,很多是自带CUDA运行时的,也就是说你系统里不一定需要单独装完整的CUDA Toolkit。这一点经常被误解。你只需要保证显卡驱动足够新,能支持PyTorch内置的那个CUDA版本即可。
2.2 三种安装方式的对比与选择
实际安装时你会面临几种选择,我把它们的适用场景整理成表格,方便你对号入座。
| 安装方式 | 命令示例 | 适用场景 | 注意事项 |
|---|---|---|---|
| pip安装 | pip install torch | 快速上手、CPU训练、虚拟环境 | 默认可能装CPU版,GPU版需指定索引 |
| conda安装 | conda install pytorch | 科学计算环境、依赖管理复杂 | 解析慢,但依赖冲突少 |
| 源码编译 | 从源码build | 需要改框架源码、特殊硬件 | 耗时长,新手不建议 |
对于绝大多数人,我推荐用conda创建独立虚拟环境,再用pip装PyTorch。这个组合兼顾了环境隔离和安装速度。具体操作是这样的:
# 创建名为dl的虚拟环境,指定Python版本 conda create -n dl python=3.10 conda activate dl # 安装PyTorch,以CUDA 11.8为例 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118注意最后那个--index-url参数,这是指定从PyTorch官方的CUDA专用源下载。如果你不加这个,pip可能给你装成CPU版本,到时候torch.cuda.is_available()返回False,你还得重装。
2.3 验证安装是否真正成功
装完之后别急着写模型,先做三步验证,这一步很多人跳过,结果后面出问题找不到原因。
import torch # 第一步:确认版本 print(torch.__version__) # 第二步:确认CUDA是否可用 print(torch.cuda.is_available()) # 第三步:确认能实际在GPU上做运算 if torch.cuda.is_available(): x = torch.randn(3, 3).cuda() print(x.device)如果第二步返回True,第三步能打印出cuda:0,说明GPU环境完全正常。如果第二步是False,先别慌,检查两件事:一是你装的是不是CPU版本,二是显卡驱动是否太旧。我遇到过好几次都是因为装成了CPU版,重装指定CUDA源就好了。
提示:虚拟环境是个好习惯,但要注意每次新开终端都要先
conda activate dl,否则你用的还是系统Python,会提示找不到torch。这个坑我踩过不止一次。
3. 张量操作:PyTorch的地基该怎么打
3.1 张量创建的几种方式及其适用场景
张量是PyTorch里最基础的数据结构,你可以把它理解成多维数组。所有的数据,不管是图像、文本还是音频,最终都要转成张量才能喂给模型。创建张量的方式有很多,但不同方式背后有不同的用途,用错了会带来隐患。
最常用的几种创建方式:
torch.tensor(data):从Python列表或NumPy数组创建,会复制数据torch.from_numpy(ndarray):从NumPy数组创建,共享内存,改一个另一个也变torch.zeros(shape)/torch.ones(shape):创建全零或全一张量torch.randn(shape):创建标准正态分布随机张量,常用于初始化torch.arange(start, end, step):创建等差数列,类似Python的range
这里重点说torch.tensor和torch.from_numpy的区别,因为这是新手最容易踩的坑。前者会复制一份数据,后者和原NumPy数组共享同一块内存。如果你后续要修改数据,用错了会导致意想不到的连锁反应。
import numpy as np import torch arr = np.array([1, 2, 3]) t1 = torch.tensor(arr) # 复制,改t1不影响arr t2 = torch.from_numpy(arr) # 共享,改t2会影响arr t2[0] = 100 print(arr) # 输出 [100, 2, 3],arr被改了3.2 形状变换:view、reshape和permute的区别
张量形状变换是日常操作中最频繁的动作之一。模型里经常需要把图像从(H, W, C)转成(C, H, W),或者把特征图展平成一维。这里有几个函数容易混淆,我逐个说清楚。
view和reshape功能类似,都是改变形状,但有个关键区别:view要求张量在内存中是连续的,否则会报错;reshape则会自动处理,必要时先复制一份。所以如果你不确定张量是否连续,用reshape更保险,代价是可能多一次内存拷贝。
permute则是交换维度顺序,它不改变数据本身,只改变维度的排列。比如把(C, H, W)变成(H, W, C),用permute就对了。而view做不到这个,因为view只是按内存顺序重新划分。
x = torch.randn(3, 4, 5) # C=3, H=4, W=5 # 交换维度,变成 (H, W, C) y = x.permute(1, 2, 0) print(y.shape) # torch.Size([4, 5, 3]) # 展平,常用于全连接层前 z = x.reshape(-1) print(z.shape) # torch.Size([60])还有一个transpose,它只能交换两个维度,功能是permute的子集。日常用permute更通用。
3.3 广播机制:让形状不同的张量也能运算
广播(broadcasting)是PyTorch里非常强大的一个特性,但也是最容易出隐蔽bug的地方。简单说,当两个形状不同的张量做运算时,PyTorch会尝试自动扩展维度使它们兼容。
规则是从最后一个维度往前对齐,每个维度要么相等,要么其中一个是1,要么其中一个不存在。举个例子:
a = torch.randn(4, 1, 3) b = torch.randn(2, 3) c = a + b print(c.shape) # torch.Size([4, 2, 3])这里b的形状(2, 3)被广播成了(1, 2, 3),再和a的(4, 1, 3)运算,最终得到(4, 2, 3)。理解广播的关键是记住“从右往左对齐”。
广播用好了能省很多代码,但用不好会得到你完全没预期的结果,而且不报错。我的建议是:在涉及广播的运算后,养成打印形状的习惯,尤其是调试阶段。
注意:广播虽然方便,但如果两个张量形状差异很大,可能产生巨大的中间结果,吃掉大量显存。遇到显存溢出时,先检查是不是广播惹的祸。
4. 自动求导:PyTorch最核心的魔法
4.1 计算图是怎么悄悄建起来的
自动求导是PyTorch区别于NumPy的根本所在,也是所有神经网络训练的基础。它的原理其实不复杂:每当你对一个需要梯度的张量做运算,PyTorch就在后台记录这个运算,形成一张计算图。反向传播时,它沿着这张图从后往前,用链式法则算出每个张量的梯度。
关键点在于requires_grad这个属性。只有设置为True的张量,PyTorch才会追踪它的运算历史。默认情况下,我们创建的张量requires_grad是False,因为大部分数据(比如输入图像)不需要梯度。需要梯度的是模型的参数。
x = torch.tensor([2.0, 3.0], requires_grad=True) y = x ** 2 + 3 * x z = y.sum() # 反向传播,计算z对x的梯度 z.backward() print(x.grad) # 输出 tensor([7., 9.])手动验证一下:z = x1² + 3x1 + x2² + 3x2,对x1求导是2x1+3=7,对x2求导是2x2+3=9,和输出一致。这就是自动求导的威力——你只管写前向计算,导数它帮你算。
4.2 梯度累积与清零:一个必须养成的习惯
新手写训练循环时,最常见的bug之一就是忘记清零梯度。PyTorch的梯度是累积的,也就是说每次调用backward(),算出来的梯度会加到已有的.grad上,而不是覆盖。
这个设计其实是有意为之的,它让梯度累积(模拟更大batch)变得方便。但对普通训练来说,如果你不在每轮迭代开始时清零,梯度就会越加越大,模型直接跑飞。
# 正确的训练循环骨架 for data, target in dataloader: optimizer.zero_grad() # 清零,必须在backward之前 output = model(data) loss = criterion(output, target) loss.backward() # 计算梯度 optimizer.step() # 更新参数顺序很重要:zero_grad → 前向 → 算loss → backward → step。这个顺序我建议你背下来,写多了就成肌肉记忆了。
4.3 什么时候需要detach和no_grad
有两个场景你会频繁用到detach()和torch.no_grad(),理解它们能帮你省显存、避免奇怪的错误。
detach()的作用是把一个张量从计算图中剥离出来,返回一个不携带梯度信息的新张量,但它和原张量共享数据。什么时候用?比如你想记录loss的数值用于打印,但又不想让这个记录操作进入计算图。
torch.no_grad()是一个上下文管理器,在这个块里的所有运算都不会被追踪。验证和测试阶段必须用它,因为推理不需要梯度,关掉追踪能显著减少显存占用、加快速度。
# 验证阶段的标准写法 model.eval() with torch.no_grad(): for data, target in val_loader: output = model(data) # 这里不需要backward我见过有人验证时不加no_grad,结果显存越用越多,最后OOM。加上之后显存占用能降一大截,尤其是大模型。
提示:
model.eval()和torch.no_grad()是两回事,前者切换Dropout和BatchNorm的行为,后者关闭梯度追踪。验证时两个都要用,缺一不可。
5. 模型构建:从nn.Module开始搭积木
5.1 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, 128) self.relu = nn.ReLU() self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return x有几个细节值得强调。第一,super().__init__()必须调用,否则PyTorch无法正确注册你定义的层,会报错。第二,层要作为实例属性赋值给self,这样PyTorch才能追踪到参数。第三,forward里不要写复杂的控制逻辑,保持清晰。
5.2 层与参数:parameters()到底返回了什么
model.parameters()返回的是模型里所有需要学习的参数,主要是各层的权重和偏置。优化器就是靠它知道要更新哪些张量。
model = SimpleNet() for name, param in model.named_parameters(): print(name, param.shape)打印出来你会看到类似fc1.weight torch.Size([128, 784])、fc1.bias torch.Size([128])这样的输出。理解这些形状对调试很有帮助,比如全连接层报形状不匹配时,你就能快速定位是输入维度还是层定义的问题。
一个常见误区是以为parameters()返回的是所有张量。不是的,它只返回requires_grad=True的参数。像Buffer(比如BatchNorm的running_mean)就不在里面,它们通过state_dict()管理。
5.3 损失函数与优化器的搭配逻辑
损失函数衡量模型输出和真实标签的差距,优化器根据这个差距更新参数。这两者的选择直接影响训练效果,不能随便配。
分类任务最常用nn.CrossEntropyLoss,它内部已经包含了Softmax,所以你的模型最后一层不要再加Softmax,否则就重复了。这是新手高频错误。
criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)优化器方面,Adam是默认的好选择,它对学习率不那么敏感,收敛快。SGD配合动量在调好的情况下泛化可能更好,但需要更多调参经验。我的建议是:先用Adam跑通,再考虑换SGD精调。
学习率lr是最重要的超参数。1e-3是Adam的常用起点,1e-2是SGD的常用起点。如果loss不下降,先怀疑学习率太大;如果下降极慢,怀疑太小。
6. 训练循环:把前面所有零件组装起来
6.1 一个完整可复现的训练骨架
前面讲的都是零件,现在把它们组装成一个能跑的训练循环。这个骨架我用了很多次,改改就能适配大部分任务。
def train(model, train_loader, val_loader, epochs=10): criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(epochs): # 训练阶段 model.train() train_loss = 0.0 for data, target in train_loader: optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step() train_loss += loss.item() # 验证阶段 model.eval() val_loss = 0.0 correct = 0 with torch.no_grad(): for data, target in val_loader: output = model(data) val_loss += criterion(output, target).item() pred = output.argmax(dim=1) correct += (pred == target).sum().item() print(f"Epoch {epoch}: train_loss={train_loss/len(train_loader):.4f}, " f"val_loss={val_loss/len(val_loader):.4f}, " f"acc={correct/len(val_loader.dataset):.4f}")这段代码里有几个值得注意的点。loss.item()把单元素张量转成Python浮点数,避免累积计算图导致显存泄漏。model.train()和model.eval()的切换不能忘。argmax(dim=1)取每行最大值的索引作为预测类别。
6.2 训练不收敛时的排查顺序
模型跑起来但loss不降,这是最让人抓狂的情况。我总结了一套排查顺序,按这个顺序走能解决大部分问题。
第一步,检查数据。打印几个batch的输入和标签,确认数据本身没问题、标签没搞错。我遇到过标签和输入错位的情况,查了半天才发现是Dataset写错了。
第二步,检查学习率。把lr调小10倍试试,如果loss开始下降,说明之前太大。或者调大10倍,看是否下降变快。
第三步,检查模型输出。在训练前用随机输入跑一次前向,看输出形状对不对、数值范围是否合理。如果输出全是同一个值,可能是初始化有问题。
第四步,过拟合一个小数据集。取10个样本,关掉正则化,看模型能不能把这10个样本训到接近100%准确。如果连这都做不到,说明模型结构或训练逻辑有根本问题。
这个“先过拟合小数据”的技巧非常实用,它能帮你快速区分是模型能力问题还是数据/调参问题。
6.3 显存不够时的几种应对策略
显存溢出(OOM)是训练大模型时的常客。除了换更大的显卡,还有几个软件层面的办法。
减小batch size是最直接的,但会影响训练稳定性。使用梯度累积可以在小batch下模拟大batch的效果:累积几次梯度再step一次。混合精度训练用torch.cuda.amp,把部分计算降到半精度,能省将近一半显存。及时释放不用的张量,用del删掉并调用torch.cuda.empty_cache()。
# 梯度累积示例 accum_steps = 4 for i, (data, target) in enumerate(train_loader): output = model(data) loss = criterion(output, target) / accum_steps loss.backward() if (i + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()注意loss要除以累积步数,这样梯度的量级才和正常batch一致。
注意:
torch.cuda.empty_cache()只是释放缓存,不会释放还被引用的张量。如果某个张量还被变量引用着,调它也没用。排查显存泄漏时,重点看有没有在循环里不断累积张量。
7. 数据加载:Dataset和DataLoader的正确用法
7.1 自定义Dataset的两个必须实现方法
PyTorch的数据加载靠两个类配合:Dataset负责定义“一条数据长什么样”,DataLoader负责“怎么批量取、怎么打乱、怎么并行”。自定义Dataset必须实现__len__和__getitem__。
from torch.utils.data import Dataset class MyDataset(Dataset): def __init__(self, data, labels): self.data = data self.labels = labels def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx], self.labels[idx]__getitem__返回的是单条样本,DataLoader会自动把它们拼成batch。这里有个性能要点:__getitem__里不要做太重的计算,因为DataLoader可能用多进程并行调用它。重计算应该放在__init__里预处理,或者用缓存。
7.2 num_workers和pin_memory的调优
DataLoader有几个参数直接影响训练速度,值得单独说。
num_workers控制用几个子进程加载数据。设为0表示在主进程加载,设大了能并行加速,但也不是越大越好。经验值是设为CPU核心数,但实际中4到8通常就够。设太大反而因为进程切换开销变慢。
pin_memory=True会把数据放到锁页内存,加速CPU到GPU的传输。用GPU训练时建议开启。
shuffle=True在每个epoch打乱数据顺序,训练集必须开,验证集不需要。
from torch.utils.data import DataLoader train_loader = DataLoader( dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True )7.3 数据预处理放在哪里最合适
数据预处理的位置选择会影响效率和灵活性。常见做法有三种:在__init__里全部预处理完、在__getitem__里实时处理、用transforms组合。
如果数据量不大、预处理结果能全放内存,第一种最快。如果数据量大或者需要在线增强(比如随机裁剪),就得用第二种。torchvision.transforms提供了一套现成的图像预处理工具,可以组合成流水线。
from torchvision import transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])注意ToTensor()会把像素值从0-255缩放到0-1,Normalize再用ImageNet的统计量做标准化。如果你用的是预训练模型,这个标准化参数要和模型训练时保持一致,否则效果会打折。
8. 模型保存与加载:别让训练成果白费
8.1 state_dict和整个模型的保存区别
训练完的模型必须保存,否则下次还得重训。PyTorch有两种保存方式,我强烈推荐只保存state_dict。
# 推荐:只存参数 torch.save(model.state_dict(), "model.pth") # 加载时需要先有模型结构 model = SimpleNet() model.load_state_dict(torch.load("model.pth"))为什么不推荐保存整个模型(torch.save(model, ...))?因为整个模型用pickle序列化,它依赖你定义模型时的代码路径。换台机器、改了类名或目录结构,加载就会失败。而state_dict只是参数的字典,和代码解耦,更健壮。
8.2 保存训练状态以便断点续训
如果训练要跑很久,光存模型参数不够,还得存优化器状态、当前epoch、学习率调度器状态等,这样才能从中断处继续。
checkpoint = { "epoch": epoch, "model_state": model.state_dict(), "optimizer_state": optimizer.state_dict(), "best_acc": best_acc } torch.save(checkpoint, "checkpoint.pth") # 恢复 checkpoint = torch.load("checkpoint.pth") model.load_state_dict(checkpoint["model_state"]) optimizer.load_state_dict(checkpoint["optimizer_state"]) start_epoch = checkpoint["epoch"] + 1优化器的状态也要存,因为像Adam这种优化器有动量等内部状态,不恢复的话续训初期会有波动。
8.3 加载预训练模型的常见坑
用预训练模型做迁移学习时,加载权重经常遇到键名不匹配的问题。比如你改了分类头,load_state_dict会报缺少某些键。
# strict=False 允许部分匹配 model.load_state_dict(pretrained_dict, strict=False)但strict=False要慎用,它可能掩盖真正的问题。更稳妥的做法是手动过滤,只加载匹配的键:
model_dict = model.state_dict() pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict and v.shape == model_dict[k].shape} model_dict.update(pretrained_dict) model.load_state_dict(model_dict)这样既加载了能用的权重,又不会因为形状不匹配报错,还能清楚知道哪些层是随机初始化的。
提示:加载预训练模型后,通常要冻结前面的层,只训练后面的分类头。做法是把前面层参数的
requires_grad设为False,这样优化器就不会更新它们,能省显存也防止过拟合。
9. 我踩过的那些坑和后来总结的经验
9.1 设备不匹配:最常见的报错来源
“Expected all tensors to be on the same device”这个报错,我敢说每个PyTorch使用者都遇到过。原因很简单:模型在GPU上,数据还在CPU上,或者反过来。
解决办法是养成一个习惯:在训练开始前,把模型和数据都明确移到同一个设备。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) # 训练循环里 data, target = data.to(device), target.to(device)我建议把device定义成全局变量,所有.to(device)都用它,这样切换CPU/GPU只改一处。另外,从DataLoader取出的数据默认在CPU,每次都要手动移,别指望它自动过去。
9.2 原地操作破坏计算图的隐蔽bug
PyTorch里带下划线的操作(如add_、mul_、relu_)是原地操作,直接修改张量本身。在需要梯度的张量上做原地操作,可能破坏计算图,导致反向传播报错或结果错误。
x = torch.tensor([1.0, 2.0], requires_grad=True) y = x.relu() # 安全 # y = x.relu_() # 危险,可能破坏计算图我的建议是:除非明确知道自己在做什么,否则避免在forward里用原地操作。多占一点内存换来的是安全。
9.3 随机种子:让实验结果可复现
深度学习有大量随机性:参数初始化、数据打乱、Dropout。如果不固定随机种子,同样的代码跑两次结果可能差很多,没法对比实验。
import torch import numpy as np import random def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False注意最后两行:deterministic=True让cudnn用确定性算法,benchmark=False关闭自动调优。这两个设置会稍微降低速度,但换来的是可复现性。做实验对比时值得开,追求极致速度时可以关。
9.4 学习率调度:让训练后期更稳
固定学习率往往不是最优的。训练初期需要大学习率快速下降,后期需要小学习率精细收敛。PyTorch提供了多种调度器。
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # 训练循环里,每个epoch后调用 scheduler.step()StepLR每10个epoch把学习率乘以0.1。还有CosineAnnealingLR按余弦曲线衰减,ReduceLROnPlateau根据验证loss自动调整。我个人常用余弦退火,它在很多任务上表现稳定。
注意:调度器的
step()调用时机要和它的类型匹配。有的按epoch调,有的按batch调,用错了学习率变化节奏就乱了。看文档确认清楚。
10. 从能跑到跑好:一些进阶方向
把基础训练循环跑通只是起点。真正要让模型效果好,还得在数据处理、模型结构、训练策略上花功夫。数据增强是最划算的投入,往往比换模型结构收益更大。对于图像任务,随机裁剪、翻转、颜色抖动这些手段能显著提升泛化能力。
模型结构方面,不要一上来就自己设计网络。先用成熟的骨干网络(如ResNet系列)做迁移学习,在大多数任务上都能拿到不错的效果。等你有了一定经验,再尝试改结构。
训练策略上,学习率预热(warmup)在训练初期能防止梯度爆炸,配合余弦退火效果很好。标签平滑(label smoothing)能缓解过拟合,在分类任务里几乎是无脑加。这些技巧在PyTorch里都有现成实现,用起来很方便。
最后说一个心态问题:PyTorch的API很多,不可能全记住。我到现在也经常查文档。重要的是理解核心概念——张量、自动求导、Module、训练循环——剩下的都是在这个框架上查漏补缺。把这篇里的骨架跑通,你就已经跨过了最难的那道门槛。