目录

Pytorch快速入门0

为什么选择PyTorch

  • 简洁:PyTorch的设计追求最少的封装,尽量避免重复造轮子。不像TensorFlow中充斥着session、graph、operation、name_scope、variable、tensor、layer等全新的概念,PyTorch的设计遵循tensor→autograd→nn.Module 三个由低到高的抽象层次,分别代表高维数组(张量)、自动求导(变量)和神经网络(层/模块),而且这三个抽象之间联系紧密,可以同时进行修改和操作。
  • 速度:PyTorch的灵活性不以速度为代价,在许多评测中,PyTorch的速度表现胜过TensorFlow和Keras等框架 。框架的运行速度和程序员的编码水平有极大关系,但同样的算法,使用PyTorch实现的那个更有可能快过用其他框架实现的。
  • 易用:PyTorch是所有的框架中面向对象设计的最优雅的一个。PyTorch的面向对象的接口设计来源于Torch,而Torch的接口设计以灵活易用而著称,Keras作者最初就是受Torch的启发才开发了Keras。PyTorch继承了Torch的衣钵,尤其是API的设计和模块的接口都与Torch高度一致。PyTorch的设计最符合人们的思维,它让用户尽可能地专注于实现自己的想法,即所思即所得,不需要考虑太多关于框架本身的束缚。
  • 活跃的社区:PyTorch提供了完整的文档,循序渐进的指南,作者亲自维护的论坛 供用户交流和求教问题。Facebook 人工智能研究院对PyTorch提供了强力支持,作为当今排名前三的深度学习研究机构,FAIR的支持足以确保PyTorch获得持续的开发更新,不至于像许多由个人开发的框架那样昙花一现。

PyTorch还有一个优点就是Torch自称为神经网络界的Numpy,它能将torch产生的tensor放在GPU中加速运算,就想Numpy会把array放在CPU中加速运算。所以在神经网络中,用Torch的tensor形式更优。我们可以把Pytorch当做Numpy来用。

PyTorch使用的是动态图,它的计算图在每次前向传播时都是从头开始构建,所以它能够使用Python控制语句(如for、if等)根据需求创建计算图。这点在自然语言处理领域中很有用,它意味着你不需要事先构建所有可能用到的图的路径,图在运行时才构建。

PyTorch的安装

  • pip安装方式
  • conda安装方式
1
2
3
4
5
# win+python3.6
pip3 install https://download.pytorch.org/whl/cu80/torch-1.0.0-cp36-cp36m-win_amd64.whl
pip3 install torchvision
# win+python3.6+conda
conda install pytorch torchvision cuda80 -c pytorch

更多方法参见此处

PyTorch的核心概念

Tensor:张量

Tensor是PyTorch中重要的数据结构,可认为是一个高维数组。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)以及更高维的数组。

Tensor和Numpy的ndarrays类似,但Tensor可以使用GPU进行加速。Tensor的使用和Numpy及Matlab的接口十分相似。

torch.Tensor是一种包含单一数据类型元素的多维矩阵。

Tensor属性

每个torch.Tensor都有torch.dtype, torch.device,和torch.layout

torch.dtype

Torch定义了七种CPU张量类型和八种GPU张量类型:

Data tyoe CPU tensor GPU tensor
32-bit floating point torch.FloatTensor torch.cuda.FloatTensor
64-bit floating point torch.DoubleTensor torch.cuda.DoubleTensor
16-bit floating point N/A torch.cuda.HalfTensor
8-bit integer (unsigned) torch.ByteTensor torch.cuda.ByteTensor
8-bit integer (signed) torch.CharTensor torch.cuda.CharTensor
16-bit integer (signed) torch.ShortTensor torch.cuda.ShortTensor
32-bit integer (signed) torch.IntTensor torch.cuda.IntTensor
64-bit integer (signed) torch.LongTensor torch.cuda.LongTensor
torch.device
  • torch.device代表将torch.Tensor分配到的设备的对象。
  • torch.device包含一个设备类型('cpu''cuda'设备类型)和可选的设备的序号。如果设备序号不存在,则为当前设备; 例如,torch.Tensor用设备构建'cuda'的结果等同于'cuda:X',其中Xtorch.cuda.current_device()的结果。
  • torch.Tensor的设备可以通过Tensor.device访问属性。
  • 构造torch.device可以通过字符串/字符串和设备编号。
1
2
3
4
5
6
torch.device('cuda:0')
# device(type='cuda', index=0)
torch.device('cpu')
# device(type='cpu')
torch.device('cuda', 0)
# device(type='cuda', index=0)

注意 torch.device函数中的参数通常可以用一个字符串替代。这允许使用代码快速构建原型。

1
2
3
4
5
# Example of a function that takes in a torch.device
cuda1 = torch.device('cuda:1')
torch.randn((2,3), device=cuda1)
# You can substitute the torch.device with a string
torch.randn((2,3), 'cuda:1')
torch.layout
  • torch.layout表示torch.Tensor内存布局的对象。目前,我们支持torch.strided(dense Tensors)并为torch.sparse_coo(sparse COO Tensors)提供实验支持。
  • torch.strided代表密集张量,是最常用的内存布局。每个strided张量都会关联 一个torch.Storage,它保存着它的数据。这些张力提供了多维度, 存储的strided视图。Strides是一个整数型列表:k-th stride表示在张量的第k维从一个元素跳转到下一个元素所需的内存。这个概念使得可以有效地执行多张量。
1
2
3
4
5
6
x = torch.Tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
x.stride()
# (5, 1)

x.t().stride()
# (1, 5)
Tensor方法

Tensor的方法中,带有_的方法代表能够修改Tensor本身。比如,torch.FloatTensor.abs_()会在原地计算绝对值并返回修改的张量,而tensor.FloatTensor.abs()将会在新张量中计算结果。

Tensor.copy_(src, async=False)

src中的元素复制到tensor中并返回这个tensor。 如果broadcast是True,则源张量必须可以使用该张量广播。否则两个tensor应该有相同数目的元素,可以是不同的数据类型或存储在不同的设备上。

参数:

  • src(Tensor) - 要复制的源张量
  • async(bool) - 如果为True,并且此副本位于CPU和GPU之间,则副本可能会相对于主机异步发生。对于其他副本,此参数无效。
  • broadcast(bool) - 如果为True,src将广播到底层张量的形状。
Tensor.cuda(device=None, async=False)

返回此对象在CPU内存中的一个副本 如果该对象已经在CUDA内存中,并且在正确的设备上,则不会执行任何副本,并返回原始对象。

参数:

  • device(int) :目标GPU ID。默认为当前设备。
  • async(bool) :如果为True并且源处于固定内存中,则该副本将相对于主机是异步的。否则,该参数没有意义。
Tensor.expand(*sizes)

返回tensor的一个新视图,单个维度扩大为更大的尺寸。 tensor也可以扩大为更高维,新增加的维度将附在前面。 扩大tensor不需要分配新内存,只是仅仅新建一个tensor的视图,其中通过将stride设为0,一维将会扩展位更高维。任何一个一维的在不分配新内存情况下可扩展为任意的数值。

参数:

  • sizes(torch.Size or int…)-需要扩展的大小
Tensor.narrow(*dimension, start, length*)

返回这个张量的缩小版本的新张量。维度dim缩小范围是startstart+length。返回的张量和该张量共享相同的底层存储。

参数:

  • dimension (int)-需要缩小的维度
  • start (int)-起始维度
  • length (int)-长度
Tensor.resize_(*sizes)

将tensor的大小调整为指定的大小。如果元素个数比当前的内存大小大,就将底层存储大小调整为与新元素数目一致的大小。如果元素个数比当前内存小,则底层存储不会被改变。原来tensor中被保存下来的元素将保持不变,但新内存将不会被初始化。

参数:

  • sizes (torch.Size or int…)-需要调整的大小

更多的方法参考此处

下面通过一些实例学习,Tensor的使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 构建 5x3 矩阵,只是分配了空间,未初始化
x = t.Tensor(5, 3)
x = t.Tensor([[1,2],[3,4]])
# tensor([[ 1.,  2.],
#         [ 3.,  4.]])
# 使用[0,1]均匀分布随机初始化二维数组
x = t.rand(5, 3)  
# tensor([[ 0.8052,  0.7188,  0.0332],
#         [ 0.6054,  0.8955,  0.8972],
#         [ 0.1107,  0.3319,  0.0336],
#         [ 0.2394,  0.5188,  0.2201],
#         [ 0.9730,  0.9370,  0.5677]])
x.size()[1], x.size(1) # 查看列的个数, 两种写法等价
# (3,3)

torch.Size 是tuple对象的子类,因此它支持tuple的所有操作,如x.size()[0]

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 加减乘除运算
y = t.rand(5, 3)
# 加法的第一种写法
x + y
# tensor([[ 0.9639,  0.8763,  0.2834],
#         [ 1.3785,  1.5090,  1.3919],
#         [ 0.7139,  0.6348,  0.8439],
#         [ 0.7022,  1.5079,  0.4776],
#         [ 1.7892,  1.6383,  0.7774]])
# 加法的第二种写法
t.add(x, y)
# tensor([[ 0.9639,  0.8763,  0.2834],
#         [ 1.3785,  1.5090,  1.3919],
#         [ 0.7139,  0.6348,  0.8439],
#         [ 0.7022,  1.5079,  0.4776],
#         [ 1.7892,  1.6383,  0.7774]])
# 加法的第三种写法:指定加法结果的输出目标为result
result = t.Tensor(5, 3) # 预先分配空间
t.add(x, y, out=result) # 输入到result
# tensor([[ 0.9639,  0.8763,  0.2834],
#         [ 1.3785,  1.5090,  1.3919],
#         [ 0.7139,  0.6348,  0.8439],
#         [ 0.7022,  1.5079,  0.4776],
#         [ 1.7892,  1.6383,  0.7774]])

print('最初y')
print(y)
# tensor([[ 0.1587,  0.1575,  0.2501],
#         [ 0.7732,  0.6135,  0.4947],
#         [ 0.6033,  0.3029,  0.8103],
#         [ 0.4628,  0.9891,  0.2575],
#         [ 0.8163,  0.7013,  0.2097]])

print('第一种加法,y的结果')
y.add(x) # 普通加法,不改变y的内容
print(y)
# 第一种加法,y的结果
# tensor([[ 0.1587,  0.1575,  0.2501],
#         [ 0.7732,  0.6135,  0.4947],
#         [ 0.6033,  0.3029,  0.8103],
#         [ 0.4628,  0.9891,  0.2575],
#         [ 0.8163,  0.7013,  0.2097]])

print('第二种加法,y的结果')
y.add_(x) # inplace 加法,y变了
print(y)
# 第二种加法,y的结果
# tensor([[ 0.9639,  0.8763,  0.2834],
#         [ 1.3785,  1.5090,  1.3919],
#         [ 0.7139,  0.6348,  0.8439],
#         [ 0.7022,  1.5079,  0.4776],
#         [ 1.7892,  1.6383,  0.7774]])

注意,函数名后面带下划线**_** 的函数会修改Tensor本身。例如,x.add_(y)x.t_()会改变 x,但x.add(y)x.t()返回一个新的Tensor, 而x不变。

Tensor还支持很多操作,包括数学运算、线性代数、选择、切片等等,其接口设计与Numpy极为相似。更详细的使用方法。

Tensor和numpy对象共享内存,所以他们之间的转换很快,而且几乎不会消耗什么资源。但这也意味着,如果其中一个变了,另外一个也会随之改变。

Tensor和Numpy的数组之间的互操作非常容易且快速。对于Tensor不支持的操作,可以先转为Numpy数组处理,之后再转回Tensor。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
a = t.ones(5) # 新建一个全1的Tensor
# tensor([ 1.,  1.,  1.,  1.,  1.])
b = a.numpy() # Tensor -> Numpy
# array([1., 1., 1., 1., 1.], dtype=float32)

a = np.ones(5)
b = t.from_numpy(a) # Numpy->Tensor
print(a)
# [1. 1. 1. 1. 1.]
print(b) 
# tensor([ 1.,  1.,  1.,  1.,  1.], dtype=torch.float64)

# 共享内存,修改numpy会修改tensor
b.add_(1) # 以`_`结尾的函数会修改自身
print(a)
# [2. 2. 2. 2. 2.]
print(b) # Tensor和Numpy共享内存
# tensor([ 2.,  2.,  2.,  2.,  2.], dtype=torch.float64)

如果你想获取某一个元素的值,可以使用scalar.item。 直接tensor[idx]得到的还是一个tensor: 一个0-dim 的tensor,一般称为scalar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
scalar = b[0]
# tensor(2., dtype=torch.float64)
scalar.size() #0-dim
# torch.Size([])
scalar.item() # 使用scalar.item()能从中取出python对象的数值
# 2.0
tensor = t.tensor([2]) # 注意和scalar的区别
tensor,scalar
# (tensor([ 2]), tensor(2., dtype=torch.float64))
tensor.size(),scalar.size()
# (torch.Size([1]), torch.Size([]))
# 只有一个元素的tensor也可以调用`tensor.item()`
tensor.item(), scalar.item()
# (2, 2.0)

PyTorch中还有一个和np.array 很类似的接口: torch.tensor, 二者的使用十分类似。

需要注意的是,t.tensor()总是会进行数据拷贝,新tensor和原来的数据不再共享内存。所以如果你想共享内存的话,建议使用torch.from_numpy()或者tensor.detach()来新建一个tensor, 二者共享内存。

Tensor可通过.cuda 方法转为GPU的Tensor,从而享受GPU带来的加速运算。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
tensor = t.tensor([3,4]) # 新建一个包含 3,4 两个元素的tensor
# 以下可以看到,新建的tensor与原来的数据不共享内存
old_tensor = tensor
new_tensor = t.tensor(old_tensor)
new_tensor[0] = 1111
# old_tensor, new_tensor
# (tensor([ 3,  4]), tensor([ 1111,     4]))

# 以下使用detach新建tensor会共享内存
new_tensor = old_tensor.detach()
new_tensor[0] = 1111
# old_tensor, new_tensor
# (tensor([ 1111,     4]), tensor([ 1111,     4]))
详解Tensor操作

接口的角度来讲,对tensor的操作可分为两类:

  1. torch.function,如torch.save等。
  2. 另一类是tensor.function,如tensor.view等。

为方便使用,对tensor的大部分操作同时支持这两类接口,

而从存储的角度来讲,对tensor的操作又可分为两类:

  1. 不会修改自身的数据,如 a.add(b), 加法的结果会返回一个新的tensor。
  2. 会修改自身的数据,如 a.add_(b), 加法的结果仍存储在a中,a被修改了。

函数名以_结尾的都是inplace方式, 即会修改调用者自己的数据,在实际应用中需加以区分。

创建Tensor

在PyTorch中新建tensor的方法有很多,具体可以参见下表。

常见新建tensor的方法

函数 功能
Tensor(*sizes) 基础构造函数
tensor(data,) 类似np.array的构造函数
ones(*sizes) 全1Tensor
zeros(*sizes) 全0Tensor
eye(*sizes) 对角线为1,其他为0
arange(s,e,step 从s到e,步长为step
linspace(s,e,steps) 从s到e,均匀切分成steps份
rand/randn(*sizes) 均匀/标准分布
normal(mean,std)/uniform(from,to) 正态分布/均匀分布
randperm(m) 随机排列

这些创建方法都可以在创建的时候指定数据类型dtype和存放device(cpu/gpu).

其中使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list的数据新建tensor,也能根据指定的形状新建tensor,还能传入其他的tensor。

PS:t.Tensor(*sizes)创建tensor时,系统不会马上分配空间,只是会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其它操作都是在创建完tensor之后马上进行空间分配。

 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
# 1.指定tensor的形状
a = t.Tensor(2, 3)
# 2.用list的数据创建tensor
b = t.Tensor([[1,2,3],[4,5,6]])
b_size = b.size()
b.tolist() # 把tensor转为list
b.numel() # b中元素总个数,2*3,等价于b.nelement()
# 3.创建一个和b形状一样的tensor
c = t.Tensor(b_size)
# 4.创建一个元素为2和3的tensor
d = t.Tensor((2, 3))
# 其他的新建Tensor的操作
t.ones(2, 3) # 全1
t.zeros(2, 3) # 全0
t.arange(1, 6, 2) # 生成序列
t.linspace(1, 10, 3) # 生成序列
t.randn(2, 3, device=t.device('cpu')) # 生成随机数
t.randperm(5) # 长度为5的随机排列
t.eye(2, 3, dtype=t.int) # 对角线为1, 不要求行列数一致

# torch.tensor()是新增加的函数,使用的方法,和参数几乎和`np.array`完全一致
scalar = t.tensor(3.14159) 
print('scalar: %s, shape of sclar: %s' %(scalar, scalar.shape))
# scalar: tensor(3.1416), shape of sclar: torch.Size([])
t.tensor([[0.11111, 0.222222, 0.3333333]],
                     dtype=t.float64,
                     device=t.device('cpu'))
# tensor([[ 0.1111,  0.2222,  0.3333]], dtype=torch.float64)
empty_tensor = t.tensor([])
empty_tensor.shape
# torch.Size([0])
Tensor的基本操作

通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身的数据,返回的新tensor与源tensor共享内存,也即更改其中的一个,另外一个也会跟着改变。

在实际应用中可能经常需要添加或减少某一维度,这时候squeezeunsqueeze两个函数就派上用场了。squeeze降维,unsqueeze升维。

resize是另一种可用来调整size的方法,但与view不同,它可以修改tensor的大小。如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存。

 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
36
import torch as t 

a = t.arange(0, 6)
a.view(2, 3)
# tensor([[ 0.,  1.,  2.],
#         [ 3.,  4.,  5.]])
b = a.view(-1, 3) # 当某一维为-1的时候,会自动计算它的大小
b.shape
# torch.Size([2, 3])
b.unsqueeze(1) # 注意形状,在第1维(下标从0开始)上增加“1” 维
#等价于 b[:,None]
b[:, None].shape
# torch.Size([2, 1, 3])
b.unsqueeze(-2) # -2表示倒数第二个维度上面增加“1’维
c = b.view(1, 1, 1, 2, 3)
c.shape
# torch.Size([1, 1, 1, 2, 3])
c.squeeze_(0) # 压缩第0维的“1”
c.shape
# torch.Size([1, 1, 2, 3])
c.squeeze() # 把所有维度为“1”的压缩降维
# view之后和原来的数据共享
a[1] = 100
b # a修改,b作为view之后的,也会跟着修改
# tensor([[   0.,  100.,    2.],
#         [   3.,    4.,    5.]])
# reseize尺寸小于原尺寸,部分数据会被保留,不显示
b.resize_(1, 3)
b
# tensor([[   0.,  100.,    2.]])
# resize尺寸大于原尺寸,如果有隐藏数据会显示,其他多出的大小会被分配新空间
b.resize_(3, 3) # 旧的数据依旧保存着,多出的大小会分配新空间
b
# tensor([[   0.0000,  100.0000,    2.0000],
#         [   3.0000,    4.0000,    5.0000],
#         [  -0.0000,    0.0000,    0.0000]])
Tensor索引操作

Tensor支持与numpy.ndarray类似的索引操作,语法上也类似。

Tensor元素操作

这部分操作会对tensor的每一个元素(point-wise,又名element-wise)进行操作,此类操作的输入与输出形状一致。常用的操作如下表所示。

常见的逐元素操作

函数 功能
abs/sqrt/div/exp/fmod/log/pow.. 绝对值/平方根/除法/指数/求余/求幂..
cos/sin/asin/atan2/cosh.. 相关三角函数
ceil/round/floor/trunc 上取整/四舍五入/下取整/只保留整数部分
clamp(input, min, max) 超过min和max部分截断
sigmod/tanh.. 激活函数

对于很多操作,例如div、mul、pow、fmod等,PyTorch都实现了运算符重载,所以可以直接使用运算符。如a ** 2 等价于torch.pow(a,2), a * 2等价于torch.mul(a,2)

其中clamp(x, min, max)的输出满足以下公式:

$$ y_i = \begin{cases} min, & \text{if } x_i \lt min \ x_i, & \text{if } min \le x_i \le max \ max, & \text{if } x_i \gt max\ \end{cases} $$

clamp常用在某些需要比较大小的地方,如取一个tensor的每个元素与另一个数的较大值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import torch as t 

# 生成数据
a = t.arange(0, 6).view(2, 3)
t.cos(a)
# tensor([[1.0000000000, 0.5403022766, -0.4161468446],
#         [-0.9899924994, -0.6536436081, 0.2836622000]])
a % 3 # 等价于t.fmod(a, 3)
# tensor([[ 0.,  1.,  2.],
#         [ 0.,  1.,  2.]])
t.clamp(a, min=3)
# tensor([[ 3.,  3.,  3.],
#         [ 3.,  4.,  5.]])
Tensor归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和。常用的归并操作如下表所示。

常用归并操作

函数 功能
mean/sum/median/mode 均值/和/中位数/众数
norm/dist 范数/距离
std/var 标准差/方差
cumsum/cumprod 累加/累乘

以上大多数函数都有一个参数**dim**,用来指定这些操作是在哪个维度上执行的。关于dim(对应于Numpy中的axis)的解释众说纷纭,这里提供一个简单的记忆方式:

假设输入的形状是(m, n, k)

  • 如果指定dim=0,输出的形状就是(1, n, k)或者(n, k)
  • 如果指定dim=1,输出的形状就是(m, 1, k)或者(m, k)
  • 如果指定dim=2,输出的形状就是(m, n, 1)或者(m, n)

size中是否有"1",取决于参数keepdimkeepdim=True会保留维度1。注意,以上只是经验总结,并非所有函数都符合这种形状变化方式,如cumsum

1
2
3
4
5
6
7
8
9
import torch as t 

# 生成数据
b = t.ones(2, 3)
b.sum(dim = 0, keepdim=True)
# tensor([[ 2.,  2.,  2.]])
# keepdim=False,不保留维度"1",注意形状
b.sum(dim=0, keepdim=False)
# tensor([ 2.,  2.,  2.])
Tensor比较操作

比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于归并操作。常用比较函数如下表所示。

常用比较函数

函数 功能
gt/lt/ge/le/eq/ne 大于/小于/大于等于/小于等于/等于/不等
topk 最大的k个数
sort 排序
max/min 比较两个tensor最大最小值

表中第一行的比较操作已经实现了运算符重载,因此可以使用a>=ba>ba!=ba==b,其返回结果是一个ByteTensor,可用来选取元素。max/min这两个操作比较特殊,以max来说,它有以下三种使用情况:

  • t.max(tensor):返回tensor中最大的一个数
  • t.max(tensor,dim):指定维上最大的数,返回tensor和下标
  • t.max(tensor1, tensor2): 比较两个tensor相比较大的元素

至于比较一个tensor和一个数,可以使用clamp函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import torch as t 

# 生成数据a
a = t.linspace(0, 15, 6).view(2, 3)
a
# tensor([[  0.,   3.,   6.],
#         [  9.,  12.,  15.]])
# 生成数据b
b = t.linspace(15, 0, 6).view(2, 3)
b
# tensor([[ 15.,  12.,   9.],
#         [  6.,   3.,   0.]])
a[a>b] # a中大于b的元素
# tensor([  9.,  12.,  15.])
t.max(b, dim=1) 
# 第一个返回值的15和6分别表示第0行和第1行最大的元素
# 第二个返回值的0和0表示上述最大的数是该行第0个元素
# (tensor([ 15.,   6.]), tensor([ 0,  0]))
Tensor线性代数

PyTorch的线性函数主要封装了BlasLapack,其用法和接口都与Numpy类似。常用的线性代数函数如下表所示。

常用的线性代数函数

函数 功能
trace 对角线元素之和(矩阵的迹)
diag 对角线元素
triu/tril 矩阵的上三角/下三角,可指定偏移量
mm/bmm 矩阵乘法,batch的矩阵乘法
addmm/addbmm/addmv/addr/badbmm.. 矩阵运算
t 转置
dot/cross 内积/外积
inverse 求逆矩阵
svd 奇异值分解

具体使用说明请参见官方文档,需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续。

Tensor广播法则

广播法则(broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。 Numpy的广播法则定义如下:

  • 让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分通过在前面加1补齐
  • 两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算
  • 当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状

PyTorch当前已经支持了自动广播法则,但是通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:

  • unsqueeze或者view,或者tensor[None],:为数据某一维的形状补1,实现法则1
  • expand或者expand_as,重复数组,实现法则3;该操作不会复制数组,所以不会占用额外的空间。

注意,repeat实现与expand相类似的功能,但是repeat会把相同数据复制多份,因此会占用额外的空间。

 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
import torch as t

# 生成数据a和b
a = t.ones(3, 2)
b = t.zeros(2, 3,1)
# 自动广播法则
# 第一步:a是2维,b是3维,所以先在较小的a前面补1 ,
#               即:a.unsqueeze(0),a的形状变成(1,3,2),b的形状是(2,3,1),
# 第二步:   a和b在第一维和第三维形状不一样,其中一个为1 ,
#               可以利用广播法则扩展,两个形状都变成了(2,3,2)
a+b
# tensor([[[ 1.,  1.],
#          [ 1.,  1.],
#          [ 1.,  1.]],

#         [[ 1.,  1.],
#          [ 1.,  1.],
#          [ 1.,  1.]]])
# 手动广播法则
# 或者 a.view(1,3,2).expand(2,3,2) + b.expand(2,3,2)
a[None].expand(2, 3, 2) + b.expand(2,3,2)
# tensor([[[ 1.,  1.],
#          [ 1.,  1.],
#          [ 1.,  1.]],

#         [[ 1.,  1.],
#          [ 1.,  1.],
#          [ 1.,  1.]]])
# expand不会占用额外空间,只会在需要的时候才扩充,可极大节省内存
e = a.unsqueeze(0).expand(10000000000000, 3,2)
Tensor内部结构

tensor的数据结构如下图所示。tensor分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于tensor中元素的数目,也即存储区的大小。

一般来说一个tensor有着与之相对应的storage, storage是在data之上封装的接口,便于使用,而不同tensor的头信息一般不同,但却可能使用相同的数据。

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/tensor_data_structure.svg

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import torch as t 
# 生成数据a
a = t.arange(0, 6)
# 查看a的storage
a.storage()
#  0.0
#  1.0
#  2.0
#  3.0
#  4.0
#  5.0
# [torch.FloatStorage of size 6]
# b由a view生成
b = a.view(2, 3)
# 查看b的storage
b.storage()
#  0.0
#  1.0
#  2.0
#  3.0
#  4.0
#  5.0
# [torch.FloatStorage of size 6]

# 一个对象的id值可以看作它在内存中的地址
# storage的内存地址一样,即是同一个storage
id(b.storage()) == id(a.storage())
# True

# a改变,b也随之改变,因为他们共享storage
a[1] = 100
b
# tensor([[   0.,  100.,    2.],
#         [   3.,    4.,    5.]])
# c由a切片得到
c = a[2:] 
c.storage()
# 0.0
# 100.0
# 2.0
# 3.0
# 4.0
# 5.0
# [torch.FloatStorage of size 6]
c.data_ptr(), a.data_ptr() # data_ptr返回tensor首元素的内存地址
# 可以看出相差8,这是因为2*4=8--相差两个元素,每个元素占4个字节(float)
# (93894489135160, 93894489135152)
c[0] = -100 # c[0]的内存地址对应a[2]的内存地址
a
# tensor([   0.,  100., -100.,    3.,    4.,    5.])
# 使用storage来初始化Tensor
d = t.Tensor(c.storage())
d[0] = 6666
b
# tensor([[ 6666.,   100.,  -100.],
#         [    3.,     4.,     5.]])
# 下面4个tensor共享storage
id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage())
# True
a.storage_offset(), c.storage_offset(), d.storage_offset()
# (0, 2, 0)
# 切片得到e
e = b[::2, ::2] # 隔2行/列取一个元素
id(e.storage()) == id(a.storage())
# True
b.stride(), e.stride()
# ((3, 1), (6, 2))
# e的数据变得不连续
e.is_contiguous()
# False

可见绝大多数操作并不修改tensor的数据,而只是修改了tensor的头信息。这种做法更节省内存,同时提升了处理速度。在使用中需要注意。 此外有些操作会导致tensor不连续,这时需调用tensor.contiguous方法将它们变成连续的数据,该方法会使数据复制一份,不再与原来的数据共享storage

其他Tensor使用技巧
GPU/CPU

tensor可以很随意的在gpu/cpu上传输。使用tensor.cuda(device_id)或者tensor.cpu()。另外一个更通用的方法是tensor.to(device)

1
2
3
4
5
6
7
import torch as t
# 生成数据
a = t.randn(3, 4)
a.device
# device(type='cpu')
device = t.device('gpu')
a.to(device)

注意

  • 尽量使用tensor.to(device), 将device设为一个可配置的参数,这样可以很轻松的使程序同时兼容GPU和CPU
  • 数据在GPU之中传输的速度要远快于内存(CPU)到显存(GPU), 所以尽量避免频繁的在内存和显存中传输数据。
持久化

Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其它GPU上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if t.cuda.is_available():
    a = a.cuda(1) # 把a转为GPU1上的tensor,
    t.save(a,'a.pth')

    # 加载为b, 存储于GPU1上(因为保存时tensor就在GPU1上)
    b = t.load('a.pth')
    # 加载为c, 存储于CPU
    c = t.load('a.pth', map_location=lambda storage, loc: storage)
    # 加载为d, 存储于GPU0上
    d = t.load('a.pth', map_location={'cuda:1':'cuda:0'})

autograd:自动微分

深度学习的算法本质上是通过反向传播求导数,而PyTorch的**autograd模块则实现了此功能。在Tensor上的所有操作,autograd**都能为它们自动提供微分,避免了手动计算导数的复杂过程。

autograd.Variable是Autograd中的核心类,它简单封装了Tensor,并支持几乎所有Tensor有的操作。Tensor在被封装为Variable之后,可以调用它的.backward实现反向传播,自动计算所有梯度 Variable的数据结构如下图所示。

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/autograd_Variable.svg

从0.4起, Variable 正式合并入Tensor, Variable 本来实现的自动微分功能,Tensor就能支持。读者还是可以使用Variable(tensor), 但是这个操作其实什么都没做。所以以后可以直接使用tensor,而不是Variable.

要想使得Tensor使用autograd功能,只需要设置tensor.requries_grad=True.

Variable主要包含三个属性。 - data:保存Variable所包含的Tensor - grad:保存data对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样。 - grad_fn:指向一个Function对象,这个Function用来反向传播计算输入的梯度。

Variable

autograd中的核心数据结构是Variable。从v0.4版本起,VariableTensor合并。我们可以认为需要求导(requires_grad)的tensor即Variable。autograd记录对tensor的操作记录用来构建计算图。

Variable提供了大部分tensor支持的函数,但其不支持部分inplace函数,因这些函数会修改tensor自身,而在反向传播中,variable需要缓存原来的tensor来计算反向传播梯度。如果想要计算各个Variable的梯度,只需调用根节点variable的backward方法,autograd会自动沿着计算图反向传播,计算每一个叶子节点的梯度。

Tensor.backward(gradient=None, retain_graph=None, create_graph=None)主要有如下参数:

  • gradient:形状与variable一致,对于y.backward(),grad_variables相当于链式法则$${dz \over dx}={dz \over dy} \times {dy \over dx}$$中的$$\textbf {dz} \over \textbf {dy}$$。grad_variables也可以是tensor或序列。
  • retain_graph:反向传播需要缓存一些中间结果,反向传播之后,这些缓存就被清空,可通过指定这个参数不清空缓存,用来多次反向传播。
  • create_graph:对反向传播过程再次构建计算图,可通过backward of backward实现求高阶导数。

计算下面这个函数的导函数: $$ y = x^2\bullet e^x $$ 它的导函数是: $$ {dy \over dx} = 2x\bullet e^x + x^2 \bullet e^x $$ 来看看autograd的计算结果与手动求导计算结果的误差。

 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
import torch as t 

def f(x):
    '''计算y'''
    y = x**2 * t.exp(x)
    return y

def gradf(x):
    '''手动求导函数'''
    dx = 2*x*t.exp(x) + x**2*t.exp(x)
    return dx
  
# 生成数据,由于对x求导,所以设定requires_grad = True
x = t.randn(3,4, requires_grad = True)
y = f(x)
y
# tensor([[ 0.0928,  0.1978,  0.6754,  0.8037],
#         [ 0.9882,  0.3546,  0.2380,  0.0002],
#         [ 0.2863,  0.0448,  0.1516,  2.9122]])
# 此处要注意,对于backward需要传入gradient的尺寸
y.backward(t.ones(y.size())) # gradient形状与y一致
x.grad
# tensor([[ -0.4611,  62.0520,  35.2313,   4.8159],
#         [  1.2937,   2.5127,  -0.2839,  -0.4043],
#         [ -0.3389,   1.9795,   1.6028,   2.0199]])
# 可以看得出来,手动求导的结果和自动求导的结果一致
gradf(x)
# tensor([[ -0.4611,  62.0520,  35.2313,   4.8159],
#         [  1.2937,   2.5127,  -0.2839,  -0.4043],
#         [ -0.3389,   1.9795,   1.6028,   2.0199]])
autograd常用方法
torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None)

计算给定变量计算图的梯度的总和。 该计算图使用链规则进行区分。如果任何Tensor非标量(即它们的数据具有多个元素)并且需要梯度,则该函数另外需要指定grad_tensors。它应该是一个匹配长度的序列,其包含差分函数对应变量的梯度(None对于不需要梯度张量的所有变量,它是可接受的值)。

此函数在树叶中累加梯度 - 在调用它之前可能需要将其清零。

参数:

  • Tensors(Tensor列表) – 将计算导数的变量 。
  • grad_tensors(序列(Tensor或者 None)) – 相应张量的每个元素。 对于标量张量或不需要渐变的张量,不能指定任何值。 如果所有grad_tensors都可以接受None值,则此参数是可选的。
  • retain_graph(bool,可选) - 如果为False,则用于计算grad的图形将被释放。请注意,在几乎所有情况下,将此选项设置为True不是必需的,通常可以以更有效的方式解决。默认值为create_graph。
  • create_graph(bool,可选) - 如果为true,则构造导数的图形,允许计算更高阶的衍生产品。默认为False,除非grad_variables包含至少一个非易失性变量。
torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)>

计算并返回输入的输出梯度的总和。

gradoutputs应该是output 包含每个输出的预先计算的梯度的长度匹配序列。如果输出不需要 grad,则梯度可以是None)。当不需要派生图的图形时,梯度可以作为Tensors给出,或者作为Variables,在这种情况下将创建图形。

如果only_inputs为True,该函数将仅返回指定输入的渐变列表。如果它是False,则仍然计算所有剩余叶子的渐变度,并将累积到其.grad 属性中。

参数:

  • outputs(可变序列) - 差分函数的输出。
  • inputs(可变序列) - 输入将返回梯度的积分(并不积累.grad)。
  • grad_outputs(Tensor 或Variable的序列) - 渐变wrd每个输出。任何张量将被自动转换为volatile,除非create_graph为True。可以为标量变量或不需要grad的值指定无值。如果所有grad_variables都可以接受None值,则该参数是可选的。
  • retain_graph(bool,可选) - 如果为False,则用于计算grad的图形将被释放。请注意,在几乎所有情况下,将此选项设置为True不是必需的,通常可以以更有效的方式解决。默认值为create_graph。
  • create_graph(bool,可选) - 如果为True,则构造导数的图形,允许计算高阶衍生产品。默认为False,除非grad_variables包含至少一个非易失性变量。
  • only_inputs(bool,可选) - 如果为True,则渐变wrt离开是图形的一部分,但不显示inputs不会被计算和累积。默认为True。

例子

 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
36
37
38
39
40
import torch as t
# 为tensor设置 requires_grad 标识,代表着需要求导数
# pytorch 会自动调用autograd 记录操作
x = t.ones(2, 2, requires_grad=True)

# 上一步等价于
# x = t.ones(2,2)
# x.requires_grad = True
x
# tensor([[ 1.,  1.],
#         [ 1.,  1.]])
y = x.sum()
y
# tensor(4.)
y.grad_fn
# <SumBackward0 at 0x7ffaa589a780>
y.backward() # 反向传播,计算梯度
# y = x.sum() = (x[0][0] + x[0][1] + x[1][0] + x[1][1])
# 每个值的梯度都为1
x.grad 
# tensor([[ 1.,  1.],
#         [ 1.,  1.]])

# 反向传播的时候,梯度注意需要清零
# 未清零,此时的梯度为
y.backward()
x.grad
# tensor([[ 2.,  2.],
#         [ 2.,  2.]])
y.backward()
x.grad
# tensor([[ 3.,  3.],
#         [ 3.,  3.]])
# 清零之后的反向传播
# 以下划线结束的函数是inplace操作,会修改自身的值,就像add_
x.grad.data.zero_()
y.backward()
x.grad
# tensor([[ 1.,  1.],
#         [ 1.,  1.]])

注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以反向传播之前需把梯度清零。

计算图

PyTorchautograd的底层采用了计算图,计算图是一种特殊的有向无环图(DAG),用于记录算子与变量之间的关系。一般用矩形表示算子,椭圆形表示变量。如表达式$$ \textbf {z = wx + b}$$可分解为$$\textbf{y = wx}$$和$$\textbf{z = y + b}$$,其计算图如下图所示,图中MULADD都是算子,$$\textbf{w}$$,$$\textbf{x}$$,$$\textbf{b}$$即变量。

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/com_graph.svg

如上有向无环图中,$$\textbf{X}$$和$$\textbf{b}$$是叶子节点(leaf node),这些节点通常由用户自己创建,不依赖于其他变量。$$\textbf{z}$$称为根节点,是计算图的最终目标。利用链式法则很容易求得各个叶子节点的梯度。

$$ \begin{align} &{\partial z \over \partial b} = 1,\space {\partial z \over \partial y} = 1 \ &{\partial y \over \partial w }= x,{\partial y \over \partial x}= w \ &{\partial z \over \partial x}= {\partial z \over \partial y} {\partial y \over \partial x}=1 * w\ &{\partial z \over \partial w}= {\partial z \over \partial y} {\partial y \over \partial w}=1 * x \ \end{align} $$

而有了计算图,上述链式求导即可利用计算图的反向传播自动完成。其计算过程如下图所示:

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/com_graph_backward.svg

在PyTorch实现中,autograd会随着用户的操作,记录生成当前variable的所有操作,并由此建立一个有向无环图。用户每进行一个操作,相应的计算图就会发生改变。更底层的实现中,图中记录了操作Function,每一个变量在图中的位置可通过其grad_fn属性在图中的位置推测得到。在反向传播过程中,autograd沿着这个图从当前变量(根节点$$\textbf{z}$$)溯源,可以利用链式求导法则计算所有叶子节点的梯度。每一个前向传播操作的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名通常以Backward结尾。

例子

 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
36
37
38
39
40
import torch as t

# 测试requires_grad
x = t.ones(1)
b = t.rand(1, requires_grad = True)
w = t.rand(1, requires_grad = True)
y = w * x # 等价于y=w.mul(x)
z = y + b # 等价于z=y.add(b)
x.requires_grad, b.requires_grad, w.requires_grad
# (False, True, True)
# 在默认情况下,requires_grad会自动传递
# 虽然未指定y.requires_grad为True,但由于y依赖于需要求导的w
# 故而y.requires_grad为True
y.requires_grad
# True
# 计算图的叶子节点
x.is_leaf, w.is_leaf, b.is_leaf
# (True, True, True)
y.is_leaf, z.is_leaf
# (False, False)
# grad_fn可以查看这个variable的反向传播函数,
# z是add函数的输出,所以它的反向传播函数是AddBackward
z.grad_fn 
# <AddBackward1 at 0x7f60b09c2630>
# next_functions保存grad_fn的输入,是一个tuple,tuple的元素也是Function
# 第一个是y,它是乘法(mul)的输出,所以对应的反向传播函数y.grad_fn是MulBackward
# 第二个是b,它是叶子节点,由用户创建
z.grad_fn.next_functions 
# ((<MulBackward1 at 0x7f60b09c2278>, 0),
# (<AccumulateGrad at 0x7f60b09c2198>, 0))
# variable的grad_fn对应着和图中的function相对应
z.grad_fn.next_functions[0][0] == y.grad_fn

# 第一个是w,叶子节点,需要求导,梯度是累加的
# 第二个是x,叶子节点,不需要求导,所以为None
y.grad_fn.next_functions
# ((<AccumulateGrad at 0x7f60b09c2898>, 0), (None, 0))
# 叶子节点的grad_fn是None
w.grad_fn,x.grad_fn
# (None, None)

变量的requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都是True。这其实很好理解,对于$$ \textbf{x}\to \textbf{y} \to \textbf{z}$$,x.requires_grad = True,当需要计算$$\partial z \over \partial x$$时,根据链式法则,$$\frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x}$$,自然也需要求$$ \frac{\partial z}{\partial y}$$,所以y.requires_grad会被自动标为True.

有些时候我们可能不希望autograd对tensor求导。认为求导需要缓存许多中间结构,增加额外的内存/显存开销,那么我们可以关闭自动求导。对于不需要反向传播的情景(如inference,即测试推理时),关闭自动求导可实现一定程度的速度提升,并节省约一半显存,因其不需要分配空间计算梯度。

例子

 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
# 两种抑制requires_grad传递的方法
x = t.ones(1, requires_grad=True)
w = t.rand(1, requires_grad=True)
y = x * w
# y依赖于w,而w.requires_grad = True
x.requires_grad, w.requires_grad, y.requires_grad
# (True, True, True)
# 1.第一种抑制的方法
with t.no_grad():
    x = t.ones(1)
    w = t.rand(1, requires_grad = True)
    y = x * w
# y依赖于w和x,虽然w.requires_grad = True,但是y的requires_grad依旧为False
x.requires_grad, w.requires_grad, y.requires_grad
# (False, True, False)
# 2.第二种抑制的方法
t.set_grad_enabled(False)
x = t.ones(1)
w = t.rand(1, requires_grad = True)
y = x * w
# y依赖于w和x,虽然w.requires_grad = True,但是y的requires_grad依旧为False
x.requires_grad, w.requires_grad, y.requires_grad
# (False, True, False)
# 恢复默认配置
t.set_grad_enabled(True)

在反向传播过程中非叶子节点的导数计算完之后即被清空。若想查看这些变量的梯度,有两种方法:

  • 使用autograd.grad函数
  • 使用hook

autograd.gradhook方法都是很强大的工具,更详细的用法参考官方api文档,这里举例说明基础的使用。推荐使用hook方法,但是在实际使用中应尽量避免修改grad的值。

 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
36
37
38
import torch as t 
# 中间节点,梯度自动清零
x = t.ones(3, requires_grad=True)
w = t.rand(3, requires_grad=True)
y = x * w
# y依赖于w,而w.requires_grad = True
z = y.sum()
x.requires_grad, w.requires_grad, y.requires_grad
# (True, True, True)
# 非叶子节点grad计算完之后自动清空,y.grad是None
z.backward()
(x.grad, w.grad, y.grad)
# (tensor([ 0.2709,  0.0473,  0.5052]), tensor([ 1.,  1.,  1.]), None)
# 第一种方法:使用grad获取中间变量的梯度
x = t.ones(3, requires_grad=True)
w = t.rand(3, requires_grad=True)
y = x * w
z = y.sum()
# z对y的梯度,隐式调用backward()
t.autograd.grad(z, y)
# (tensor([ 1.,  1.,  1.]),)

# 第二种方法:使用hook
# hook是一个函数,输入是梯度,不应该有返回值
def variable_hook(grad):
    print('y的梯度:',grad)

x = t.ones(3, requires_grad=True)
w = t.rand(3, requires_grad=True)
y = x * w
# 注册hook
hook_handle = y.register_hook(variable_hook)
z = y.sum()
z.backward()

# 除非你每次都要用hook,否则用完之后记得移除hook
hook_handle.remove()
# y的梯度: tensor([ 1.,  1.,  1.])

关于variable中grad属性和backward函数grad_variables参数的含义:

  • variable $$\textbf{x}$$的梯度是目标函数$${f(x)} $$对$$\textbf{x}$$的梯度,$$\frac{df(x)}{dx} = (\frac {df(x)}{dx_0},\frac {df(x)}{dx_1},…,\frac {df(x)}{dx_N})$$,形状和$$\textbf{x}$$一致。
  • 对于y.backward(grad_variables)中的grad_variables相当于链式求导法则中的$$\frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x}$$中的$$\frac{\partial z}{\partial y}$$。z是目标函数,一般是一个标量,故而$$\frac{\partial z}{\partial y}$$的形状与variable $$\textbf{y}$$的形状一致。z.backward()在一定程度上等价于y.backward(grad_y)。z.backward()省略了grad_variables参数,是因为$$z$$是一个标量,而$$\frac{\partial z}{\partial z} = 1$$

例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import torch as t 

# 默认参数下的反向传播,默认情况下,z必须是标量
x = t.arange(0,3, requires_grad=True)
y = x**2 + x*2
z = y.sum()
z.backward() # 从z开始反向传播
x.grad
# tensor([ 2.,  4.,  6.])
# 指定Variable的反向传播,如果z不是标量,不指定Variable,会出错
x = t.arange(0,3, requires_grad=True)
y = x**2 + x*2
z = y.sum()
y_gradient = t.Tensor([1,1,1]) # dz/dy
y.backward(y_gradient) #从y开始反向传播
x.grad
# tensor([ 2.,  4.,  6.])

PyTorch中计算图的特点可总结如下:

  • autograd根据用户对variable的操作构建其计算图。对变量的操作抽象为Function
  • 对于那些不是任何函数(Function)的输出,由用户创建的节点称为叶子节点,叶子节点的grad_fn为None。叶子节点中需要求导的variable,具有AccumulateGrad标识,因其梯度是累加的。
  • variable默认是不需要求导的,即requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都为True。
  • variablevolatile属性默认为False,如果某一个variablevolatile属性被设为True,那么所有依赖它的节点volatile属性都为True。volatile属性为True的节点不会求导,volatile的优先级比requires_grad高。
  • 多次反向传播时,梯度是累加的。反向传播的中间缓存会被清空,为进行多次反向传播需指定retain_graph=True来保存这些缓存。
  • 非叶子节点的梯度计算完之后即被清空,可以使用autograd.gradhook技术获取非叶子节点的值。
  • variablegrad与data形状一致,应避免直接修改variable.data,因为对data的直接操作无法利用autograd进行反向传播
  • 反向传播函数backward的参数grad_variables可以看成链式求导的中间结果,如果是标量,可以省略,默认为1
  • PyTorch采用动态图设计,可以很方便地查看中间层的输出,动态的设计计算图结构。

autograd高级用法

Pytorch提供的大部分函数能自动实现反向传播,但如果需要自己写一个复杂的函数,不支持自动反向求导的时候,我们就需要手动实现反向传播函数。

Pytorch提供了两种方法来扩展autograd:

第一种自定义Function

  • 自定义的Function需要继承autograd.Function,没有构造函数__init__forwardbackward函数都是静态方法
  • backward函数的输出和forward函数的输入一一对应,backward函数的输入和forward函数的输出一一对应
  • backward函数的grad_output参数即t.autograd.backward中的grad_variables
  • 如果某一个输入不需要求导,直接返回None,如forward中的输入参数x_requires_grad显然无法对它求导,直接返回None即可
  • 反向传播可能需要利用前向传播的某些中间结果,需要进行保存,否则前向传播结束后这些对象即被释放

Function的使用利用Function.apply(variable)

第二种方法:

PyTorch提供了一个装饰器@once_differentiable,能够在backward函数中自动将输入的variable提取成tensor,把计算结果的tensor自动封装成variable。有了这个特性我们就能够很方便的使用numpy/scipy中的函数,操作不再局限于variable所支持的操作。但是这种做法正如名字中所暗示的那样只能求导一次,它打断了反向传播图,不再支持高阶求导。

例子

 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
from torch.autograd import Function
class MultiplyAdd(Function):
                                                            
    @staticmethod
    def forward(ctx, w, x, b):                              
        ctx.save_for_backward(w,x)
        output = w * x + b
        return output
        
    @staticmethod
    def backward(ctx, grad_output):                         
        w,x = ctx.saved_tensors
        grad_w = grad_output * x
        grad_x = grad_output * w
        grad_b = grad_output * 1
        return grad_w, grad_x, grad_b             
      
      
x = t.ones(1)
w = t.rand(1, requires_grad = True)
b = t.rand(1, requires_grad = True)
# 开始前向传播
z=MultiplyAdd.apply(w, x, b)
# 开始反向传播
z.backward()
# x不需要求导,中间过程还是会计算它的导数,但随后被清空
x.grad, w.grad, b.grad
# (None, tensor([ 1.]), tensor([ 1.]))

Pytorch实例线性回归

三种方法实现线性回归,第一种:自动计算导数,第二种:使用autograd来计算导数,第三种:使用优化器自动优化

线性回归0

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import torch as t
%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib import style
from IPython import display
style.use("ggplot")


device = t.device('cuda') #如果你想用gpu,改成t.device('cuda:0')

# 设置随机数种子,保证在不同电脑上运行时下面的输出一致
t.manual_seed(1000) 

def get_fake_data(batch_size=8):
    ''' 产生随机数据:y=x*2+3,加上了一些噪声'''
    x = t.rand(batch_size, 1, device=device) * 5
    y = x * 2 + 3 +  t.randn(batch_size, 1, device=device)
    return x, y 
  
# 随机初始化参数
w = t.rand(1, 1).to(device)
b = t.zeros(1, 1).to(device)

lr =0.02 # 学习率
num_epochs = 500
# 训练500次
for ii in range(num_epochs):
  	# 获取数据
    x, y = get_fake_data(batch_size=4)
    
    # forward:计算loss
    y_pred = x.mm(w) + b.expand_as(y) # x@W等价于x.mm(w);for python3 only
    loss = 0.5 * (y_pred - y) ** 2 # 均方误差
    loss = loss.mean()
    
    # backward:手动计算梯度
    # 模拟计算图的方式计算误差
    dloss = 1
    dy_pred = dloss * (y_pred - y)
    
    dw = x.t().mm(dy_pred)
    db = dy_pred.sum()
    
    # 更新参数
    w.sub_(lr * dw)
    b.sub_(lr * db)
    
    if ii%50 ==0:
        # 画图
        # 使用display实现动态图
        display.clear_output(wait=True)
        x = t.arange(0, 6).view(-1, 1).to(device)
        y = x.mm(w) + b.expand_as(x)
        plt.plot(x.cpu().numpy(), y.cpu().numpy(),c = 'green') # predicted
        
        x2, y2 = get_fake_data(batch_size=32) 
        plt.scatter(x2.cpu().numpy(), y2.cpu().numpy()) # true data
        
        plt.xlim(0, 5)
        plt.ylim(0, 13)
        plt.show()
        plt.pause(0.5)
        
print('w: ', w.item(), 'b: ', b.item())

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/huigui.gif

线性回归1

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import torch as t
%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib import style
from IPython import display
style.use("ggplot")


# 注意,此处如果使用cuda,那么计算梯度的时候,会出错
device = t.device('cpu') 

# 设置随机数种子,保证在不同电脑上运行时下面的输出一致
t.manual_seed(1000) 

def get_fake_data(batch_size=8):
    ''' 产生随机数据:y=x*2+3,加上了一些噪声'''
    x = t.rand(batch_size, 1, device=device) * 5
    y = x * 2 + 3 +  t.randn(batch_size, 1, device=device)
    return x, y 
  
# 随机初始化参数
w = t.rand(1, 1).to(device)
b = t.zeros(1, 1).to(device)

lr =0.02 # 学习率
num_epochs = 500
# 训练500次

for ii in range(num_epochs):
    x, y = get_fake_data(batch_size=32)
    
    # forward:计算loss
    y_pred = x.mm(w) + b.expand_as(y)
    loss = 0.5 * (y_pred - y) ** 2
    loss = loss.sum()
    losses[ii] = loss.item()
    
    # backward:手动计算梯度
    loss.backward()
    
    # 更新参数
    w.data.sub_(lr * w.grad.data)
    b.data.sub_(lr * b.grad.data)
    
    # 梯度清零
    w.grad.data.zero_()
    b.grad.data.zero_()
    
    if ii % 10 ==0:
        # 画图
        display.clear_output(wait=True)
        x = t.arange(0, 6).view(-1, 1)
        y = x.mm(w.data) + b.data.expand_as(x)
        plt.plot(x.numpy(), y.numpy(),c = 'blue') # predicted
        print(w.item(),b.item())
        
        x2, y2 = get_fake_data(batch_size=20) 
        plt.scatter(x2.numpy(), y2.numpy()) # true data
        
        plt.xlim(0,5)
        plt.ylim(0,13)   
        plt.show()
        plt.pause(0.5)
        
print(w.item(), b.item())
# 1.9642277956008911 3.0166714191436768

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/huigui_1.gif

线性回归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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import torch as t
%matplotlib inline
from matplotlib import pyplot as plt
from torch import optim
from torch import nn 
from matplotlib import style
from IPython import display
style.use("ggplot")


# 不知道为啥使用cuda梯度为不存在了
device = t.device('cpu') 

# 设置随机数种子,保证在不同电脑上运行时下面的输出一致
t.manual_seed(1000) 

def get_fake_data(batch_size=8):
    ''' 产生随机数据:y=x*2+3,加上了一些噪声'''
    x = t.rand(batch_size, 1, device=device) * 5
    y = x * 2 + 3 +  t.randn(batch_size, 1, device=device)
    return x, y 
 
# 随机初始化参数
w = t.rand(1,1, requires_grad=True).to(device)
b = t.zeros(1,1, requires_grad=True).to(device)

lr = 0.0005 # 学习率不能太大
num_epochs = 500
losses = np.zeros(500)

# 导入优化器
opt = optim.SGD([w,b],lr = lr)
# 引入mse损失函数
loss_func = nn.MSELoss()

for ii in range(num_epochs):
    x, y = get_fake_data(batch_size=32)
    
    # forward:计算loss
    y_pred = x.mm(w) + b.expand_as(y)
    loss = loss_func(y_pred,y)
    losses[ii] = loss.item()
    # 优化器梯度清零
    opt.zero_grad()
    # backward:手动计算梯度
    loss.backward()
    # 逐步优化
    opt.step()
    
    
    if ii % 10 == 0:
        # 画图
        display.clear_output(wait=True)
        x = t.arange(0, 6).view(-1, 1)
        y = x.mm(w.data) + b.data.expand_as(x)
        plt.plot(x.numpy(), y.numpy(),c = 'blue') # predicted
        print(w.item(),b.item())
        
        x2, y2 = get_fake_data(batch_size=20) 
        plt.scatter(x2.numpy(), y2.numpy()) # true data
        
        plt.xlim(0,5)
        plt.ylim(0,13)   
        plt.show()
        plt.pause(0.5)
        
print(w.item(), b.item())

https://blog-1253453438.cos.ap-beijing.myqcloud.com/pytorch/huigui_0.gif

参考

深度学习之Pytorch(陈云)