资讯动态

PyTorch创建Tensor全解析:从底层原理到踩坑避雷

发布时间:2026/10/20 17:02:04 来源:尧图企业网站定制
很多人喊“PyTorch创建Tensor”的时候心里想的大概都是torch.tensor([1,2,3])这种一行代码的事。但实际用下来你会发现这个基础操作里藏着一堆容易踩的坑torch.Tensor和torch.tensor行为居然不一样转 Numpy 时张量会“偷看”内存GPU 上生成的随机数跟 CPU 上对不上更别说高阶的new_zeros、rand_like这些方法新手根本分不清什么时候该用哪个。这玩意儿就像写文章先学会用标点符号看起来简单真用得顺手却需要搞清楚背后的规则。这篇内容就是专门讲创建Tensor这件事的从底层原理、全部 API 背后的逻辑到实际跑模型时的常见坑一次性说透。适合刚入门 PyTorch 的开发者也适合写了一段时间但总觉得代码里创建张量有点“脏”的同学参考。1. 建Tensor之前先搞懂它在内存里长什么样1.1 Tensor不是“多维数组”那么简单很多从 Numpy 转过来的同学天然会把 Tensor 理解成多维数组。这么想没错但 PyTorch 的 Tensor 实际上比 Numpy 多了一层“自动求导引擎”的钩子底层还绑定了计算设备CPU/GPU甚至内存布局都由自己管理。一个 Tensor 对象内部大致由三块构成一块 Storage真正存数据的内存区、一组元数据shape、stride、dtype、device以及一个可选的 AutogradMeta记录梯度计算图。我习惯用一个生活例子来类比Tensor 相当于一家仓库的取货单Storage 是仓库货架上的货物shape 和 stride 是告诉你怎么沿着过道去取货的路线图而 device 决定这个仓库建在“本机库房”还是“显卡库房”。你可能拿着一张写着 3 行 4 列的取货单但如果货架排列方式不同比如转置后的张量取的路径也会完全不同。所以创建 Tensor 不是光填数值就行你其实是在决定货物放在哪、用什么顺序排、用什么尺子量dtype。这里最大的隐藏问题是两个 Tensor 可能共享同一个 Storage但元数据不同所以它们表面上看起来是“两个独立张量”实际改动其中一个另一个也会跟着变。比如下面的操作就很容易让人迷惑import torch import numpy as np a torch.arange(12).reshape(3, 4) b a.t() # 转置不复制数据只是改了 shape 和 stride a[0, 0] 999 print(b[0, 0]) # 输出 tensor(999)这就是元数据和 Storage 分离带来的现象。创建 Tensor 时如果你不知道底层是不是“视图”后续调试会非常痛苦。理解这一点是正确使用所有创建 API 的第一步。1.2 创建Tensor时要关注“存储位置和格式”创建张量不是把数据扔进去就完事我一向把它分成四个维度数值value、形状shape、数据类型dtype、设备device。有时候还要关心内存格式contiguous 还是非连续尤其是跟矩阵乘法、卷积操作打交道的时候。举个例子你用列表传入一个浮点数矩阵PyTorch 默认会帮你转成torch.float32但如果你不小心传入了整数列表生成的就是torch.int64后续算 loss 的时候可能直接报错data [[1, 2], [3, 4]] t torch.tensor(data) # dtype 是 torch.int64 print(t.dtype) # torch.int64很多新手在这上面栽跟头模型输入要求 float32但创建的时候忘了指定dtypetorch.float32结果前面所有计算全部在整数上跑模型参数梯度直接断裂。所以我们创建 Tensor 前最好明确回答这几个问题这个张量会被送到模型里做浮点运算吗要不要在 GPU 上跑需要参与自动求导吗这三个问题对应着dtype、device、requires_grad三个参数。提前想清楚后面能省掉大把 debug 时间。2. 直接创建的API全家桶一张表理清十一类函数2.1 torch.tensor vs torch.Tensor同名不同命最容易搞混的一对就是torch.tensor和torch.Tensor。虽然名字只差一个大小写但行为差别巨大。torch.tensor是一个函数会尽可能忠实地复制你传进去的数据并且默认情况下它会做copy也就是说你之后改原来的 list 或 Numpy 数组不会影响生成的 Tensorarr np.array([1, 2, 3]) t1 torch.tensor(arr) # 会 copy 数据 arr[0] 100 print(t1) # tensor([1, 2, 3])不受影响而torch.Tensor是类构造函数它默认生成的是 float32 类型的空张量或者“用未初始化的内存填充的张量”行为跟torch.FloatTensor是一样的。如果你写成torch.Tensor([1, 2, 3])它也能接收 list 并转成 float32但很多人以为传整数会保留整数实际不会。更糟糕的是torch.Tensor(3, 4)在很多老版本里是创建了一个 3x4 的未初始化张量里面的值是脏数据直接参与运算可能得到莫名其妙的结果。我建议在没有特殊理由的情况下一律用torch.tensor来从显式数据创建张量。只有当你明确要创建一个 float32 类型的空张量来接收后续数据时才考虑用torch.Tensor或torch.empty。2.2 按形状和内容创建的六种常用API日常创建 Tensor 时我真正高频使用的按形状创建函数是这几类zeros、ones、empty、full、eye、arange。它们各有各的适用场景我把它们的参数和推荐用途整理成了一张速查表函数常用签名核心用途坑点提醒torch.zerostorch.zeros(3, 4)初始化全零张量例如掩码、偏置默认 dtype 是 float32torch.onestorch.ones(size)初始化全 1 张量注意传的是 size不是 list of valuestorch.fulltorch.full((3, 4), 7)填充指定常量的张量第一个参数必须是 tuple不要写错torch.emptytorch.empty(size)只分配内存不赋值适合大数据初始化内容是脏值不要直接参与运算torch.eyetorch.eye(4)生成单位矩阵只支持 2D超过就报错torch.arangetorch.arange(start, end, step)生成等差数列常用于索引end 不包含步长支持浮点但要注意精度torch.zeros和torch.ones的底层逻辑其实都是基于torch.empty再填充值所以当你要声明一个很大的张量时用zeros的性能和用empty再手动赋值其实差不多。真正需要在意的是empty的“未初始化”问题如果在调试时打印empty张量你会看到一堆特别大的随机数那是内存里原来的残留值不是随机种子生成的随机数。所以在没搞清楚旧数据会不会污染逻辑之前别随便用empty。torch.arange也是高频使用比如你要生成一个 0 到 99 的索引列表来打乱样本顺序。但有个细节如果start、end、step都是整数默认 dtype 是 int64如果是浮点数默认 dtype 是 float32。当你后续要用这些索引去 index 其他张量时int64 基本没问题但如果想作为 GPU Embedding 的索引传入时可能需要.long()转换。2.3 随机张量的三类宝藏函数随机张量在模型里太常用了比如权重初始化、随机 dropout mask、样本采样。PyTorch 在torch.rand、torch.randn、torch.randint、torch.randperm等方面做得不算复杂但选择哪个函数经常让新手懵。函数分布输出范围应用场景torch.rand(size)均匀分布[0, 1)生成概率值、dropout masktorch.randn(size)标准正态分布均值 0方差 1权重初始化、噪声torch.randint(low, high, size)离散均匀分布[low, high)随机索引、类别标签torch.randperm(n)无放回排列0 到 n-1打乱数据集顺序这里有个容易让人忽略的限制torch.randperm只能生成一维张量想得到多维的随机排列你得用 loop 或torch.stack把多个排列叠起来。这在批量随机采样时特别麻烦。另外创建随机张量时一定要记住设置随机种子。以前我调试一个推荐系统因为没固定种子每次跑出来的模型效果都不一样导致我以为是代码逻辑有问题。排查了一个下午最后发现就是没有设置torch.manual_seed(42)。这个操作放在训练脚本最前面就行import torch torch.manual_seed(42) # 如果使用 CUDA torch.cuda.manual_seed_all(42)如果你在中途还需要“可复现但每次不同”的随机张量可以考虑用torch.Generator来局部管理种子gen torch.Generator().manual_seed(2024) rand_t torch.rand(3, 3, generatorgen)这样不要把全局种子搞乱也是一个实用技巧。3. 高手都在用的“衍生创建”从已有Tensor生成新Tensor3.1 new_系列与类_like系列把dtype/device复制过来创建 Tensor 时有一种经常被忽略的场景你手头已经有一个 Tensor希望创建一个和它具备相同 dtype、device 的新张量只是形状或内容不同。如果这时候还用torch.zeros(3, 3)在 CPU 上没问题但一旦在 GPU 上训练你会莫名触发“device 不匹配”的报错。正确做法是用new_系列方法或*_like系列函数。new_zeros、new_ones、new_empty、new_full这些方法都挂在任意 Tensor 对象下调用时会自动继承原 Tensor 的 dtype、device、requires_grad 以外的属性ref torch.zeros(2, 2, dtypetorch.float64, devicecuda) new_t ref.new_zeros(4, 4) # float64, 且在 cuda 上*_like系列函数比如torch.zeros_like(ref)、torch.ones_like(ref)、torch.rand_like(ref)则是完全仿照ref的形状和 layout生成同形状张量。两者差别在于new_zeros是方法可以指定新的 shapezeros_like是函数生成和传参张量一模一样的 shape。什么时候用哪种我的经验是如果你只是需要“同设备同 dtype但形状不同”的新张量优先用new_zeros(size)如果你需要一个“跟某个 feature map 完全同 shape 的掩码”用zeros_like(feature_map)这样最省心。它们的共同优势是消除了 device/dtype 的硬编码代码迁移到多卡或 GPU 环境时几乎不用改。3.2 clone、detach、as_subclass拷贝还是共享有时候你需要从已有 Tensor 创建新 Tensor但既不是改形状也不是全零填充而是要一份“独立的数据副本”。最先想到的往往是t.clone()它会真正复制数据同时保留参与梯度计算图的连接除非你在 clone 之前先 detach 或加上requires_gradFalse。a torch.tensor([1., 2., 3.], requires_gradTrue) b a.clone() # 数据独立但计算图里 b 是 a 的复制节点 print(b) # tensor([1., 2., 3.], grad_fnCloneBackward0)如果你只是想拿一份当前数值不希望跟任何梯度产生关联那应该写成a.detach().clone()或者更简洁地只用a.detach()前提是你想要的是一个不共享内存但也不记录梯度的新张量吗不是detach 返回的是共享 Storage 的视图。这里有个经典误区t.detach()不等于深拷贝。我之前写强化学习算法时把 target_net 的一个参数q_target q_net(x).detach()直接拿去更新结果发现 q_net 的参数一变q_target 也跟着变导致 loss 一直降不下去。原因就是 detach 只是把 copy 的 grad_fn 丢掉底层数据还是同一个 Storage。如果你要让 target 与当前网络彻底断开必须使用q_target q_net(x).detach().clone()clone在振荡更新目标网络时是常客。很多框架实现里都会见到target_model.load_state_dict(model.state_dict())那是复制参数而非张量。如果你只是维护 target Q 值建议记住detach 断梯度、clone 拷贝数据二者经常要组合。3.3 通过拼接、堆叠与重复组合出复杂结构最后创建 Tensor 的方式不是“无中生有”而是把多个小张量组合成大张量。torch.cat、torch.stack、torch.repeat、torch.expand这四个是高频操作。torch.cat用于在已有维度上拼接比如将多个批次特征拼起来torch.stack则是新建一个维度把每个张量当成新维度的元素。它们的区别可以用一个例子感受x torch.tensor([1, 2, 3]) y torch.tensor([4, 5, 6]) print(torch.cat([x, y]).shape) # torch.Size([6])数值顺序拼接 print(torch.stack([x, y]).shape) # torch.Size([2, 3])在开头新增维度expand和repeat的差异更容易被忽略。expand不会分配新内存只是调整 stride 成一个逻辑上重复的视图读取速度快但不能改写写入会报错repeat是真正分配新内存、拷贝数据。通常在创建掩码或广播时我尽量用expand来节省内存base torch.tensor([[1, 2, 3]]) logical base.expand(4, 3) # 4行共享同一份数据不占额外内存 physical base.repeat(4, 1) # 真正的 4x3 新张量如果你要的是能够参与梯度更新的可写入张量就别用expand因为它返回的是视图对某些算子不兼容。在没把握时用repeat更安全代价是多占内存。4. 用代码实操一个完整的张量创建到模型输入流水线4.1 场景设计模拟一批图像数据纸上谈兵没意思。我拿一个常见的图像分类训练场景从头到尾演示创建 Tensor 时怎么选择 API。假定我们有一批 32x32 的 RGB 图片一共 8 张要把它们组成一个形状为[8, 3, 32, 32]的输入张量送进 CNN。第一步从读取原始数据开始假设原始图片数据读出来是 Numpy 数组形状是[H, W, C]也就是高度、宽度、通道。而 PyTorch 卷积层要求输入是[N, C, H, W]所以创建数据的流程通常是import torch import numpy as np # 模拟从磁盘读出 8 张 32x32x3 的图片 fake_images [np.random.rand(32, 32, 3).astype(np.float32) for _ in range(8)] # 方式一逐张转换后用 stack 叠成 batch image_tensors [torch.from_numpy(img).permute(2, 0, 1) for img in fake_images] batch torch.stack(image_tensors) print(batch.shape) # torch.Size([8, 3, 32, 32])这里用torch.from_numpy而不是torch.tensor是因为从 Numpy 转 Tensor 时from_numpy不会复制数据转换速度极快而且 Numpy 数组必须是 C 连续的内存布局。图片读取时如果不连续先np.ascontiguousarray(img)再转换。4.2 实操数据张量的创建、叠加与平台适配继续刚才的场景图片数据转成[N, C, H, W]后我们通常还会做归一化。归一化本身也会创建新张量比如减均值、除方差这里会触发 broadcasting但如果你不小心在创建均值张量时指定了错误 shape很容易广播到意外结果。推荐使用mean torch.tensor([0.485, 0.456, 0.406], devicebatch.device).view(1, 3, 1, 1) std torch.tensor([0.229, 0.224, 0.225], devicebatch.device).view(1, 3, 1, 1) normalized (batch - mean) / std注意我在创建mean和std时强行指定了devicebatch.device。很多人会忽略这一点本来就是 CPU 张量batch 一旦搬到 GPU减均值时直接报设备不一致错误。如果不想每次手动传 device更稳妥的写法是用batch.new_tensor([0.485, 0.456, 0.406])这样它会自动继承 batch 的 dtype 和 devicemean batch.new_tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)接下来说说 mask 创建。模型在训练时常需要根据标签生成 one-hot 或 mask。我通常的做法是labels torch.randint(0, 10, (8,)) # 8 个样本的标签 one_hot torch.zeros(8, 10) one_hot.scatter_(1, labels.unsqueeze(1), 1) # 在指定位置填 1这里scatter_可以原地改张量避免再创建一个新张量。如果你不想用zeros再填也可以直接用F.one_hot(labels, num_classes10)它会自动创建 one-hot 张量并返回新的 Tensor。4.3 扩展创建模型参数张量除了数据张量模型参数本身的初始化也属于创建 Tensor 的范畴。你在自定义网络时通常会在__init__里注册参数用nn.Parameter包装一个 Tensor。很多人的写法是self.weight nn.Parameter(torch.Tensor(out_channels, in_channels))这是一个非常典型的坑torch.Tensor(size1, size2)创建的是未初始化张量里面的脏数据会导致模型初始 loss 异常巨大。更规范的做法是self.weight nn.Parameter(torch.empty(out_channels, in_channels)) nn.init.kaiming_uniform_(self.weight, amath.sqrt(5))这里的逻辑是先创建一块未初始化的内存再交给初始化函数填充。直接torch.randn初始化也不是不行但很多预训练网络期望特定的初始化分布所以用torch.emptynn.init是最通用、最可控的。再补充一个创建 mask 的典型场景。Transformer 里的 attention mask 通常由布尔张量表示会用到torch.triu或比较运算来创建seq_len 5 mask torch.triu(torch.ones(seq_len, seq_len, dtypetorch.bool), diagonal1) print(mask)这个 mask 后续扩张成[batch, 1, seq_len, seq_len]时可以用mask.unsqueeze(0).expand(batch, -1, -1, -1)注意expand在 new dimension 上的 -1 是保持原尺寸。这又是创建 Tensor 时结合形状变换的常见手法。5. 创建Tensor时最容易踩的5个坑5.1 坑一用torch.Tensor创建随机数据你会发现一个奇怪现象torch.Tensor(3, 3)打印出来也是 3x3 的矩阵里面既有正数也有负数看起来像随机数于是有人拿它初始化权重。问题是这个“随机”是内存垃圾不是随机分布没有任何数学性质保证。后续模型收敛不稳定却又很难排查到这一层。提示我建议在代码里搜索一下大写开头的torch.Tensor(凡是用来创建带初始值张量的基本都要改成torch.tensor(...)或torch.randn(...)。类构造器只保留在“空张量后续初始化”场景。5.2 坑二Numpy转Tensor共享内存导致意外改动torch.from_numpy创建的 Tensor 与原始 Numpy 数组共享内存。这在数据预处理阶段是好事能减少拷贝但一旦你在后面对 Tensor 做了原地操作Numpy 数据也会变。如果这个 Numpy 数组还被别的逻辑引用就会引发连锁 bug。arr np.ones((3, 3), dtypenp.float32) t torch.from_numpy(arr) t.fill_(0) print(arr) # 全 0arr 被意外改了如果你不希望 Numpy 被改动用torch.tensor(arr)替代它会主动拷贝数据。这两者的选择原则很简单追求性能和内存共享用from_numpy追求数据隔离用torch.tensor。在 Dataloader 中读取数据时我们其实希望共享内存因为 worker 进程要高效传递数据但在特征工程逻辑里隔离数据往往更安全。5.3 坑三GPU张量无法直接用numpy()转回这是一个高频报错先.cuda()再.numpy()会提示“cant convert cuda:0 device type tensor to numpy”。很多人在 debug 时习惯打印张量然后随手调.numpy()。解决办法是先.cpu()再.numpy()但不要写成t.cpu().numpy()后还试图用这个结果改回原 GPU 张量因为cpu()默认会拷贝数据除非你用的是t.cpu()和原来的t不是同一块内存。另外如果 tensor 需要梯度直接.numpy()也会报错需要先.detach()。所以最稳妥的 debug 打印姿势是numpy_arr tensor.detach().cpu().numpy()这个坑在断点调试时特别烦我专门在代码里封装过一个to_numpy(t)小函数来统一处理。5.4 坑四requires_gradTrue的数据张量引发梯度回溯创建数据张量时如果你给它加上requires_gradTrue并且之后拿它去参与 loss 计算计算图会一路回溯到这个数据张量更新参数时把数据张量的grad也填上。很多人在自定义 loss 时临时把 label 张量设置成 requires_grad 想试探梯度结果发现模型参数不更新反而报“梯度是 None”或者“在 leaf Variable 上执行 in-place 操作”的错。我的建议是非参数张量一律不设置requires_gradTrue。参数统一用nn.Parameter包装数据张量保持默认的 False。如果确需手动梯度用tensor.requires_grad_(True)前确认它的 dtype 是浮点数否则直接报错。这类问题一般在写自定义 autograd Function 时才会碰到日常训练中尽量避免。5.5 坑五empty()创建的大张量导致内存爆炸很多人为了“性能”上来就torch.empty(100000, 1024, 1024)以为不初始化值会省内存。其实empty和zeros分配的内存总量是一样的只是少了一步填充。如果做图像分割或大规模特征存储真正该关心的是dtype和device以及是否需要 pin memory。比如 CPU 张量转成 GPU 时可能触发额外拷贝如果你要反复传输固定的输入数据用torch.empty(..., pin_memoryTrue)可以提速pin_tensor torch.empty((8, 3, 32, 32), dtypetorch.float32, pin_memoryTrue)但这个函数依赖device参数而且 pin_memory 只在 CPU 张量上有效。初学者不必过度优化先把张量形状、dtype、设备搞清楚再考虑内存布局。下面把常见坑整理成速查表方便以后排查症状常见原因解决方案初始化后 loss 巨大或 NaN用torch.Tensor(size)创建带脏数据张量改用torch.tensor、torch.emptyinitNumpy 数据被莫名修改from_numpy共享内存改用torch.tensor拷贝GPU 张量numpy()报错设备不匹配或需要梯度detach().cpu().numpy()模型参数不更新数据张量误开requires_grad参数用nn.Parameter数据用普通张量大张量创建卡顿内存分配 填充开销换 dtype、pin_memory、分块创建写在最后说了这么多其实想表达的核心是创建 Tensor 是 PyTorch 里最基础但最容易被忽视的技能。我自己早期写代码时习惯走到哪写到哪哪里需要数据就现场torch.tensor(...)结果代码里全是设备判断和 dtype 转换乱得不行。后来总结出一个习惯在每个训练脚本开头先把输入张量的 shape、dtype、device 定义成一个固定“模板”后面所有衍生张量都通过new_zeros或zeros_like从模板生成这样不仅代码干净迁移到 GPU 时也几乎不用改。最后再分享一个小技巧如果你不确定某个创建 API 会不会拷贝数据、会不会覆盖 dtype写个 3 行小 demo 打印一下tensor.dtype、tensor.device、tensor.stride()、以及改原来数据后再打印 tensor 看看有没有联动变化。这比翻文档快得多。Tensor 这个东西你越是把它当成普通数据结构越容易忽略它的内存语义反过来你每次创建时都多看一眼底层有没有共享、有没有拷贝踩坑的概率会小很多。

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

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

免费获取报价 →
↑