张量操作详解
数学运算
逐元素运算
逐元素运算是对张量中每个元素独立进行的运算,要求参与运算的张量形状相同(或可广播)。
python
import torch
a = torch.tensor([1.0, 2.0, 3.0, 4.0])
b = torch.tensor([2.0, 3.0, 4.0, 5.0])
# 加法
c = a + b # 运算符写法
c = torch.add(a, b) # 函数写法
print(f"加法: {c}") # tensor([3., 5., 7., 9.])
# 减法
c = a - b
c = torch.sub(a, b)
print(f"减法: {c}") # tensor([-1., -1., -1., -1.])
# 乘法(逐元素)
c = a * b
c = torch.mul(a, b)
print(f"乘法: {c}") # tensor([ 2., 6., 12., 20.])
# 除法
c = a / b
c = torch.div(a, b)
print(f"除法: {c}") # tensor([0.5000, 0.6667, 0.7500, 0.8000])
# 整除
c = torch.div(b, a, rounding_mode="trunc")
print(f"整除: {c}") # tensor([2., 1., 1., 1.])
# 取余
c = torch.remainder(b, a)
print(f"取余: {c}") # tensor([0., 1., 1., 1.])
# 幂运算
c = a ** 2
c = torch.pow(a, 2)
print(f"幂运算: {c}") # tensor([ 1., 4., 9., 16.])
# 平方根
c = torch.sqrt(a)
print(f"平方根: {c}") # tensor([1.0000, 1.4142, 1.7321, 2.0000])
# 指数
c = torch.exp(a)
print(f"指数: {c}") # tensor([ 2.7183, 7.3891, 20.0855, 54.5981])
# 对数
c = torch.log(a)
print(f"自然对数: {c}") # tensor([0.0000, 0.6931, 1.0986, 1.3863])
# 绝对值
c = torch.abs(torch.tensor([-1, -2, 3]))
print(f"绝对值: {c}") # tensor([1, 2, 3])
# 取反
c = -a
c = torch.neg(a)
print(f"取反: {c}") # tensor([-1., -2., -3., -4.])矩阵乘法
PyTorch 提供了多种矩阵乘法方式,适用于不同的维度场景。
python
import torch
# ========== 2D 矩阵乘法 ==========
A = torch.tensor([[1.0, 2.0],
[3.0, 4.0]]) # 形状 (2, 2)
B = torch.tensor([[5.0, 6.0],
[7.0, 8.0]]) # 形状 (2, 2)
# 方法一: torch.matmul 或 @ 运算符
C = torch.matmul(A, B)
C = A @ B
print(f"矩阵乘法 (2x2 @ 2x2):\n{C}")
# tensor([[19., 22.],
# [43., 50.]])
# 方法二: torch.mm(仅用于 2D 矩阵)
C = torch.mm(A, B)
print(f"torch.mm:\n{C}")
# 方法三: torch.bmm(用于 3D 批量矩阵乘法)
batch_A = torch.randn(3, 2, 4) # 3 个 2x4 矩阵
batch_B = torch.randn(3, 4, 5) # 3 个 4x5 矩阵
batch_C = torch.bmm(batch_A, batch_B)
print(f"批量矩阵乘法形状: {batch_C.shape}") # torch.Size([3, 2, 5])
# ========== 高维矩阵乘法(广播) ==========
# matmul 支持广播,可以处理高维张量
x = torch.randn(2, 3, 4) # 形状 (2, 3, 4)
y = torch.randn(2, 4, 5) # 形状 (2, 4, 5)
z = torch.matmul(x, y) # 形状 (2, 3, 5)
print(f"高维 matmul 形状: {z.shape}")
# 广播场景
x = torch.randn(2, 1, 3, 4) # 形状 (2, 1, 3, 4)
y = torch.randn( 5, 4, 2) # 形状 (5, 4, 2)
z = x @ y # 广播后: (2, 5, 3, 2)
print(f"广播 matmul 形状: {z.shape}")
# ========== 向量运算 ==========
v1 = torch.tensor([1.0, 2.0, 3.0])
v2 = torch.tensor([4.0, 5.0, 6.0])
# 点积(内积)
dot = torch.dot(v1, v2)
print(f"点积: {dot}") # tensor(32.)
# 等价于 (v1 * v2).sum()
# 外积
outer = torch.outer(v1, v2)
print(f"外积形状: {outer.shape}") # torch.Size([3, 3])广播机制详解
广播(Broadcasting)是 PyTorch 中一个强大的特性,允许在不同形状的张量之间进行算术运算,无需显式地复制数据。
广播规则:
- 如果两个张量的维度数不同,较小维度的张量会在左侧补 1
- 如果两个张量在某个维度上的大小不同,且其中一个为 1,则该维度会扩展为另一个张量的大小
- 如果两个张量在某个维度上的大小不同且都不为 1,则无法广播,会报错
python
import torch
# ========== 广播示例 ==========
# 示例 1: 标量与张量
a = torch.tensor([1, 2, 3])
b = 2
print(f"张量 + 标量: {a + b}") # tensor([3, 4, 5])
# 标量 2 被广播为 [2, 2, 2]
# 示例 2: 不同维度的张量
# 形状 (3, 4) 和 (4,)
x = torch.ones(3, 4)
y = torch.tensor([1, 2, 3, 4])
print(f"(3,4) + (4,) 结果形状: {(x + y).shape}") # torch.Size([3, 4])
# y 的形状从 (4,) 广播为 (1, 4) 再扩展为 (3, 4)
# 示例 3: 维度补 1
# 形状 (3, 1) 和 (1, 4)
a = torch.tensor([[1], [2], [3]]) # 形状 (3, 1)
b = torch.tensor([[10, 20, 30, 40]]) # 形状 (1, 4)
c = a + b
print(f"(3,1) + (1,4) 结果形状: {c.shape}") # torch.Size([3, 4])
print(f"结果:\n{c}")
# tensor([[11, 21, 31, 41],
# [12, 22, 32, 42],
# [13, 23, 33, 43]])
# 示例 4: 无法广播的情况
# 形状 (3, 4) 和 (3, 5) 无法广播
try:
a = torch.ones(3, 4)
b = torch.ones(3, 5)
c = a + b
except RuntimeError as e:
print(f"广播错误: {e}")
# 示例 5: 多维广播
# 形状 (2, 3, 1) 和 (1, 1, 4)
a = torch.ones(2, 3, 1)
b = torch.ones(1, 1, 4)
c = a + b
print(f"(2,3,1) + (1,1,4) = {c.shape}") # torch.Size([2, 3, 4])
# ========== 手动扩展广播 ==========
# expand: 不复制数据的扩展(只读)
a = torch.tensor([[1], [2], [3]]) # 形状 (3, 1)
b = a.expand(3, 4) # 扩展为 (3, 4)
print(f"expand 结果:\n{b}")
# repeat: 复制数据的扩展
c = a.repeat(1, 4) # 在第 1 维重复 4 次
print(f"repeat 结果:\n{c}")
# 区别: expand 更高效(共享内存),repeat 会实际复制比较运算
比较运算返回布尔类型的张量,常用于条件判断、掩码生成和索引筛选。
python
import torch
a = torch.tensor([1, 2, 3, 4, 5])
b = torch.tensor([1, 3, 3, 2, 5])
# 等于
eq = a == b
eq = torch.eq(a, b)
print(f"等于: {eq}") # tensor([ True, False, True, False, True])
# 不等于
ne = a != b
ne = torch.ne(a, b)
print(f"不等于: {ne}") # tensor([False, True, False, True, False])
# 大于
gt = a > b
gt = torch.gt(a, b)
print(f"大于: {gt}") # tensor([False, False, False, True, False])
# 小于
lt = a < b
lt = torch.lt(a, b)
print(f"小于: {lt}") # tensor([False, True, False, False, False])
# 大于等于
ge = a >= b
ge = torch.ge(a, b)
print(f"大于等于: {ge}") # tensor([ True, False, True, True, True])
# 小于等于
le = a <= b
le = torch.le(a, b)
print(f"小于等于: {le}") # tensor([ True, True, True, False, True])
# 结合布尔运算
mask = (a > 2) & (a < 5) # 逻辑与
print(f"2 < a < 5 的掩码: {mask}") # tensor([False, False, True, True, False])
mask = (a < 2) | (a > 4) # 逻辑或
print(f"a < 2 或 a > 4: {mask}") # tensor([ True, False, False, False, True])
mask = ~(a == 3) # 逻辑非
print(f"a != 3: {mask}") # tensor([ True, True, False, True, True])
# any() 和 all()
x = torch.tensor([True, False, True])
print(f"any: {x.any()}") # tensor(True)
print(f"all: {x.all()}") # tensor(False)
# 排序后的索引比较
sorted_vals, sorted_idx = a.sort()
print(f"排序: {sorted_vals}, 索引: {sorted_idx}")索引与切片
基础索引与切片
基础索引与 Python 列表和 NumPy 的切片语法一致,但返回的是**视图(view)**而非副本。
python
import torch
# 创建一个 2D 张量
x = torch.tensor([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(f"原始张量:\n{x}")
# 单元素索引
print(f"x[0, 0]: {x[0, 0]}") # tensor(1)
print(f"x[1, 2]: {x[1, 2]}") # tensor(7)
# 行切片
print(f"x[0]: {x[0]}") # tensor([1, 2, 3, 4])
print(f"x[0:2]:\n{x[0:2]}") # 前两行
print(f"x[-1]: {x[-1]}") # tensor([9, 10, 11, 12]) 最后一行
# 列切片
print(f"x[:, 0]: {x[:, 0]}") # tensor([1, 5, 9])
print(f"x[:, 1:3]:\n{x[:, 1:3]}") # 第 1、2 列
# 行列组合切片
print(f"x[0:2, 1:3]:\n{x[0:2, 1:3]}")
# 步长切片
print(f"x[::2]:\n{x[::2]}") # 隔行取
print(f"x[:, ::2]:\n{x[:, ::2]}") # 隔列取
# 基础索引返回视图(共享内存)
sub = x[0:2, 0:2]
sub[0, 0] = 999
print(f"修改子张量后原始张量也变了:\n{x}")
# 注意: 这是视图操作,修改 sub 会影响 x高级索引
高级索引使用整数数组或张量进行索引,返回的是副本而非视图。
python
import torch
x = torch.tensor([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
# 花式索引(Fancy Indexing):使用整数列表/张量索引
# 选择特定行
rows = torch.tensor([0, 2])
print(f"选择第 0、2 行:\n{x[rows]}")
# tensor([[10, 20, 30],
# [70, 80, 90]])
# 选择特定列
cols = torch.tensor([0, 2])
print(f"选择第 0、2 列:\n{x[:, cols]}")
# 行列组合索引
print(f"x[[0, 1, 2], [0, 1, 2]]: {x[[0, 1, 2], [0, 1, 2]]}")
# 等价于选择 x[0,0], x[1,1], x[2,2]
# tensor([10, 50, 90])
# 使用 torch.index_select
# 按维度选择
selected = torch.index_select(x, dim=0, index=torch.tensor([0, 2]))
print(f"index_select(dim=0):\n{selected}")
selected = torch.index_select(x, dim=1, index=torch.tensor([1]))
print(f"index_select(dim=1):\n{selected}")
# 使用 gather
# gather 在指定维度上按索引收集值
src = torch.tensor([[1, 2, 3],
[4, 5, 6]])
# dim=1 时,index 的形状决定输出形状
idx = torch.tensor([[0, 1],
[2, 0]])
result = torch.gather(src, dim=1, index=idx)
print(f"gather 结果:\n{result}")
# tensor([[1, 2],
# [6, 4]])
# 第一行: 取 src[0,0]=1, src[0,1]=2
# 第二行: 取 src[1,2]=6, src[1,0]=4
# 使用 scatter(gather 的逆操作)
result = torch.zeros(2, 3, dtype=torch.long)
idx = torch.tensor([[0, 1, 2],
[2, 0, 1]])
src_vals = torch.tensor([[10, 20, 30],
[40, 50, 60]])
result.scatter_(dim=1, index=idx, src=src_vals)
print(f"scatter 结果:\n{result}")
# tensor([[10, 20, 30],
# [50, 60, 40]])布尔索引
布尔索引(Boolean Indexing)使用布尔张量作为掩码来选择元素。
python
import torch
x = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 布尔掩码
mask = x > 4
print(f"大于 4 的掩码:\n{mask}")
# 使用布尔掩码选择元素(返回 1D 张量)
selected = x[mask]
print(f"大于 4 的元素: {selected}") # tensor([5, 6, 7, 8, 9])
# 直接使用布尔表达式
print(f"偶数: {x[x % 2 == 0]}") # tensor([2, 4, 6, 8])
# 布尔索引结合条件
print(f"大于 3 且小于 8 的元素: {x[(x > 3) & (x < 8)]}")
# 使用 torch.where(三元操作)
# torch.where(condition, x, y) 满足条件取 x,否则取 y
y = torch.where(x > 5, x, torch.zeros_like(x))
print(f"大于 5 保留,否则置 0:\n{y}")
# 用 where 实现 ReLU 激活函数
a = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
relu = torch.where(a > 0, a, torch.zeros_like(a))
print(f"ReLU: {relu}") # tensor([0., 0., 0., 1., 2.])
# torch.masked_select
mask = x > 5
result = torch.masked_select(x, mask)
print(f"masked_select: {result}") # tensor([6, 7, 8, 9])
# 使用 nonzero 获取非零元素的索引
nz = torch.tensor([0, 3, 0, 7, 0]).nonzero()
print(f"非零元素索引: {nz.squeeze()}")形状操作
reshape 和 view
python
import torch
x = torch.arange(12)
print(f"原始: {x}") # tensor([ 0, 1, 2, ..., 11])
# reshape: 可以返回视图或副本(取决于内存是否连续)
y = x.reshape(3, 4)
print(f"reshape(3, 4):\n{y}")
# 使用 -1 自动推断维度
y = x.reshape(3, -1) # -1 自动计算为 4
print(f"reshape(3, -1):\n{y}")
y = x.reshape(-1, 4) # -1 自动计算为 3
print(f"reshape(-1, 4):\n{y}")
# view: 要求张量在内存中连续(更严格)
z = x.view(3, 4)
print(f"view(3, 4):\n{z}")
# view 的限制
t = torch.randn(2, 3)
t_t = t.t() # 转置后内存不连续
try:
t_t.view(6)
except RuntimeError as e:
print(f"view 错误: {e}")
# 解决方法: 先 contiguous() 再 view
t_contiguous = t_t.contiguous()
print(f"contiguous 后 view: {t_contiguous.view(6)}")
# reshape 和 view 的区别:
# - view 要求连续内存,不连续时会报错
# - reshape 不要求连续内存,不连续时会返回副本
# - 两者在连续内存时都返回视图transpose 和 permute
python
import torch
# transpose: 交换两个维度
x = torch.randn(2, 3, 4)
y = x.transpose(0, 1) # 交换第 0 维和第 1 维
print(f"transpose(0,1) 形状: {y.shape}") # torch.Size([3, 2, 4])
y = x.transpose(0, 2) # 交换第 0 维和第 2 维
print(f"transpose(0,2) 形状: {y.shape}") # torch.Size([4, 3, 2])
# 2D 转置
m = torch.tensor([[1, 2, 3],
[4, 5, 6]])
print(f"转置:\n{m.t()}") # 等价于 m.transpose(0, 1)
print(f"转置:\n{m.T}")
# permute: 同时重新排列所有维度
x = torch.randn(2, 3, 4, 5) # (N, C, H, W)
y = x.permute(0, 2, 3, 1) # -> (N, H, W, C)
print(f"permute(0,2,3,1) 形状: {y.shape}") # torch.Size([2, 4, 5, 3])
# 常见用法: 图像从 (C, H, W) 转为 (H, W, C)
img = torch.randn(3, 224, 224) # (C, H, W)
img_hwc = img.permute(1, 2, 0) # (H, W, C)
print(f"图像格式转换: {img.shape} -> {img_hwc.shape}")squeeze 和 unsqueeze
python
import torch
# squeeze: 移除大小为 1 的维度
x = torch.randn(1, 3, 1, 4)
print(f"原始形状: {x.shape}") # torch.Size([1, 3, 1, 4])
y = x.squeeze() # 移除所有大小为 1 的维度
print(f"squeeze() 形状: {y.shape}") # torch.Size([3, 4])
y = x.squeeze(0) # 仅移除第 0 维
print(f"squeeze(0) 形状: {y.shape}") # torch.Size([3, 1, 4])
y = x.squeeze(2) # 仅移除第 2 维
print(f"squeeze(2) 形状: {y.shape}") # torch.Size([1, 3, 4])
# unsqueeze: 在指定位置插入大小为 1 的维度
x = torch.tensor([1, 2, 3, 4])
print(f"原始形状: {x.shape}") # torch.Size([4])
y = x.unsqueeze(0) # 在第 0 维插入
print(f"unsqueeze(0) 形状: {y.shape}") # torch.Size([1, 4])
print(f"unsqueeze(0):\n{y}")
y = x.unsqueeze(1) # 在第 1 维插入
print(f"unsqueeze(1) 形状: {y.shape}") # torch.Size([4, 1])
print(f"unsqueeze(1):\n{y}")
# 常见用法: 给数据添加 batch 维度
single_img = torch.randn(3, 224, 224) # 单张图像
batch_img = single_img.unsqueeze(0) # 添加 batch 维度
print(f"添加 batch 维度: {single_img.shape} -> {batch_img.shape}")
# torch.Size([3, 224, 224]) -> torch.Size([1, 3, 224, 224])flatten 和 cat / stack
python
import torch
# flatten: 将张量展平
x = torch.randn(2, 3, 4)
y = x.flatten() # 展平为 1D
print(f"flatten() 形状: {y.shape}") # torch.Size([24])
y = x.flatten(start_dim=1) # 从第 1 维开始展平
print(f"flatten(1) 形状: {y.shape}") # torch.Size([2, 12])
y = x.flatten(start_dim=0, end_dim=1) # 展平第 0-1 维
print(f"flatten(0,1) 形状: {y.shape}") # torch.Size([6, 4])
# cat: 沿现有维度拼接
a = torch.randn(2, 3)
b = torch.randn(2, 3)
c = torch.cat([a, b], dim=0) # 沿第 0 维拼接
print(f"cat dim=0 形状: {c.shape}") # torch.Size([4, 3])
c = torch.cat([a, b], dim=1) # 沿第 1 维拼接
print(f"cat dim=1 形状: {c.shape}") # torch.Size([2, 6])
# 多个张量拼接
c = torch.cat([a, b, a], dim=0)
print(f"三个张量 cat: {c.shape}") # torch.Size([6, 3])
# stack: 沿新维度堆叠
c = torch.stack([a, b], dim=0) # 在第 0 维堆叠
print(f"stack dim=0 形状: {c.shape}") # torch.Size([2, 2, 3])
c = torch.stack([a, b], dim=1) # 在第 1 维堆叠
print(f"stack dim=1 形状: {c.shape}") # torch.Size([2, 2, 3])
c = torch.stack([a, b], dim=2) # 在第 2 维堆叠
print(f"stack dim=2 形状: {c.shape}") # torch.Size([2, 3, 2])
# cat vs stack 区别:
# cat: 沿已有维度拼接,不增加维度数
# stack: 沿新维度堆叠,增加一个维度split 和 chunk
python
import torch
x = torch.arange(10).float()
print(f"原始: {x}")
# split: 按指定大小拆分
parts = x.split(3) # 每份 3 个元素(最后一份可以更少)
for i, p in enumerate(parts):
print(f"split(3) 第 {i} 部分: {p}")
# tensor([0., 1., 2.])
# tensor([3., 4., 5.])
# tensor([6., 7., 8.])
# tensor([9.])
# 指定各部分大小
parts = x.split([2, 3, 5])
for i, p in enumerate(parts):
print(f"split([2,3,5]) 第 {i} 部分: {p}")
# chunk: 按数量拆分
parts = x.chunk(4) # 拆成 4 份
for i, p in enumerate(parts):
print(f"chunk(4) 第 {i} 部分: {p}")
# 2D 张量的 split
y = torch.randn(6, 4)
parts = y.split(2, dim=0) # 沿第 0 维拆分,每份 2 行
for i, p in enumerate(parts):
print(f"split(2, dim=0) 第 {i} 部分形状: {p.shape}")repeat 和 expand
python
import torch
x = torch.tensor([[1, 2, 3]]) # 形状 (1, 3)
# repeat: 沿各维度重复(数据被实际复制)
y = x.repeat(3, 1) # 第 0 维重复 3 次,第 1 维重复 1 次
print(f"repeat(3, 1):\n{y}")
# tensor([[1, 2, 3],
# [1, 2, 3],
# [1, 2, 3]])
y = x.repeat(3, 2) # 第 0 维重复 3 次,第 1 维重复 2 次
print(f"repeat(3, 2):\n{y}")
# expand: 扩展大小为 1 的维度(不复制数据,更高效)
x = torch.tensor([[1, 2, 3]]) # 形状 (1, 3)
y = x.expand(3, 3) # 扩展第 0 维
print(f"expand(3, 3):\n{y}")
# tensor([[1, 2, 3],
# [1, 2, 3],
# [1, 2, 3]])
# expand 的限制: 只能扩展大小为 1 的维度
x = torch.tensor([[1, 2, 3]])
try:
y = x.expand(3, 4) # 第 1 维大小为 3,不能扩展到 4
except RuntimeError as e:
print(f"expand 错误: {e}")
# 使用 -1 保持该维度不变
x = torch.randn(1, 3, 1, 5)
y = x.expand(2, -1, 4, -1) # -1 表示保持原大小
print(f"expand 后形状: {y.shape}") # torch.Size([2, 3, 4, 5])
# repeat 与 expand 的区别:
# - expand 不复制数据,返回视图(只读),更高效
# - repeat 实际复制数据,占用更多内存
# - expand 只能扩展大小为 1 的维度,repeat 可以任意重复归约操作
sum、mean、max、min
python
import torch
x = torch.tensor([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]])
# sum: 求和
print(f"总和: {x.sum()}") # tensor(21.)
print(f"按列求和: {x.sum(dim=0)}") # tensor([5., 7., 9.])
print(f"按行求和: {x.sum(dim=1)}") # tensor([ 6., 15.])
# 保持维度
print(f"按列求和(保持维度):\n{x.sum(dim=0, keepdim=True)}")
# tensor([[5., 7., 9.]]) 形状 (1, 3)
# mean: 均值
print(f"均值: {x.mean()}") # tensor(3.5000)
print(f"按列均值: {x.mean(dim=0)}") # tensor([2.5000, 3.5000, 4.5000])
# max / min: 最大/最小值
print(f"最大值: {x.max()}") # tensor(6.)
print(f"每列最大值: {x.max(dim=0)}") # 返回 (values, indices)
# 分别获取值和索引
max_vals, max_indices = x.max(dim=1)
print(f"每行最大值: {max_vals}") # tensor([3., 6.])
print(f"每行最大值索引: {max_indices}") # tensor([2, 2])
# min 同理
min_vals, min_indices = x.min(dim=0)
print(f"每列最小值: {min_vals}") # tensor([1., 2., 3.])
# prod: 连乘
print(f"总乘积: {x.prod()}") # tensor(720.)argmax、argmin、topk、sort
python
import torch
x = torch.tensor([[3.0, 1.0, 4.0],
[1.0, 5.0, 9.0],
[2.0, 6.0, 5.0]])
# argmax: 最大值的索引
print(f"全局 argmax: {x.argmax()}") # tensor(5) 展平后的索引
print(f"每行 argmax: {x.argmax(dim=1)}") # tensor([2, 2, 1])
print(f"每列 argmax: {x.argmax(dim=0)}") # tensor([0, 2, 1])
# argmin: 最小值的索引
print(f"每行 argmin: {x.argmin(dim=1)}") # tensor([1, 0, 0])
# topk: 获取最大的 k 个值
values, indices = x.topk(2, dim=1)
print(f"每行最大的 2 个值:\n{values}")
print(f"对应索引:\n{indices}")
# 最小的 k 个值
values, indices = x.topk(2, dim=1, largest=False)
print(f"每行最小的 2 个值:\n{values}")
# sort: 排序
sorted_vals, sorted_indices = x.sort(dim=1)
print(f"每行排序:\n{sorted_vals}")
print(f"排序索引:\n{sorted_indices}")
# 降序排序
sorted_vals, sorted_indices = x.sort(dim=1, descending=True)
print(f"每行降序排序:\n{sorted_vals}")
# argsort: 仅返回排序索引
indices = x.argsort(dim=1)
print(f"argsort 结果:\n{indices}")原地操作与非原地操作
原地操作概述
在 PyTorch 中,原地操作(In-place Operation)通过在方法名后加下划线 _ 来标识。原地操作直接修改张量本身,而非返回新的张量。
python
import torch
# ========== 非原地操作(返回新张量) ==========
x = torch.tensor([1.0, 2.0, 3.0])
y = x.add(1) # 返回新张量
print(f"x 不变: {x}") # tensor([1., 2., 3.])
print(f"新张量 y: {y}") # tensor([2., 3., 4.])
# ========== 原地操作(修改原始张量) ==========
x = torch.tensor([1.0, 2.0, 3.0])
x.add_(1) # 原地加 1
print(f"原地操作后 x: {x}") # tensor([2., 3., 4.])
# 常见的原地操作
x = torch.tensor([1.0, 2.0, 3.0, 4.0])
x.add_(10) # 原地加法
print(f"add_: {x}")
x.mul_(2) # 原地乘法
print(f"mul_: {x}")
x.sub_(5) # 原地减法
print(f"sub_: {x}")
x.div_(3) # 原地除法
print(f"div_: {x}")
x.clamp_(min=0) # 原地裁剪(ReLU)
print(f"clamp_: {x}")
x.zero_() # 原地置零
print(f"zero_: {x}")
x.fill_(3.14) # 原地填充
print(f"fill_: {x}")
x.abs_() # 原地取绝对值
x.relu_() # 原地 ReLU
x.sigmoid_() # 原地 sigmoid
x.tanh_() # 原地 tanh原地操作的注意事项
python
import torch
# ========== 注意 1: requires_grad=True 的张量不能原地操作 ==========
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
try:
x.add_(1)
except RuntimeError as e:
print(f"错误: {e}")
# RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
# ========== 注意 2: 原地操作可能破坏计算图 ==========
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2
# z = y + x # 这样是安全的
# x.add_(1) # 如果原地修改 x,y 的计算历史会被破坏
# ========== 注意 3: 切片原地赋值是允许的 ==========
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = x * 2
y[0] = 100 # 切片赋值是允许的
# 或
y.detach()[0] = 100
# ========== 最佳实践 ==========
# 1. 在不需要梯度追踪时使用原地操作节省内存
# 2. 在模型训练循环中,避免对需要梯度的张量使用原地操作
# 3. 对于叶子张量(leaf tensor),使用 .data 属性进行原地操作
x = torch.tensor([1.0, 2.0], requires_grad=True)
x.data.add_(1) # 允许,但会跳过梯度追踪
print(f"data.add_ 后: {x}")实用技巧
einsum: 爱因斯坦求和
torch.einsum 是一个极其灵活的函数,可以用简洁的字符串表达各种张量运算。
python
import torch
A = torch.randn(3, 4)
B = torch.randn(4, 5)
# 矩阵乘法: C_ij = A_ik * B_kj
C = torch.einsum('ik,kj->ij', A, B)
print(f"矩阵乘法形状: {C.shape}") # torch.Size([3, 5])
# 向量点积
v1 = torch.randn(5)
v2 = torch.randn(5)
dot = torch.einsum('i,i->', v1, v2)
print(f"点积: {dot}")
# 矩阵转置
T = torch.einsum('ij->ji', A)
print(f"转置形状: {T.shape}") # torch.Size([4, 3])
# 矩阵迹(对角线之和)
M = torch.randn(4, 4)
trace = torch.einsum('ii->', M)
print(f"矩阵的迹: {trace}")
# 批量矩阵乘法
batch_A = torch.randn(8, 3, 4)
batch_B = torch.randn(8, 4, 5)
batch_C = torch.einsum('bik,bkj->bij', batch_A, batch_B)
print(f"批量矩阵乘法形状: {batch_C.shape}") # torch.Size([8, 3, 5])
# 注意力机制中的 QKV 计算
# Q: (batch, seq_len, d_k), K: (batch, seq_len, d_k), V: (batch, seq_len, d_v)
batch, seq_len, d_k, d_v = 2, 10, 64, 32
Q = torch.randn(batch, seq_len, d_k)
K = torch.randn(batch, seq_len, d_k)
V = torch.randn(batch, seq_len, d_v)
# 注意力分数: (batch, seq_len, seq_len)
attn = torch.einsum('bqd,bkd->bqk', Q, K) / (d_k ** 0.5)
attn = torch.softmax(attn, dim=-1)
# 加权求和: (batch, seq_len, d_v)
output = torch.einsum('bqk,bvd->bqd', attn, V)
print(f"注意力输出形状: {output.shape}") # torch.Size([2, 10, 32])
# 张量缩并(trace of product)
A = torch.randn(3, 4)
B = torch.randn(3, 4)
result = torch.einsum('ij,ij->', A, B) # Frobenius 内积
print(f"Frobenius 内积: {result}")
# 求对角线元素
M = torch.randn(4, 4)
diag = torch.einsum('ii->i', M)
print(f"对角线元素: {diag}")where 和 clamp
python
import torch
# ========== where: 条件选择 ==========
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
# 基本用法: where(condition, x, y)
# condition 为 True 取 x,否则取 y
result = torch.where(x > 0, x, torch.tensor(0.0))
print(f"ReLU: {result}") # tensor([0., 0., 0., 1., 2.])
# 两个张量之间选择
a = torch.tensor([1, 2, 3, 4, 5])
b = torch.tensor([10, 20, 30, 40, 50])
mask = torch.tensor([True, False, True, False, True])
result = torch.where(mask, a, b)
print(f"条件选择: {result}") # tensor([10, 2, 30, 4, 50])
# 2D 张量示例
x = torch.randn(3, 4)
result = torch.where(x > 0, x, torch.tensor(0.0))
print(f"负值置零:\n{result}")
# ========== clamp: 裁剪 ==========
x = torch.tensor([0.5, 1.5, 2.5, 3.5, 4.5])
# 限制在 [min, max] 范围内
y = torch.clamp(x, min=1.0, max=3.0)
print(f"clamp(1.0, 3.0): {y}") # tensor([1.0000, 1.5000, 2.5000, 3.0000, 3.0000])
# 仅设置最小值
y = torch.clamp(x, min=2.0)
print(f"clamp(min=2.0): {y}") # tensor([2.0000, 2.0000, 2.5000, 3.5000, 4.5000])
# 仅设置最大值
y = torch.clamp(x, max=3.0)
print(f"clamp(max=3.0): {y}") # tensor([0.5000, 1.5000, 2.5000, 3.0000, 3.0000])
# clamp_min 和 clamp_max 的简写
y = torch.clamp_min(x, 2.0)
y = torch.clamp_max(x, 3.0)
# 梯度裁剪中的应用
x = torch.randn(3, requires_grad=True)
y = x ** 2
y.sum().backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_([x], max_norm=1.0)unique: 唯一值操作
python
import torch
x = torch.tensor([1, 2, 3, 2, 1, 4, 3, 5, 1])
# 基本用法: 返回唯一值
unique_vals = torch.unique(x)
print(f"唯一值: {unique_vals}") # tensor([1, 2, 3, 4, 5])
# 排序后的唯一值(默认已排序)
unique_vals = torch.unique(x, sorted=True)
print(f"排序后的唯一值: {unique_vals}")
# 返回逆索引(用于重建原始张量)
unique_vals, inverse_indices = torch.unique(x, return_inverse=True)
print(f"唯一值: {unique_vals}")
print(f"逆索引: {inverse_indices}")
# 重建: unique_vals[inverse_indices] == x
# 返回每个唯一值的计数
unique_vals, counts = torch.unique(x, return_counts=True)
print(f"唯一值: {unique_vals}")
print(f"计数: {counts}")
# 2D 张量的 unique
x = torch.tensor([[1, 2], [3, 2], [1, 2]])
unique_rows = torch.unique(x, dim=0)
print(f"唯一行:\n{unique_rows}")
# 常见用途: 统计类别数
labels = torch.tensor([0, 1, 2, 1, 0, 2, 3, 1])
num_classes = torch.unique(labels).numel()
print(f"类别数: {num_classes}") # 4
# 配合 where 使用: 获取某个类别的所有索引
target_class = 1
indices = torch.where(labels == target_class)[0]
print(f"类别 {target_class} 的索引: {indices}")
# 使用 bincount 统计各类别数量
counts = torch.bincount(labels)
print(f"各类别数量: {counts}") # tensor([2, 3, 2, 1])更多实用技巧
python
import torch
# ========== masked_fill: 掩码填充 ==========
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
mask = torch.tensor([True, False, True, False, True])
result = x.masked_fill(mask, value=0.0)
print(f"masked_fill: {result}") # tensor([0., 2., 0., 4., 0.])
# 常见用途: 注意力掩码
seq_len = 5
# 创建上三角掩码(因果掩码)
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
print(f"因果掩码:\n{~mask}")
# ========== norm: 范数计算 ==========
x = torch.tensor([3.0, 4.0])
print(f"L1 范数: {x.norm(p=1)}") # tensor(7.)
print(f"L2 范数: {x.norm(p=2)}") # tensor(5.)
print(f"无穷范数: {x.norm(p=float('inf'))}") # tensor(4.)
# 矩阵范数
M = torch.randn(3, 4)
print(f"Frobenius 范数: {M.norm()}")
# ========== cumsum / cumprod: 累积运算 ==========
x = torch.tensor([1.0, 2.0, 3.0, 4.0])
print(f"累积和: {torch.cumsum(x, dim=0)}") # tensor([ 1., 3., 6., 10.])
print(f"累积积: {torch.cumprod(x, dim=0)}") # tensor([ 1., 2., 6., 24.])
# ========== cross_entropy 简化示例 ==========
# 在实际训练中常用的组合操作
logits = torch.randn(3, 5) # 3 个样本,5 个类别
targets = torch.tensor([1, 3, 4])
loss = torch.nn.functional.cross_entropy(logits, targets)
print(f"交叉熵损失: {loss}")
# ========== detach: 从计算图中分离 ==========
x = torch.randn(3, requires_grad=True)
y = x * 2
z = y.detach() # z 不再追踪梯度
print(f"y requires_grad: {y.requires_grad}") # True
print(f"z requires_grad: {z.requires_grad}") # False