这套教程我前前后后刷了两遍,第一遍跟着敲代码,第二遍才算是真正理解了里面那些“为什么”。今天把上半部分的笔记整理出来,梳理从环境搭建到模型保存的完整脉络,重点放在Tensor创建、自动求导、线性回归案例、数据加载和TensorBoard可视化这几个核心环节上。整理过程中会把教程里一笔带过的关键点展开讲透,也会补充一些我在实际运行时踩过的坑。
如果你正准备入门PyTorch,或者已经跟着教程学了一半但感觉知识点还是散的,这份笔记应该能帮你把前后的逻辑串起来。内容围绕小土堆教程的核心脉络展开,以PyTorch基础操作为主,提供可直接运行的代码示例和参数说明。
1. 先搞清楚教程在讲什么:从宏观视角把握PyTorch的学习主线
1.1 教程的知识脉络拆解
小土堆的PyTorch教程之所以适合入门,核心原因在于对学习顺序的安排非常符合认知规律:先建立对数据的基本操作能力,再理解自动求导这一核心机制,接着用一个小案例把前面的知识点串联起来,最后补上数据加载、可视化和模型存取这些工程化必备技能。
教程的前半部分实际上围绕一条主线展开:数据用什么结构表示、数据如何参与计算并自动求导、模型如何训练、训练过程中的数据从哪里来、训练结果如何保存。这五个问题对应的就是Tensor、autograd、损失函数与优化器、Dataset/DataLoader、模型序列化这几个知识点。
我第一遍学习时最大的问题是只顾着抄代码,没有抬头看路线图。等到学完Tensor的操作后直接跳到线性回归,发现代码能跑通但逻辑是断的——为什么weight需要设置requires_grad=True,为什么loss.backward()之后weight.grad就有值了,这些关键的衔接点都需要理解计算图才能串起来。所以这份笔记会用比较大的篇幅把计算图的逻辑讲清楚。
1.2 环境准备阶段的注意事项
小土堆教程中环境搭建部分相对简略,但这一步恰恰是新手最容易被卡住的地方。PyTorch的安装需要根据本机情况选择CPU版本还是GPU版本,判断依据是显卡的型号和驱动支持的CUDA版本。如果安装的版本与CUDA不匹配,运行时会直接提示找不到可用的GPU。
我自己测试下来的经验是,入门阶段完全可以用CPU版本完成教程中所有代码实践,因为上半部分的模型和数据量都非常小,CPU训练耗时在秒级甚至毫秒级。安装命令会根据官方给出的指引选择对应的pip或conda命令即可,推荐使用conda创建独立环境,这样可以避免和系统Python环境中的其他包发生依赖冲突。
安装成功后,建议在Python环境中执行一次验证,确认PyTorch能够正常导入且版本信息正确。如果安装了GPU版本,还需要确认CUDA是否可用。
import torch print(torch.__version__) print(torch.cuda.is_available())这一步输出的结果可以帮你快速定位问题:CPU版本显示CUDA不可用是正常现象;GPU版本如果显示False,就要检查驱动和PyTorch版本之间的匹配关系了。我遇到过的情况是显卡驱动版本过旧导致识别不了,更新驱动后问题就解决了。
2. Tensor基础操作:创建、属性与转换,每一个细节都有讲究
2.1 Tensor的创建方式与适用场景
Tensor是PyTorch中最基础的数据结构,可以理解为一个支持GPU计算和自动求导的多维数组。教程中介绍了多种创建方式,我按照使用频率从高到低整理成表格:
| 创建方式 | 适用场景 | 注意事项 |
|---|---|---|
| torch.tensor([1, 2, 3]) | 从现有数据创建,最常用 | 会自动推断dtype |
| torch.Tensor([1, 2, 3]) | 从现有数据创建 | 行为与torch.tensor有差异 |
| torch.rand(2, 3) | 生成0到1之间的均匀分布随机数 | 可作为初始化权重 |
| torch.randn(2, 3) | 生成标准正态分布随机数 | 权重的常用初始化方式 |
| torch.zeros(2, 3) | 生成全0矩阵 | 可作为偏置初始值 |
| torch.arange(0, 10, 2) | 生成等差序列 | 常用于生成标签或索引 |
| torch.from_numpy(np_array) | NumPy数组转Tensor | 两者共享内存,注意副本问题 |
这里有一个新手容易搞混的地方:torch.tensor和torch.Tensor的行为差异。torch.tensor是一个函数,会根据传入的数据自动推断数据类型,而torch.Tensor是一个类构造函数,接收shape参数时生成未初始化的空张量。比如torch.Tensor(3)生成的是包含3个元素但数值不确定的Tensor,而torch.tensor(3)生成的是标量3。这个坑我在刚开始使用时踩到过,用torch.Tensor(3)想创建一个3元素的序列,结果拿到的是一堆随机值。
实际训练中,权重初始化的选择会直接影响模型的收敛情况。教程中的线性回归案例因为模型简单,直接初始化也能收敛,但在更复杂的网络中,合理的初始化策略能显著加快训练速度。torch.nn.init模块中提供了多种初始化方法,比如xavier_uniform和kaiming_normal,这些在实际项目中非常常用。
2.2 dtype与device:Tensor的两个关键属性
dtype表示Tensor的数据类型,device表示Tensor所在的设备。这两个属性决定了Tensor计算的正确性和性能。
关于dtype,PyTorch默认的浮点类型是torch.float32,但不同场景下可能需要不同的精度。模型推理时经常使用torch.float16来减少显存占用,而科学计算中可能需要torch.float64来保证精度。如果一个float32的Tensor和一个float64的Tensor直接运算,会抛出类型不匹配的错误。解决方法是提前统一类型:tensor.float()或tensor.double()。
关于device,Tensor在CPU和GPU之间的移动通过to方法完成。GPU上的计算速度快很多,但CPU和GPU之间的数据拷贝开销也不能忽略。一个典型的性能陷阱是:在训练循环内反复调用data.to(device)和.cpu(),导致大量时间浪费在数据传输上。正确做法是在循环外完成数据搬运。
我在入门时还踩过一个关于device的坑:模型在GPU上,输入数据在CPU上,直接前向传播会报错。判断当前Tensor在哪个设备上,可以用tensor.device属性查看。小土堆教程里的例子默认使用CPU,所以很多初学者没有意识到device管理的重要性——但一旦接触真实项目,大部分情况下都会用到GPU,提前养成检查device的习惯会省掉很多排查时间。
2.3 Tensor与NumPy的转换细节
在数据预处理阶段,Tensor和NumPy数组之间的转换非常频繁。torch.from_numpy()和tensor.numpy()是两种方向的转换方式。需要注意的一点是,如果CPU上的Tensor和NumPy数组共享底层内存,那么对一个数据的修改会影响另一个。这在教程的示例中不常显式指出,但实际处理数据时容易踩坑。
例如,对一个NumPy数组执行in-place操作后,对应的Tensor也会同步变化。想切断这种共享关系,最简单的方法是克隆一份独立数据。GPU上的Tensor则不能直接转成NumPy,需要先调用.cpu()移动到CPU上再做转换。
另外一个小坑是数据类型的隐式转换。整数类型的NumPy数组转成Tensor后,在参与某些运算时可能与预期不符。比如两个整数型Tensor相除,结果会被截断为整数而不是浮点数。这个问题在计算损失函数时尤其隐蔽——损失值变成整数会导致梯度计算异常,但程序本身不会报错。
3. 自动求导机制:PyTorch真正强大之处
3.1 计算图与backward()的关系
自动求导是PyTorch区别于NumPy的核心能力。实现自动求导的基础是计算图——PyTorch会在Tensor的运算过程中动态记录操作序列,构建一个有向无环图。
当执行loss.backward()时,PyTorch会沿着计算图从输出反向传播,根据链式法则计算每个叶子节点的梯度。叶子节点指的是用户自行创建、不依赖其他Tensor计算的节点。例如在y = w * x这个表达式中,w和x就是叶子节点,它们的梯度在backward()调用后会被存储到对应的.grad属性中。
一个需要特别注意的点:只有设置了requires_grad=True的叶子节点才会保存梯度。如果某个中间变量没有设置该属性,它的梯度不会被保留。实际调试中,排查梯度为None的问题时,首先检查requires_grad是否正确设置。
3.2 requires_grad与detach()的正确理解
requires_grad是一个控制是否参与自动求导的属性。在模型训练中,需要更新的参数必须设置requires_grad=True,而输入数据通常不需要求导。PyTorch中定义的模型参数默认requires_grad=True,而用户自行创建的Tensor默认为False。
有时我们需要将某个已经参与计算但不再需要梯度的Tensor从计算图中分离出来,这时候要使用detach()。detach()返回一个与当前Tensor共享数据但不再追踪计算的新Tensor,在固定某些层的参数或计算某个指标(如验证集上的准确率)时非常常用。如果不调用detach(),在验证阶段仍然把Tensor留在计算图中,会导致内存不断累积,运行一段时间后内存溢出。
教程中的线性回归示例虽然没有用到detach(),但理解这个操作对后续学习迁移学习、模型微调等场景至关重要。例如在冻结预训练模型的特征提取层时,就是通过设置requires_grad=False或使用detach()来避免不必要的梯度计算,从而节省显存和计算时间。
3.3 梯度累积与梯度清零
训练循环中,一个极其容易犯的错误是忘记调用optimizer.zero_grad()。PyTorch的设计是梯度默认累积的,也就是说每次调用backward()时,新的梯度值会累加到已有的梯度上。
为什么PyTorch要采用累积的设计?这个设计在某些场景下是有意为之的特殊用法——当显存不足无法使用大批量时,可以分多次小批量计算并把梯度累加,模拟一次大批量训练的效果。但对普通的训练循环来说,在每次反向传播前不清零,梯度值就会不断叠加,导致参数更新方向失真,最终模型不会收敛。
教程中的训练循环写法是关键操作点,每次loss.backward()之后、执行optimizer.step()更新参数之前,必须先执行optimizer.zero_grad()来清空梯度。建议在编写训练循环时固定一个习惯:zero_grad在forward之前调用或backward之后调用都行,但必须保证每个batch只做一次有效的梯度更新。
这里补充一个排查技巧:如果训练过程中损失值来回震荡且幅度很大,第一步就应该检查是否漏掉了zero_grad。我曾经遇到过类似问题,模型一直不收敛,检查了很久才发现是梯度累积导致的,清除一行代码之后训练立刻恢复了稳定。这种低级错误排查起来非常耗时间,建议在一开始写训练循环时就把结构固定下来,避免反复踩坑。
4. 完整案例:用PyTorch实现一个线性回归模型
4.1 数据准备:为什么需要Tensor格式
教程中的线性回归案例使用了小型数据集,输入特征和标签分别是1.0到3.0以及对应的2.0到6.0。目标是通过梯度下降学习出权重w,使得预测值y_hat尽可能接近真实值y。
数据准备的第一步是将Python列表转换成Tensor。这一步看似简单,但背后有一个重要的原因:只有Tensor才能参与PyTorch的自动求导机制。如果直接使用Python原生列表或NumPy数组参与运算,虽然可以完成数值计算,但无法获得每个参数的梯度,也就无法用梯度下降来更新参数。
在实际项目中,数据量往往远大于教程中的数值规模,一次处理一个样本或一个batch是非常必要的。掌握数据转换成Tensor这一步,是后续所有训练任务的基础操作。需要注意的是,转换后的Tensor中,如果希望模型参数能被更新,其requires_grad必须为True;如果是输入数据的Tensor,则保持默认的False即可。
4.2 模型定义与损失函数的选择
教程中的线性回归模型只有权重w一个参数,使用y_hat = x * w作为前向计算。在更一般的情况下,模型通常包含多个参数,因此推荐用torch.nn.Parameter来定义可训练的权重。
对于线性回归任务,损失函数最常用的选择是均方误差(MSE),即预测值与真实值之差的平方的平均值。这个函数的特点是连续可微,方便反向传播时计算梯度。
PyTorch中已经内置了大量损失函数,包括均方误差损失、交叉熵损失等。在训练分类任务和回归任务时选择完全不同,需要根据任务类型来决定。教程中的线性回归是回归任务,所以使用均方误差是合适的。
计算MSE时要注意损失值的量级对学习率选择的影响。如果损失值非常大而学习率设置得比较大,梯度更新时参数可能会发散。小土堆教程中给出的学习率设置是0.01,这个值对于当前的小数据集是可以正常工作的。换成其他数据集时,需要根据损失值的量级调整学习率。
4.3 优化器SGD与训练循环
优化器的选择直接决定了训练能否收敛以及收敛速度。SGD(随机梯度下降)是最基础的优化器,它根据当前梯度直接更新参数。PyTorch提供了多种优化器,包括带动量的SGD、自适应学习率的Adam等。
教程中使用SGD作为优化器,因为它的逻辑最清晰,适合理解梯度下降的核心思想。在训练循环中,每个epoch的流程如下:
- 调用优化器清零梯度
- 计算预测值
- 计算损失值
- 反向传播计算梯度
- 根据梯度更新参数
这里需要额外理解的一个细节是optimizer和模型参数之间的关系。当我们创建优化器时,需要把模型的参数传入,例如optimizer = torch.optim.SGD([w], lr=0.01)等价于将w作为优化器要更新的参数列表。如果模型有多个参数,可以写成model.parameters(),它会自动返回所有可训练参数。
运行完整训练过程后可以发现,随着epoch数增加,损失值逐渐下降,权重w不断逼近目标值2.0。这说明梯度下降算法在学习过程中确实在起作用。如果损失值没有下降趋势,需要优先检查梯度是否正常、学习率是否过大。小土堆教程的建议是将整个训练过程进行可视化,这也是后续要等到学习TensorBoard时才真正理解的部分。
5. 数据加载与可视化:Dataset、DataLoader与TensorBoard
5.1 Dataset的自定义方法:三个方法缺一不可
在真实项目中,数据往往不是一次性加载到内存的,而是以文件或数据库的形式存在磁盘上。PyTorch提供了Dataset类来统一管理数据的索引和读取逻辑。
自定义Dataset类需要继承torch.utils.data.Dataset,并实现三个核心方法:__init__方法用于初始化数据路径和标签信息;__len__方法返回数据集的总样本数;__getitem__方法根据索引返回一个样本及其对应的标签。
需要注意的是__getitem__方法通常会同时执行数据预处理操作,比如归一化、数据增强、转为Tensor等。教程中的例子比较简单,实际项目中瓶颈经常发生在数据读取环节,尤其是当数据存储在磁盘上时,读写速度可能成为性能瓶颈。
补充一个常见的错误:在__getitem__中做过多计算,导致每个epoch都重复执行数据处理流程,拖慢了整体训练速度。建议把与样本无关的、可复用的预处理结果放在__init__阶段完成,而把与单个样本强相关的操作放在__getitem__中。
5.2 DataLoader的参数选择:batch_size与shuffle的取舍
Dataset定义了数据的读取方式,DataLoader则负责在训练时按批次取数据,并提供多线程加载等能力。DataLoader的核心参数包括batch_size、shuffle和num_workers。
batch_size表示每个批次包含的样本数。如果batch_size太大,训练过程可能占用大量内存且收敛缓慢;如果太小,梯度更新频繁,训练震荡会更明显。在深度学习实践中,batch_size的选择通常取决于显存容量和数据特性,常见取值包括32、64、128等。
shuffle参数表示每个epoch是否重新打乱数据顺序。训练集需要设置shuffle=True,防止模型学习到数据中出现顺序性偏差;验证集和测试集则不需要打乱,保持原始顺序即可。
num_workers表示数据加载使用的子进程数。设置为大于0的值可以加速数据加载过程,尤其是在数据文件较大时效果明显。但num_workers也不是越大越好,因为进程之间频繁切换也会消耗资源。建议从2到4起步,根据实际吞吐量来调整。
5.3 TensorBoard可视化:从标量到图像的完整流程
TensorBoard是训练过程可视化的利器。小土堆教程中推荐的SummaryWriter是一个高级封装,简化了使用流程。同时也要理解底层对应的add_scalar和add_image方法。
add_scalar用于记录单个数值随训练步数的变化,比如loss和accuracy的曲线。使用方法需要提供标签名、数值值和当前步数。add_image则需要传入图像数据,需要注意图像的数据格式。
TensorBoard启动后,在命令行中执行tensorboard --logdir=logs命令即可,官方指定的日志目录通常在8000端口监听。浏览器打开后看到的第一个页面是Scalars标签页,里面展示了记录的所有标量曲线。
在学习笔记中补充说明一个问题:TensorBoard保存的日志文件会占用磁盘空间,尤其是记录的标量过多或图像过大时。建议定期清理无用的日志目录,或者使用exp_name参数把不同实验的日志分目录存放,方便后续对比。
6. 模型保存与加载:常用的两种方式对比
6.1 state_dict:模型的本质参数集合
训练完成后,模型的参数以state_dict形式保存。state_dict是一个字典,其中键是层的名称,值是对应的参数Tensor。保存模型参数使用model.state_dict()方法获取这个字典,然后通过torch.save()写入磁盘文件。
存储state_dict而不是整个模型,是目前更推荐的保存方式。原因是它只保存参数而不保存模型结构,文件体积更小,并且在不同环境之间的迁移性更好,未来修改模型结构时也能更灵活地加载部分参数。
教程中的示例模型参数只有一个权重w,对应state_dict中的键为weight。对于复杂的网络结构,state_dict的键会包含多个层的参数,比如"conv1.weight"和"fc.bias"这样的命名模式。
6.2 模型加载的两种场景与注意事项
模型加载通常是推理或继续训练的第一步。整体的加载流程是先根据网络结构创建一个新的模型实例,然后调用load_state_dict方法把文件中的参数复制到模型里。加载后还需要调用model.eval()切换到推理模式,这一步经常被遗忘。
在推理模式下,模型中的dropout层和batch normalization层的行为会发生变化。如果不调用eval(),模型在推理时可能仍然使用训练模式下的行为,导致结果不一致。同时,在继续训练的场景下,需要在加载参数后调用model.train()切回训练模式。
torch.load()方法默认加载整个保存文件,其中既包含模型参数也可能包含其他辅助信息。如果你保存时使用的是torch.save(model.state_dict(), path),加载时就需要使用model.load_state_dict(torch.load(path))。如果保存的是整个模型对象,加载方式会不同,但因为保存整个模型容易导致网络结构耦合、兼容性差,不推荐这种方式。
教程中提供了一个简单的加载示例,核心逻辑是先用一个相同的模型结构去接收参数。这里最容易出错的地方是模型结构和state_dict中的键名必须完全匹配。如果在模型定义中修改了某层的名称,加载时会提示缺少对应键。解决方法是让保存模型和加载模型的结构保持一致,避免临时改动。
7. 踩过的坑和一条更高效的学习路径
7.1 上半部分最容易出错的三个问题
回顾整个教程学习过程中,我发现有三类问题是最消耗时间的。把它们单独列出来仔细说明:
第一类问题是dtype不匹配。在数据预处理中,我把某一列数据保持了整型,后续做除法时得到了截断的结果,导致loss不降反升。排查方式是在关键计算节点打印tensor.dtype,确认所有参与计算的张量类型一致。
第二类问题是device不匹配。当你在GPU环境下运行代码时,模型、输入数据、损失函数内部的计算,必须全部存在于同一个设备上。最稳妥的方式是定义模型和数据时都指定device,或者用统一的to(device)方法转移到同一设备。遇到RuntimeError提示"Expected all tensors to be on the same device"时,先在代码里用print语句检查每个关键Tensor的device属性,比盲目改代码要快得多。
第三类问题是训练循环中优化器状态的错误使用。如果漏了zero_grad,梯度会不断累积,loss曲线呈周期性震荡或持续发散。排查这个问题的快速方法是打印每个batch后的weight.grad值,如果grad一直在增大而不是变化方向,基本可以确认是梯度累积。
7.2 后续内容展望与学习方法建议
小土堆教程的下半部分会进入更复杂的模型结构、数据增强技巧和实战项目演练。如果上半部分的基础没有打牢,下半部分学习时很容易被各种API的具体细节绊住脚。
我的建议是在开始下半部分之前,先自行完成两个小任务:一是用手写数字识别数据集训练一个简单分类模型,熟练运用本文提到的Dataset、DataLoader、TensorBoard和模型存取这套完整流程;二是尝试修改线性回归案例中的模型结构和优化器,观察不同设置对训练效果的影响。
这种尝试式的学习方法比单纯跟着教程敲代码更有收获。教程的价值在于帮助你建立知识框架,但真正内化知识的过程必然包含实验、报错、排查、修正这一系列完整的实践流程。
写在最后
这篇文章整理了小土堆PyTorch教程上半部分的核心笔记,侧重于对关键技术点的展开说明和踩坑记录。我自己在整理过程中也有不少新的收获,比如之前一直没搞清楚的梯度累积问题,现在再看就明白了设计的初衷,也理解了为什么PyTorch官方要保留这样一个容易让新手出错的行为模式。
如果你正在学这套教程,我的建议是不要一味追求速度。每学完一个章节就结合本文梳理的要点,回顾一下代码中哪些参数是关键,哪些API是高频操作。等学完了在下半部分所涉及的内容,你会发现在回头理解前半部分设计的意图,比单纯地背诵代码要有效得多。上面的步骤只要踏踏实实执行一遍,你会发现PyTorch的入门其实没有想象中那么难。