本文为学习Datawhale 2021.10组队学习深入浅出Pytorch笔记
原学习文档地址:https://github.com/datawhalechina/thorough-pytorch
另外参考了动手学深度学习pytorch版:https://github.com/ShusenTang/Dive-into-DL-PyTorch
1 数据操作
1.1 创建张量
from __future__ import print_function
import torch
x = torch.rand(4, 3)
print(x)
tensor([[0.8139, 0.5880, 0.8233],
[0.1217, 0.3791, 0.6448],
[0.8015, 0.5228, 0.9957],
[0.8883, 0.4443, 0.2189]])
x = torch.zeros(4, 3, dtype=torch.long)
print(x)
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
# 根据数据创建
x = torch.tensor([5,5,3])
print(x)
tensor([5, 5, 3])
x = x.new_ones(4, 3, dtype=torch.double)
print(x)
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
x = torch.randn_like(x, dtype=torch.float)
print(x)
tensor([[ 0.6082, -2.2406, -0.1449],
[ 0.5737, -0.2438, 1.1611],
[ 0.1805, -1.5532, -0.3325],
[ 1.5098, 0.2555, -0.7982]])
x = torch.empty(4, 3)
print(x)
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
x.shape
torch.Size([4, 3])
还有一些常见的构造Tensor的函数:
| 函数 | 功能 |
|---|---|
| Tensor(*sizes) | 基础构造函数 |
| tensor(data) | 类似于np.array |
| ones(*sizes) | 全1 |
| zeros(*sizes) | 全0 |
| eye(*sizes) | 对角为1,其余为0 |
| arange(s,e,step) | 从s到e,步长为step |
| linspace(s,e,steps) | 从s到e,均匀分成step份 |
| rand/randn(*sizes) | |
| normal(mean,std)/uniform(from,to) | 正态分布/均匀分布 |
| randperm(m) | 随机排列 |
1.2 操作
1.2.1 加法
y = torch.rand(4, 3)
print(x + y)
tensor([[0.8143, 0.5493, 0.0512],
[0.9674, 0.4809, 0.5157],
[0.0600, 0.1058, 0.1841],
[0.9795, 0.4970, 0.8882]])
print(torch.add(x, y))
tensor([[0.8143, 0.5493, 0.0512],
[0.9674, 0.4809, 0.5157],
[0.0600, 0.1058, 0.1841],
[0.9795, 0.4970, 0.8882]])
# 指定输出
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)
tensor([[0.8143, 0.5493, 0.0512],
[0.9674, 0.4809, 0.5157],
[0.0600, 0.1058, 0.1841],
[0.9795, 0.4970, 0.8882]])
# inpalce,后缀有_回改变原来的变量
y.add_(x)
y
tensor([[0.8143, 0.5493, 0.0512],
[0.9674, 0.4809, 0.5157],
[0.0600, 0.1058, 0.1841],
[0.9795, 0.4970, 0.8882]])
1.2.2 索引
注意索引出的结果与原数据共享内存
y = x[0, :]
y += 1
print(y)
print(x[0, :])
tensor([2., 2., 2.])
tensor([2., 2., 2.])
高级的选择函数

1.2.3 改变形状
# view()
y = x.view(12)
z = x.view(-1, 4)
print(x.size(), y.size(), z.size())
torch.Size([4, 3]) torch.Size([12]) torch.Size([3, 4])
注意 view() 返回的新tensor与源tensor共享内存(其实是同⼀一个tensor),也即更更改其中的⼀一个,另
外⼀一个也会跟着改变。 (顾名思义, view仅仅是改变了了对这个张量量的观察⻆角度)
x += 1
print(x)
print(y)
tensor([[3., 3., 3.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
tensor([3., 3., 3., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
Pytorch还提供了了一个 reshape() 可以改变形状,但是此函数并不能保证返回的是其拷⻉,就是可能进行copy或者view
所以不不推荐使用。推荐先用 clone 创造一个副本然后再使用 view
另外,使用clone会被记录在计算图中,即梯度回传到副本时也会传到源Tensor
y = torch.clone(x)
y = y.view(-1, 2)
y
tensor([[3., 3.],
[3., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]])
x -= 1
print(x)
print(y)
tensor([[2., 2., 2.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
tensor([[3., 3.],
[3., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]])
# 使用item将标量Tensor转换成Python number
x = torch.randn(1)
print(x)
print(x.item())
tensor([1.3483])
1.3483331203460693
1.2.4 线性代数

用到的时候参考官方文档查
1.3 广播
对两个形状不同的Tensor进行操作,可能会触发广播机制
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)
tensor([[1, 2]])
tensor([[1],
[2],
[3]])
tensor([[2, 3],
[3, 4],
[4, 5]])
1.4 运算的内存开销
使用 id 函数查看实例的内存地址
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x # y+x 会新开内存,然后将y指向新内存
print(id(y) == id_before)
False
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y[:] = y + x # 将 y+x 的结果通过[:]写进y对应的内存中
print(id(y) == id_before)
True
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
torch.add(x, y, out=y) # 效果同上,也可以用 y += x 或 y.add_(x)
print(id(y) == id_before)
True
1.5 Tensor 和 Numpy 的转换
# 使用numpy() 将 Tensor 转 Numpy
a = torch.ones(5)
b = a.numpy()
print(a, b)
tensor([1., 1., 1., 1., 1.]) [1. 1. 1. 1. 1.]
# 使用 from_numpy() 将 numpy 转 Tensor
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)
[1. 1. 1. 1. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
torch.tensor() 将NumPy数组转换成 Tensor ,需要注意的是该方法总是会进⾏行数据拷⻉贝,返回的 Tensor 和原来的数据不再共享内存。
c = torch.tensor(a)
a += 1
print(a, c)
[2. 2. 2. 2. 2.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
注意:所有在CPU上的Tensor(除了CharTensor)都支持与Numpy数组相互转换
1.6 Tensor on GPU
if torch.cuda.is_available():
device = torch.device("cuda:0")
y = torch.ones_like(x, device=device)
x = x.to(device)
z = x + y
print(z)
print(z.to("cpu", torch.double)) # to()的同时可以更爱数据类型
tensor([2, 3], device='cuda:0')
tensor([2., 3.], dtype=torch.float64)
2 自动求梯度
PyTorch 中,所有神经网络的核心是 autograd包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 ( define-by-run )的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。
torch.Tensor是这个包的核心类。如果设置它的属性.requires_grad 为 True,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。
注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。
要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad():中。在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。
还有一个类对于autograd的实现非常重要:Function。Tensor和Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fn是 None )。
如果需要计算导数,可以在 Tensor 上调用 .backward()。如果Tensor 是一个标量(即它包含一个元素的数据),则不需要为 backward()指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。
原文这样说![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0YZuTTJ-1634188215186)(attachment:image.png)]](https://code84.com/wp-content/uploads/2022/10/5e845f984eca4b1d94b1a811b3491003.png)
Function部分看的云里雾里的,看看下面的例子
2.1 张量
x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
做一下计算,计算的结果y,会有一个为"AddBackward"的grad_fn,而 x 这样直接创建的grad_fn是None,我们称之为叶子节点。这里的"AddBackward"就是function吗
y = x + 2
print(y)
print(y.grad_fn)
print(x.is_leaf, y.is_leaf)
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)
<AddBackward0 object at 0x000002BB0FDB4088>
True False
z = y * y * 3
out = z.mean()
print(z, out)
print(z.requires_grad)
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
True
x 的 requires_grad=True 导致了z,y的都为True
创建一个变量时默认的requires_grad=False
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.requires_grad)
False
True
True
2.2 梯度
现在开始进行反向传播,因为out 是一个标量,因此out.backward()和out.backward(torch.tensor(1.)) 等价。
out.backward()
print(x.grad)
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])

2.2.1 理解y.backward()里的grad_variables参数。
如果y是标量,则不需要为backward()传入任何参数,否则需要传入一个与y同形的Tensor记作w
这样做的目的是为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导
torch.autograd.backward(y, w), 或者说 y.backward(w) 的含义是:先计算 l = torch.sum(y * w),然后求 l 对(能够影响到 y 的)所有变量 x 的导数。这里,y 和 w 是同型 Tensor。也就是说,可以理解成先按照 w 对 y 的各个分量加权,加权求和之后得到真正的 loss,再计算这个 loss 对于所有相关变量的导数。
这里w的值如何设呢?
- 如果 l 是 PyTorch 计算图的最终结点,向前计算反向传播梯度的话:
若 l 为标量(scalar):那么 w 可以不写,l.backward();也可以是l.backward(1.0); 不过没有区别,因为,l若为标量,则w默认为1.0;
若 l 为向量(vector):w 应为与 l 同型(相同维度)的ones向量,即l.backward(torch.ones(l.shape)).(w就理解为权重,也就是 l 对 y 的导数,那么如果)
- l 不是 PyTorch 计算图的最终结点,如果不是最终结点的话,即 l.backward(w) —> n.backward(w),即 l 泛化为计算图的中间结点 n;
此时,n.backward(w),也可以执行反向传播,不过前提是要保存有PyTorch 计算图的最终结点 l 关于 中间结点 n 的梯度,即 dl/dn;此时,w的值即为 w=dl/dn;因此,通过 n.backward(dl/dn) 执行反向传播。
总结
假设 x 经过一番计算得到 y,那么 y.backward(w) 求的不是 y 对 x 的导数,而是 l = torch.sum(y*w) 对 x 的导数。w 可以视为 y 的各分量的权重,也可以视为遥远的损失函数 l 对 y 的偏导数。也就是说,不一定需要从计算图最后的节点 y 往前反向传播,从中间某个节点 n 开始传也可以,只要你能把损失函数 l 关于这个节点的导数 dl/dn 记录下来,n.backward(dl/dn) 照样能往前回传,正确地计算出损失函数 l 对于节点 n 之前的节点的导数。特别地,若 y 为标量,w 取默认值 1.0,才是按照我们通常理解的那样,求 y 对 x 的导数。
参考:https://zhuanlan.zhihu.com/p/29923090
举个例子
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)
tensor([[2., 4.],
[6., 8.]], grad_fn=<ViewBackward>)
# 现在y不是一个标量,所以在调用backward时需要传入一个和y同形的权重向量进行加权求和得到一个标量
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad) # x.grad是和x同形的张量
tensor([2.0000, 0.2000, 0.0200, 0.0020])
中断梯度追踪的例子
x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
y2 = x ** 3
y3 = y1 + y2
print(x.requires_grad)
print(y1, y1.requires_grad)
print(y2, y2.requires_grad)
print(y3, y3.requires_grad)
True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<AddBackward0>) True
y3.backward()
print(x.grad)
# 如果y2不被no_grad包裹,结果本应该是5,但被包裹后,y2有关的梯度不会回传,所以只由于y1有关的梯度会回传
tensor(2.)
修改tensor数值,又不想影响反向传播,可以对tensor.data进行操作
x = torch.ones(1, requires_grad=True)
print(x.data)
print(x.data.requires_grad) # 独立于计算图之外的
y = 2 * x
x.data *= 100 # 不影响反向传播
y.backward()
print(x)
print(x.grad)
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])
3 并行计算
在利用PyTorch做深度学习的过程中,可能会遇到数据量较大无法在单块GPU上完成,或者需要提升计算速度的场景,这时就需要用到并行计算。本节让我们来简单地了解一下并行计算的基本概念和主要实现方式,具体的内容会在课程的第二部分详细介绍。
3.1 为什么要做并行计算
我们学习PyTorch的目的就是可以编写我们自己的框架,来完成特定的任务。可以说,在深度学习时代,GPU的出现让我们可以训练的更快,更好。所以,如何充分利用GPU的性能来提高我们模型学习的效果,这一技能是我们必须要学习的。这一节,我们主要讲的就是PyTorch的并行计算。PyTorch可以在编写完模型之后,让多个GPU来参与训练。
3.2 CUDA是个啥
CUDA是我们使用GPU的提供商——NVIDIA提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。但是,在我们使用PyTorch编写深度学习代码时,使用的CUDA又是另一个意思。在PyTorch使用 CUDA表示要开始要求我们的模型或者数据开始使用GPU了。
在编写程序中,当我们使用了 cuda() 时,其功能是让我们的模型或者数据迁移到GPU当中,通过GPU开始计算。
3.3 做并行的方法:
- 网络结构分布到不同的设备中(Network partitioning)
在刚开始做模型并行的时候,这个方案使用的比较多。其中主要的思路是,将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。其架构如下:

这里遇到的问题就是,不同模型组件在不同的GPU上时,GPU之间的传输就很重要,对于GPU之间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到。所有这个方式慢慢淡出了视野,
- 同一层的任务分布到不同数据中(Layer-wise partitioning)
第二种方式就是,同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。其架构如下:

这样可以保证在不同组件之间传输的问题,但是在我们需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题。
- 不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)
第三种方式有点不一样,它的逻辑是,我不再拆分模型,我训练的时候模型都是一整个模型。但是我将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传。其架构如下:

这种方式可以解决之前模式遇到的通讯问题。
PS:现在的主流方式是数据并行的方式(Data parallelism)