一:PyTorch 基础与张量操作
如何创建一个形状为(3,4)的全零张量,并指定数据类型为float32?¶
创建全零张量最直接的方式是使用 torch.zeros 工厂函数,它允许我们通过 dtype 参数显式地指定张量的数据类型。
import torch
# 创建一个形状为 (3, 4) 的张量,并用0填充,指定类型为32位浮点数
x = torch.zeros(3, 4, dtype=torch.float32)
print(x.shape) # torch.Size([3, 4])
print(x.dtype) # torch.float32
深入理解与最佳实践:
-
torch.zeros申请的内存并不会被立刻物理分配和归零,而是采用了“延迟初始化”或“写时复制”的机制,仅在第一次被访问时才会真正分配。这使得创建大尺寸张量非常高效。 -
与
torch.zeros类似的还有torch.ones和torch.empty。torch.empty不会对内存进行任何初始化,速度快但包含“脏数据”,使用时需要立即覆盖所有值,否则可能导致难以调试的数值问题。 -
在深度学习微调实践中,强烈建议始终显式指定
dtype。例如,在混合精度训练(AMP)时,明确指定torch.float16或torch.bfloat16可以避免因框架默认推断导致的类型不匹配错误。
torch.tensor 与 torch.Tensor 在使用上有何区别?¶
这是一个非常经典的“陷阱”问题。torch.tensor 是一个函数,而 torch.Tensor 是一个类。这个区别看似微小,但在实际编码中至关重要。
torch.tensor(data)(函数)- 数据拷贝:它会为传入的
data创建一个全新的副本,并推断其数据类型。 - 灵活性:你可以通过
dtype参数强制指定返回的张量类型,实现int到float的转换。 -
推荐用法:这是目前PyTorch官方推荐的构造方式,因为它的行为是确定的、可预测的。
-
torch.Tensor(data)(类构造器) - 数据共享:如果传入的
data是一个Python列表,行为与torch.tensor类似。但如果传入一个NumPy数组,它会尽可能地共享内存,而不是拷贝。这意味修改原始NumPy数组会影响到张量,反之亦然。 - 类型锁定:当传入Python列表时,
torch.Tensor会强制将其转换为全局默认的张量类型(torch.float32),无论原始列表是什么类型。它不接受dtype参数,无法在构造时改变类型。这个行为非常隐蔽,很容易导致输入整数却得到浮点数的bug。
import numpy as np
# 场景1:传入 Python 列表
data_list = [1, 2, 3]
a = torch.tensor(data_list) # 推断为 torch.int64
b = torch.Tensor(data_list) # 强制变为 torch.float32 !这是bug之源
# 场景2:传入 NumPy 数组
data_np = np.array([1.0, 2.0, 3.0], dtype=np.float64)
c = torch.tensor(data_np) # 拷贝并转换为 float32 (默认) 或保留 float64
d = torch.Tensor(data_np) # 共享内存,张量类型为 torch.float64
data_np[0] = 99.0
print(d[0]) # 输出 99.0,张量随之改变
结论:除非你明确需要torch.Tensor的内存共享特性或创建未初始化张量,否则在所有场景下都应使用 torch.tensor 或 torch.as_tensor。
怎样获取一个张量的形状、维度数和元素总数?¶
这三个属性是理解张量形态的基础。
x = torch.randn(3, 4, 5)
# 形状 (Shape): 返回一个 torch.Size 对象,本质是一个元组
print(x.shape) # torch.Size([3, 4, 5])
print(x.size()) # 等价于 x.shape
# 维度数 (Number of dimensions),也称为阶 (rank)
print(x.ndim) # 3
# 元素总数 (Total number of elements)
print(x.numel()) # 3 * 4 * 5 = 60
深入理解:
-
x.shape或x.size()返回的是torch.Size对象,它是一个元组,支持所有元组操作(如索引)。 -
x.numel()是x.nelement()的别名,两者的含义和返回值完全相同。
如何安全地将张量从CPU移动到GPU?请写出通用代码。¶
编写能同时兼容CPU和GPU环境的代码,是高鲁棒性工程实践的基本要求。
# 最佳实践:自动检测可用设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# CPU 上的张量
x_cpu = torch.randn(3, 4)
# 移动到目标设备
x_device = x_cpu.to(device)
更优雅的写法——直接创建在目标设备上:
现代PyTorch已经支持在创建张量时直接指定设备,这避免了不必要的数据搬运,效率更高。
常见陷阱:
-
跨设备运算会报错:CPU和GPU张量不能直接进行四则运算,必须先将它们放在同一设备上。
-
.cuda()方法的局限性:旧的代码习惯用x.cuda(),这在没有GPU的环境下会直接崩溃。始终使用.to(device)是更安全的做法。
view() 和 reshape() 的区别是什么?什么情况下 view 会失败?¶
它们都用于改变张量形状,但底层机制不同:view 要求物理内存连续,而 reshape 总能成功,因为它会自动处理不连续的情况。
-
view():返回一个与原始张量共享内存的新视图。这要求原始张量的底层内存是连续的。如果内存不连续,必须先用contiguous()将其变为连续张量。 -
reshape():行为更鲁棒。如果内存连续,它直接返回一个视图(类似view);如果内存不连续,它会先悄悄创建一个连续副本,再返回其视图。代价是可能有额外的内存拷贝。
view 失败的经典场景——转置:
x = torch.randn(2, 3)
y = x.t() # 转置操作破坏了内存连续性
print(y.is_contiguous()) # 输出 False
try:
y_v = y.view(-1) # 报错!view 不支持不连续的内存
except RuntimeError as e:
print("view failed:", e)
y_r = y.reshape(-1) # 成功!reshape 会自动处理
性能优化决策:如果你确认内存是连续的(或通过 contiguous() 确保了连续性),则优先使用 view() 以避免不必要的拷贝,对性能极致追求的场景(如训练循环内部)有细微帮助。在其他常规场景,reshape() 是更安全、不会被”坑“的选择。
squeeze() 和 unsqueeze() 分别实现什么功能?各举一例。¶
-
squeeze(dim):移除张量中长度为1的维度。如果不指定dim,则移除所有长度为1的维度。 -
unsqueeze(dim):在指定位置dim插入一个长度为1的新维度。
应用场景实例:
# 图像数据: (3, 1, 256, 256)
img = torch.randn(3, 1, 256, 256)
# 移除长度为1的通道维度,变为 (3, 256, 256)
img_squeezed = img.squeeze(1)
print(img_squeezed.shape) # torch.Size([3, 256, 256])
# 文本数据: (seq_len, feature_dim)
text = torch.randn(128, 512)
# 增加一个 batch 维度,使其成为 (1, 128, 512)
text_batched = text.unsqueeze(0)
print(text_batched.shape) # torch.Size([1, 128, 512])
常见陷阱:无参数的 squeeze() 会移除所有大小为1的维度,但这可能意外移除了你希望保留的维度(例如 batch_size 恰好为1),导致形状不匹配的错误。建议总是显式指定 dim 参数。
如何对二维张量进行转置?高维张量如何交换指定维度?¶
-
二维转置:
x.t()或x.transpose(0, 1)。 -
高维操作:
x.transpose(dim0, dim1)交换两个指定维度;x.permute(*dims)可以一次性重排所有维度的顺序。
x2d = torch.randn(3, 4)
print(x2d.t().shape) # (4, 3),最简单的二维转置
x4d = torch.randn(3, 4, 5, 6)
# 交换第0维和第3维
a = x4d.transpose(0, 3) # (6, 4, 5, 3)
# 一次性将所有维度倒序排列
b = x4d.permute(3, 2, 1, 0) # (6, 5, 4, 3)
permute 的应用:在NLP的注意力机制中,多头注意力的计算经常需要从 (batch_size, seq_len, n_heads, head_dim) 变为 (batch_size, n_heads, seq_len, head_dim),这种复杂的维度重组正是 permute 的用武之地,无法通过简单的 transpose 一步完成。
PyTorch的广播机制遵循哪些规则?请举例说明两个形状可否广播。¶
广播机制允许我们对形状不完全匹配的张量进行逐元素运算,而无需显式地复制数据。这极大地提升了代码的简洁性和效率。
广播遵循两条黄金法则:
-
尾部对齐:从右向左比较两个张量的维度。
-
兼容则扩展:对应维度要么相等,要么其中之一为 1,或者其中一个张量没有该维度(即维度缺失)。否则,广播失败。
举例说明:
-
形状
(3, 1)和(1, 4):可以广播。维度1是 1 和 4(满足一个为1),维度0是 3 和 1(满足一个为1)。结果是(3, 4)。 -
形状
(15, 3, 5)和(15, 1, 5):可以广播。从右向左,维度2(5对5)和维度0(15对15)相等,维度1(3对1)满足一个为1。结果是(15, 3, 5)。 -
形状
(15, 3, 5)和(3, 5):可以广播。(3,5)的维度少于(15,3,5),规则会将其在左侧补1,变为(1, 3, 5),然后按规则成功广播。 -
形状
(8, 1, 6, 1)和(7, 1, 5):无法广播。从右向左,对应维度为 1 和 5(满足),6 和 1(满足),1 和 7(满足)。但接下来,(7,1,5)没有对应(8,1,6,1)的第0维,此时广播会尝试将(7,1,5)扩展为(1, 7, 1, 5),其第0维是1,而另一个张量是8,两者不相等,失败。
形状(3,1)和(1,4)相加后结果形状是什么?¶
根据广播规则:(3, 1) + (1, 4)。
-
从右向左比较:第1维是 1 和 4(满足一个为1,广播为4)。
-
第0维是 3 和 1(满足一个为1,广播为3)。
-
结果形状为 (3, 4)。
这个操作在深度学习中使用频繁,例如为一批输入数据 (batch, feature) 的每一个样本加上不同的偏置 (1, feature)。
如何将NumPy数组转为张量?共享内存与复制数据的方式有何不同?¶
将NumPy数组转为PyTorch张量主要有两种方式,核心区别在于内存管理。
- 共享内存:
torch.from_numpy(ndarray) - 机制:创建的张量与原始NumPy数组共享同一个底层内存块。对任何一个的 in-place 修改都会立刻反映在另一个上。
- 性能:转换速度极快,没有任何数据复制开销。
-
限制:NumPy数组必须是CPU上的,且数据类型需要与PyTorch兼容(如 float64)。
-
复制数据:
torch.tensor(ndarray) - 机制:复制一份新的内存来存放数据。转换后的张量与原始数组完全独立。
- 性能:需要额外的内存和时间来复制数据,对于大数组影响显著。
import numpy as np
arr = np.array([1, 2, 3], dtype=np.float64)
# 方式1:共享内存
t_shared = torch.from_numpy(arr)
arr[0] = 99
print(t_shared[0]) # 输出 99.0,受到了 numpy 修改的影响
# 方式2:复制数据
arr[0] = 100 # 恢复 arr
t_copy = torch.tensor(arr)
arr[0] = 101
print(t_copy[0]) # 输出 100.0,不受 numpy 修改的影响
选择建议:在数据加载等性能敏感环节,优先使用 torch.from_numpy(),但要格外小心 inadvertent in-place 修改带来的bug。在需要保障数据独立性、逻辑清晰的场景,使用 torch.tensor() 更稳妥。
用 torch.from_numpy() 创建的张量修改后会影响原数组吗?¶
会。 torch.from_numpy() 创建的张量与原始 NumPy 数组共享同一块底层内存。这意味着对张量的任何 in-place 修改(例如 tensor[0] = 0, tensor.add_(1))都会立刻反映在原 NumPy 数组上,反之亦然
import numpy as np
import torch
arr = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(arr)
# in-place 修改张量
t[0] = 99.0
print(arr) # [99. 2. 3.] —— 原数组发生了改变!
# in-place 修改数组
arr[1] = 88.0
print(t) # tensor([99., 88., 3.]) —— 张量也随之改变!
深入理解与避坑指南:
-
速度 vs. 安全性:这种共享内存的设计使得数据在 NumPy 和 PyTorch 之间传输几乎零开销,非常适合数据加载等性能敏感场景。但它的陷阱在于,如果不小心进行了 in-place 操作,可能导致难以追踪的 bug,例如预处理的数据被意外修改。
-
如何避免:如果你希望保持 NumPy 数组的独立性,请使用
torch.tensor(arr)来创建张量,它会显式地复制一份数据,从而切断内存共享。 -
检测是否共享内存:可以通过
tensor.data_ptr() == arr.ctypes.data来判断它们是否指向同一内存地址。
张量的 requires_grad 属性默认是什么?何时需要手动设为 True?¶
-
默认值:对于通过
torch.randn、torch.zeros等工厂函数创建的普通张量,requires_grad默认为False。 -
例外:如果张量是通过
nn.Parameter包裹的,或者是从一个requires_grad=True的张量经过运算得到的,那么它的requires_grad会自动变为True。
何时手动设为 True:
在以下两种主要场景,我们需要手动开启梯度追踪:
-
微调预训练模型的部分层:在微调时,我们通常冻结大部分参数,只训练少数几层。这时需要为那些我们希望训练的层的参数显式设置
requires_grad=True。 -
定义非标准可训练参数:除了
nn.Parameter,有时我们需要将一个中间张量或者一个自定义的权重矩阵视为可学习参数,就需要手动设置其requires_grad=True并将其注册到优化器中。
import torch.nn as nn
model = nn.Linear(10, 2)
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
# 手动启用某一层的梯度
model.bias.requires_grad = True
重要规则:只有数据类型为浮点类型(float32, float64)的张量才能追踪梯度,整数类型无法设置 requires_grad=True。在需要进行梯度计算时,务必确保张量类型正确。
如何查看张量当前所在的设备和数据类型?¶
使用 device 和 dtype 属性。
x = torch.randn(3, 4)
print(x.device) # cpu
print(x.dtype) # torch.float32
if torch.cuda.is_available():
x_gpu = x.to('cuda')
print(x_gpu.device) # cuda:0
深入细节:
-
x.device返回一个torch.device对象,你可以直接将其用于.to()方法。 -
x.dtype返回一个torch.dtype对象,常用于混合精度训练时检查类型或进行类型转换,例如x.to(torch.float16)。
torch.cat 和 torch.stack 的核心区别是什么?各自适合什么场景?¶
两者都用于拼接张量,但工作机制根本不同。
-
torch.cat(tensors, dim):沿着已存在的维度将多个张量拼接起来。要求所有张量在其他维度上形状必须一致。它不会增加新的维度。 -
torch.stack(tensors, dim):沿着一个全新的维度将多个张量堆叠起来。要求被堆叠的所有张量必须拥有完全相同的形状。它会插入一个新的维度。
场景举例:
-
cat:你有三个不同的句子,每个句子已经编码为形状(seq_len, feature_dim)。你想将它们拼接成一个长序列,使用torch.cat([sent1, sent2, sent3], dim=0),得到形状(total_len, feature_dim)。 -
stack:你想将这三个句子堆叠成一个 batch,使用torch.stack([sent1, sent2, sent3], dim=0),得到形状(3, seq_len, feature_dim)。这里,dim=0就是为 batch 新增的维度。
a = torch.randn(2, 3)
b = torch.randn(2, 3)
c_cat = torch.cat([a, b], dim=0) # (4, 3)
c_stack = torch.stack([a, b], dim=0) # (2, 2, 3)
如何在指定维度上分割张量?torch.chunk 和 torch.split 有何不同?¶
-
torch.chunk(tensor, chunks, dim):将张量沿指定维度等分为chunks份。如果无法整除,最后一份会比其他份稍小。 -
torch.split(tensor, split_size_or_sections, dim):更灵活,可以自定义每份的大小。既可以传入一个整数表示每份的大小,也可以传入一个整数列表来精确指定每份的大小。
x = torch.arange(10)
# chunk 等分为3份
c1, c2, c3 = torch.chunk(x, 3) # 4, 4, 2
# split 自定义每份大小
s1, s2 = torch.split(x, [3, 7]) # 3, 7
应用场景:在多头注意力机制中,将张量按头数均分常用 chunk;在处理变长序列的批数据时,使用 split 按序列长度切分更为合适。
torch.mm、torch.matmul 和 @ 运算符的使用场景有何异同?¶
-
torch.mm(a, b):严格用于两个二维矩阵的乘法。这是最底层的矩阵乘操作,要求输入必须为2维。 -
torch.matmul(a, b)或a @ b:是通用的矩阵乘法,完全等价于@运算符。它可以自动处理更高维度的张量,遵循广播规则并进行批矩阵乘法。这是日常代码中的首选。
# 二维矩阵
a = torch.randn(3, 4); b = torch.randn(4, 5)
r1 = torch.mm(a, b)
r2 = torch.matmul(a, b) # 与 r1 相同
# 批矩阵乘法:形状 (batch, 3, 4) 和 (batch, 4, 5)
a_batch = torch.randn(10, 3, 4); b_batch = torch.randn(10, 4, 5)
r_batch = a_batch @ b_batch # (10, 3, 5)
选择建议:除非有特殊理由限制为二维,始终使用 @ 或 torch.matmul,它们更通用,能减少因维度错误导致的bug。
如何计算张量沿某维度的和、均值、最大值?keepdim 参数的作用?¶
使用 torch.sum、torch.mean、torch.max 等函数,并指定 dim 参数。
keepdim 参数的作用:默认为 False,运算后该维度会被消除(挤压)。设为 True 时,该维度会被保留为1,这对于后续的广播运算至关重要。
x = torch.randn(2, 3, 4)
# 沿 dim=1 求和,形状变为 (2, 4)
sum1 = x.sum(dim=1) # shape: (2, 4)
# 保持维度,形状变为 (2, 1, 4)
sum2 = x.sum(dim=1, keepdim=True) # shape: (2, 1, 4)
# 示例:用 keepdim 实现归一化
mean = x.mean(dim=-1, keepdim=True)
std = x.std(dim=-1, keepdim=True)
normalized = (x - mean) / std # 广播成功
torch.gather 的作用是什么?请给出一个二维索引的示例。¶
torch.gather 用于沿着指定维度,根据索引张量从源张量中收集数据。可以理解为一种可微分的“查表”操作,在实现分类任务的采样、强化学习中的动作选择等场景非常有用。
二维示例:
假设我们有一个2D张量 src,现在想取第0行第2列、第1行第0列、第2行第1列的元素,可以使用 gather。
src = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 索引张量,形状必须与 src 相同
index = torch.tensor([[2], # 第0行取第2列
[0], # 第1行取第0列
[1]]) # 第2行取第1列
# 沿 dim=1(列)收集
result = src.gather(dim=1, index=index)
# 输出: tensor([[3],
# [4],
# [8]])
工作原理:gather 为每个位置 (i, j) 确定源中的位置:如果 dim=1,则取 src[i, index[i][j]]。
torch.where 的两种典型用法分别是什么?¶
torch.where 有两种完全不同的用法,取决于参数数量。
用法一:三元选择(condition, x, y)
根据条件 condition(一个布尔张量),从 x 或 y 中对应位置选择元素。condition 为 True 选 x,否则选 y。这是最常见用法。
cond = torch.tensor([True, False, True])
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
result = torch.where(cond, a, b) # tensor([1, 5, 3])
用法二:返回非零元素的索引(condition)
当只传入一个 condition 参数时,它会返回 condition 中所有 True 位置的索引(类似于 np.where)。对于多维张量,返回的是一个索引元组。
x = torch.tensor([[1, 0, 3],
[0, 5, 0]])
indices = torch.where(x > 2) # 找到大于2的元素索引
# (tensor([0, 1]), tensor([2, 1])) 表示 (0,2) 和 (1,1) 位置
生成随机张量的常用函数有哪些?rand、randn、randint 的区别。¶
-
torch.rand(*size):从 [0, 1) 之间的均匀分布 中采样。 -
torch.randn(*size):从均值为0、标准差为1的标准正态分布(高斯分布)中采样。 -
torch.randint(low, high, *size):从[low, high)的整数均匀分布中采样。
u = torch.rand(2, 3) # [0, 1) 均匀分布
n = torch.randn(2, 3) # N(0,1) 正态分布
i = torch.randint(0, 10, (2, 3)) # [0, 10) 整数
应用:rand 常用于 Dropout,randn 常用于权重初始化(如Kaiming初始化),randint 用于生成随机标签或索引。
如何固定随机种子以保证实验可复现?涉及哪些种子?¶
为了达到最大程度的可复现性,我们需要固定以下几个层面的随机种子:
-
Python 自身:
random.seed(seed) -
NumPy 库:
np.random.seed(seed) -
PyTorch CPU:
torch.manual_seed(seed) -
PyTorch GPU(所有卡):
torch.cuda.manual_seed_all(seed) -
PyTorch GPU(当前卡):
torch.cuda.manual_seed(seed) -
强制 cuDNN 使用确定性算法(会降低性能):
通用封装函数:
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
警告:即使固定了所有种子,由于GPU并行计算的浮点运算顺序不确定,不同硬件或不同CUDA版本之间的结果仍然可能无法做到位级别复现,但统计级别上的性能指标是可复现的。
张量的存储 (Storage)、形状 (Size) 和步长 (Stride) 之间有什么关系?¶
这是理解张量底层机制的关键。一个张量由存储(Storage)和元信息(形状, 步长)两部分构成。
-
存储 (Storage):一块一维的、连续的内存块,实际存有张量的所有数据。
-
形状 (Size):我们如何“看待”这块一维存储的逻辑维度组合。
-
步长 (Stride):从存储到逻辑视图的映射规则。它是一个元组,表示在每一个逻辑维度上,要跨越多少个存储单元(元素)才能到达下一个元素。
关系:对于存储中的位置 ii,其在逻辑视图的坐标 (d0,d1,...,dn)(d0,d1,...,dn) 可以通过公式 i=offset+∑kstride[k]×coordinate[k]i=offset+∑kstride[k]×coordinate[k] 计算得出。view() 等操作之所以能高效实现,就是因为它们仅仅是改变了逻辑视图(形状和步长),而底层的存储不变。
举例:对一个形状为 (3, 4) 的张量进行转置,得到形状 (4, 3) 的张量。新张量的存储并未改变,只是步长从 (4, 1) 变成了 (1, 4)。这就是为什么转置后的张量内存变得不连续(is_contiguous() == False)的原因。
x = torch.randn(3, 4)
print(x.stride()) # (4, 1) —— 第0维跳4个元素,第1维跳1个元素
y = x.t() # 转置,形状变为 (4, 3)
print(y.stride()) # (1, 4) —— 存储未变,步长改变
print(y.is_contiguous()) # False
理解存储、形状和步长的关系,是掌握 view、reshape、permute 等操作以及编写高效PyTorch代码的理论基础。
如何在不复制数据的情况下改变张量形状?¶
使用 view() 方法可以在不复制底层数据的情况下改变张量形状。view() 返回一个新的张量,它与原张量共享同一块存储,仅仅是改变了形状(和步长)。但前提是原张量的内存必须是连续的(contiguous)。
x = torch.arange(12).reshape(3, 4) # 形状 (3,4),内存连续
y = x.view(2, 6) # 形状变为 (2,6),共享存储
y[0, 0] = 99
print(x[0, 0]) # 99,证明了共享内存
如果张量内存不连续(例如经过转置),则需要先调用 .contiguous() 使其内存连续,再使用 view()。但注意 .contiguous() 本身会生成一个新的副本。若想完全避免拷贝,可以使用 reshape(),它在可能的情况下会返回视图,否则才创建副本。
最佳实践:尽量在内存连续时使用 view(),以获得零拷贝的性能优势;如果无法保证连续性,则使用更鲁棒的 reshape()。
什么是张量的连续性?何时必须调用 .contiguous()?¶
连续性是指张量在内存中按照逻辑视图的顺序逐行存储,没有任何“跳跃”。具体检查可以通过 x.is_contiguous() 来判断。如果张量连续,view() 等操作就能直接映射而无需数据复制。
必须调用 .contiguous() 的典型场景:
-
在调用
view()之前:如果张量不连续,view()会抛出RuntimeError。 -
在调用某些底层算子或自定义CUDA内核前:它们往往要求输入连续。
-
在将GPU张量拷贝到CPU时(见第25问)。
x = torch.randn(3, 4).t() # 转置后不连续
print(x.is_contiguous()) # False
# x.view(-1) # 会报错
x_cont = x.contiguous() # 创建一个连续的副本
print(x_cont.is_contiguous()) # True
x_cont.view(-1) # 正常工作
注意:contiguous() 会创建一个新的、内存连续的张量,这涉及到数据拷贝,应谨慎使用以平衡性能。
从GPU拷贝张量到CPU时,为什么有时需要先调用 .contiguous()?¶
这是由于许多GPU操作(如 transpose、permute、narrow)产生的张量视图在底层内存中并不连续。当调用 .cpu() 将数据传回主机时,PyTorch 内部实际上需要将数据从 GPU 显存复制到 CPU 内存。如果原张量不连续,底层驱动程序或框架可能无法直接进行高效的 DMA 传输,而必须先将其整理为连续块。
典型症状:直接在不连续的 GPU 张量上调用 .cpu() 可能会得到一个仍然不连续的 CPU 张量,或者在某些情况下(特别是与 NumPy 互操作时)会触发隐式的数据拷贝和警告。为了确保数据在 CPU 上是标准连续布局,并避免潜在的性能问题和错误,最佳实践是在 .cpu() 之前调用 .contiguous()。
x_gpu = torch.randn(3, 4, device='cuda').t() # GPU上不连续
# 先变为连续,再拷贝到 CPU
x_cpu = x_gpu.contiguous().cpu()
print(x_cpu.is_contiguous()) # True
这保证了 x_cpu 是一个内存连续的张量,可以直接用于 view、numpy() 等操作而不会出错。
如何实现批量矩阵乘法?torch.bmm 与 torch.matmul 的区别。¶
-
torch.bmm(batch1, batch2):严格用于两个三维张量的批量矩阵乘法。要求两个输入形状分别为(b, n, m)和(b, m, p),输出形状为(b, n, p)。它不支持广播。 -
torch.matmul(a, b)(或@):通用矩阵乘法,能自动处理更高维度。当输入为三维时,它与bmm效果相同,但matmul支持广播机制,可以处理形状为(b, 1, n, m)与(b, m, p)的乘法,广播维度到相同形状后再批量乘。
a = torch.randn(10, 3, 4) # batch=10
b = torch.randn(10, 4, 5)
# 两者等价
r_bmm = torch.bmm(a, b) # (10, 3, 5)
r_mat = torch.matmul(a, b) # (10, 3, 5)
# matmul 支持广播
c = torch.randn(1, 4, 5) # 只有1个矩阵
r_broadcast = torch.matmul(a, c) # (10, 3, 5),c被广播
选择:日常使用推荐 @ 或 matmul,因为它们更灵活。只有在严格要求输入为三维且不需要广播的场合(比如某些性能敏感的底层库接口),才用 bmm。
torch.einsum 的优势是什么?请用 einsum 表示矩阵乘法。¶
torch.einsum 使用爱因斯坦求和约定,通过一个简洁的字符串表达式定义张量操作,可以清晰地表示矩阵乘法、转置、点积、批量乘、收缩等多种运算,避免了繁琐的维度变换和乘法调用。它的优势在于代码可读性高、表达能力强,且现代 PyTorch 对 einsum 进行了高度优化,性能不输给传统的组合操作。
矩阵乘法(A @ B)用 einsum 表示:
假设 A 形状 (i, j),B 形状 (j, k),则 C = A @ B 的 einsum 写法为 "ij,jk->ik"。
-
ij表示 A 的两个维度,分别命名为i和j。 -
jk表示 B 的两个维度,命名为j和k。 -
->ik表示输出维度为i和k,消失的j维度会被求和(收缩)。
其他常见操作:
-
转置:
ij->ji -
点积:
i,i-> -
批量矩阵乘:
bij,bjk->bik
对于复杂张量操作(如注意力机制的 QK^T 计算),einsum 让代码的数学意图一目了然。
如何对张量进行排序?torch.sort 与 torch.argsort 的输出有何不同?¶
-
torch.sort(input, dim, descending=False):返回两个张量——排序后的值和对应的原始索引。 -
torch.argsort(input, dim, descending=False):只返回排序后的索引(即torch.sort返回的第二个张量),不返回值。
x = torch.tensor([3.0, 1.0, 2.0])
# torch.sort 返回 (排序值, 索引)
values, indices = torch.sort(x)
# values = [1., 2., 3.], indices = [1, 2, 0]
# torch.argsort 仅返回索引
idx = torch.argsort(x) # [1, 2, 0]
应用场景:
-
需要排序后的实际数据用于后续计算时,用
sort。 -
只需要知道元素的大小顺序(如排名、top-k 的索引)时,用
argsort更省内存。
如何获取张量中前K大的值及其索引?torch.topk 的用法。¶
torch.topk(input, k, dim, largest=True) 返回沿指定维度前 k 个最大(或最小)的值及其索引。
x = torch.tensor([[4, 1, 3],
[2, 6, 5]])
# 沿最后一维取前2大
values, indices = torch.topk(x, k=2, dim=-1)
# values: [[4, 3],
# [6, 5]]
# indices: [[0, 2],
# [1, 2]]
关键参数:
-
largest=True:返回最大的k个;设为False则返回最小的k个。 -
sorted=True(默认):返回的k个值按降序(largest时)或升序(largest=False时)排列。
topk 在语言模型生成(如 Top-K 采样)、信息检索(取最相关文档)中极为常用。
怎样将张量元素限制在某个范围内?clamp 的作用。¶
torch.clamp(input, min, max) 将张量中所有元素限制在 [min, max] 区间内。小于 min 的值设为 min,大于 max 的值设为 max。
变体:
-
torch.clamp(input, min=val):只设下限。 -
torch.clamp(input, max=val):只设上限。
应用:在梯度裁剪、概率值归一化、生成对抗网络(GAN)的输出控制、防止数值溢出等场景中非常常用。
什么是就地操作?举例说明其优缺点及在自动求导中的限制。¶
就地操作(in-place operation) 是指直接修改张量本身的数据而不创建新张量的操作,通常以 _ 后缀命名,如 x.add_(1)、x.zero_()。
优点:
- 节省显存:不需要为结果分配新内存,对大模型训练尤其珍贵。
缺点与限制:
-
破坏计算图:在自动求导(autograd)中,如果对一个需要梯度的叶子张量或者计算图中的中间结果进行就地操作,会覆盖掉反向传播所需的历史值,导致梯度计算错误或报错
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation。 -
易引发难以追踪的 bug:如果多个变量引用同一块内存,就地修改会意外改变其他变量的值。
# 错误示例:对需要梯度的叶子节点进行就地操作
x = torch.randn(3, requires_grad=True)
x.add_(1) # 会覆盖 x 的历史数据,导致后续 backward 出错
最佳实践:对需要梯度的张量避免使用就地操作。在推理阶段或对不需要梯度的张量(如模型参数更新时 param.add_(grad * lr))可以谨慎使用,但要确保该张量不是其他张量的依赖。
如何转换张量数据类型?如 float32 转为 int64。¶
使用 .to() 方法,传入目标 dtype。
x = torch.tensor([1.7, 2.4], dtype=torch.float32)
y = x.to(torch.int64) # 直接截断小数,变为 [1, 2]
# 或者更简洁的方式
y = x.int() # 转换为 int32
y = x.long() # 转换为 int64
注意:浮点转整型是直接截断(truncate),不是四舍五入。如果需要四舍五入,应先 round() 再转换。
如何判断两个张量是否共享底层内存?¶
使用 .data_ptr() 方法获取张量存储的起始内存地址,若地址相同则共享。
a = torch.randn(3, 4)
b = a.view(2, 6)
print(a.data_ptr() == b.data_ptr()) # True,共享存储
c = a.t()
print(a.data_ptr() == c.data_ptr()) # True,转置也共享存储,但形状和步长不同
d = a.clone()
print(a.data_ptr() == d.data_ptr()) # False,clone 创建了新存储
34. item() 方法适用于什么类型的张量?有什么注意事项?¶
item() 只能用于只包含一个元素的张量(0维张量或形状为(1,)等),它将这个唯一的元素以 Python 标量(int, float 等)的形式返回。
x = torch.tensor(3.14)
print(x.item()) # 3.14,Python float
# 注意:如果张量在GPU上,item() 会触发隐式的设备同步(cuda synchronize),可能影响性能
x_gpu = torch.tensor(1.0, device='cuda')
val = x_gpu.item() # 会自动等待当前 GPU 流完成,产生同步点
注意事项:对于 GPU 张量,频繁调用 item() 会强制 CPU 和 GPU 的同步,破坏异步执行的流水线,严重降低性能。在训练循环中应尽量避免,或使用 .detach().cpu().item() 显式控制同步。
如何将只含一个元素的张量转为 Python 标量?¶
除了 item(),还可以用:
-
.tolist()转换为列表再取第一个元素(但会生成列表,开销稍大)。 -
对0维张量,
int(x)或float(x)有时也可行,但不如item()规范。
推荐:始终使用 x.item(),它是官方推荐、最清晰的方式。