资讯动态

PyTorch自动求导基石:从Tensor到Variable的核心机制解析

发布时间:2026/10/17 14:28:32 来源:尧图企业网站定制
2. 从Tensor到VariablePyTorch自动求导的基石2.1 开篇一个绕不开的起点不论你是因为参加竞赛、做毕业设计还是想转行搞深度学习才点开这篇文章只要你打算用PyTorch张量Tensor就是第一道门槛。而“张量与变量”这个标题看起来像是两个概念并列但在PyTorch的版本演进里它们其实是两个时代的东西。我最早接触PyTorch的时候网上的教程还处于“张量是张量变量是变量”的阶段。当时每次定义一个需要求梯度的参数都得写Variable(torch.tensor(...), requires_gradTrue)代码里到处套着Variable壳子。后来PyTorch 0.4.0版本发布Tensor和Variable合并了官方直接说“以后统一用Tensor想要梯度就设置requires_gradTrue”大家一阵欢呼。所以你手里的PyTorch 2.x里其实已经没有独立的Variable类了你看到的只有一个「支持跟踪梯度的Tensor」。但这不代表“变量”这个概念消失了。恰恰相反它是理解梯度计算、反向传播、计算图这些核心机制的关键钥匙。“张量与变量一”这个系列我打算给你讲透这层关系这一篇先把地基打好——张量的创建、属性和基本操作以及变量和自动求导的来龙去脉。看完了你就能明白为什么说PyTorch的自动求导精妙以及你写代码时那些requires_grad、no_grad、backward到底在背后干了什么。2.2 从最日常的创建开始很多人一上来就喜欢抄那些包含randn、zeros之类的代码但不理解每个函数背后的数据形态后面整个模型都会出问题。我建议你先把“创建张量”这件事想透它只有几类场景从已有数据创建、从特定分布生成、从NumPy数组转换以及创建特殊形状的空张量。import torch import numpy as np # 从Python列表创建 t1 torch.tensor([1, 2, 3], dtypetorch.float32) # 从NumPy数组创建 arr np.array([[1, 2], [3, 4]]) t2 torch.from_numpy(arr) # 按形状创建全零/全一张量 t3 torch.zeros(2, 3) t4 torch.ones(2, 3) # 用随机值填充模拟初始化 t5 torch.randn(2, 3) # 标准正态分布 t6 torch.rand(2, 3) # 0~1均匀分布这里有个老生常谈但必须拎出来说的坑torch.tensor和torch.Tensor是两种东西。torch.tensor()是个工厂函数它会根据你传入的数据推断或指定dtypetorch.Tensor()则直接调用默认构造器在没传数据的情况下就生成一个未初始化的张量里面可能是内存里的垃圾数字。我见过不少人用torch.Tensor(2, 3)想生成零矩阵结果打印出一堆莫名其妙的值然后在群里问“是不是PyTorch有bug”。没有bug是你用错了API。再说torch.from_numpy()和NumPy数组的共享关系。torch.from_numpy()创建的张量与原来的NumPy数组共享底层内存意味着你改NumPy里的值Tensor那边也会变。这在数据处理时很省内存但也容易出事。如果你做了一个原地操作比如t2.add_(1)NumPy数组也会被改掉。真实项目里我处理数据时稍不注意就得重新加载后来养成习惯凡是需要复制又不想互相干扰的情况就用torch.tensor(arr.copy())或t2.clone()。从随机生成角度说randn和rand的选择不是随便来的。神经网络权重初始化常用的小随机数如果只是均匀分布rand(n, m)是0到1之间而参数初始化通常还希望正负对称所以randn更常见它生成的是均值0、方差1的标准正态分布。实际项目中配合a * randn(...) b就能控制缩放和偏移这比先均匀分布再变换要直观。2.3 张量到底“长”什么样三条命脉要真正用好张量光会创建不行你得随时能回答出三个问题它的形状shape是什么、里面存的数据类型dtype是什么、它待在哪个设备device上。我管这三样叫“数据三件套”几乎所有报错和奇怪行为的根源都能落到这三样上。t torch.randn(4, 5) print(t.shape) # torch.Size([4, 5]) print(t.dtype) # torch.float32 print(t.device) # cpushape表示各维度大小。维度数叫“秩”一个标量是0维一维向量、二维矩阵、三维批量图像分别是1、2、3维。搞深度学习最常见的错误就是维度对不上比如模型输入是[batch, 3, 224, 224]你喂给它的是[3, 224, 224]一跑就是一个维度错误。这个真的急不得拿到数据第一步永远是print(data.shape)。dtypePyTorch默认浮点数类型是float32在GPU上这就是核心算力主打的精度。整数类型默认是int64。你如果不小心用整型数据去跑模型很多层会直接罢工。像是字符串、bool这些类型也只能在不参与梯度计算的场景里用用。deviceCPU上创建的张量没法直接喂给GPU上的模型强行运行会直接报错提示“device mismatch”。所以真正的工程代码里经常能看到model.to(device)、data.to(device)这种操作把参数和数据搬到同一个设备上去。这三个东西就像炖菜的调味料——少了一样菜可能还能吃但味道一定不对。调试模型时先用一段小代码把输入张量、模型参数、目标张量的这三样都打印出来能省掉大量排查时间。2.4 索引、切片和“假装变了个形状”张量操作里最常用到的是索引和形状变换。索引规则和Python的列表、NumPy数组基本一致但很多人一上来会栽在“共享存储”这个点上。t torch.arange(10).reshape(2, 5) row t[0] # 第一行 cols t[:, 1:3] # 所有行的第1~2列 sub t[:1] sub[0, 0] 999 # 原t也会变t[0]拿到的不是复制品而是原始张量的一块视图。视图和原数据是共享内存的改视图里的值原张量跟着变。这个特性在操作大矩阵、降低内存占用时很有用但对没意识到它的人来说莫名其妙改了原数据排查半天都找不到原因。需要独立副本时老老实实加个.clone()。形状变换里最常用的是view和reshape。区别有意思view要求内存连续所以有时得先.contiguous()reshape更灵活它会自己在背后处理连续性问题。还有一点不要试图用view去改一个非连续张量的形状会直接报错。从实用角度说你直接在代码里用reshape基本能覆盖九成需求但面试或者读老项目时很多地方还是用view所以两个都要懂。permute和transpose则负责调换维度顺序经常处理图像数据时用。图像前是[批次, 高度, 宽度, 通道]绝大多数模型期望[批次, 通道, 高度, 宽度]于是img.permute(0, 3, 1, 2)就是家常便饭这行代码你要是理解不深从数据集处理到模型前向一路都会满头问号。我觉得学张量操作最有效的方式是拿一个小张量打印出来然后做一步打印一步。张量的每一步操作都会产生一个新张量看它的shape和值的变化看多了自然就有手感。2.5 重新认识“变量”自动求导的起点如果只看张量本身它只是一个装着数字的容器。可一旦打开了requires_grad开关这个容器立刻就能记住“它是怎么被算出来的”这就是变量Variable要做的事。在早期版本里PyTorch把“能追踪梯度的张量”单独封装成了Variable要求所有需要梯度的操作都在Variable上执行。后来版本合并Tensor自己在requires_gradTrue时就能挂上一系列回溯信息。可以这样理解张量本身是菜变量是给菜贴的标签。# 老写法PyTorch 0.4.0之前 # from torch.autograd import Variable # x Variable(torch.tensor(2.0), requires_gradTrue) # 新写法0.4.0之后至今 x torch.tensor(2.0, requires_gradTrue)在这里requires_grad就是那个决定“我叫不叫变量”的开关。打开它意味着只要这个张量参与了某个运算之后你调backward()它就能沿着这次运算的记录去计算和累积梯度。这就要求我们只有真正需要求梯度的叶子张量才打开这个开关。如果你给一个不需要梯度的中间结果也开了计算图就会造得又大又费内存前向和反向都会慢一大截。围绕“如何开、何时关”PyTorch给了三个不同的手段我经常收到这类提问整理出来给大家手段作用场景requires_gradTrue创建张量时直接开启追踪用于模型参数或需要更新值的输入requires_grad_()对已有张量原地修改用于把某个本来不追踪的张量临时改成追踪torch.no_grad()关闭梯度追踪的上下文管理器用于推理阶段、模型评估或避免多余计算图实际写模型时评估集和验证集上我们通常会包一层with torch.no_grad():。这样做除了省显存更重要的是避免因为反向传播把梯度栈越叠越深导致内存爆掉。有人只是测试个分类准确率忘写了结果显存越占越多最后程序崩了入职第一周就在这上面栽跟头。2.6 计算图一次回放式记账自动求导的底层是计算图Computational Graph。你不需要一开始就看得特别深但至少要明白它的思路把一次前向运算拆成许多个小步骤记录每个步骤的输入和输出、以及每个步骤对输入的导数关系然后反向传播时按照链式法则一步步算出梯度。用生活里的话说就是“记账本”——每一步怎么算来的全都有记录。x torch.tensor(2.0, requires_gradTrue) y x * 3 z y * y上面这段代码里PyTorch会建立一张这样的关系x是叶子节点y是中间结果z是最终输出。当你调用z.backward()它会从z出发回推z对y的导数是2*yy对x的导数是3所以z对x的导数是2*y*3 36。这个36就是自动求导帮你算出来的结果。z.backward() print(x.grad) # tensor(36.)这里的grad属性就是该张量“作为叶子节点”累积到的梯度。可以对比手算验证一下y 6z 36z对x的确导数为2×6×336。只要你会链式求导就能自行验证代码算得对不对。我在刚学PyTorch的第二个月就干过这种事写个自定义公式先手算一遍导数再用backward()去对答案。这个习惯帮我避开了无数后来真正跑模型时的“梯度为None”和“渐变爆炸”问题。还有一点如果你的最终结果不是一个标量而是向量或矩阵backward()需要传一个与输出形状相同的“梯度种子”参数。这个参数就是你赋给输出“最外层梯度”的初始值。比如输出是[2, 2]的矩阵你希望它一开始的梯度是全是1就传torch.ones(2, 2)。这个细节很多人一开始完全不知道只知道报错说“grad can be implicitly created only for scalar outputs”弄明白之后就会恍然大悟。2.7 数据处理的最后一公里和NumPy与Matplotlib配合如果你是为了做实验或跑模型用的张量那还有两件事基本跑不掉一是张量和NumPy互相转换二是把结果可视化。张量转NumPy的坑在于共享内存确实太隐蔽了。t torch.tensor([1.0, 2.0]) n t.numpy() n[0] 100.0 print(t) # tensor([100., 2.]) 原张量跟着变了我当年做数据可视化时把一个tensor导成numpy准备画曲线画完了想把曲线再处理一下回头发现原始张量的值被动过追溯了好一阵才意识到是这行t.numpy()的问题。现在我的处理习惯是如果只是为了转一份继续折腾就用t.cpu().numpy()让它落回CPU如果要保留两者各自独立就.clone().detach().cpu().numpy()。detach()用来从计算图上摘掉梯度信息cpu()搬到CPUclone()负责复制三个一起才是安全的“导出给其他库用”的姿势。t.cpu().detach().numpy()关于可视化很多人在深度学习入门时会拿MNIST那种手写数字数据集来练手用matplotlib画出来就能直观看到模型输入长什么样。这里要注意tensor图像通常是以[通道, 高, 宽]存储的而matplotlib认的是[高, 宽, 通道]。你需要img.permute(1, 2, 0)调换一下维度。这个小细节做图像的人天天用但初学者几乎都会踩一次。2.8 常见问题与排查实录这里把入门阶段最容易踩的几个坑和对应的解决办法直接整理成一张表方便你遇到事时快速查阅报错/现象原因处理建议RuntimeError: mat1 and mat2 shapes cannot be multiplied矩阵乘法时后一个矩阵的行数不等于前一个矩阵的列数维度对上错了打印两个张量的shape手动算一下相乘后的形状RuntimeError: expected scalar type Float but found Longdtype不匹配模型或算子要求float喂进去的是int64检查输入是否忘了.float()RuntimeError: Input type and weight type should be the same设备和dtype不一致比如输入是CPU张量模型参数在GPU上统一用to(device)模型和数据都放同一个设备UserWarning: grad can be implicitly created only for scalar outputsbackward()时输出不是标量先.sum()变标量或者传入同形状的gradient参数RuntimeError: Trying to backward through the graph a second time反复调用同一个图的backward()而默认retain_graphFalse第一次反向前先传retain_graphTrue但更推荐重新前向一次构建新图ValueError: cannot reshape array of size ...用view或reshape时元素总数对不上检查总元素个数是否等于各维度乘积所有的梯度都是None叶子张量没设requires_gradTrue或者中途用了detach()检查叶子节点是否开了追踪检查是否在中途把梯度链条断掉了我最想强调的是最后一条。新手兴奋地搭完一个小模型发现参数梯度全是None接着就在群里问“是不是PyTorch坏了”。绝大多数时候不是框架坏是你把图链断了。比如用了detach()之后又对detach出来的张量调用backward()那么原来的那个分支根本不会得到梯度。你只需要简单想想它的“求导账本”是不是还连着原来的路径就能自己判断出来。排查时我有个固定流程打印模型参数是否requires_gradTrue打印输入张量是否requires_gradTrue检查前向过程有没有调用detach()检查loss是不是标量。这套下来基本能定位九成问题。2.9 这个帖子还能往后怎么用张量和变量这个话题一次是不可能讲到底的。后面如果继续往下写我会接着讲张量的广播机制、合并与拆分、范数计算、以及torch.nn.Parameter如何和优化器配合。这些内容才是真正能把模型搭起来不踩坑的关键。就我自己带人入门的小经验来说最建议你的学习路径是先把张量的创建和基础操作过一遍然后去做一个极小的线性回归亲手实现前向传播、计算loss、调用backward、再用优化器更新参数。只要这一整个闭环亲手跑通你对自动求导的理解就会从“听说过”变成“会用”后面学什么网络结构都顺了。那就先这样下一篇继续。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑