☰
PyTorch张量创建:torch.Tensor与torch.empty到底怎么选?
2026/10/2 13:24:35 网站建设 项目流程

有人跟我吐槽过,说PyTorch里创建张量的方法太多了,torch.Tensor()、torch.tensor()、torch.empty()、torch.zeros()、torch.ones()……看着就很晕,特别是torch.Tensor()和torch.empty(),一段代码里出现,感觉都差不多,到底有什么区别?为什么官方文档总在强调"推荐使用torch.empty"?这个问题要是没弄清楚,轻则代码跑出来的结果莫名其妙,重则模型训练直接不收敛,定位问题能抓狂好几天。

这篇文章我就把torch.Tensor()和torch.empty()这两个方法彻底讲明白。我做了多年PyTorch开发,带过不少新人,几乎每次培训都得专门讲一次张量创建的底层逻辑。所以我会结合真实项目踩坑经验,从内存分配机制、函数语义、实际执行效率到团队协作的代码规范,一点点拆开给你看。适合刚入门PyTorch的读者,也适合已经在用但不清楚细节的老手,看完至少能搞清楚:这两个API底层在做什么,什么时候用哪个,为什么"看起来一样"但官方却推荐另一个。

1. 为什么"创建张量"这件事值得专门研究

很多人觉得创建张量不就是"拿一块内存放数据"吗?对,但恰恰是这个"拿内存"的过程拉开了差距。张量是深度学习里所有数据流动的载体,它的创建方式直接影响计算图的构建、内存占用、执行速度,甚至模型的确定性。我从一个实际线上事故说起。

1.1 从一次推理结果乱跳的bug说起

有次项目上线前做压力测试,发现同一个输入样本,连续预测几次,结果都不完全一样。模型权重没变,输入没变,为什么结果会变?一开始怀疑是随机种子没设好,查了一圈发现问题出在预处理环节:有人用torch.Tensor(input_list)把数据转成张量,然后把结果直接传进模型。本来这没问题,但如果这个input_list的某个位置是空值或者没被正确填充,torch.Tensor()在转换时并不会主动帮你把缺失的数据填成0,而是保留底层内存里原来的垃圾数值。这些垃圾值在不同次运行中可能一样也可能不一样,推理结果自然就飘了。

那次事故之后,我要求团队所有人在项目里统一张量创建方式的规范:明确区分"由现有数据构造张量"和"分配一块新内存"两个动作。这就是torch.Tensor()和torch.empty()最本质的分界线。

1.2 现代深度学习框架里的张量到底是什么

要理解这两个方法,得先看一下张量在内存里长什么样。张量本质上是一个多维数组,包含一个数据指针、一个形状、一个步长、一个数据类型(dtype),还有一个设备信息(CPU/GPU)。创建张量可以拆成两步:分配一块内存,往内存里填东西。这两步可以一起来,也可以分开做。

  • torch.zeros():分配内存,并且把所有位置填成0。
  • torch.tensor(data):分配内存,把data里的值拷贝进去。
  • torch.empty(shape):只分配内存,不填任何东西,内存里原来的数值是什么,张量里就是什么。

而torch.Tensor()这个看起来很简单的调用,其实做了两步中的哪一步,取决于你传了什么参数。这就是后续所有混乱的根源。

2.torch.Tensor():一个带历史包袱的构造器

torch.Tensor是PyTorch里的一个类,也是torch.FloatTensor的别名。你可能经常看到有人写torch.Tensor(3, 4)来创建张量,或者用torch.Tensor([1, 2, 3])来转换数据。同一个名字,传的形状和传的列表,结果完全不一样,它其实是一张"变形脸"。

2.1 三种调用方式背后的不同行为

方式一:传形状参数

import torch # 创建了一个2x3的张量 a = torch.Tensor(2, 3) print(a)

这个用法和torch.empty(2, 3)的行为几乎一样,分配了一块2x3的存储空间,里面的值是随机垃圾数据。你没看错,torch.Tensor(2, 3)不会自动把值初始化为0。很多人初次接触时会以为它会填0,结果打印出来之后看到了诡异的数字。

方式二:传一个Python序列

b = torch.Tensor([1, 2, 3]) print(b)

这个用法和torch.tensor([1, 2, 3])看起来差不多,结果也确实一样,都是把列表里的数值拿过来。但它有一个非常隐晦的区别:类型是固定的torch.float32。假设你写torch.Tensor([1, 2, 3]),得到的是一个浮点张量。如果直接用torch.tensor([1, 2, 3]),它会根据输入推断类型,得到的是整数张量int64。更麻烦的是,如果你不小心把类型转换写得太隐晦,比如一个包含大整数的列表:

c = torch.Tensor([999999999999])

得到的结果会因为是float32而丢失精度,变成1e+12,这个坑项目里经常出现。

方式三:不传任何参数

d = torch.Tensor() print(d)

这会创建一个空的、形状为torch.Size([])的张量,相当于一个标量占位符,但这种写法在实际项目里几乎不会用到,因为后面没法接运算。大家统一用torch.empty(0)或者torch.tensor(0)就好。

为了直观,我把三种行为整理在表格里:

调用方式行为等价API数据类型初始化
torch.Tensor(2, 3)分配2x3内存torch.empty(2, 3)固定torch.float32不初始化,内存随机值
torch.Tensor([1, 2])将列表数据拷贝到新张量torch.tensor([1.0, 2.0])固定torch.float32用列表数据初始化
torch.Tensor()创建空张量torch.empty(0)固定torch.float32不初始化

2.2 为什么说它是历史遗留的别名

PyTorch早期大量沿用了旧版Torch的设计风格,torch.Tensor就是用来直接创建浮点张量的。后来框架越来越完善,官方推荐使用torch.tensor()替代序列转换功能,使用torch.empty()替代形状分配功能。但为了兼容老代码,torch.Tensor类一直保留,并且在某些内部实现里还在用。所以它不是"错误"的API,只是语义不够清晰,一把钥匙开了两扇门,你永远不知道自己打开的是哪一扇。

我见过不少教科书和网络博客里直接用torch.Tensor(3, 4)做例子,确实会误导新人。另一种坑是老代码里写的是torch.Tensor(5)表示创建5个元素的未初始化张量,但后来有人改成torch.Tensor([5])想看结果,却创建了一个包含数字5的张量。这种"改了半个参数就变语义"的设计,对团队维护极其不友好。

2.3 当你不小心把Tensor当Function用

还有一个更隐蔽的问题:torch.Tensor是一个类,不是普通函数。当你在代码里写torch.Tensor(...),实际上是在调用类的构造函数。这意味着如果你需要把它作为某个高阶函数里的参数,可能会踩到"在实例化和构造之间混淆"的诡异错误。比如你想用torch.Tensor来复制一遍数据,但不小心传了shape参数,你会拿到一个垃圾张量,而不是原始数据的拷贝。规范一点的代码里,除非你非常明确自己就是要创建未初始化的float32张量,否则建议完全避开torch.Tensor()作为主动调用对象。

3.torch.empty():分配内存的极简方案

torch.empty这个函数的设计目标非常简单:给你一个指定形状的张量,不保证里面的值是什么。它解耦了"分配内存"和"初始化数据"两件事,让你可以根据需要后续再填数据。

3.1 未初始化内存究竟是什么概念

这里需要说说操作系统层面的内存分配。我们用torch.empty(1000)申请内存,PyTorch会在堆上申请一块空间。这块空间可能之前被其他变量用过,里面残留各种二进制数据。PyTorch为了性能不会主动清理它,因为清零是一个耗时操作。张量拿到这块空间后,里面的数值直接读出来,就是"垃圾值"。

举一个生活化的例子:你去酒店开房,房间钥匙交给你,但床单上可能有之前客人的头发,浴巾也可能是湿的,除非你要求客房服务重新打扫,否则你不会知道房间里是什么状态。torch.empty就是"不打扫直接给你钥匙",torch.zeros是"先全部换成新毛巾再给你"。

所以下面的代码:

import torch e = torch.empty(3) print(e)

理论上每次运行可能打印出完全不同的数字,甚至上次运行留下的张量数据。如果你在初始化权重时用了这样的张量而没有及时覆盖,训练过程就会充满不确定性。

3.2torch.empty的核心参数逐个拆解

torch.empty最完整的调用签名是:

torch.empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False)

重点理解几个参数:

  • *size:可以传多个整数,比如torch.empty(2, 3),也可以传一个元组,比如torch.empty((2, 3))。两种写法等价。
  • dtype:指定张量数据类型,默认torch.float32。你可以明确指定torch.float64、torch.int32、torch.bool等。
  • device:指定设备,CPU还是GPU。在分布式或多卡训练时尤为重要。
  • layout:指定张量在内存中的排布方式,常见的是默认的torch.strided。稀疏张量会用到torch.sparse_coo,但普通张量创建很少改这个参数。
  • requires_grad:是否记录梯度。如果要在神经网络里自定义一个需要优化的参数,可以设成True。

举个例子:

x = torch.empty(4, 5, dtype=torch.float64, device='cpu', requires_grad=True) print(x.shape, x.dtype)

这个张量形状是4x5,数据类型float64,可以求梯度。但注意,如果后续没有往x.data里填值,计算出来的梯度也是无意义的。

3.3 为什么不推荐用torch.empty之后直接参与运算

因为未初始化内存里的数值可能是nan、inf或者极端大数。如果直接把这个张量丢进神经网络,哪怕只是一次矩阵乘法,也可能导致梯度爆炸。比如:

w = torch.empty(3, 3) y = torch.matmul(w, x)

如果w里碰巧藏着非常大的数,y的值会飞出天外。所以实际工程里,torch.empty往往是和后续初始化逻辑成对出现的,比如配合torch.nn.init模块来填充权重:

weight = torch.empty(64, 32) torch.nn.init.kaiming_uniform_(weight, a=math.sqrt(5))

这里用torch.empty先分好内存,再用初始化器覆盖所有值,一步到位还不浪费性能。这种方式在源码里很常见。

4.torch.Tensor()与torch.empty()的底层对比与性能考量

把二者并列放在一起看,很多之前模糊的地方就清楚了。它们的共同点是:当传入形状时都会分配未初始化内存。区别在于torch.Tensor还承担了"从序列构造数据"的工作,并且数据类型被锁死为float32;torch.empty则更纯粹,只负责分配内存,其他选项全部开放。

4.1 直观的差异表格

我整理了一个对比表,供你在项目里随时查阅:

对比维度torch.Tensor(shape)torch.empty(shape)
是否初始化内存否否
默认数据类型torch.float32,且不可修改torch.float32,可通过dtype修改
支持传入现有数据支持不支持
支持device参数不支持支持
支持requires_grad不支持支持
可读性语义模糊,可能是"构造数据"也可能是"分配空间"明确标记"只分配空间"
官方推荐度不推荐用于新代码推荐用于分配空张量

看到没,torch.Tensor连device参数都不能直接传,如果你在GPU上想创建一个大张量,还得先建在CPU再转到GPU,白白多一次拷贝。torch.empty则可以直接指定device='cuda',一步到位。

4.2 执行速度测试:empty并不慢,反而省掉初始化时间

你可能想问:分配未初始化的内存,比分配并初始化快多少?我在一块普通CPU上做了个粗略测试:

import torch import time for _ in range(5): t0 = time.time() a = torch.Tensor(1024, 1024) t1 = time.time() b = torch.empty(1024, 1024) t2 = time.time() c = torch.zeros(1024, 1024) t3 = time.time() print(f"Tensor: {t1 - t0:.6f}s, empty: {t2 - t1:.6f}s, zeros: {t3 - t2:.6f}s")

实测下来torch.Tensor和torch.empty速度差不太多,因为两者都不做数据填充,只是申请内存。而torch.zeros需要把整块区域清零,耗时明显更高。这个测试说明,如果后续还要用初始化函数覆盖所有数据,用torch.empty先拿内存是一个合理的性能优化选择。但注意,性能差距在超大张量上才明显,平时写代码不要为了这点性能牺牲可读性。

4.3 可读性与团队协作价值

代码是写给人看的,不是写给机器看的。torch.empty这个名字读起来就是"给我一块空的内存",意图非常明确。torch.Tensor这个名字太泛了,看的人还得结合上下文猜:这句是要转换数据,还是创建空张量?如果团队没有代码审查,很容易在不同地方出现两种风格,非常别扭。

我个人的项目规范是:

  • 转换现有数据用torch.tensor(...)或torch.from_numpy(...)。
  • 创建全0张量用torch.zeros(...)。
  • 创建未初始化张量用torch.empty(...)。
  • 禁止使用torch.Tensor(...)来初始化。

这套规范落地之后,新人上手代码库的速度明显变快,因为每个函数名都在告诉你它要干什么。

5. 张量创建中的真实踩坑与排查思路

这部分我分享几个实际踩过的坑和排查过程,帮你避开类似的坑。

5.1 第一个坑:torch.Tensor转换整数列表导致类型错误

有一次朋友发来一段代码,用torch.Tensor([1, 2, 3])创建标签张量,然后跟另一批int64的标签做比较,结果怎么都不相等。排查了很久才发现torch.Tensor得到的是float32,而torch.tensor([1, 2, 3])得到的是int64。两个张量在数值上一样,但数据类型不同,直接==比较时某些情况下会返回False。

解决方案很简单:

labels = torch.tensor([1, 2, 3], dtype=torch.long)

这提醒我们:涉及整数索引、标签、mask的时候,一定要显式指定dtype,不能依赖默认行为。

5.2 第二个坑:未初始化张量带来的梯度问题

我调试过一段自定义Layer的代码,里面直接写:

def forward(self, x): weight = torch.Tensor(x.shape[1], 128).cuda() return torch.mm(x, weight)

问题就是weight根本没有初始化,里面全是垃圾值。第一次前向计算正常,第二次可能就出现nan。这个问题最难排查的点在于:日志里每次出现的异常数值都不一样,看起来像随机bug,其实只是底层内存没被覆盖。正确写法是:

weight = torch.empty(x.shape[1], 128, device=x.device) torch.nn.init.kaiming_uniform_(weight, a=math.sqrt(5))

或者干脆把它注册成模型参数:

self.weight = torch.nn.Parameter(torch.empty(x.shape[1], 128)) torch.nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))

所有对权重有初始化的需求都建议交给torch.nn.init,它支持的各种统一初始化方法,能按照正确分布生成初始值。

5.3 第三个坑:CPU和GPU内存分配带来的设备不匹配

torch.Tensor不接受device参数,所以如果代码这样写:

data = torch.Tensor(10, 10).cuda()

没问题,先CPU建好再转移到GPU。但如果GPU显存已经紧张,巨型张量的转移会临时多占一份CPU内存。现代写法是:

data = torch.empty(10, 10, device='cuda')

直接在某块GPU设备上分配,省掉中间拷贝,也更符合"尽量在目标设备上创建张量"的原则。

5.4 排查这类问题的一般思路

当你发现模型输出出现随机不稳定的nan,或者同样的输入多次推理结果不一样时,可以按下面顺序查:

  1. 查看数据加载部分是否存在未完全填充的张量,比如用torch.empty创建后直接参与计算。
  2. 查看初始化模块是否覆盖了所有自定义权重,不要漏掉偏置项。
  3. 查看有没有写torch.Tensor(shape)然后又直接对它做乘法/加法。
  4. 在关键位置打印tensor.isnan().sum()和tensor.mean(),观察异常是否在某个节点之后才出现。

这些排查经验在PyTorch项目里非常有用,胜过你把模型结构改来改去。

6. 我的建议:项目里到底该用哪个方法

现在问到底用哪个,答案取决于你的意图,而不在于"哪个更高级"。

6.1 三种典型场景的选择标准

第一,如果你手头有一个Python列表、NumPy数组或者已经存在的张量,想转成PyTorch张量继续运算,用torch.tensor()或者torch.from_numpy()。绝对不要用torch.Tensor(),因为它固定float32的设定会让整数类型转错。

arr = np.array([1, 2, 3]) t = torch.from_numpy(arr) # 与arr共享内存

第二,如果你需要一个全0、全1或者服从某种初始化分布的张量,直接用对应的torch.zeros、torch.ones或者torch.nn.init函数。

第三,如果你确定后面会直接往这个张量里填数据,比如你在实现一个需要先分配缓冲区的算法,或者准备用优化器更新参数,用torch.empty()会非常合适。

buffer = torch.empty((batch_size, seq_len, hidden_size), device='cuda') # 后续填充 buffer.copy_(some_data)

6.2 旧代码迁移与新规范落地

如果你手头有大量老代码还在用torch.Tensor,我的建议是先整理出所有调用点,按照调用参数分类:

  • 传了序列:改成torch.tensor(...),同时检查数据类型。
  • 传了形状:改成torch.empty(...),同时显式补充dtype。
  • 传了0个参数:改成torch.empty(0)或torch.tensor([])。

迁移过程中务必跑一遍全量测试,因为即使数值一样,dtype、device的变化也可能引发连锁反应。我经历过一次这样的重构,排查出的隐藏bug比预期多得多,但也因此把代码库里很多"试探性写法"替换成了统一风格。

6.3 最后的个人体会

分享一个我自己坚持很多年的习惯:不管用哪种方式创建张量,创建后第一件事就是写注释,说明这个张量在后续计算里扮演的角色以及预期数据类型。比如:

# 为每个序列创建attention mask,形状[seq_len, seq_len],bool类型 mask = torch.zeros(seq_len, seq_len, dtype=torch.bool, device=x.device)

注释不要写"创建张量"这种废话,而是写"为什么放这里""不初始化后面会怎么填"。这样做过两三年后,你维护老代码的速度会远超没有注释的版本。竖着看一遍张量创建的调用,基本能快速判断哪里有内存相关的问题。

PyTorch的张量创建看起来简单,但细节决定了模型稳定性和迭代效率。torch.Tensor()和torch.empty()的区别不只是"一个可以传数据,一个只能传形状",更是"你是在表达'用它来转换数据'还是'我要一片内存设备'"。理解到了这一层,你就不会在初始化上栽跟头,也能写出让队友一眼看懂的可维护代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询