1. 从零上手 PyTorch:为什么它是深度学习入门的首选框架
如果你刚开始接触深度学习,或者从其他框架迁移过来,大概率会听到身边人推荐 PyTorch。但推荐归推荐,真正动手写第一行代码的时候,很多人还是会卡在环境配置、张量操作、自动求导这些基础环节上。我自己最初从静态图框架转过来的时候,最直观的感受就是:PyTorch 的代码写起来像在写普通的 Python 程序,调试的时候能直接用 print 看中间结果,这种体验对新手来说太友好了。
PyTorch 本质上是一个基于动态计算图的深度学习框架。所谓动态图,就是计算图在每次前向传播时实时构建,而不是提前定义好一张固定的图再反复执行。这个特性带来的直接好处是:你可以像调试普通 Python 代码一样,在任意位置打断点、打印张量形状、修改网络结构,而不需要重新编译整个图。对于做研究或者快速验证想法的人来说,这一点非常关键。
这篇文章面向的是刚接触 PyTorch 的开发者,或者用过一段时间但基础不够扎实、想系统梳理一遍的人。我会从环境搭建讲起,把张量操作、自动求导、模型定义、训练循环、数据加载、GPU 加速、模型保存与加载这些核心环节逐一拆开,补充我在实际项目中踩过的坑和总结出来的经验。每部分都会解释“为什么这样做”,而不只是给一段能跑的代码。读完你应该能独立完成一个完整的训练流程,并且知道每个环节背后发生了什么。
2. 环境搭建:版本选择与安装路径的取舍
2.1 版本匹配是第一个容易翻车的地方
PyTorch 的安装看起来简单,一条 pip 命令就完事,但实际最容易出问题的就是版本匹配。你需要同时考虑三个东西:Python 版本、PyTorch 版本、CUDA 版本。这三者之间有一个兼容矩阵,如果对不上,轻则装完 import 报错,重则训练时 GPU 用不了。
我的建议是:先确定你的显卡驱动支持的最高 CUDA 版本,然后去 PyTorch 官网的安装页面选择对应的组合。比如你的驱动支持 CUDA 11.8,那就选 CUDA 11.8 对应的 PyTorch 版本。不要盲目装最新版,最新版往往要求更新的驱动,而服务器上的驱动不一定能随便升级。
另一个常见误区是:很多人以为装了 CUDA Toolkit 才能用 GPU。实际上 PyTorch 的 pip 包和 conda 包里已经自带了 CUDA 运行时,你只需要显卡驱动足够新就行,不需要单独装完整的 CUDA Toolkit。这一点能省掉很多麻烦。
2.2 conda 还是 pip:不同场景下的选择逻辑
这个问题没有绝对答案,但可以根据你的使用场景来定。如果你需要管理多个 Python 环境,或者团队里有人用 conda 管理依赖,那用 conda 装 PyTorch 会更省心,因为 conda 会自动处理 CUDA 相关的依赖。如果你是在 Docker 容器里或者 CI 流程中,pip 通常更轻量、更快。
我个人的习惯是:本地开发机用 conda 创建独立环境,服务器上用 pip 配合虚拟环境。这样本地调试方便,部署时也不会因为 conda 的环境体积太大而拖慢镜像构建。
安装完成后,验证是否成功的方式很简单:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.device_count())如果torch.cuda.is_available()返回 False,先别急着怀疑安装有问题,大概率是驱动版本不够新,或者装的是 CPU 版本的包。这时候可以先用nvidia-smi看一下驱动支持的 CUDA 版本,再对照 PyTorch 官网的兼容表重新安装。
提示:如果你在 Windows 上开发,建议直接用 WSL2 或者 Linux 环境。Windows 原生环境下 PyTorch 的某些算子性能不如 Linux,而且多进程数据加载在 Windows 上限制较多。
3. 张量操作:把数据装进正确的容器
3.1 张量不只是多维数组
很多人把 PyTorch 的张量简单理解成 NumPy 数组的 GPU 版本,这个理解不算错,但不够全面。张量除了支持 GPU 加速之外,还多了两个关键能力:自动求导和计算图追踪。也就是说,张量不仅存数据,还记录了它是怎么算出来的。
创建张量的方式有很多种,常用的包括:
import torch # 从 Python 列表创建 a = torch.tensor([[1, 2], [3, 4]]) # 创建全零、全一、随机张量 b = torch.zeros(2, 3) c = torch.ones(2, 3) d = torch.randn(2, 3) # 从 NumPy 数组转换 import numpy as np e = torch.from_numpy(np.array([1, 2, 3])) # 创建指定形状的单位矩阵 f = torch.eye(3)这里有一个细节值得注意:torch.tensor()和torch.Tensor()是两个不同的东西。前者是根据传入的数据推断类型,后者是创建默认浮点类型的张量。实际写代码时建议统一用torch.tensor(),避免类型不一致导致的隐式转换问题。
3.2 形状操作是调试中最常打交道的部分
深度学习里大量的 bug 都跟张量形状有关。比如你定义了一个全连接层,输入维度是 784,结果传进去的张量形状是(batch, 28, 28),那就会直接报维度不匹配。所以熟练掌握形状操作是基本功。
常用的形状操作包括view、reshape、permute、transpose、squeeze、unsqueeze。其中view和reshape的区别经常被问到:view要求张量在内存中是连续的,而reshape会在必要时自动复制一份数据。所以如果你不确定张量是否连续,用reshape更安全,但view在连续张量上效率更高。
x = torch.randn(4, 3, 28, 28) # batch, channel, height, width # 展平成 (4, 3*28*28) x_flat = x.reshape(4, -1) # 调整维度顺序为 (4, 28, 28, 3) x_perm = x.permute(0, 2, 3, 1) # 去掉维度为 1 的轴 y = torch.randn(1, 3, 1, 5) y_squeezed = y.squeeze() # 变成 (3, 5)我在实际项目里养成了一个习惯:每写完一个网络层或者一个数据变换,就打印一下张量的 shape。这个习惯帮我省掉了大量排查时间。因为形状错误往往不会在出错的那一行报错,而是会在后面某一层才暴露出来,到时候回溯起来很麻烦。
3.3 广播机制:方便但容易埋雷
PyTorch 的广播机制和 NumPy 类似,允许不同形状的张量进行运算。比如一个(3, 1)的张量和一个(1, 4)的张量相加,结果会是(3, 4)。这个机制在写损失函数或者归一化的时候很方便,但也容易在不经意间产生错误的结果。
举个例子,你想把一个(batch, 1)的标签张量扩展成(batch, num_classes)的 one-hot 形式,如果不小心用了广播,可能会得到一个形状完全不对的结果,而且不会报错。所以我的建议是:在涉及形状变化的操作后面,显式地检查一下结果的 shape,不要完全依赖广播。
4. 自动求导:理解 backward 到底做了什么
4.1 计算图的构建与释放
PyTorch 的自动求导核心是autograd模块。当你对一个设置了requires_grad=True的张量做运算时,PyTorch 会记录下这些运算,构建一张计算图。调用backward()的时候,它会沿着这张图反向传播,计算每个叶子节点的梯度。
x = torch.tensor([2.0, 3.0], requires_grad=True) y = x ** 2 z = y.sum() z.backward() print(x.grad) # 输出 tensor([4., 6.])这里有一个非常重要的细节:每次调用backward()之后,计算图默认会被释放。如果你想让同一个计算图多次反向传播,需要设置retain_graph=True。但大多数情况下你不需要这么做,因为每个训练 step 都会重新前向传播构建新的图。
另一个常见问题是梯度累积。如果你在训练循环里忘记调用optimizer.zero_grad(),梯度会在多个 batch 之间累加,导致更新方向错误。这个坑几乎每个新手都会踩一次,而且症状不明显——loss 可能还在下降,但收敛速度明显变慢。
4.2 哪些操作会断开梯度
不是所有操作都会保留梯度信息。比如你把一个需要梯度的张量转成 NumPy 数组,或者用.item()取出标量值,梯度链就断了。再比如,如果你在计算过程中用了torch.no_grad()上下文管理器,那里面所有操作的梯度都不会被记录。
这个特性在推理阶段非常有用,因为不需要计算梯度可以节省大量显存和计算时间。但在训练阶段,如果你不小心在某个关键步骤用了no_grad,那部分参数就永远得不到更新,而且不会报错,只是训练效果变差。所以我在写训练代码时,会把no_grad严格限制在验证和推理部分。
# 训练阶段 model.train() for x, y in train_loader: optimizer.zero_grad() output = model(x) loss = criterion(output, y) loss.backward() optimizer.step() # 验证阶段 model.eval() with torch.no_grad(): for x, y in val_loader: output = model(x) # 只计算指标,不更新参数4.3 梯度裁剪与梯度消失的应对
在训练 RNN 或者深层网络时,梯度爆炸和梯度消失是常见问题。PyTorch 提供了torch.nn.utils.clip_grad_norm_来做梯度裁剪,用法很简单:
loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step()这里的max_norm需要根据具体任务调整。设得太小会导致梯度被过度压缩,模型学不动;设得太大又起不到防止爆炸的作用。我的经验是先从 1.0 开始试,观察梯度范数的变化再调整。
5. 模型定义:从 nn.Module 到实际网络结构
5.1 nn.Module 的约定与常见写法
PyTorch 里定义模型的标准方式是继承nn.Module,在__init__里声明层,在forward里定义前向传播逻辑。这个模式看起来简单,但有几个约定需要遵守。
首先,所有需要学习的参数必须通过nn.Parameter或者包含参数的层来定义,不能直接用普通张量。否则model.parameters()拿不到这些参数,优化器也就无法更新它们。其次,forward方法的输入输出建议保持张量形式,不要在中间转成 NumPy,否则会断开计算图。
import torch.nn as nn class SimpleNet(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super().__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_dim, output_dim) self.dropout = nn.Dropout(0.5) def forward(self, x): x = self.fc1(x) x = self.relu(x) x = self.dropout(x) x = self.fc2(x) return x这里有一个小技巧:把无参数的层(如 ReLU、Dropout)也定义在__init__里,而不是在forward里直接调用函数式 API。这样做的好处是模型结构更清晰,保存和加载时也更一致。当然,如果你需要动态调整某些行为,用函数式 API 也没问题。
5.2 层初始化:默认值不一定适合你的任务
PyTorch 的层都有默认的初始化方式,比如nn.Linear默认用均匀分布初始化权重。对于大多数任务来说,默认初始化够用,但在深层网络中,不合适的初始化会导致训练初期梯度消失或爆炸。
常见的做法是根据激活函数选择初始化方法。比如用 ReLU 作为激活函数时,Kaiming 初始化通常效果更好;用 tanh 或 sigmoid 时,Xavier 初始化更合适。PyTorch 提供了torch.nn.init模块来实现这些初始化:
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 = SimpleNet(784, 256, 10) model.apply(init_weights)model.apply()会递归地把函数应用到每个子模块上,这个模式在初始化、统计参数量、冻结部分层等场景下都很常用。
5.3 模型打印与参数量统计
调试模型结构时,直接print(model)可以看到每一层的定义和输出形状。但如果你想知道具体的参数量,需要自己写一个小函数:
def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) print(f"可训练参数量: {count_parameters(model)}")这个数字在评估模型复杂度、估算显存占用时很有参考价值。一般来说,参数量越大,模型容量越高,但也越容易过拟合,训练和推理的显存开销也越大。
6. 数据加载:Dataset 与 DataLoader 的配合
6.1 自定义 Dataset 的两种写法
PyTorch 的数据加载围绕两个类展开:Dataset和DataLoader。Dataset负责定义单条数据怎么取,DataLoader负责批量组装、打乱顺序、多进程加载。
自定义Dataset有两种常见写法。一种是继承torch.utils.data.Dataset,实现__len__和__getitem__;另一种是用TensorDataset直接包装张量。前者更灵活,适合数据来源复杂的情况;后者更简洁,适合数据已经全部加载到内存的场景。
from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, features, labels, transform=None): self.features = features self.labels = labels self.transform = transform def __len__(self): return len(self.labels) def __getitem__(self, idx): x = self.features[idx] y = self.labels[idx] if self.transform: x = self.transform(x) return x, y dataset = MyDataset(features, labels) loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)6.2 num_workers 设置多少合适
num_workers控制数据加载的进程数。设成 0 表示在主进程里加载,设成大于 0 的值会启动额外进程并行加载。这个参数对训练速度影响很大,但也不是越大越好。
我的经验是:先从 4 开始试,观察 GPU 利用率。如果 GPU 利用率经常掉到很低,说明数据加载是瓶颈,可以适当增加num_workers。但如果设得太大,进程间通信和内存开销反而会拖慢速度。在 Windows 上,num_workers大于 0 时需要注意把数据加载代码放在if __name__ == '__main__'保护块里,否则会报错。
另一个细节是pin_memory。当使用 GPU 训练时,把pin_memory=True可以让数据加载器把张量放在锁页内存中,从而加快 CPU 到 GPU 的传输速度。这个选项在显存充足的情况下建议开启。
6.3 数据增强放在哪里做
数据增强的位置有两种选择:放在Dataset.__getitem__里,或者放在训练循环里。前者更常见,因为可以和DataLoader的多进程加载配合,把增强的计算分散到多个进程。后者更灵活,适合需要根据训练进度动态调整增强策略的场景。
需要注意的是,验证集和测试集通常不做随机增强,只做必要的确定性变换(如归一化、resize)。所以我在定义Dataset时会传入不同的transform,训练集用带随机性的增强,验证集用确定性变换。
7. 训练循环:把各个组件串起来
7.1 标准训练循环的骨架
一个完整的训练循环包含前向传播、损失计算、反向传播、参数更新四个步骤。再加上日志记录、验证评估、模型保存等辅助逻辑。下面是一个比较通用的骨架:
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() output = model(x) loss = criterion(output, y) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(loader)这里有几个细节值得展开。model.train()和model.eval()会影响 Dropout 和 BatchNorm 的行为,训练时必须调用train(),验证时必须调用eval()。loss.item()把标量张量转成 Python 浮点数,避免把计算图带到累加变量里导致显存泄漏。
7.2 学习率调度与早停
学习率是训练中最重要的超参数之一。固定学习率往往不是最优的,常见做法是配合调度器动态调整。PyTorch 提供了torch.optim.lr_scheduler模块,常用的有StepLR、CosineAnnealingLR、ReduceLROnPlateau等。
from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device) scheduler.step() # 验证和保存逻辑早停是另一种防止过拟合的手段。基本思路是:如果验证集损失连续多个 epoch 没有下降,就停止训练。实现上可以维护一个最佳验证损失和计数器,每次验证后更新。
7.3 混合精度训练:省显存又提速
如果你的显卡支持混合精度(大多数较新的 NVIDIA 显卡都支持),可以用torch.cuda.amp来加速训练并减少显存占用。核心是用autocast上下文管理器自动把部分运算转成半精度,同时用GradScaler处理梯度缩放,避免半精度下的数值下溢。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() with autocast(): output = model(x) loss = criterion(output, y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()实测下来,混合精度通常能带来 1.5 到 2 倍的速度提升,显存占用也能减少三成左右。但要注意,某些算子在半精度下可能不稳定,如果遇到 loss 变成 NaN 的情况,可以先关掉混合精度排查。
8. GPU 加速与显存管理:把硬件用到位
8.1 设备选择与张量迁移
PyTorch 里所有张量和模型都需要显式地迁移到 GPU 上,不会自动完成。常见的写法是先定义device,然后在训练循环里把数据搬到设备上:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) x = x.to(device)这里有一个容易忽略的点:model.to(device)是原地修改模型并返回自身,而x.to(device)是返回一个新的张量。如果你写x.to(device)但没有赋值回去,那 x 还在 CPU 上。这个错误在刚上手时很常见,而且不会报错,只是训练速度极慢。
8.2 显存不够用的排查思路
显存不足是训练大模型时最常见的问题。排查思路可以按以下顺序来:
| 排查项 | 可能原因 | 应对方式 |
|---|---|---|
| batch size 过大 | 单次前向传播占用显存过多 | 减小 batch size,配合梯度累积 |
| 计算图未释放 | 在循环里累加了带梯度的张量 | 用.item()或.detach()断开 |
| 缓存未清理 | PyTorch 缓存分配器保留显存 | 调用torch.cuda.empty_cache() |
| 模型本身太大 | 参数量超出显存容量 | 用混合精度、梯度检查点或模型并行 |
其中梯度累积是一个很实用的技巧:用小 batch 跑多次前向传播,累加梯度后再更新一次参数,效果等价于大 batch,但显存占用小得多。
accumulation_steps = 4 optimizer.zero_grad() for i, (x, y) in enumerate(loader): output = model(x) loss = criterion(output, y) / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()8.3 多卡训练的两种模式
如果你有多张显卡,PyTorch 提供了两种并行方式:DataParallel和DistributedDataParallel。前者写法简单,一行代码就能用,但效率较低,而且只支持单进程多线程。后者效率高,是官方推荐的方式,但配置稍复杂,需要启动多个进程。
我的建议是:如果只是快速验证,用DataParallel够了;如果是正式训练,尤其是需要长时间跑的任务,直接上DistributedDataParallel。虽然后者配置麻烦一点,但省下来的训练时间完全值得。
9. 模型保存与加载:别让训练成果白费
9.1 state_dict 与整个模型的区别
PyTorch 保存模型有两种方式:保存整个模型对象,或者只保存state_dict。前者用torch.save(model, path),后者用torch.save(model.state_dict(), path)。官方推荐后者,因为state_dict只包含参数,不依赖模型类的定义,加载时更灵活,也不容易因为代码变动而失败。
# 保存 torch.save(model.state_dict(), 'model.pth') # 加载 model = SimpleNet(784, 256, 10) model.load_state_dict(torch.load('model.pth')) model.to(device) model.eval()加载时需要注意map_location参数。如果你在 GPU 上训练并保存,然后在 CPU 上加载,不加map_location会报错。写成torch.load('model.pth', map_location='cpu')或者map_location=device都可以。
9.2 保存训练状态以便恢复
如果你需要中断后继续训练,光保存模型参数不够,还需要保存优化器状态、当前 epoch、学习率调度器状态等。常见的做法是保存一个字典:
checkpoint = { 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': scheduler.state_dict(), 'best_val_loss': best_val_loss, } torch.save(checkpoint, 'checkpoint.pth')恢复时把这些状态逐一加载回去,就能从断点继续训练。这个机制在长时间训练任务里非常必要,因为谁也不想因为一次意外中断就从头再来。
9.3 加载预训练模型的常见坑
加载预训练模型时,最常见的问题是键名不匹配。比如你修改了模型结构,或者预训练模型是在DataParallel下保存的,state_dict的键会多一个module.前缀。这时候可以用load_state_dict的strict=False参数来忽略不匹配的键,或者手动处理键名:
state_dict = torch.load('pretrained.pth') new_state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()} model.load_state_dict(new_state_dict, strict=False)用strict=False时要小心,它会静默忽略不匹配的键,可能导致部分层没有加载到预训练权重而你不知道。所以加载后最好打印一下哪些键被忽略了,确认符合预期。
10. 几个让我印象深刻的踩坑经历
10.1 忘记 zero_grad 导致 loss 震荡
这个坑我在刚开始用 PyTorch 的时候踩过。当时写了一个训练循环,loss 曲线一直上下震荡,怎么调学习率都不管用。后来才发现是忘记在每次迭代开始时调用optimizer.zero_grad(),梯度在多个 batch 之间累加,导致更新方向完全乱了。加上这一行之后,loss 曲线立刻变得平滑。这个错误的隐蔽性在于它不会报错,只是训练效果变差,所以排查起来需要一定的经验。
10.2 在 Dataset 里做了耗时操作
有一次我写了一个自定义 Dataset,在__getitem__里做了一些比较耗时的数据预处理,比如读文件、解码图片。结果训练速度极慢,GPU 利用率一直在 20% 以下。后来把预处理提前到__init__里做,或者用num_workers并行加载,速度才提上来。这个经历让我意识到:__getitem__里应该只做轻量级的操作,重活要么提前做,要么交给多进程。
10.3 验证时忘记 eval 模式
Dropout 和 BatchNorm 在训练和验证时的行为不同。有一次我在验证时忘记调用model.eval(),Dropout 仍然在随机丢弃神经元,导致验证指标波动很大,误以为模型不稳定。加上model.eval()之后,验证指标立刻稳定下来。这个坑的教训是:训练和验证的切换一定要成对出现,train()和eval()要严格对应。
10.4 显存泄漏的排查过程
还有一次训练到一半突然 OOM,但 batch size 和模型都没变。排查后发现是在日志里累加了带梯度的 loss 张量,导致计算图一直没释放。改成loss.item()之后问题解决。这个经历让我养成了一个习惯:任何需要累加的标量,都先转成 Python 浮点数再累加,不要直接累加张量。
11. 从能跑到跑好:一些实用的进阶建议
11.1 用 TensorBoard 或 WandB 记录训练过程
光看终端输出的 loss 数字很难发现趋势性问题。用 TensorBoard 或者 WandB 把 loss、学习率、验证指标画成曲线,能直观地看到训练是否正常。PyTorch 提供了torch.utils.tensorboard模块,几行代码就能接入:
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('runs/experiment_1') writer.add_scalar('Loss/train', train_loss, epoch) writer.add_scalar('Loss/val', val_loss, epoch) writer.close()11.2 固定随机种子保证可复现
深度学习里有很多随机性来源:权重初始化、数据打乱、Dropout、数据增强等。如果你需要复现实验结果,固定随机种子是第一步:
import random import numpy as np 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需要注意的是,完全可复现往往会牺牲一些性能,因为cudnn.deterministic=True会禁用一些非确定性的加速算法。所以如果只是日常训练,不一定要开这个选项。
11.3 用 profiler 找到性能瓶颈
PyTorch 自带的 profiler 可以帮你定位训练中的性能瓶颈,比如哪个算子耗时最长、显存占用峰值在哪里:
from torch.profiler import profile, ProfilerActivity with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: train_one_epoch(model, loader, optimizer, criterion, device) print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))这个工具在优化训练速度时非常有用,能帮你把精力花在真正影响性能的环节上,而不是盲目调参。
11.4 保持代码模块化
最后一条经验是关于代码组织的。我见过很多训练脚本把所有逻辑塞在一个文件里,几百行下来很难维护。更好的做法是把数据加载、模型定义、训练循环、评估逻辑拆成独立的模块或函数。这样不仅方便调试,也方便复用。比如你可以把train_one_epoch和evaluate写成通用函数,换个模型和数据集照样能用。
PyTorch 的生态还在持续演进,新的特性和工具不断出现。但上面这些基础环节是相对稳定的,把它们的原理和细节吃透,后面无论遇到什么新东西,上手都会快很多。我在实际项目里最深的体会是:与其追求花哨的技巧,不如把数据加载、梯度管理、显存控制这些基本功做扎实,训练效率和模型效果自然就上来了。