
第 22 章 第一次使用 PyTorch文章目录第 22 章 第一次使用 PyTorch 关于《AI 零基础 36 讲》1. 先建一个单独环境2. 张量与 NumPy 数组的联系3. 先复现上一章的自动求导4. 梯度需要每轮清除5. 数据、模型、损失与优化器6. 一个最小训练循环7. 查看完整 CPU 训练结果8. 训练模式、评估模式与设备9. 常见卡点与独立练习上一章用 NumPy 逐项计算梯度九个参数还能手动核对。参数更多时可以让 PyTorch 记录计算关系并自动求导。我们仍然需要提供数据、定义模型、选择损失和执行更新框架会帮助计算每个参数怎样影响损失。本章先让 PyTorch 算出与第 21 章相同的梯度再拟合一组带少量噪声的直线数据。你会得到能在 CPU 上运行的训练脚本、损失曲线、已执行 Notebook 和保存的参数。请先完成第 21 章并熟悉数组形状、平方损失与参数更新。 关于《AI 零基础 36 讲》零基础也能开始学 AI《AI 零基础 36 讲》是一套面向初学者的 AI 入门教程。课程从认识和使用 AI 开始逐步学习Python、数据处理、机器学习与深度学习再进入大模型应用开发最后完成个人知识库助手的综合实践。本篇是正式讲解文章通过具体例子、原创图示和操作结果讲清概念。涉及编程的章节提供可运行代码方便你边读边试。读完以后动手检验建议按专栏顺序学习并完成对应的课后训练检查自己能否独立运用本章知识。博客专栏《AI 零基础 36 讲》1. 先建一个单独环境从本章目录运行。CPU 版适用于本次验证使用的 Windows也提供 Linux 的相应安装包不要求显卡。py-3.12-m venv.venv.\.venv\Scripts\python.exe-m pip install-r requirements-cpu.txt.\.venv\Scripts\python.exe-m pip install-r requirements.txt.\.venv\Scripts\python.exe-cimport torch; print(torch.__version__); print(torch.cuda.is_available())本次输出版本为2.9.0cpuCUDA 可用状态为False。这是 CPU 安装的正常结果。requirements-cpu.txt使用 PyTorch 官方 CPU 软件源其他依赖放在requirements.txt。两份文件都要安装不能只安装 NumPy 和绘图库就开始训练。macOS 不使用这里的cpu包名应在自己的环境安装torch2.9.0其余依赖相同。平台与版本安装规则见官方历史版本页面。打开 Notebook 时再安装requirements-notebook.txt并选择本章.venv解释器。如果编辑器显示的解释器与安装时不同先更正环境重复安装到另一个环境不能解决当前 Notebook 的导入问题。2. 张量与 NumPy 数组的联系张量在本章可以理解为带形状、数值类型和设备信息的多维数组。向量是一维张量表格可以表示为二维张量。PyTorch 还能追踪相关运算为后面求导提供信息。importnumpyasnpimporttorch arraynp.array([1.0,2.0],dtypenp.float32)tensortorch.from_numpy(array)print(tensor.shape,tensor.dtype,tensor.device)tensor[0]9.0print(NumPy 数组,array)independenttensor.clone()independent[0]3.0print(原张量,tensor)形状为torch.Size([2])类型为torch.float32设备为cpu。修改张量后数组第一项也变成 9。torch.from_numpy与原数组共享内存修改一边会影响另一边这一点与官方说明一致。clone()创建独立副本改副本后原张量仍为[9, 2]。共享内存可以避免不必要的复制但你要知道当前修改会波及哪一份数据。需要保留原始输入时先复制再改动。数值类型也有区别。torch.tensor([[1], [2]])默认得到整数张量本章线性层参数默认是 32 位浮点数直接相乘会报类型不一致。用于回归输入时明确写dtypetorch.float32或用.float()转换。3. 先复现上一章的自动求导自动求导会按实际执行的运算建立计算关系并在反向阶段求梯度。requires_gradTrue表示需要追踪这个张量参与的求导计算叶子参数的梯度通常写入.grad。它不会替我们选择正确目标或保证数据划分合理。先调用本章的教学入口。fromexamples.torch_coreimportautograd_demo resultautograd_demo()print(预测与损失,result[prediction],result[loss])print(W1 梯度,result[gradients][W1])print(W2 梯度,result[gradients][W2])print(输出偏置梯度,result[gradients][b2])预测与损失为5.5 2.25W1 梯度为[[6, 0], [12, 0]]W2 梯度为[7.5, 0]输出偏置梯度为 3与第 21 章手算一致。这个核对保留了输入、权重和行列含义只有求梯度的方式改变。函数先计算预测再计算平方损失最后调用loss.backward()。backward()从损失往回处理相关计算将参数梯度累积到.grad。此处使用 64 位浮点数以便核对手算。正式直线拟合使用 32 位浮点数它在这组小实验中已经足够不需要因为手算示例用了 double 就把所有训练都改成 double。4. 梯度需要每轮清除同一个参数连续执行两次反向计算默认会把两次梯度加起来。用参数 w 等于 2损失(3w - 1)²导数为2 × (3w - 1) × 3 30。importtorch wtorch.tensor(2.0,requires_gradTrue)for_inrange(2):(w*3-1).square().backward()print(累计两次,w.grad.item())w.gradNone(w*3-1).square().backward()print(清除后再计算,w.grad.item())输出分别为 60 和 30。这里没有更新 w所以每次单独计算的梯度都是 30。两次累积不是框架随机出错程序需要明确选择是否保留上一轮梯度。本章每轮使用全部训练数据不需要跨轮累积因此在前向计算前调用optimizer.zero_grad(set_to_noneTrue)。它清除优化器管理的参数梯度。真正需要梯度累积的训练会另有批次与缩放规则本章先按每轮清除的方式跑通。5. 数据、模型、损失与优化器本章生成 41 个 x从 -1 到 1按y 2x 1 噪声得到标签。噪声由固定种子 42 的随机数生成标准差设为 0.05。这是我们安排的教学关系程序会导出全部数值和划分见数据说明。固定划分 32 条训练、9 条测试。测试标签不进入参数更新也没有在每轮根据测试分数调整参数。本章目标是把训练循环写明白学习率 0.1 与轮数 120 在运行前固定。九条测试只描述同一生成范围的小实验不支持范围外的外推。模型采用torch.nn.Linear(1, 1)接收每条记录的一个输入输出一个数内部有一个权重和一个偏置。这里用线性模型练习框架模型本身还没有隐藏层后面图片分类时再增加层与激活。一批 32 条输入的形状为(32, 1)目标与预测也都应为(32, 1)。批次就是一次一起交给模型的一组记录。训练轮次表示对训练数据完成一轮处理本章每轮只有这一个完整批次所以一轮恰好更新一次。损失使用MSELoss默认先计算每个对应数值的平方误差再取平均。本章每条仅一个目标因此就是 32 条平方误差的均值。优化器使用 SGD可以理解为按学习率和梯度调整参数的组件。6. 一个最小训练循环下面是一段可独立运行的无噪声训练例子使用三条原创教学记录方便直接看到输入与目标。importtorch torch.manual_seed(42)xtorch.tensor([[-1.0],[0.0],[1.0]])ytorch.tensor([[-1.0],[1.0],[3.0]])modeltorch.nn.Linear(1,1)loss_fntorch.nn.MSELoss()optimizertorch.optim.SGD(model.parameters(),lr0.1)model.train()forepochinrange(120):optimizer.zero_grad(set_to_noneTrue)predictionmodel(x)lossloss_fn(prediction,y)loss.backward()optimizer.step()model.eval()withtorch.no_grad():print(预测,model(x).ravel().tolist())print(最终损失,loss_fn(model(x),y).item())末次三个预测分别接近 -1、1、3损失接近零。前向计算、损失、反向求梯度、优化器更新这几个动作有先后依赖不能先更新再计算梯度。model.parameters()将模型中需要训练的参数交给优化器。loss.backward()负责求梯度optimizer.step()负责更新两个动作分别执行。写循环时若只调用反向而遗漏更新参数会保持原值。正文这个三条无噪声例子与完整 41 条带噪声实验分开记录。前者只帮助读懂循环后者用于导出损失曲线和固定测试结果不能混用它们的分数。7. 查看完整 CPU 训练结果运行章节主程序会使用前面定义的 41 条数据。.\.venv\Scripts\python.exe examples/run_training.py--device cpu.\.venv\Scripts\python.exe-m unittest discover-s tests-v本次 CPU、单计算线程120 轮训练含模型与优化器准备约用 1.20 秒。这个时间来自本机的一次测量不包含环境安装、绘图或首次启动解释器不作为其他机器的保证。九项测试另检查同一 CPU 环境重跑得到相同数值。项目实际值初始训练 MSE0.553925最终训练 MSE0.001547测试 MSE0.001899学到的权重2.009538学到的偏置1.005363曲线的第零轮是更新前第 120 轮是完成 120 次更新后因此损失 CSV有 121 行。训练结果接近生成数据时的系数 2 和偏置 1仍然保留噪声造成的小误差。图中的蓝点是训练真实值橙点是测试真实值绿线是模型预测。对于输入 -1、0、1预测约为 -1.004176、1.005363、3.014901。模型状态保存在results/linear-state.pt程序重新加载后检查了所有 41 条预测的一致性。状态文件只保存参数不自动保存模型类的定义。重新创建相同结构后再加载参数。本章加载本地自行生成的文件使用weights_onlyTrue数据来源仍要可信。8. 训练模式、评估模式与设备model.train()和model.eval()改变模块的运行模式。一些模块在两个模式下行为不同例如 Dropout 在训练时随机舍去部分数值评估时保持输入。当前线性层没有这类行为所以两种模式下的数值相同程序仍显式切换为后面网络做好约定。eval()不会自动停止求导。推理时通常同时使用评估模式与torch.no_grad()后者使上下文内的相关运算不记录反向求导所需信息。两者作用应分别检查。importtorch modeltorch.nn.Linear(1,1)model.eval()print(eval 仍记录求导,model(torch.ones(1,1)).requires_grad)withtorch.no_grad():print(no_grad 下,model(torch.ones(1,1)).requires_grad)输出为 True 和 False。训练的损失计算不能放进no_grad()后再调用反向。设备表示当前数值与运算在哪里执行。本章默认 CPU模型、特征和标签都放在同一设备。入口也接受--device auto先检查 CUDA 是否可用显式选择不可用的 CUDA 会给出提示。CPU 环境中的本次验证没有测试 GPU 性能。固定随机种子有助于复现但跨版本、操作系统或 CPU 与 GPU之间仍可能出现差异。9. 常见卡点与独立练习报整数与浮点类型不一致时检查输入 dtype将回归输入改成 float32。报损失不需要梯度时检查是否提前用了detach()、转成 NumPy或在no_grad()内算了训练损失。梯度越来越大时先确认每轮有清除梯度再查学习率与数据尺度。预测形状(32, 1)与标签(32,)混用时某些运算会发生广播形成(32, 32)的比较。检查目标形状与预测一致不能只因为损失是一个有限数就认为配对正确。独立练习先将三条无噪声数据的关系改为y 3x - 2。只修改标签保持循环结构检查模型的权重与偏置接近多少。再说明为什么评估模式下仍要按需要关闭求导。参考答案与其余练习见【零基础学AI】第 22 章课后练习与答案。