PyTorch张量与Variable详解:从创建操作到自动求导实战

发布时间:2026/10/14 15:02:20
PyTorch张量与Variable详解:从创建操作到自动求导实战 说实话我每次带新人入门深度学习第一课讲的一定是张量。原因很简单不管你是要做图像分类、自然语言处理还是强化学习在PyTorch的世界里一切数据的流动和计算都围绕张量展开。你初始化一个模型模型参数是张量喂入一批图片输入数据是张量前向传播算损失、反向传播算梯度中间产物和梯度结果也都是张量。可以说搞懂张量就是在给后面所有内容打地基。这篇笔记我会把张量Tensor和变量Variable一起拆开讲。很多刚接触PyTorch的朋友会有个困惑为什么网上一搜教程老文章里全是Variable而新版代码里直接就是Tensor这其实是PyTorch发展历程中的一次重要合并。我会把这件事的来龙去脉讲清楚同时把张量创建、索引切片、形状变换、广播机制、自动求导这些核心用法过一遍最后附上实操中最容易踩的坑和排查思路。适合已经开始读PyTorch代码但总感觉哪里“隔着一层”的朋友也适合刚入门、被一堆API劝退的新手。1. 张量与变量为什么它们是PyTorch学习的起点1.1 一切计算都围绕张量展开先聊一个很多人忽略的事实神经网络训练的本质就是让一批数值在“正向传播-计算损失-反向传播-更新参数”的循环里不断流动。而这些数值的载体就是张量。你可以把张量理解成一个“集装箱”。普通的Python列表是散装货看着直观但没法高效计算而张量是一个规格统一的集装箱里面的数据按照固定的形状排布并且被底层C/C引擎优化过算起来飞快。更关键的是它会记下自己在计算路径里的“足迹”——这为后面的自动求导打下了基础。以一张常见的图像数据为例我们通常用形状为[batch_size, channels, height, width]的四维张量来表示比如[32, 3, 224, 224]。如果把张量当“集装箱”那这里的形状就是集装箱的“规格标签”里面的每个数值是像素点的亮度或颜色通道值。模型读取这批数据经过一系列矩阵乘法、卷积操作每一步操作都会产生新的张量。理解了这个概念你就明白为什么张量是深度学习最基础、最核心的数据结构。1.2 理解Variable的来龙去脉很多人在翻旧教程时会被Variable整懵。在早期的PyTorch版本里Variable和Tensor是分开的两个类Tensor只负责存数据、做计算而Variable在Tensor外面包了一层额外记录“梯度”和“是否需要计算梯度”这些信息。这个设计在当时的框架里是合理的但也带来了不少麻烦。你写代码时得区分到底在用Tensor还是Variable取数据要用.data取梯度要用.grad一套操作下来别扭得很。到了0.4.0版本PyTorch直接把Variable合并进了Tensor。从此以后Tensor本身就能记录梯度了你只要在创建时设置requires_gradTrue后面的一切都不需要额外包装。这也是为什么现在新教程里几乎看不到Variable的原因。但问题来了搜索引擎里沉淀了大量用Variable写的老代码论坛问答里也还有一堆Vintage回答。如果你完全不懂Variable看这些内容会一脸懵。所以这篇笔记特意把两者放在一起讲一方面帮你理解当前Tensor的设计逻辑另一方面给你一份“考古指南”看到老代码时能迅速明白它在干什么。2. 张量的创建基础操作与核心概念2.1 先搞清张量的三个核心属性拿到一个张量第一个动作一定是确认三件事形状shape、数据类型dtype、所在设备device。这三个属性几乎决定了后续所有操作会不会报错。用一段代码感受一下import torch x torch.tensor([[1, 2, 3], [4, 5, 6]], dtypetorch.float32) print(x.shape) # torch.Size([2, 3]) print(x.dtype) # torch.float32 print(x.device) # cpu形状就是维度大小。[2, 3]表示两行三列的矩阵。数据类型控制数值的精度和内存占用。设备决定这个张量是在CPU上还是在GPU上计算。新手最容易犯的错误就是模型参数在GPU上输入数据在CPU上一运算直接报错。所以拿到任何张量先看一眼这三个属性能省掉后面一大半的排查时间。2.2 创建张量的多种方式与选择PyTorch创建张量的API非常多很多初学者会困惑到底该用哪个。我的建议是先记住下面表格里的核心方法其他的用到了再查文档。创建方式说明典型使用场景torch.tensor([...])从Python列表/数组创建会复制数据手动构造小规模测试数据torch.Tensor([...])与torch.FloatTensor相同默认float32需要指定默认类型的快速创建torch.as_tensor([...])尽量共享内存不复制则复用原数据转换已有数据减少拷贝开销torch.from_numpy(np_array)从NumPy数组创建共享内存数据处理流水线中与NumPy互转torch.zeros/ones(shape)生成全0/全1张量初始化掩码、占位张量torch.eye(n)生成单位矩阵批处理中的单位变换torch.arange(start, end, step)生成等差数列生成序列索引torch.linspace(start, end, steps)生成等间隔数值可视化、采样坐标torch.randn(shape)标准正态分布随机数初始化权重配合nn.Parametertorch.rand(shape)均匀分布[0,1)随机数初始化偏置、Dropout掩码等torch.randint(low, high, shape)整数随机数生成随机标签、索引这里有个容易踩的坑torch.tensor和torch.Tensor看起来很像行为却有差异。torch.Tensor([1, 2])等价于torch.FloatTensor([1, 2])不管传入什么类型的数据结果都是float32而torch.tensor([1, 2])会保留整数int64类型。如果你后续要做浮点运算用torch.tensor时最好显式指定dtypetorch.float32。我自己的习惯是构造测试数据一律用torch.tensor并写清dtype从NumPy拿数据用torch.from_numpy或者torch.as_tensor初始化模型参数则交给torch.randn或torch.zeros配合nn.Parameter使用。2.3 类型与设备为什么深度学习默认用float32先回答一个常见疑问为什么PyTorch里默认浮点类型是float32而不是float64双精度核心原因是效率和内存的平衡。深度学习模型的参数量和中间激活值动辄几百万、几十亿如果用float64内存占用直接翻倍GPU计算速度也会明显下降。而float32的精度约7位有效数字在实际训练场景下已经足够——梯度下降本身就带随机性Loss的微小抖动根本不影响最终收敛效果。无脑用高精度只会拖慢训练速度、增加显存开销。只有极少场景需要额外精度比如某些数值稳定性敏感的科学计算。普通模型训练float32是默认最优解。再说设备管理。CPU和GPU的张量可以直接通过.to()互转x torch.randn(3, 3) x_gpu x.to(cuda) # 转到GPU x_cpu x_gpu.to(cpu) # 转回CPU老代码里常见的.cuda()方法现在也还能用但.to()更通用——你可以把设备写成一个变量方便做环境判断device torch.device(cuda if torch.cuda.is_available() else cpu) x torch.randn(3, 3).to(device)这样写的好处是在没GPU的机器上跑也不会报错。2.4 NumPy与PyTorch的互转做深度学习的人几乎都绕不开NumPy。PyTorch和NumPy之间的互转没有想象中复杂但有个细节必须记住互转时数据可能共享底层内存。import numpy as np arr np.array([1, 2, 3]) t torch.from_numpy(arr) # 共享内存 t[0] 99 print(arr) # [99 2 3] 原数组也被改了用torch.from_numpy()转出来的张量与原始NumPy数组共享内存修改其中一方会影响另一方。这在数据预处理时很高效但如果不了解这一点代码里会出现“数据莫名其妙被改动”的诡异问题。如果需要完全独立的副本显式调用.clone()或.copy()。反向转换更简单t.numpy()。不过要提醒一点带requires_gradTrue的张量不能直接转NumPy需要先.detach()再转。3. 张量的常用操作索引、变换与广播3.1 索引与切片和NumPy一致的体验PyTorch的索引和切片语法基本照搬NumPy只要你会用NumPy上手几乎零成本。x torch.arange(12).reshape(3, 4) # tensor([[ 0, 1, 2, 3], # [ 4, 5, 6, 7], # [ 8, 9, 10, 11]]) print(x[1, 2]) # tensor(6) 取单个元素 print(x[:, 1]) # tensor([1, 5, 9]) 取所有行、第1列 print(x[1:, :2]) # tensor([[4, 5], [8, 9]]) 子区域 print(x[x 5]) # tensor([ 6, 7, 8, 9, 10, 11]) 布尔索引常见的坑有两个。一是布尔索引返回的是一维张量形状会“展平”不是保留原始结构。二是切片产生的张量共享存储如果你修改了切片原张量也会跟着变。这个特性和Python列表完全不同但和NumPy一致写代码时必须心里有数。3.2 形状变换view、reshape、permute与contiguous的坑形状变换是张量操作里最常用也最容易报错的部分。下面这些是实际项目里出现频率最高的操作方法作用典型场景view(shape)按逻辑顺序重新排列元素展平、重排批量数据reshape(shape)类似view但更包容处理非连续张量时不报错transpose(dim0, dim1)交换两个维度矩阵转置、交换通道顺序permute(*dims)按新顺序重排维度图像NCHW转NHWC等squeeze(dim)删除长度为1的维度去掉批量维unsqueeze(dim)插入长度为1的维度增加通道维、批量维view和reshape是新手最容易搞混的一对。view要求张量在内存中是“连续”的contiguous否则会报错view size is not compatible with input tensors size and stride。而reshape在当前实现里更灵活它在数据不连续时会自动拷贝一份保证结果正确。举一个典型报错场景x torch.arange(12).reshape(3, 4) y x.t() # 转置后内存布局不再连续 print(y.is_contiguous()) # False z y.view(2, 6) # 报错因为 y 不连续 z y.reshape(2, 6) # 正常底层自动拷贝所以我的建议是普通情况直接用reshape省心但如果你对性能敏感、确定张量连续用view可以避免一次拷贝。要查看是否连续用is_contiguous()判断。transpose和permute的区别也值得说清楚。transpose只能交换两个维度permute可以任意重排所有维度。图像处理里常见的NCHW转NHWC用permute(0, 2, 3, 1)一步搞定。这类操作返回的张量通常非连续如果后面接view就得先.contiguous()。3.3 广播机制广播是PyTorch里一个极其好用的特性一句话概括不同形状的张量做运算时小张量会自动“扩展”成大张量的形状。规则只有两条从尾部维度开始对齐维度长度相等或者其中一个为1时才能广播。举个例子a torch.ones(3, 1) # 形状 [3, 1] b torch.ones(1, 4) # 形状 [1, 4] c a b # 结果形状 [3, 4]a的列数1和b的行数1都会被自动扩展到3和4两个矩阵相加得到完整的3行4列。你可以理解成广播就是让每个“长得比较小”的维度被拉伸到对方对应维度的大小。实际使用中给一批图片加均值、给所有样本减一个统计量都是靠广播一行代码完成的。但广播也有隐患——如果两个张量的形状在某个维度上既不等长也不为1就会报错a torch.ones(3, 4) b torch.ones(2, 4) c a b # 报错维度3与2无法匹配排查这类问题的方法是打印两个张量的.shape看尾部维度对不对得上。养成“相加之前先确认形状”的习惯能帮你规避大量潜在bug。4. Variable与自动微分从手推梯度到自动求导4.1 在Variable时代写代码有多别扭我把历史中的用法展示出来大家体会一下。# 早期版本的写法 from torch.autograd import Variable x Variable(torch.Tensor([2]), requires_gradTrue) y x ** 2 y.backward() print(x.grad) # 梯度存在这里 print(x.data) # 数值存在这里整个训练过程里你都必须清晰地意识到“哪个是数值、哪个是梯度、哪个是需要被优化器更新的参数”。尤其取数据时要写.data否则一不小心就把整个Variable表达式带进了后续计算。写起来别扭排查起来更累。4.2 合并之后Tensor全面接管自动求导PyTorch 0.4.0之后同样的代码简化成x torch.tensor(2.0, requires_gradTrue) y x ** 2 y.backward() print(x.grad) # tensor(4.)不需要再包一层VariableTensor天生就有requires_grad属性和grad属性。这个合并的意义不只是少敲几个字而是把“数据”和“梯度”的概念统一了让用户和框架的交互更直观。那自动求导的底层原理是什么PyTorch采用动态计算图每一次前向计算框架会记录你是用什么操作、由哪些输入得到了哪些输出生成一张有向无环图。调用backward()时从输出节点出发用链式法则一路反向计算每个叶子节点的梯度。举一个具体的例子x torch.tensor(3.0, requires_gradTrue) w torch.tensor(2.0, requires_gradTrue) b torch.tensor(1.0, requires_gradTrue) y w * x b # 前向计算 y.backward() # 反向传播 print(x.grad) # 2.0 dy/dx w print(w.grad) # 3.0 dy/dw x print(b.grad) # 1.0 dy/db 1你看backward()之后每个设置了requires_gradTrue的张量都自动拿到了对应的偏导。如果当初我手推梯度就是这个结果。这就是深度学习框架给开发者省下的最大力气——你只需要定义模型结构梯度会自动算好。4.3 梯度陷阱累积、叶子节点、in-place操作自动求导也不是“一键免操心”下面三个坑是我见到的最常见的梯度累积问题。每次调用backward()梯度会累加到.grad上而不是重置。这是为了支持梯度累积的训练技巧但对新手是个大坑。如果忘记在每个batch训练前清零梯度梯度会越滚越大Loss直接飞掉。所以标准的训练循环里一定有这一行optimizer.zero_grad()叶子节点概念。只有用户直接创建、且requires_gradTrue的张量才是叶子节点。由叶子节点计算出来的中间张量不是叶子节点它们的.grad默认不会被保存。如果你想拿到某个中间结果的梯度需要用register_hook。新手遇到“为什么梯度是None”时先查一下自己关心的张量是不是叶子节点。in-place操作。像x.add_(1)这种原地修改数值的操作会破坏计算图里的记录导致反向传播时找不齐历史数据轻则梯度算错重则直接报错。PyTorch默认对这种情况非常严格。我的建议是凡是requires_gradTrue的张量一律使用x x 1这种非原地操作千万别图省事。4.4 老代码“考古”指南遇到Variable怎么办翻老项目时看到Variable不需要慌做两个动作就行第一把Variable(...)换成Tensor写法。比如Variable(torch.ones(3, 3), requires_gradTrue)改成torch.ones(3, 3, requires_gradTrue)功能基本等价。第二把.data的取数习惯去掉。老代码里的.data现在不推荐使用直接用张量本身。比如老代码里的x.data.numpy()新写法是x.detach().numpy()。.detach()表示从计算图中分离出来不再追踪梯度这在保存模型权重、可视化特征图时都很常用。还有一类更隐蔽的老代码会把Variable用在nn.Parameter的包装上类似Variable(nn.Parameter(torch.randn(...)))。这种冗余包装直接删掉就行nn.Parameter本身就是一个带requires_gradTrue的Tensor。5. 常见问题与排查技巧实录5.1 报错速查表把实操中高频出现的错误整理成一张速查表遇到问题第一眼就能定位到方向。报错信息可能原因解决方式view size is not compatible with input tensors size and stride在非连续张量上调用view先.contiguous()再view或用reshapeExpected all tensors to be on the same deviceCPU张量与GPU张量混算用.to(device)统一设备grad can be implicitly created only for scalar outputsbackward()传入非标量输出先对非标量求sum()或mean()或给backward()传gradient参数a leaf Variable that requires grad is being used in an in-place operation对叶子张量做原地修改改用非原地操作如x x 1shape mismatch广播维度不匹配打印两者.shape检查尾部维度梯度是None计算路径上没有梯度、requires_grad未开启或该张量不是叶子确认requires_gradTrue检查从输入到输出路径是不是全被detach了5.2 我踩过的三个真实坑第一个坑是忘清零梯度。早年间我跑一个文本分类模型Loss前几步明明在下降突然爆到几百。排查半天最后发现是每个batch没有zero_grad()梯度跨batch累积几步之后参数更新幅度越来越大。从那时起我把训练循环的模板固定成“forward、loss、backward、step、zero_grad”的顺序再没犯过这类错。第二个坑是view和permute的组合。有一次做图像数据预处理想把[N, C, H, W]转成[N, H, W, C]再展平。直接permute(...).view(...)报错原因是permute后的张量不连续。加一个.contiguous()立刻解决。后来我总结了一条经验只要看到view报错第一反应检查前面有没有transpose、permute、.t()这类重排维度的操作。第三个坑是in-place操作。我在给某个中间特征做归一化时图省事写了x / x.max()结果反向传播报错。原因是PyTorch需要保留原始值去计算梯度原地修改等于把中间结果覆盖了。从那以后所有需要梯度传播的张量我都写成x x / x.max()。5.3 新手排查三板斧如果你刚接触PyTorch遇到问题不知道从哪下手记住下面三招能解决大多数入门阶段的问题。第一招打印形状、类型和设备。任何报错第一反应都是加一行print(x.shape, x.dtype, x.device)看到底哪个环节和预期不符。第二招构造极小案例手动验证。不用先上真实数据用torch.tensor([[1.0, 2.0], [3.0, 4.0]])这类小矩阵试一下把中间的每一步结果打印出来。小案例跑通再换大数据问题定位会快很多。第三招从叶子节点往回看计算图。梯度为None时沿着输入到输出的路径检查是不是某一步用了.detach()是不是requires_grad没有传下去是不是在某个非标量上直接调了backward()绝大多数梯度问题逃不出这三种情况。我个人带新人学PyTorch还有一个习惯会专门让他们动手写一个小脚本手动实现一个最简单的线性回归用requires_gradTruebackward()optimizer.step()把这个闭环跑通。别看这个例子小它能帮你把张量、梯度、计算图这些概念真正焊死在脑子里。学完这篇笔记之后建议你也去试一下不需要用现成的nn.Linear就纯手写一个y w * x b然后自己训练几步。跑通的那一刻你对张量和Variable的理解就真正落地了。这个系列接下来还会接着写自动求导的进阶用法、模型构建的常用模块、数据装载还有一些实战中跑出来的小技巧。第一篇能把地基打牢后面就顺理成章了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询