Datawhale组队学习_Pytorch基础知识学习笔记

本文为学习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_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。

注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。

要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad():中。在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。

还有一个类对于autograd的实现非常重要:FunctionTensorFunction 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fnNone )。

如果需要计算导数,可以在 Tensor 上调用 .backward()。如果Tensor 是一个标量(即它包含一个元素的数据),则不需要为 backward()指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。

原文这样说
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0YZuTTJ-1634188215186)(attachment:image.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)


版权声明:本文为weixin_43634785原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。