PyTorch学习笔记(一):基本操作
简介
PyTorch是一个基于Torch的Python开源机器学习库,用于自然语言处理等应用程序。它主要由Facebook的人工智能小组开发,不仅能够 实现强大的GPU加速,同时还支持动态神经网络,这一点是现在很多主流框架如TensorFlow都不支持的。
PyTorch提供了两个高级功能:
- 具有强大的GPU加速的张量计算
- 包含自动求导系统的深度神经网络
注:我个人为什么学?因为目前较多AI方面的前沿论文都是用Pytorch实现的。
环境搭建
详细可见Win10下安装CUDA11.0+CUDNN8.0+pytorch1.7.0
数据操作
Tensor
在PyTorch中, torch.Tensor 是存储和变换数据的主要⼯具。如果你之前用过NumPy,你会发现Tensor 和NumPy的多维数组非常类似。然而, Tensor 提供GPU计算和自动求梯度等更多功能,这些使 Tensor 更加适合深度学习。
- 注:"tensor"这个单词⼀般可译作“张量”,张量可以看作是⼀个多维数组。标量可以看作是0维张量,向量可以看作1维张量,矩阵可以看作是⼆维张量。
创建Tensor
导入Pytorch
from __future__ import print_function
import torch
创建一个未初始的Tensor
x = torch.empty(5, 3)
print(x)
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
创建一个随机初始化的Tensor
x = torch.rand(5, 3)
print(x)
tensor([[0.9580, 0.7338, 0.3720],
[0.3307, 0.0427, 0.8126],
[0.0494, 0.0169, 0.8895],
[0.2350, 0.7337, 0.6464],
[0.9296, 0.4270, 0.7882]])
创建一个long型全0的Tensor
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
直接创建Tensor
x = torch.tensor([5.5, 3])
print(x)
tensor([5.5000, 3.0000])
返回的Tensor默认为相同dtype和devide
x = x.new_ones(5, 3, dtype=torch.double)
print(x)
tensor([[1., 1., 1.],
[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([[-1.6097, 1.0037, -0.6797],
[-0.6844, 1.0612, -2.0448],
[ 2.4757, -1.5552, -0.1672],
[ 0.1911, 0.2325, -0.6412],
[-0.6324, 0.3932, 0.4635]])
获取Tensor形状
print(x.size())
print(x.shape)
torch.Size([5, 3])
torch.Size([5, 3])
加法操作
y = torch.rand(5, 3)
print(x + y)
tensor([[-1.1765, 1.3147, 0.1662],
[-0.1216, 1.2384, -1.9638],
[ 3.4737, -1.5171, 0.1034],
[ 0.9739, 0.7428, 0.1927],
[-0.2120, 0.8767, 1.2454]])
print(torch.add(x, y))
tensor([[-1.1765, 1.3147, 0.1662],
[-0.1216, 1.2384, -1.9638],
[ 3.4737, -1.5171, 0.1034],
[ 0.9739, 0.7428, 0.1927],
[-0.2120, 0.8767, 1.2454]])
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)
tensor([[-1.1765, 1.3147, 0.1662],
[-0.1216, 1.2384, -1.9638],
[ 3.4737, -1.5171, 0.1034],
[ 0.9739, 0.7428, 0.1927],
[-0.2120, 0.8767, 1.2454]])
y.add_(x)
print(y)
tensor([[-1.1765, 1.3147, 0.1662],
[-0.1216, 1.2384, -1.9638],
[ 3.4737, -1.5171, 0.1034],
[ 0.9739, 0.7428, 0.1927],
[-0.2120, 0.8767, 1.2454]])
索引
- 索引出来的结果与原数据内存共享,即一个修改,另一个也修改。
y = x[0, :]
print(x[0, :]) # 源tensor
print(y)
y += 1
print(y)
print(x[0, :]) # 源tensor也修改了
tensor([-1.6097, 1.0037, -0.6797])
tensor([-1.6097, 1.0037, -0.6797])
tensor([-0.6097, 2.0037, 0.3203])
tensor([-0.6097, 2.0037, 0.3203])
改变形状
⽤ view() 来改变 Tensor 的形状:
y = x.view(15)
z = x.view(-1, 5) # -1所指的维度可以根据其他维度的值推出来
print(x.size(), y.size(), z.size())
torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])
注意:view() 返回的新tensor与源tensor共享内存(其实是同⼀个tensor),view仅仅是改变了对这个张量的观察⻆度。
如果我们想返回⼀个真正新的副本(即不共享内存)该怎么办呢?推荐先⽤ clone 创造⼀个副本然后再使⽤ view
print(x) # 源tensor
x_cp = x.clone().view(15)
x -= 1
print(x) # 源tensor修改了
print(x_cp) # 新tensor
tensor([[-0.6097, 2.0037, 0.3203],
[-0.6844, 1.0612, -2.0448],
[ 2.4757, -1.5552, -0.1672],
[ 0.1911, 0.2325, -0.6412],
[-0.6324, 0.3932, 0.4635]])
tensor([[-1.6097, 1.0037, -0.6797],
[-1.6844, 0.0612, -3.0448],
[ 1.4757, -2.5552, -1.1672],
[-0.8089, -0.7675, -1.6412],
[-1.6324, -0.6068, -0.5365]])
tensor([-0.6097, 2.0037, 0.3203, -0.6844, 1.0612, -2.0448, 2.4757, -1.5552,
-0.1672, 0.1911, 0.2325, -0.6412, -0.6324, 0.3932, 0.4635])
item() , 它可以将⼀个标量 Tensor 转换成⼀个Python number
x = torch.randn(1)
print(x)
print(x.item())
y = torch.tensor(2.5)
print(y)
print(y.item())
tensor([-0.5951])
-0.5950911045074463
tensor(2.5000)
2.5
线性代数
PyTorch支持⼀些线性函数,具体用法参考PyTorch官方文档
广播机制
广播(Broadcasting)机制是对不同形状(shape)的Tensor进行数值计算的一种方式。其原理是:先适当复制元素使这两个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]])
内存开销
索引、 view()是不会开辟新内存的,而像 y = x + y 这样的运算是会新开内存的,然后将 y 指向新内存。
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x
print("y运算前的ID:{}".format(id_before))
print("y运算后的ID:{}".format(id(y)))
y运算前的ID:1782315793720
y运算后的ID:1782315743344
注:Python⾃带的 id 函数:如果两个实例的ID⼀致,那么它们所对应的内存地址相同;反之则不同。
想指定结果到原来的 y 的内存,我们可以使⽤索引来进⾏替换操作。
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y[:] = y + x
print("y运算前的ID:{}".format(id_before))
print("y运算后的ID:{}".format(id(y)))
y运算前的ID:1782332408384
y运算后的ID:1782332408384
还可以使用运算符全名函数中的 out 参数或者自加运算符 += (或 add_() )达到上述效果,例如:
torch.add(x, y, out=y) 和 y += x (或 y.add_(x) )。
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("y运算前的ID:{}".format(id_before))
print("y运算后的ID:{}".format(id(y)))
y运算前的ID:1782332408888
y运算后的ID:1782332408888
Tensor与Numpy相互转换
⽤ numpy() 和 from_numpy() 将 Tensor 和NumPy中的数组相互转换。
注意: 这两个函数所产⽣的的 Tensor 和NumPy中的数组共享相同的内存,即改变其中⼀个时,另⼀个也会改变,所以他们之间的转换很快!
Tensor转Numpy
a = torch.ones(5)
b = a.numpy()
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)
tensor([1., 1., 1., 1., 1.]) [1. 1. 1. 1. 1.]
tensor([2., 2., 2., 2., 2.]) [2. 2. 2. 2. 2.]
tensor([3., 3., 3., 3., 3.]) [3. 3. 3. 3. 3.]
Numpy转Tensor
from_numpy()将NumPy数组转换成 Tensor
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)
[1. 1. 1. 1. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
[2. 2. 2. 2. 2.] tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
[3. 3. 3. 3. 3.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)
注:所有在CPU上的 Tensor (除了 CharTensor )都⽀持与NumPy数组相互转换。
还有⼀个常⽤的将NumPy中的array转换成 Tensor 的⽅法就是 torch.tensor()
注意:此⽅法总是会进⾏数据拷⻉(就会消耗更多的时间和空间),所以返回的 Tensor 和原来的数据不再共享内存。
c = torch.tensor(a)
a += 1
print(a, c)
[4. 4. 4. 4. 4.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)
Tensor在GPU上
⽤用⽅方法 to() 可以将 Tensor 在CPU和GPU(需要硬件⽀支持)之间相互移动。
if torch.cuda.is_available():
device = torch.device("cuda") # GPU
y = torch.ones_like(x, device=device) # 直接创建一个在GPU上的Tensor
x = x.to(device) # 等价于 .to("cuda")
z = x + y
print(z)
print(z.to("cpu", torch.double)) # to()还可以同时更更改数据类型
print(z.to("cuda", torch.double)) # to()还可以同时更更改数据类型
tensor([2, 3], device='cuda:0')
tensor([2., 3.], dtype=torch.float64)
tensor([2., 3.], device='cuda:0', dtype=torch.float64)
自动求梯度
在深度学习中,我们经常需要对函数求梯度(gradient)。PyTorch提供的autograd 包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。
基本概念
Tensor 是这个包的核心类,如果将其属性 .requires_grad 设置为 True ,它将开始追踪(track)在其上的所有操作(这样就可以利利用链式法则进行梯度传播了)。完成计算后,可以调用.backward() 来完成所有梯度计算。此 Tensor 的梯度将累积到 .grad 属性中。
不想要被继续追踪,可以调⽤ .detach() 将其从追踪记录中分离出来,这样就可以防⽌将来的计算被追踪,这样梯度就传不过去了。此外,还可以⽤ with torch.no_grad() 将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数
( requires_grad=True )的梯度。
Function 是另外一个很重要的类。 Tensor 和 Function 互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个 Tensor 都有一个 .grad_fn 属性,该属性即创建该 Tensor 的Function , 就是说该 Tensor 是不是通过某些运算得到的,若是,则 grad_fn 返回一个与这些运算相关的对象,否则是None。
创建一个 Tensor 并设置 requires_grad=True :
x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
b_i = torch.tensor([1,2])
y = x + b_i
print(y)
print(y.grad_fn)
tensor([[2., 3.],
[2., 3.]], grad_fn=<AddBackward0>)
<AddBackward0 object at 0x0000019ED5CDD5C0>
注意:x是直接创建的,所以它没有 grad_fn , 而y是通过一个加法操作创建的,所以它有一个为 的 grad_fn 。
像x这种直接创建的称为叶子节点,叶子节点对应的 grad_fn 是 None 。
print(x.is_leaf, y.is_leaf) # True False
True False
z = y * y * 3 # 对应元素相乘
out = z.mean()
print(z, out)
tensor([[12., 27.],
[12., 27.]], grad_fn=<MulBackward0>) tensor(19.5000, grad_fn=<MeanBackward0>)
通过 .requires_grad_() 来⽤用in-place的⽅方式改变 requires_grad 属性:
a = torch.randn(2, 2) # 默认情况下 requires_grad = False
a = ((a * 3) / (a - 1))
print(a)
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b)
print(b.grad_fn)
tensor([[-353.2621, 19.9213],
[ 0.8479, 8.7197]])
False
True
tensor(125267.7500, grad_fn=<SumBackward0>)
<SumBackward0 object at 0x0000019EFA4C80F0>
梯度
out 是一个标量,所以调用 backward() 时不需要指定求导变量:
x = torch.ones(2, 2, requires_grad=True)
print(x)
b_i = torch.tensor([1,2])
y = x + b_i
print(y)
z = y * y * 3 # 对应元素相乘
out = z.mean()
print(z)
print(out)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
tensor([[2., 3.],
[2., 3.]], grad_fn=<AddBackward0>)
tensor([[12., 27.],
[12., 27.]], grad_fn=<MulBackward0>)
tensor(19.5000, grad_fn=<MeanBackward0>)
o u t = 1 n ∑ i = 1 n z i = 1 n ∑ i = 1 n y 2 ∗ 3 = 1 n ∑ i = 1 n ( x i + b i ) 2 ∗ 3 = 1 4 [ ( x 1 + b 1 ) 2 ∗ 3 + ( x 2 + b 2 ) 2 ∗ 3 + ( x 3 + b 3 ) 2 ∗ 3 + ( x 4 + b 4 ) 2 ∗ 3 ] = 19.5 out = \frac{1}{n}\sum\limits_{i=1}^nz_i = \frac{1}{n}\sum\limits_{i=1}^ny^2*3 = \frac{1}{n}\sum\limits_{i=1}^n(x_i+b_i)^2*3 = \frac{1}{4}[(x_1+b_1)^2*3+(x_2+b_2)^2*3+(x_3+b_3)^2*3+(x_4+b_4)^2*3]= 19.5 out=n1i=1∑nzi=n1i=1∑ny2∗3=n1i=1∑n(xi+bi)2∗3=41[(x1+b1)2∗3+(x2+b2)2∗3+(x3+b3)2∗3+(x4+b4)2∗3]=19.5
out.backward() # 等价于 out.backward(torch.tensor(1.))
out 关于 x 的梯度 d ( o u t ) d x = 1 n ∗ 2 ∗ ( x + b i ) ∗ 1 ∗ 3 \frac{d(out)}{dx}= \frac{1}{n}*2*(x+b_i)*1*3 dxd(out)=n1∗2∗(x+bi)∗1∗3,例如, d ( o u t ) d x 1 ∣ x 1 = 1 = 1 n ∗ 2 ∗ ( x 1 + b 1 ) ∗ 1 ∗ 3 = 1 4 ∗ 2 ∗ ( 1 + 1 ) ∗ 1 ∗ 3 = 3 , d ( o u t ) d x 2 ∣ x 2 = 1 = 1 n ∗ 2 ∗ ( x 2 + b 2 ) ∗ 1 ∗ 3 = 1 4 ∗ 2 ∗ ( 1 + 2 ) ∗ 1 ∗ 3 = 4.5 \frac{d(out)}{dx_1}|_{x_1=1}= \frac{1}{n}*2*(x_1+b_1)*1*3=\frac{1}{4}*2*(1+1)*1*3=3,\frac{d(out)}{dx_2}|_{x_2=1}= \frac{1}{n}*2*(x_2+b_2)*1*3=\frac{1}{4}*2*(1+2)*1*3=4.5 dx1d(out)∣x1=1=n1∗2∗(x1+b1)∗1∗3=41∗2∗(1+1)∗1∗3=3,dx2d(out)∣x2=1=n1∗2∗(x2+b2)∗1∗3=41∗2∗(1+2)∗1∗3=4.5
print(x.grad)
print(x.grad_fn)
tensor([[3.0000, 4.5000],
[3.0000, 4.5000]])
None
向量函数
y
⃗
=
f
(
x
⃗
)
\vec{y}=f(\vec{x})
y=f(x),那么关于的梯度就是一个雅可比矩阵(Jacobian matrix):
而 torch.autograd 这个包就是用来计算⼀一些雅克比矩阵的乘积的。例如,如果是一个标量函数
l
=
g
(
y
⃗
)
l=g(\vec{y})
l=g(y)的梯度:
那么根据链式法则我们有
l
l
l关于
g
(
y
⃗
)
g(\vec{y})
g(y)的雅克比矩阵就为:
注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。
# 再来反向传播⼀一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
out3 = x.sum()
x.grad.data.zero_() # 之前梯度清零
out3.backward()
print(x.grad)
tensor([[4.0000, 5.5000],
[4.0000, 5.5000]])
tensor([[1., 1.],
[1., 1.]])
为什么在 y.backward() 时,如果 y 是标量量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的 Tensor ?
简单来说就是为了了避免向量(甚至更更高维张量)对张量求导,⽽而转换成标量对张量求导。
举个例子,假设形状为 m × n m × n m×n的矩阵 X 经过运算得到了 p × q p × q p×q的矩阵 Y,Y 又经过运算得到了 s × t s × t s×t的矩阵 Z。那么按照前面讲的规则,dZ/dY 应该是一个 s × t × p × q s × t × p × q s×t×p×q四维张量,dY/dX 是一个 p × q × m × n p × q × m × n p×q×m×n的四维张量。问题来了,怎样反向传播?怎样将两个四维张量相乘?这要怎么乘?就算能解决两个四维张量怎么乘的问题,四维和三维的张量又怎么乘?导数的导数又怎么求,这一连串串的问题,感觉要疯掉…… 为了了避免这个问题,这里不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量量同形的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量。
举个例子,假设 y y y由自变量 x x x计算而来, w w w是和 y y y同形的张量,则 y.backward(w) 的含义是:先计算 l l l= torch.sum(y * w) ,则 l l l是个标量,然后求 l l l对自变量 x x x 的导数。详细请点击此链接
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
print(x)
y = 2 * x
z = y.view(2, 2)
print(z)
tensor([1., 2., 3., 4.], requires_grad=True)
tensor([[2., 4.],
[6., 8.]], grad_fn=<ViewBackward>)
现在 y 不是一个标量,所以在调用 backward 时需要传入一个和 y 同形的权重向量v进行加权求和得到一个标量。
z.backward(v) 的含义是:先计算 𝑙 = torch.sum(z * v) ,则 𝑙 是个标量,然后求 𝑙 对自变量 𝑥 的导数。
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad)
tensor([2.0000, 0.2000, 0.0200, 0.0020])
注意, x.grad 是和 x 同形的张量
中断梯度追踪
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) # True
print(y2, y2.requires_grad) # False
print(y3, y3.requires_grad) # True
True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<AddBackward0>) True
上面的 y2 是没有 grad_fn 而且 y2.requires_grad=False 的,而 y3 是有 grad_fn的。如果我们将 y3 对 x 求梯度的话会是多少呢?
y3.backward()
print(x.grad)
tensor(2.)
为什么是2呢? y 3 = y 1 + y 2 = x 2 + x 3 y_3=y_1+y_2 = x^2+x^3 y3=y1+y2=x2+x3 ,当 x = 1 x=1 x=1时, d y 3 d x \frac{dy_3}{dx} dxdy3不应该是5吗?事实上,由于 y 2 y_2 y2的定义是被 torch.no_grad(): 包裹的,所以与y_2有关的梯度是不会回传的,只有与y_1有关的梯度才会回传,即 x 2 x^2 x2对 x x x的梯度。
由于y2.requires_grad=False ,所以不能调用 y2.backward() ,会报错:
y2.backward()
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-38-495d723fc69e> in <module>
----> 1 y2.backward()
E:\Users\TFX\Anaconda3\envs\tensorflow24\lib\site-packages\torch\tensor.py in backward(self, gradient, retain_graph, create_graph)
219 retain_graph=retain_graph,
220 create_graph=create_graph)
--> 221 torch.autograd.backward(self, gradient, retain_graph, create_graph)
222
223 def register_hook(self, hook):
E:\Users\TFX\Anaconda3\envs\tensorflow24\lib\site-packages\torch\autograd\__init__.py in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables)
130 Variable._execution_engine.run_backward(
131 tensors, grad_tensors_, retain_graph, create_graph,
--> 132 allow_unreachable=True) # allow_unreachable flag
133
134
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
想要修改 tensor 的数值,但是又不希望被 autograd 记录(即不会影响反向传播),那么可以对 tensor.data 进行操作。
x = torch.ones(1,requires_grad=True)
print(x.data) # 还是一个tensor
print(x.data.requires_grad) # 但是已经是独⽴立于计算图之外
y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])
参考链接
https://pytorch.org/docs/stable/index.html
https://github.com/ShusenTang/Dive-into-DL-PyTorch