有人跟我吐槽过说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而丢失精度变成1e12这个坑项目里经常出现。方式三不传任何参数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, *, outNone, dtypeNone, layouttorch.strided, deviceNone, requires_gradFalse, pin_memoryFalse)重点理解几个参数*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, dtypetorch.float64, devicecpu, requires_gradTrue) 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, amath.sqrt(5))这里用torch.empty先分好内存再用初始化器覆盖所有值一步到位还不浪费性能。这种方式在源码里很常见。4.torch.Tensor()与torch.empty()的底层对比与性能考量把二者并列放在一起看很多之前模糊的地方就清楚了。它们的共同点是当传入形状时都会分配未初始化内存。区别在于torch.Tensor还承担了从序列构造数据的工作并且数据类型被锁死为float32torch.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则可以直接指定devicecuda一步到位。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(fTensor: {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], dtypetorch.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, devicex.device) torch.nn.init.kaiming_uniform_(weight, amath.sqrt(5))或者干脆把它注册成模型参数self.weight torch.nn.Parameter(torch.empty(x.shape[1], 128)) torch.nn.init.kaiming_uniform_(self.weight, amath.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, devicecuda)直接在某块GPU设备上分配省掉中间拷贝也更符合尽量在目标设备上创建张量的原则。5.4 排查这类问题的一般思路当你发现模型输出出现随机不稳定的nan或者同样的输入多次推理结果不一样时可以按下面顺序查查看数据加载部分是否存在未完全填充的张量比如用torch.empty创建后直接参与计算。查看初始化模块是否覆盖了所有自定义权重不要漏掉偏置项。查看有没有写torch.Tensor(shape)然后又直接对它做乘法/加法。在关键位置打印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), devicecuda) # 后续填充 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, dtypetorch.bool, devicex.device)注释不要写创建张量这种废话而是写为什么放这里不初始化后面会怎么填。这样做过两三年后你维护老代码的速度会远超没有注释的版本。竖着看一遍张量创建的调用基本能快速判断哪里有内存相关的问题。PyTorch的张量创建看起来简单但细节决定了模型稳定性和迭代效率。torch.Tensor()和torch.empty()的区别不只是一个可以传数据一个只能传形状更是你是在表达用它来转换数据还是我要一片内存设备。理解到了这一层你就不会在初始化上栽跟头也能写出让队友一眼看懂的可维护代码。