1. 从零理解“创建Tensor”到底在做什么
刚接触深度学习框架的人,第一次看到torch.tensor([1, 2, 3])这行代码时,心里多半会嘀咕:这不就是个数组吗,为什么非要叫 Tensor?我当初也是这么想的,直到后来在调试一个图像分类模型时,因为一个 Tensor 的形状和数据类型没对齐,整整卡了一个下午,才真正意识到“创建 Tensor”这件事远没有看上去那么简单。它不是一个孤立的 API 调用,而是整个计算图构建的起点,是数据从现实世界进入数学世界的入口。
所谓“创建 Tensor”,本质上是在内存中申请一块连续或非连续的区域,按照指定的数据类型、设备位置和维度结构,把原始数据组织成框架能够识别和运算的张量对象。它解决的问题是:让标量、向量、矩阵乃至高维数据,都能以统一的接口参与自动求导、GPU 加速和分布式计算。适合阅读这篇内容的人,包括刚入门深度学习的新手、从 NumPy 迁移过来的开发者,以及需要排查张量相关 Bug 的工程人员。不管你用的是 PyTorch、TensorFlow 还是其他框架,创建 Tensor 的核心逻辑是相通的,只是 API 风格不同。
我在实际项目里见过太多因为创建方式不当导致的隐蔽问题:有人用 Python 列表直接创建 Tensor 导致训练速度慢了一半,有人忘了指定dtype造成精度损失,还有人把需要求导的张量和不需要求导的张量混在一起,结果反向传播时报错。这些问题追根溯源,都出在“创建”这一步。所以我想把这件事从头到尾讲透,不只是告诉你有哪些函数,更要讲清楚每个参数背后的取舍逻辑。
2. 创建Tensor的几种核心路径与选型逻辑
2.1 从已有数据创建:最直观但也最容易踩坑
从 Python 列表或 NumPy 数组创建 Tensor 是最常见的方式,torch.tensor()和torch.from_numpy()都能做到,但两者的行为差异很大。torch.tensor()会复制数据,也就是说它和原始数据不再共享内存;而torch.from_numpy()是共享内存的,修改其中一个,另一个也会跟着变。这个区别在实际开发中非常关键。
我举个例子,假设你有一个 NumPy 数组arr,用torch.from_numpy(arr)创建了 Tensort。如果你后续对t做了原地操作,比如t.add_(1),那么arr的值也会被改变。这在数据预处理流水线里可能引发难以追踪的 Bug,因为你的原始数据被悄悄修改了。反过来,如果你明确需要共享内存来节省显存,那from_numpy就是更优选择。
import torch import numpy as np # 方式一:复制数据,安全但占内存 arr = np.array([1, 2, 3]) t1 = torch.tensor(arr) t1.add_(1) print(arr) # [1 2 3],原始数据不变 # 方式二:共享内存,高效但需谨慎 t2 = torch.from_numpy(arr) t2.add_(1) print(arr) # [2 3 4],原始数据被修改注意:
torch.tensor()总是复制数据,而torch.from_numpy()和torch.as_tensor()在可能的情况下会共享内存。选择哪个取决于你是否需要保留原始数据的独立性。
还有一个细节是数据类型推断。torch.tensor([1, 2, 3])默认会推断为int64,而torch.tensor([1.0, 2.0, 3.0])会推断为float32。如果你混合了整数和浮点数,比如[1, 2.0],它会统一推断为float32。这个自动推断在简单场景下很方便,但在需要精确控制精度的场景下就是隐患。我一般建议显式指定dtype,比如torch.tensor([1, 2, 3], dtype=torch.float32),这样代码的可读性和可维护性都更好。
2.2 从形状创建:初始化策略决定训练成败
很多时候我们并不是从已有数据创建 Tensor,而是先定义一个形状,然后用特定规则填充它。torch.zeros()、torch.ones()、torch.empty()、torch.randn()、torch.arange()这些函数就是干这个的。它们看起来简单,但用错了初始化方式,模型可能根本训练不起来。
torch.empty()是最快的,因为它只申请内存,不初始化值。但里面的值是未定义的,可能是任意垃圾数据。如果你用torch.empty()创建了一个 Tensor 然后直接参与计算,结果就是不可预测的。我见过有人在写自定义层时用empty创建权重,忘了后续初始化,导致损失一直是 NaN。所以除非你确定接下来会立即覆盖所有元素,否则不要用empty。
torch.zeros()和torch.ones()适合创建偏置项或掩码。torch.randn()生成标准正态分布,是权重初始化的常用选择。torch.arange()则常用于生成索引序列或位置编码。这里有个经验:如果你要创建一个用于累加的变量,用zeros;如果你要创建一个用于乘法缩放的变量,用ones;如果你要初始化神经网络权重,用randn并乘以一个缩放因子。
# 创建形状为 (3, 4) 的全零张量 zeros_t = torch.zeros(3, 4) # 创建形状为 (2, 3) 的全一张量,指定数据类型 ones_t = torch.ones(2, 3, dtype=torch.float64) # 创建形状为 (5,) 的正态分布随机张量 randn_t = torch.randn(5) # 创建从 0 到 9 的整数序列 arange_t = torch.arange(0, 10, step=1) # 创建形状为 (2, 2) 的未初始化张量(危险,仅作演示) empty_t = torch.empty(2, 2)提示:
torch.empty()创建后如果直接打印,你可能会看到一些奇怪的数值,这是正常的,因为那块内存之前可能被其他数据占用过。不要依赖它的初始值。
2.3 从设备与精度维度选择:CPU、GPU与dtype的三角关系
创建 Tensor 时,device和dtype是两个必须同时考虑的参数。默认情况下,Tensor 创建在 CPU 上,数据类型是float32。但在实际训练中,我们通常需要把 Tensor 放到 GPU 上,并且可能需要float16或bfloat16来节省显存。
这里有个常见的误区:很多人先创建 CPU Tensor,然后再调用.cuda()或.to(device)迁移到 GPU。这样做虽然可行,但效率不高,因为数据先在 CPU 内存中分配,再拷贝到显存,多了一次搬运。更好的做法是在创建时就指定device,比如torch.zeros(3, 4, device='cuda'),这样直接在显存中分配,省去了拷贝开销。
# 不推荐:先创建再迁移 t = torch.zeros(1000, 1000) t = t.to('cuda') # 推荐:创建时直接指定设备 t = torch.zeros(1000, 1000, device='cuda') # 同时指定设备和精度 t = torch.randn(256, 128, device='cuda', dtype=torch.float16)关于dtype的选择,我整理了一个简单的对照表,方便你根据场景快速决策:
| 数据类型 | 典型用途 | 显存占用 | 精度风险 |
|---|---|---|---|
| float32 | 默认训练精度 | 4字节 | 低 |
| float16 | 混合精度训练 | 2字节 | 中,需配合缩放 |
| bfloat16 | 大模型训练 | 2字节 | 中,动态范围大 |
| int64 | 索引、标签 | 8字节 | 无 |
| bool | 掩码、条件判断 | 1字节 | 无 |
注意:在混合精度训练中,模型权重通常保持 float32,而前向传播的中间激活值用 float16。创建 Tensor 时要明确它在流水线中的角色,不要一刀切地全部用 float16。
3. 创建Tensor时的形状操作与内存布局
3.1 形状参数的正确传递方式
创建 Tensor 时,形状的传递方式有两种:可变参数和元组。torch.zeros(3, 4)和torch.zeros((3, 4))是等价的,但前者更常用。不过当形状是动态计算出来的时候,用元组会更方便,比如torch.zeros(shape),其中shape是一个元组变量。
这里有个容易忽略的点:torch.tensor()和torch.zeros()对形状参数的处理不同。torch.tensor([1, 2, 3])接收的是数据本身,形状是从数据推断出来的;而torch.zeros(3)接收的是形状,创建的是长度为 3 的全零向量。如果你把torch.zeros([3])写成torch.zeros(3),结果一样;但如果你写torch.tensor(3),它会创建一个标量张量,而不是长度为 3 的向量。这个区别在调试时经常让人困惑。
# 创建标量张量 scalar = torch.tensor(3) print(scalar.shape) # torch.Size([]) # 创建长度为 3 的向量 vector = torch.tensor([3]) print(vector.shape) # torch.Size([3]) # 创建形状为 (3,) 的全零向量 zeros_vec = torch.zeros(3) print(zeros_vec.shape) # torch.Size([3])提示:当你从变量传递形状时,建议统一用元组形式,比如
torch.zeros(tuple(shape_list)),避免因为参数展开导致的意外行为。
3.2 内存布局:连续与非连续的隐形成本
Tensor 在内存中的布局分为连续和非连续两种。默认情况下,通过torch.zeros()、torch.ones()等创建的 Tensor 是连续的,也就是元素按行优先顺序依次排列。但当你对 Tensor 进行转置、切片等操作后,它可能变成非连续的,这时候再创建新的 Tensor 或者做某些运算,框架可能需要先做一次内存拷贝。
这个细节在大多数时候不影响正确性,但会影响性能。我曾在处理一个大型特征矩阵时,因为频繁对非连续 Tensor 做view()操作,导致训练速度明显下降。后来改成先.contiguous()再view(),速度就恢复了。所以如果你在创建 Tensor 后要做形状变换,最好先确认它的内存布局。
# 创建一个连续张量 t = torch.randn(4, 4) print(t.is_contiguous()) # True # 转置后变成非连续 t_transposed = t.t() print(t_transposed.is_contiguous()) # False # 强制连续化 t_contiguous = t_transposed.contiguous() print(t_contiguous.is_contiguous()) # True注意:
view()只能用于连续 Tensor,而reshape()可以处理非连续 Tensor,但内部可能会触发拷贝。如果你对性能敏感,优先用view()并确保 Tensor 是连续的。
3.3 广播机制对创建形状的启示
创建 Tensor 时,形状不仅要考虑当前操作,还要考虑后续的广播。广播机制允许不同形状的 Tensor 进行运算,但前提是它们的维度从右往左对齐后,每个维度要么相等,要么其中一个为 1。如果你创建的 Tensor 形状不满足广播规则,运算时就会报错。
举个例子,你想把一个形状为(3, 1)的列向量和一个形状为(1, 4)的行向量相加,得到(3, 4)的矩阵。这时候创建 Tensor 时就要注意形状的写法。如果你写成(3,)和(4,),它们无法广播成(3, 4),因为从右对齐后第一个维度是 3 和 4,不相等且都不为 1。正确的做法是显式地写成(3, 1)和(1, 4)。
# 正确的广播形状 col = torch.randn(3, 1) row = torch.randn(1, 4) result = col + row print(result.shape) # torch.Size([3, 4]) # 错误的广播形状 a = torch.randn(3) b = torch.randn(4) # a + b 会报错,因为形状不兼容提示:在创建用于广播的 Tensor 时,养成显式写出所有维度的习惯,不要依赖自动推断。这样代码的意图更清晰,也更容易排查形状错误。
4. 创建Tensor的完整实操流程与参数计算
4.1 一个图像分类任务中的Tensor创建实录
假设我们要构建一个简单的图像分类模型,输入是 32x32 的 RGB 图像,批量大小为 64。我们需要创建以下几类 Tensor:输入数据、权重、偏置、标签。下面我一步步展示创建过程,并解释每个参数的计算依据。
首先,输入数据的形状是(batch_size, channels, height, width),即(64, 3, 32, 32)。我们通常用torch.randn()来模拟输入数据,因为真实数据会从数据加载器中获取。
batch_size = 64 channels = 3 height = 32 width = 32 # 模拟输入图像数据 inputs = torch.randn(batch_size, channels, height, width, device='cuda') print(inputs.shape) # torch.Size([64, 3, 32, 32])接下来是权重。假设第一层是卷积层,输入通道 3,输出通道 16,卷积核大小 3x3。权重的形状是(out_channels, in_channels, kernel_size, kernel_size),即(16, 3, 3, 3)。偏置的形状是(out_channels,),即(16,)。
out_channels = 16 kernel_size = 3 # 创建卷积核权重,使用 Kaiming 初始化 weights = torch.randn(out_channels, channels, kernel_size, kernel_size, device='cuda') weights = weights * (2.0 / (channels * kernel_size * kernel_size)) ** 0.5 # 创建偏置,初始化为零 bias = torch.zeros(out_channels, device='cuda')这里的缩放因子(2.0 / fan_in) ** 0.5是 Kaiming 初始化的简化形式,其中fan_in = channels * kernel_size * kernel_size = 3 * 3 * 3 = 27。计算一下:2.0 / 27 ≈ 0.074,开方后约等于0.272。这个缩放是为了让前向传播时每层的输出方差保持一致,避免梯度消失或爆炸。
标签的形状是(batch_size,),数据类型是int64,因为分类任务用的是交叉熵损失,需要整数标签。
# 创建标签,10 分类任务 num_classes = 10 labels = torch.randint(0, num_classes, (batch_size,), device='cuda') print(labels.shape) # torch.Size([64]) print(labels.dtype) # torch.int64注意:
torch.randint()的上界是开区间,所以torch.randint(0, 10, ...)生成的是 0 到 9 的整数。如果你写成torch.randint(0, 9, ...),就永远得不到标签 9,这是一个非常隐蔽的错误。
4.2 参数计算:为什么这些数字是这样来的
在上面的例子中,每个参数都不是随便写的。批量大小 64 是显存和梯度稳定性的折中:太小则梯度噪声大,太大则显存不够。通道数 3 是 RGB 图像的固定值。图像尺寸 32x32 是常见的小型图像数据集标准。
卷积核数量 16 是一个经验值,通常从较小的数开始,随着网络加深逐渐翻倍。卷积核大小 3x3 是最常用的,因为它在感受野和计算量之间取得了很好的平衡。两个 3x3 卷积堆叠的感受野相当于一个 5x5 卷积,但参数更少,非线性更强。
权重初始化中的fan_in计算方式是:输入通道数乘以卷积核面积。对于第一层,fan_in = 3 * 3 * 3 = 27。缩放因子sqrt(2 / fan_in)适用于 ReLU 激活函数,这是 Kaiming 初始化的推荐做法。如果你用的是 Sigmoid 或 Tanh,应该用 Xavier 初始化,缩放因子是sqrt(1 / fan_in)。
# Kaiming 初始化(适用于 ReLU) fan_in = channels * kernel_size * kernel_size kaiming_scale = (2.0 / fan_in) ** 0.5 # Xavier 初始化(适用于 Sigmoid/Tanh) xavier_scale = (1.0 / fan_in) ** 0.5 print(f"fan_in: {fan_in}") print(f"Kaiming scale: {kaiming_scale:.4f}") print(f"Xavier scale: {xavier_scale:.4f}")提示:现代框架通常提供了
torch.nn.init模块,里面封装了各种初始化方法,比如kaiming_normal_、xavier_uniform_等。直接调用这些函数比自己手算缩放因子更可靠,也更不容易出错。
4.3 从数据加载到Tensor创建的完整链路
在实际项目中,Tensor 的创建往往不是孤立的,而是嵌入在数据加载和预处理流水线中。一个典型的链路是:原始数据(如图片文件)→ 解码为 NumPy 数组 → 转换为 Tensor → 归一化 → 放到 GPU → 送入模型。
每一步都有创建 Tensor 的时机和方式选择。比如在解码阶段,你可能用torch.from_numpy()来避免拷贝;在归一化阶段,你可能用torch.tensor()来创建均值和标准差张量;在放到 GPU 阶段,你可能用.to(device, non_blocking=True)来异步传输。
# 模拟完整链路 import numpy as np # 假设从文件读取到 NumPy 数组 raw_image = np.random.randint(0, 256, (32, 32, 3), dtype=np.uint8) # 转换为 Tensor,共享内存 image_tensor = torch.from_numpy(raw_image).float() # 调整维度顺序:HWC -> CHW image_tensor = image_tensor.permute(2, 0, 1) # 归一化 mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1) std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1) image_tensor = (image_tensor / 255.0 - mean) / std # 增加批量维度 image_tensor = image_tensor.unsqueeze(0) # 异步传输到 GPU image_tensor = image_tensor.to('cuda', non_blocking=True) print(image_tensor.shape) # torch.Size([1, 3, 32, 32])注意:
non_blocking=True只在数据从锁页内存(pinned memory)传输到 GPU 时才能真正异步。如果原始数据在普通内存中,这个参数不会带来性能提升,反而可能引入同步问题。要使用锁页内存,需要在 DataLoader 中设置pin_memory=True。
5. 常见问题与排查技巧实录
5.1 形状不匹配:最常见的报错与解决思路
形状不匹配是创建 Tensor 后最常遇到的问题。报错信息通常是RuntimeError: shape '[a, b]' is invalid for input of size c或者The size of tensor a (x) must match the size of tensor b (y)。排查这类问题的第一步是打印所有相关 Tensor 的.shape,确认每个维度的含义。
我习惯在关键步骤后加一行print(x.shape),虽然看起来笨,但比调试器还管用。另一个技巧是用torch.Size的元组比较,比如assert x.shape == (64, 3, 32, 32), f"Unexpected shape: {x.shape}",这样一旦形状不对,报错信息会直接告诉你期望值和实际值。
| 报错信息 | 常见原因 | 解决方法 |
|---|---|---|
| shape invalid for input of size | view/reshape 的目标形状与元素总数不符 | 检查元素总数是否一致 |
| size of tensor a must match b | 广播规则不满足 | 调整形状使维度对齐 |
| expected scalar type Float but found Double | dtype 不一致 | 显式指定 dtype 或统一转换 |
| expected device cuda but got cpu | 设备不一致 | 用 .to(device) 统一设备 |
提示:PyTorch 的报错信息通常会给出具体的维度数字,仔细读一遍往往就能定位问题。不要一看到报错就慌,先看数字。
5.2 数据类型陷阱:float32与float64的隐式转换
NumPy 默认的浮点类型是float64,而 PyTorch 默认是float32。当你用torch.from_numpy()从 NumPy 数组创建 Tensor 时,如果数组是float64,Tensor 也会是float64。然后你把它送入一个float32的模型,就会报类型不匹配的错误。
解决方法是显式转换:torch.from_numpy(arr).float()或者torch.tensor(arr, dtype=torch.float32)。我建议在数据加载的早期就统一转换为float32,避免后续到处做类型转换。
arr = np.array([1.0, 2.0, 3.0]) # 默认 float64 t = torch.from_numpy(arr) print(t.dtype) # torch.float64 # 转换为 float32 t = t.float() print(t.dtype) # torch.float32注意:
torch.tensor()在从 Python 列表创建时,如果列表里是浮点数,默认会推断为float32,而不是float64。这一点和 NumPy 不同,容易混淆。
5.3 设备不一致:CPU与GPU混用的典型错误
设备不一致的报错通常是RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!。这个问题的根源是有些 Tensor 在 CPU 上创建,有些在 GPU 上创建,运算时框架不知道该在哪里执行。
排查方法是检查所有参与运算的 Tensor 的.device属性。我通常会在模型的前向传播开头加一行assert x.device == next(self.parameters()).device,确保输入和模型在同一设备上。对于中间创建的 Tensor,比如掩码、位置编码等,也要确保它们和输入在同一设备上。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 创建时统一指定设备 x = torch.randn(64, 3, 32, 32, device=device) mask = torch.ones(64, 32, 32, device=device) position = torch.arange(0, 100, device=device) # 检查设备一致性 print(x.device, mask.device, position.device)提示:如果你在写一个可复用的模块,不要硬编码
'cuda',而是用device = x.device来动态获取输入所在的设备,然后基于它创建新的 Tensor。这样代码在 CPU 和 GPU 环境下都能运行。
5.4 内存溢出:创建大Tensor时的显存管理
创建大 Tensor 时,显存溢出是另一个常见问题。报错信息是RuntimeError: CUDA out of memory。这时候你需要计算一下 Tensor 的显存占用:元素数量乘以每个元素的字节数。比如torch.randn(10000, 10000, device='cuda')会占用10000 * 10000 * 4 = 400MB显存。如果你同时创建多个这样的大 Tensor,显存很快就满了。
解决方法包括:使用更小的批量大小、用float16代替float32、及时用del删除不再使用的 Tensor 并调用torch.cuda.empty_cache()。但要注意,empty_cache()只是释放缓存,不会释放正在使用的显存。
# 计算显存占用 def tensor_memory_mb(shape, dtype=torch.float32): num_elements = 1 for dim in shape: num_elements *= dim bytes_per_element = torch.tensor([], dtype=dtype).element_size() return num_elements * bytes_per_element / (1024 ** 2) print(tensor_memory_mb((10000, 10000))) # 381.47 MB print(tensor_memory_mb((10000, 10000), dtype=torch.float16)) # 190.73 MB注意:
torch.cuda.memory_allocated()可以查看当前已分配的显存,torch.cuda.max_memory_allocated()可以查看峰值显存。在调试显存问题时,这两个函数比nvidia-smi更精确,因为它们只统计 PyTorch 的分配。
6. 创建Tensor的进阶技巧与性能优化
6.1 就地操作与内存复用
创建 Tensor 时,如果能复用已有的内存,可以显著减少分配开销。PyTorch 提供了torch.empty_like()、torch.zeros_like()等函数,它们根据已有 Tensor 的形状和设备创建新 Tensor,但不会复制数据。这在循环中特别有用,比如在 RNN 的每个时间步创建隐藏状态。
# 根据已有张量创建同形状的新张量 x = torch.randn(64, 128, device='cuda') h = torch.zeros_like(x) # 形状和设备与 x 相同 print(h.shape, h.device) # torch.Size([64, 128]) cuda:0另一个技巧是用out=参数来指定输出 Tensor,避免创建新的。比如torch.add(a, b, out=c)会把结果写入c,而不是创建新的 Tensor。这在需要反复计算的场景下能减少内存分配次数。
提示:就地操作(如
add_())虽然省内存,但会破坏原始数据,影响自动求导。在需要梯度的 Tensor 上慎用就地操作,否则可能报RuntimeError: a leaf Variable that requires grad is being used in an in-place operation。
6.2 固定内存与异步传输
当数据需要从 CPU 传输到 GPU 时,使用锁页内存(pinned memory)可以加速传输。锁页内存是指不会被操作系统换出到磁盘的内存页,GPU 可以直接访问它,从而实现异步传输。在创建 Tensor 时,可以用pin_memory()方法把 Tensor 放到锁页内存中。
# 创建锁页内存 Tensor cpu_tensor = torch.randn(64, 3, 32, 32).pin_memory() # 异步传输到 GPU gpu_tensor = cpu_tensor.to('cuda', non_blocking=True)注意:锁页内存会占用更多系统内存,而且分配和释放的开销更大。不要把所有 Tensor 都放到锁页内存中,只对需要频繁传输的数据使用。
6.3 创建Tensor时的随机种子控制
在需要复现实验的场景下,创建随机 Tensor 时必须控制随机种子。PyTorch 的随机种子分为 CPU 和 GPU 两部分,需要分别设置。torch.manual_seed()设置 CPU 种子,torch.cuda.manual_seed_all()设置所有 GPU 的种子。
def set_seed(seed=42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) set_seed(42) t1 = torch.randn(3, 3) set_seed(42) t2 = torch.randn(3, 3) print(torch.equal(t1, t2)) # True提示:即使设置了种子,某些操作(如 CUDA 的原子操作)仍然可能引入非确定性。如果对复现性要求极高,还需要设置
torch.backends.cudnn.deterministic = True,但这会牺牲一些性能。
6.4 从文件或流中创建Tensor
除了从内存数据创建,Tensor 还可以从文件或流中直接读取。torch.from_file()可以从文件创建 Tensor,torch.frombuffer()可以从字节缓冲区创建。这些方法适合处理大型二进制数据,比如预训练的词向量文件。
# 从文件创建 Tensor(假设文件是 float32 二进制) # t = torch.from_file('data.bin', dtype=torch.float32, size=1000) # 从字节缓冲区创建 buffer = bytearray(16) t = torch.frombuffer(buffer, dtype=torch.float32) print(t.shape) # torch.Size([4])注意:
torch.frombuffer()创建的 Tensor 与原始缓冲区共享内存,缓冲区的生命周期必须覆盖 Tensor 的使用期,否则可能导致未定义行为。
7. 我在实际项目中的几点体会
创建 Tensor 这件事,看起来是深度学习里最基础的操作,但真正把它用对、用好,需要你对内存、设备、数据类型和计算图有清晰的认识。我踩过的最大的坑是在一个多任务模型里,不同任务的标签 Tensor 用了不同的 dtype,结果在计算损失时框架自动做了类型提升,导致显存翻倍。后来统一了所有标签的 dtype,问题才解决。
另一个体会是,不要害怕显式地写dtype和device。虽然代码看起来啰嗦了一点,但可读性和可维护性会好很多。尤其是在团队协作中,显式声明能避免很多“为什么你那边能跑我这边报错”的扯皮。
最后分享一个小技巧:如果你不确定一个操作会创建新的 Tensor 还是复用已有的,可以用id()函数查看对象的内存地址。如果地址变了,就是新创建的;如果没变,就是复用的。这个方法在排查内存泄漏时特别有用。
a = torch.randn(3) print(id(a)) b = a.view(3) print(id(b)) # 与 a 不同,view 创建了新对象但共享数据 c = a.add(1) print(id(c)) # 与 a 不同,add 创建了新对象和新数据