一句话解释
Tensors are a specialized data structure that are very similar to arrays and matrices.
similar to NumPy’s ndarrays,but it can run on GPUs or other hardware accelerators
tensors and NumPy arrays can often share the same underlying memory
最小例子
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
Shape、device
shape
shape is a tuple of tensor dimensions. In the functions below, it determines the dimensionality of the output tensor.
shape = (2,3)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")
device
一般是cpu,根据需要可以改成cuda, xpu, mps
Tensor Operations
(tensor dim)
转置,切分,索引都和向量操作相同
Joining tensors需要注意:
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)
这里dim取的值是指的对于n维张量[tensor]的第n-1个维度,取负时代表从末位数到数第n个维度。 对于n维张量,dim合法范围是 -n 到 n-1 对于张量的维度,越高的维度越靠外编号越小,越低的维度越靠内(感觉就是把n维数组从左往右读出来,比如一个array[4][2][3],dim=0,指array[4]的这层)
并且
在存储、显示和索引上,维度从外到内排列;在数学和业务意义上,各个维度通常只是不同的坐标轴,不天然存在高低或从属关系。
既然维度本身顺序没有固定含义,为什么我们需要用permute()改变他们的顺序呢?(permute() 中的数字表示“按照什么顺序重新排列原来的轴”)
# 如果图像张量(N, C, H, W): (图片数量,颜色通道,高度,宽度)
# 从 (N, C, H, W) 改成 (N, H, W, C)
images = images.permute(0, 2, 3, 1)
PyTorch 不会自动规定每个维度代表什么;但一旦我们人为规定了含义,维度顺序就非常重要。
-
不同库采用不同的数据排列约定
PyTorch 常用: (N, C, H, W) 图像工具常用: (N, H, W, C)
-
不同模型或函数要求不同的输入格式
例如 PyTorch 卷积层常用: ```text (C, H, W) ``` 而图像显示工具通常使用: ```text (H, W, C) ``` 显示一张 PyTorch 图片时,经常需要: ```python # (C, H, W) → (H, W, C) image = image.permute(1, 2, 0) plt.imshow(image) ``` 这不是因为数学上必须这样,而是因为两个库对维度顺序的约定不同。 -
某些计算具有固定含义,需要先调整顺序(矩阵乘法等)
**矩阵乘法通常把最后两个维度视为矩阵维度** 假设: ```python x.shape # (batch, rows, columns) ``` 执行: ```python result = x @ weight ``` PyTorch 会把 `x` 最后的两个维度: ```text (rows, columns) ``` 用于矩阵乘法,并把前面的维度当作批次维度。 如果矩阵相关的维度没有放在最后,不能通过 `dim=` 告诉 `@` 使用其他维度。这时可能需要: ```python x = x.permute(...) ``` 或者使用更灵活的 `einsum` 等操作。 也就是说,有些函数提供 `dim`: ```python torch.sum(x, dim=...) torch.softmax(x, dim=...) torch.cat(..., dim=...) ``` 有些操作则按照固定规则识别维度: ```python x @ weight torch.matmul(x, weight) ``` **广播机制从最后一个维度开始对齐** 假设: ```python x.shape # (32, 224, 224, 3) ``` 表示: ```text (N, H, W, C) ``` 现在想给 RGB 三个通道分别乘不同的系数: ```python scale = torch.tensor([1.0, 0.5, 0.2]) ``` 因为通道位于最后一维,形状是 `(3,)` 的 `scale` 可以直接广播: ```python result = x * scale ``` PyTorch 从右向左对齐: ```text x: (32, 224, 224, 3) scale: (3) ``` 但如果 `x` 的形状是: ```text (N, C, H, W) = (32, 3, 224, 224) ``` 直接写: ```python x * scale ``` 就无法正确广播,因为末尾的 `224` 和 `3` 对不上。这时需要把 `scale` 改成: ```python scale = scale.reshape(1, 3, 1, 1) result = x * scale ``` 这里不一定要调整 `x` 的维度,也可以调整另一个张量的形状。哪一种更方便,取决于后续操作。 -
某些模型层要求固定的输入顺序
例如 PyTorch 的二维卷积层 `nn.Conv2d` 通常要求输入格式为: ```text (N, C, H, W) ``` 也就是: ```text 批次数、通道数、高度、宽度 ``` 但有些图像数据可能采用: ```text (N, H, W, C) ``` 这种情况下不能写: ```python conv(x, channel_dim=3) # Conv2d 没有这个参数 ``` 必须先调整: ```python # (N, H, W, C) → (N, C, H, W) x = x.permute(0, 3, 1, 2) output = conv(x) ``` 这是因为 `Conv2d` 的接口已经约定了通道必须在 `dim=1`。 -
内存布局可能影响计算效率
张量在内存中通常按某种顺序排列,最后一个维度的数据往往彼此更接近。例如: ```text 形状:(2, 3, 4) ``` 最后一维的 4 个元素通常连续存放。沿连续方向读取数据,硬件通常更容易高效利用缓存和并行计算。 不过,`permute()` 本身通常只是改变观察数据的方式,并不会马上重新排列底层数据,因此结果可能不是连续的: ```python y = x.permute(0, 2, 1) print(y.is_contiguous()) # False ``` 某些操作需要连续内存时,可以调用: ```python y = y.contiguous() ``` 但这会真正复制和重新排列数据,因此不应该为了“可能更快”就随意使用。通常先遵守模型和函数要求的格式即可。 shape 描述数据“看起来如何排列”,stride 描述访问各维度时在内存里“走多远”,is_contiguous() 表示当前逻辑顺序是否与实际存储顺序连续一致。
最核心的判断是:
操作可指定 dim
→ 通常直接指定,不用 permute
操作要求固定的维度位置
→ 需要 permute 或采用其他等价写法
两个库的数据格式不同
→ 在接口处调整维度顺序
只是为了性能
→ 不要凭感觉调整,需要实际测试
调换维度通常不是为了让 PyTorch“理解计算”,而是为了满足某个操作的固定输入规则、配合广播规则,或者在不同格式约定之间转换。只要函数允许直接指定
dim,直接指定通常就是最清楚的做法。
add()和add_()的区别
有指针的问题,add()是赋值,add_()是直接改变原内存空间内的值
tensor和NumPy array
既然二者在cpu环境中可以直接相互使用,那如果在cuda环境中呢?在mps环境中呢? NumPy array只能放到cpu内存中吗? Answer: 如果tensor在cuda和mps环境中,需要.cpu()到cpu环境再操作。 训练中常见的写法 如果 Tensor 参与自动求导,还需要先与计算图分离:
n = tensor.detach().cpu().numpy()
自动求导
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x * x
# y = x²
因为设置了requires_grad=True, PyTorch 会记录 y 是如何通过 x 计算出来的。 这意味着:
y.backward() #此时,PyTorch会自动计算dy/dx = 2x
print(x.grad)
# 输出tensor(4.)
计算图
PyTorch会把相关计算记录成一张图 对于:
x = torch.tensor(2.0, requires_grad=True)
a = x * 3
y = a ** 2
计算过程为:“x ──乘以3──> a ──平方──> y” 计算图为:“x → a → y” 当执行y.backward()时,PyTorch会从后向前计算:y → a → x (backward()和反向传播有什么关系?)
detach() 做了什么
detach() 的作用可以概括为:
得到一个数值相同的 Tensor,但切断它与之前自动求导计算过程的联系。
要理解它,需要先了解自动求导和计算图。
什么是自动求导?
训练神经网络时,需要计算损失对模型参数的导数,也就是梯度。
例如:
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x * x
数学上:
y = x²
因为设置了:
requires_grad=True
PyTorch 会记录 y 是如何通过 x 计算出来的。
调用:
y.backward()
PyTorch 会自动计算:
dy/dx = 2x
当 x=2 时:
print(x.grad)
# tensor(4.)
这就是自动求导:PyTorch 根据之前记录的计算过程,自动使用链式法则计算梯度。
什么是计算图?
PyTorch 会把相关计算记录成一张图。
例如:
x = torch.tensor(2.0, requires_grad=True)
a = x * 3
y = a ** 2
计算过程是:
x ──乘以3──> a ──平方──> y
对应数学表达式:
a = 3x
y = a² = (3x)²
这就是一张简单的计算图:
x → a → y
当执行:
y.backward()
PyTorch 会从后向前计算:
y → a → x
最终得到:
dy/dx = 18x
当 x=2 时:
print(x.grad)
# tensor(36.)
detach() 做了什么?
现在对中间结果调用:
a_detached = a.detach()
可以理解为从计算图中剪断连接:
原来的计算图:
x → a → y
detach 后:
x → a a_detached
↑
独立起点
a_detached 的数值和 a 相同:
print(a)
# tensor(6., grad_fn=<MulBackward0>)
print(a_detached)
# tensor(6.)
但它不再记录自己是由 x * 3 得到的:
print(a.requires_grad)
# True
print(a_detached.requires_grad)
# False
print(a_detached.grad_fn)
# None
因此,之后通过 a_detached 进行的普通计算不会把梯度传回 x。
例如:
x = torch.tensor(2.0, requires_grad=True)
a = x * 3
b = a.detach()
loss = a**2 + b**2
loss.backward()
print(x.grad)
# tensor(36.)
这里虽然:
a = 6
b = 6
loss = a² + b²
但只有第一条路径参与梯度计算:
参与求导:x → a → a²
停止求导:x → a ┤ detach → b → b²
因此 b**2 的值参与了 loss,但它产生的梯度不会传回 x。
为什么转成 NumPy 前需要 detach()?
假设:
tensor = torch.tensor([1.0, 2.0], requires_grad=True)
直接调用:
array = tensor.numpy()
通常会报错,因为 NumPy 不理解 PyTorch 的计算图,也不会记录 NumPy 操作供 PyTorch 反向传播。
PyTorch 要求你明确表示:
我只是想取出数值,后面的 NumPy 操作不需要参与梯度计算。
所以写成:
array = tensor.detach().cpu().numpy()
过程是:
tensor
│
├─ detach():停止追踪这条结果的梯度历史
│
├─ cpu():确保数据位于 CPU
│
└─ numpy():转换成 NumPy 数组
detach() 会复制数据吗?
通常不会。detach() 返回的新 Tensor 与原 Tensor 共享底层存储:
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x.detach()
x 和 y 是不同的 Tensor 对象,但可能查看同一块数据。因此原地修改需要谨慎:
y.add_(10)
print(x)
# tensor([11., 12.], requires_grad=True)
如果既想切断计算图,又想获得一份独立的数据副本,可以写:
y = x.detach().clone()
两者区别是:
x.detach() → 切断计算图,通常共享数据
x.detach().clone() → 切断计算图,并复制数据
最后可以这样记:
requires_grad=True → 请记录计算过程,以后需要求导
计算图 → PyTorch 记录的数据计算路线
backward() → 沿计算路线反向计算梯度
detach() → 从当前位置切断梯度传播路线
常见错误及排查
30~60 秒面试回答
仍不确定的问题
tensor的内存布局具体会怎么影响计算效率
Conversation
评论
欢迎交流这篇文章。评论公开保存在 GitHub Discussions,发表前需要登录 GitHub。