
这套教程我前前后后刷了两遍第一遍跟着敲代码第二遍才算是真正理解了里面那些“为什么”。今天把上半部分的笔记整理出来梳理从环境搭建到模型保存的完整脉络重点放在Tensor创建、自动求导、线性回归案例、数据加载和TensorBoard可视化这几个核心环节上。整理过程中会把教程里一笔带过的关键点展开讲透也会补充一些我在实际运行时踩过的坑。如果你正准备入门PyTorch或者已经跟着教程学了一半但感觉知识点还是散的这份笔记应该能帮你把前后的逻辑串起来。内容围绕小土堆教程的核心脉络展开以PyTorch基础操作为主提供可直接运行的代码示例和参数说明。1. 先搞清楚教程在讲什么从宏观视角把握PyTorch的学习主线1.1 教程的知识脉络拆解小土堆的PyTorch教程之所以适合入门核心原因在于对学习顺序的安排非常符合认知规律先建立对数据的基本操作能力再理解自动求导这一核心机制接着用一个小案例把前面的知识点串联起来最后补上数据加载、可视化和模型存取这些工程化必备技能。教程的前半部分实际上围绕一条主线展开数据用什么结构表示、数据如何参与计算并自动求导、模型如何训练、训练过程中的数据从哪里来、训练结果如何保存。这五个问题对应的就是Tensor、autograd、损失函数与优化器、Dataset/DataLoader、模型序列化这几个知识点。我第一遍学习时最大的问题是只顾着抄代码没有抬头看路线图。等到学完Tensor的操作后直接跳到线性回归发现代码能跑通但逻辑是断的——为什么weight需要设置requires_gradTrue为什么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])从现有数据创建最常用会自动推断dtypetorch.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与deviceTensor的两个关键属性dtype表示Tensor的数据类型device表示Tensor所在的设备。这两个属性决定了Tensor计算的正确性和性能。关于dtypePyTorch默认的浮点类型是torch.float32但不同场景下可能需要不同的精度。模型推理时经常使用torch.float16来减少显存占用而科学计算中可能需要torch.float64来保证精度。如果一个float32的Tensor和一个float64的Tensor直接运算会抛出类型不匹配的错误。解决方法是提前统一类型tensor.float()或tensor.double()。关于deviceTensor在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_gradTrue的叶子节点才会保存梯度。如果某个中间变量没有设置该属性它的梯度不会被保留。实际调试中排查梯度为None的问题时首先检查requires_grad是否正确设置。3.2 requires_grad与detach()的正确理解requires_grad是一个控制是否参与自动求导的属性。在模型训练中需要更新的参数必须设置requires_gradTrue而输入数据通常不需要求导。PyTorch中定义的模型参数默认requires_gradTrue而用户自行创建的Tensor默认为False。有时我们需要将某个已经参与计算但不再需要梯度的Tensor从计算图中分离出来这时候要使用detach()。detach()返回一个与当前Tensor共享数据但不再追踪计算的新Tensor在固定某些层的参数或计算某个指标如验证集上的准确率时非常常用。如果不调用detach()在验证阶段仍然把Tensor留在计算图中会导致内存不断累积运行一段时间后内存溢出。教程中的线性回归示例虽然没有用到detach()但理解这个操作对后续学习迁移学习、模型微调等场景至关重要。例如在冻结预训练模型的特征提取层时就是通过设置requires_gradFalse或使用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], lr0.01)等价于将w作为优化器要更新的参数列表。如果模型有多个参数可以写成model.parameters()它会自动返回所有可训练参数。运行完整训练过程后可以发现随着epoch数增加损失值逐渐下降权重w不断逼近目标值2.0。这说明梯度下降算法在学习过程中确实在起作用。如果损失值没有下降趋势需要优先检查梯度是否正常、学习率是否过大。小土堆教程的建议是将整个训练过程进行可视化这也是后续要等到学习TensorBoard时才真正理解的部分。5. 数据加载与可视化Dataset、DataLoader与TensorBoard5.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是否重新打乱数据顺序。训练集需要设置shuffleTrue防止模型学习到数据中出现顺序性偏差验证集和测试集则不需要打乱保持原始顺序即可。num_workers表示数据加载使用的子进程数。设置为大于0的值可以加速数据加载过程尤其是在数据文件较大时效果明显。但num_workers也不是越大越好因为进程之间频繁切换也会消耗资源。建议从2到4起步根据实际吞吐量来调整。5.3 TensorBoard可视化从标量到图像的完整流程TensorBoard是训练过程可视化的利器。小土堆教程中推荐的SummaryWriter是一个高级封装简化了使用流程。同时也要理解底层对应的add_scalar和add_image方法。add_scalar用于记录单个数值随训练步数的变化比如loss和accuracy的曲线。使用方法需要提供标签名、数值值和当前步数。add_image则需要传入图像数据需要注意图像的数据格式。TensorBoard启动后在命令行中执行tensorboard --logdirlogs命令即可官方指定的日志目录通常在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的入门其实没有想象中那么难。