1 张量的创建
张量是向量、矩阵的扩展,和 Numpy 的 ndarray 类似,但张量可以在 GPU
上运行,从而加速计算。
| 0维张量 |
代表的是标量(数字) |
| 1维张量 |
代表的是向量 |
| 2维张量 |
代表的是矩阵 |
| 3维张量 |
时间序列数据 股价 文本数据 单张彩色图片(RGB) |
1 2
| import torch import numpy as np
|
tensor():类似np.array,直接来自数据
tensor([1, 2, 3])
Tensor():基础构造函数,有更多参数选项,可以直接指定形状
tensor([[0., 0., 0., 0.],
[0., 0., 0., 0.]])
和 Numpy 类似,常见的构造 Tensor 的方法:
| Tensor(sizes) |
基础构造函数 |
| tensor(data) |
类似于np.array |
| ones(sizes) |
全1 |
| zeros(sizes) |
全0 |
| eye(sizes) |
对角为1,其余为0 |
| arange(s,e,step) |
从s到e,步长为step |
| randint(low,high,size) |
随机生成整数 |
| linspace(s,e,steps) |
从s到e,均匀分成step份 |
| rand/randn(sizes) |
rand是[0,1)均匀分布;randn是服从N(0,1)的正态分布 |
| normal(mean,std) |
正态分布(均值为mean,标准差是std) |
| randperm(m) |
随机排列 |
还有一些之前 Numpy 没介绍的 Tensor 方法,在此介绍。
ones_like():除非显式声明,否则创建与参数张量的形状、dtype、device
一样的全 1 张量。
1 2 3 4 5 6
| torch.manual_seed(66) x = torch.rand(2, 3)
display(x) display(torch.ones_like(x))
|
tensor([[0.4976, 0.0230, 0.2271],
[0.5708, 0.8475, 0.7140]])
tensor([[1., 1., 1.],
[1., 1., 1.]])
1
| torch.ones_like(x, dtype=torch.int64)
|
tensor([[1, 1, 1],
[1, 1, 1]])
rand_like():创建一个属性相同的随机张量。
1
| torch.rand_like(x, dtype=torch.float32)
|
tensor([[0.8519, 0.6986, 0.7792],
[0.6343, 0.3099, 0.9380]])
此外还有 zeros_like(), full_like() 等很多
xxx_like() 创建方法。
2 张量的属性
张量属性描述其形状、数据类型以及存储它们的设备。
1 2 3 4 5
| t = torch.rand(3, 4)
print('形状:', t.shape) print('类型:', t.dtype) print('存储设备:', t.device)
|
形状: torch.Size([3, 4])
类型: torch.float32
存储设备: cpu
如果只想查看张量的元素个数,可以使用 numel()。
1
| print('元素个数:', t.numel())
|
元素个数: 12
3 张量的操作
3.1 张量转换
1 2 3 4 5
| t = torch.randint(0, 10, (5,)) print('type:', t.type(torch.float32))
print('float:', t.float())
|
type: tensor([4., 6., 6., 8., 0.])
float: tensor([4., 6., 6., 8., 0.])
from_numpy():将 ndarray 转换为 Tensor,共享内存。使用
copy() 避免共享内存。
1 2 3
| a = np.array([[1, 2, 3], [4, 5, 6]]) x = torch.from_numpy(a) x
|
tensor([[1, 2, 3],
[4, 5, 6]])
tensor():将 ndarray 转换为 Tensor,不共享内存。
1 2 3
| a = np.array([[1, 2, 3], [4, 5, 6]]) x = torch.tensor(a) x
|
tensor([[1, 2, 3],
[4, 5, 6]])
也可以通过numpy()方法将 Tensor 转为 Numpy 数组。
array([[1, 2, 3],
[4, 5, 6]])
如果张量中只有一个元素,可以使用 item()
取出一个普通的值。
1 2
| e = torch.tensor([5]) e.item()
|
5
3.2 运算操作
官方文档提供了包括转置、索引、切片、数学运算、线性代数、随机采样等等,它们都可以在
GPU 上运行。
1 2 3 4 5
| t3 = torch.rand(2, 3) if torch.cuda.is_available(): t3 = t3.to('cuda') print(f"Device tensor is stored on: {t3.device}")
|
Device tensor is stored on: cuda:0
+、-、*、/:加减乘除
add()、sub()、mul()、div():加减乘除
**、pow()、pow_():求幂
-、neg()、neg_():取负
sqrt()、sqrt_():求平方根
exp()、exp_():以 e 为底数求幂
log()、log_():以 e 为底求对数
运算和 Numpy 类似,多了一种使用后缀 _
的原地操作。
1 2 3 4 5 6 7 8
| display(t3 + t3)
display(torch.add(t3, t3))
display(t3.add_(10))
|
tensor([[1.8163, 0.5864, 0.2897],
[1.3990, 0.3196, 0.0220]], device='cuda:0')
tensor([[1.8163, 0.5864, 0.2897],
[1.3990, 0.3196, 0.0220]], device='cuda:0')
tensor([[10.9082, 10.2932, 10.1449],
[10.6995, 10.1598, 10.0110]], device='cuda:0')
原地操作可以节省一些内存,但由于会立即丢失历史数据,因此在计算导数时可能会出现问题。因此,不建议使用原地操作。
点乘和 Numpy 也是类似的。
1 2 3
| display(torch.matmul(t3, t3.T))
display(t3 @ t3.T)
|
tensor([[327.8561, 322.8491],
[322.8491, 317.9212]], device='cuda:0')
tensor([[327.8561, 322.8491],
[322.8491, 317.9212]], device='cuda:0')
sum():求和
mean():求均值
max()/min():求最大/最小值及其索引
argmax()/argmin():求最大值/最小值的索引
std():求标准差
unique():去重
sort():排序
可以通过 dim 指定维度。
1 2 3
| display(t3)
display(t3.sum(dim=0))
|
tensor([[10.9082, 10.2932, 10.1449],
[10.6995, 10.1598, 10.0110]], device='cuda:0')
tensor([21.6077, 20.4530, 20.1559], device='cuda:0')
3.3 索引操作
和 Numpy
类似,布尔索引、花式索引、切片索引都是支持的。注意索引结果与原数据共享内存。如果不想影响原数据,可以考虑使用
copy() 等方法。
1 2 3
| display(t3)
display(t3[:, 1:])
|
tensor([[10.9082, 10.2932, 10.1449],
[10.6995, 10.1598, 10.0110]], device='cuda:0')
tensor([[10.2932, 10.1449],
[10.1598, 10.0110]], device='cuda:0')
3.4 维度和形状
3.4.1 维度调整
1 2 3 4
| x2 = torch.rand(2, 3) display(x2)
display(x2.flatten())
|
tensor([[0.3597, 0.0678, 0.1368],
[0.3185, 0.4533, 0.4754]])
tensor([0.3597, 0.0678, 0.1368, 0.3185, 0.4533, 0.4754])
transpose(input, dim1, dim2):交换两个维度
1 2 3 4 5 6 7
| t4 = torch.randint(1, 10, size=[2, 2, 3]) print(t4)
print(torch.transpose(t4, 1, 2))
print(t4.transpose(1, 2))
|
tensor([[[4, 7, 4],
[2, 2, 5]],
[[8, 3, 6],
[8, 6, 4]]])
tensor([[[4, 2],
[7, 2],
[4, 5]],
[[8, 8],
[3, 6],
[6, 4]]])
tensor([[[4, 2],
[7, 2],
[4, 5]],
[[8, 8],
[3, 6],
[6, 4]]])
pemute(input, dims):重新排列维度顺序
1 2 3 4 5 6 7 8 9 10 11 12 13
| t4 = torch.randn(2, 3, 4) display(t4) print('permute前:', t4.shape)
t4 = t4.permute((1, 0, 2)) display(t4) print('permute后:', t4.shape)
|
tensor([[[ 1.0098, -1.1005, 2.8977, -0.8842],
[ 1.2498, 0.6263, -0.4922, 1.4868],
[-0.4247, 0.2663, 0.8949, 0.8392]],
[[-0.2499, -0.1474, -1.1676, 1.8240],
[ 1.1016, -0.6056, 0.9351, 0.0119],
[ 0.2244, 0.2427, -2.1742, -0.5351]]])
permute前: torch.Size([2, 3, 4])
tensor([[[ 1.0098, -1.1005, 2.8977, -0.8842],
[-0.2499, -0.1474, -1.1676, 1.8240]],
[[ 1.2498, 0.6263, -0.4922, 1.4868],
[ 1.1016, -0.6056, 0.9351, 0.0119]],
[[-0.4247, 0.2663, 0.8949, 0.8392],
[ 0.2244, 0.2427, -2.1742, -0.5351]]])
permute后: torch.Size([3, 2, 4])
unsqueeze():在指定位置插入 1 个维度,大小为 1
1 2 3 4
| t4 = torch.tensor([1, 2, 3])
print('在 0 维度插入:\n', t4.unsqueeze(0)) print('\n在 1 维度插入:\n', t4.unsqueeze(1))
|
在 0 维度插入:
tensor([[1, 2, 3]])
在 1 维度插入:
tensor([[1],
[2],
[3]])
1 2
| t4 = torch.randn(2, 1, 3, 1) t4.squeeze()
|
tensor([[-2.0466, 0.0907, -0.5900],
[-1.4429, 0.0549, 0.1932]])
3.4.2 形状调整
张量的形状变换有 view() 和
reshape(),都可以通过-1来自动计算某维度,如
reshape(-1, 1) 变换为 1 列,行自动计算。 -
view():必须是连续内存的张量,否则报错,共享底层内存,不复制数据,是浅拷贝
-
reshape():若连续则返回视图(浅拷贝),不连续则返回副本(深拷贝),保证可以使用
1 2 3 4 5
| x1 = torch.randn(4, 3) y1 = x1.view(12)
y1 += 10 print(x1)
|
tensor([[11.4299, 8.8268, 8.1305],
[ 8.5055, 8.7648, 10.3116],
[ 9.6589, 10.2913, 10.4617],
[11.0716, 11.6512, 9.4457]])
1 2 3 4 5 6 7 8 9 10 11 12 13
| x2 = torch.randn(4, 3) y2 = x2.reshape(12)
y2 += 10 print('连续返回视图:\n', x2)
x2 = torch.randn(4, 3).T y2 = x2.reshape(12)
y2 += 10 print('不连续返回副本:\n', x2)
|
连续返回视图:
tensor([[10.1255, 10.5621, 9.7059],
[ 9.3825, 10.3619, 9.8099],
[ 9.3883, 7.9280, 8.6108],
[ 9.4097, 7.4755, 9.9311]])
不连续返回副本:
tensor([[ 0.2168, 0.2555, -0.3164, -0.0119],
[ 0.3482, -0.5855, -0.4436, 0.9016],
[ 0.2345, 1.9516, 0.0829, 0.9176]])
一个张量可能因为 transpose()、permute()
等操作变成非连续的。如果内存不连续,则使用 view
会报错,可以使用 is_contiguous() 判断是否内存连续,并使用
contiguous() 转换为连续内存。
1 2 3 4 5 6
| print('转置前:', x2.is_contiguous()) x2 = x2.T print('转置后:', x2.is_contiguous())
x2 = x2.contiguous() print('转换为连续:', x2.is_contiguous())
|
转置前: False
转置后: True
转换为连续: True
由于 reshape()
并不保证返回的是拷贝值,所以不推荐使用这个方法,推荐使用
clone() 创造副本之后使用 view()
进行维度变换。
使用 clone()
还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源 Tensor
。
3.5 张量拼接
使用 dim 指定拼接维度,类似 Numpy 的
axis,默认为 0 -
cat():在指定维度上拼接张量,除拼接维度外,其他维度必须相等
- stack():在新维度上堆叠张量,所有维度必须相等
1 2 3 4
| x4 = torch.arange(6).view(2, 3)
display(torch.cat([x4, x4])) display(torch.cat([x4, x4], dim=1))
|
tensor([[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]])
tensor([[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]])
1 2 3
| display(torch.stack([x4, x4])) display(torch.stack([x4, x4], dim=1))
|
tensor([[[0, 1, 2],
[3, 4, 5]],
[[0, 1, 2],
[3, 4, 5]]])
tensor([[[0, 1, 2],
[0, 1, 2]],
[[3, 4, 5],
[3, 4, 5]]])
3.6 广播机制
和 Numpy 类似的广播机制,先复制元素使这两个 Tensor
形状相同后再按元素运算。
1 2 3
| a = torch.arange(6) b = torch.arange(3).view(-1, 1) a + b
|
tensor([[0, 1, 2, 3, 4, 5],
[1, 2, 3, 4, 5, 6],
[2, 3, 4, 5, 6, 7]])
3.7 节省内存
直接使用 X = X + 10
会分配新的内存,在机器学习中,可能有数百兆的参数,并且在一秒内多次更新所有参数。如果不原地更新,其他引用仍然指向旧地址,可能会无意中引用旧的参数。通常情况下,我们希望原地执行这些更新。
1 2 3 4
| X = torch.tensor([1, 2, 3]) before = id(X) X = X + 10 id(X) == before
|
False
+= 会原地修改。
1 2 3
| before = id(X) X += 10 id(X) == before
|
True
可以使用切片表示法执行原地操作,X[:] = <expression>,
1 2 3
| before = id(X) X[:] = X + 10 id(X) == before
|
True
4 自动微分
PyTorch 的自动微分是基于计算图(computational graph)的。每做一次涉及
tensor
的计算,会构建一棵有向无环图,图的节点是张量及其产生操作(operation),边表示依赖关系。
对最终标量(通常是 loss)调用 .backward() 时,PyTorch
会沿着图反向传播,应用链式法则计算每个需要梯度的叶子张量(leaf
tensor)的梯度。
4.1 微分流程
requires_grad:标记为 True
的张量会被追踪所有针对该张量的操作,计算得到的张量也会被追踪。每个操作都会产生一个新的
Tensor,并记录下生成它的操作(grad_fn)。
通常会将模型参数(如权重和偏置)的 requires_grad
属性设置为 True,输入数据设置为 False。
当完成前向传播并计算出损失(Loss,通常是一个标量)后,就可以调用
backward() 方法来启动反向传播,计算梯度。
backward():沿着计算图从损失节点开始,使用链式法则计算梯度累加到所有节点的
.grad 属性中。
每次新的训练迭代开始时,都需要调用优化器或手动将 .grad
清零(optimizer.zero_grad() 或
tensor.grad.zero_())
叶子张量:由用户直接创建,而不是通过计算图生成的张量。
.grad:调用 backward() 后,叶子张量的
.grad 会保存其梯度。若非叶子,.grad 常为
None,除非调用 retain_grad()。
grad_fn:对于非叶子张量,grad_fn
指向生成它的函数(计算图中的节点),而叶子张量 grad_fn 为
None。

初始时输入x,模型参数w和b设为requires_grad=True进行跟踪,经过wx+b的运算,得到预测值z,与真实值通过CE损失函数计算损失loss,调用backward()进行反向传播,得到梯度w.grad和b.grad,并更新参数w和b。
4.2 detach 分离梯度
开启自动微分的张量不能转换为Numpy数组,可以使用detach()方法,该方法返回一个新的张量,该张量:
- 共享同一块数据内存(浅拷贝)
- 不再与计算图绑定,不记录梯度,不参与反向传播
1 2 3 4 5 6 7 8 9
| x = torch.tensor([10, 20], requires_grad=True, dtype=torch.float32)
try: a = torch.numpy(x) except Exception as e: print('numpy() is not supported') a = x.detach().numpy() print(a)
|
numpy() is not supported
[10. 20.]
4.3 手动模拟
模拟求解 f(x)=x^2
的最小值,相当于实际应用中求解损失函数的最小值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| def f(x): return x ** 2
x = torch.tensor([10], requires_grad=True, dtype=torch.float32)
epochs = 10 lr = 0.2 for epoch in range(epochs): y_pred = f(x) if x.grad is not None: x.grad.zero_() y_pred.sum().backward() with torch.no_grad(): x -= lr * x.grad print(f'epoch{epoch + 1}: x: {x.item():.3f}, y: {y_pred.item():.3f}, grad: {x.grad.item():.3f}')
|
epoch1: x: 6.000, y: 100.000, grad: 20.000
epoch2: x: 3.600, y: 36.000, grad: 12.000
epoch3: x: 2.160, y: 12.960, grad: 7.200
epoch4: x: 1.296, y: 4.666, grad: 4.320
epoch5: x: 0.778, y: 1.680, grad: 2.592
epoch6: x: 0.467, y: 0.605, grad: 1.555
epoch7: x: 0.280, y: 0.218, grad: 0.933
epoch8: x: 0.168, y: 0.078, grad: 0.560
epoch9: x: 0.101, y: 0.028, grad: 0.336
epoch10: x: 0.060, y: 0.010, grad: 0.202
4.4 实际应用
实际使用中,数据集的分批加载由DataLoader()完成,损失函数由nn.xxxLoss()完成,优化方法由optim.xxx()完成。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader
X = torch.randn(100, 1) y = 5 * X + 3
dataset = TensorDataset(X, y)
dataloader = DataLoader( dataset=dataset, batch_size=16, shuffle=True )
model = nn.Linear(in_features=1, out_features=1)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
epochs = 100 for epoch in range(epochs): for X_train, y_train in dataloader: y_pred = model(X_train) loss = criterion(y_train, y_pred) optimizer.zero_grad() loss.sum().backward() optimizer.step() print(f'模型参数 w:{model.weight.item():.3f}, b: {model.bias.item():.3f}')
|
模型参数 w:5.000, b: 3.000