学习文档地址:https://tangshusen.me/Dive-into-DL-PyTorch/

本博客系列主要记载自己阅读文档时的一些想法和记忆方式

文章内容部分是原文(例如代码部分),部分是自己的一些理解和对原文的补充(删减)。

一. 数据操作

Tensor:也就是张量,和NumPy的多维数组非常类似,向量形式。

标量可以看作是0维张量,向量可以看作1维张量,矩阵可以看作是二维张量。

1. 创建Tensor

例子:

创建一个5x3的未初始化Tensor

x = torch.empty(5, 3) 

如果想要个随机化Tensor

x = torch.rand(5, 3)

全0Tensor

# torch.long是创建数据的类型,为long
x = torch.zeros(5, 3, dtype=torch.long)

直接通过数据创建Tensor

x = torch.tensor([5.5, 3])

通过现有的Tensor创建:

# 返回的tensor默认具有相同的torch.dtype和torch.device
x = x.new_ones(5, 3, dtype=torch.float64)  
# 指定新的数据类型
x = torch.randn_like(x, dtype=torch.float)

可以直接使用dtype来指定数据类型,如果不指定,此方法会默认重用输入Tensor的一些属性,例如数据类型

通过shape或者size()获取Tensor的形状:

print(x.size())
print(x.shape)

创建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)

2. 一些常用操作

2.1 算术操作

三种加法方法:

  1. 朴素

     y = torch.rand(5, 3)
     print(x + y)
  2. 使用torch.add()函数

    print(torch.add(x, y))

    指定输出:

    # 将x+y赋值给result
    result = torch.empty(5, 3)
    torch.add(x, y, out=result)
    print(result) 
  3. inplace方法

    # 将x的值加在y上
    y.add_(x)
    print(y)

    注:PyTorch操作inplace版本都有后缀_, 例如x.copy_(y), x.t_()

2.2 索引

类似NumPy的索引操作来访问Tensor的一部分

注:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改(有点像numpy的机制)。

y = x[0, :]
y += 1
print(y)
# 源tensor也被改了(当y被修改,x也会同样被修改)
print(x[0, :]) 

结果为:

tensor([1.6035, 1.8110, 0.9549])
tensor([1.6035, 1.8110, 0.9549])

PyTorch的一些高级的选择函数:

函数功能
index_select(input, dim, index)在指定维度dim上选取,比如选取某些行、某些列
masked_select(input, mask)例子如上,a[a>0],使用ByteTensor进行选取
nonzero(input)非0元素的下标
gather(input, dim, index)根据index,在dim维度上选取数据,输出的size与index一样

2.3 改变形状

使用view()改变Tensor的形状:

x = torch.rand(5, 3)
# 形状改变为:1*15
y = x.view(15)
# -1所指的维度可以根据其他维度的值推出来
z = x.view(-1, 5) 
print(x.size(), y.size(), z.size())

这里-1表示一个不确定的数,就是你如果不确定你想要reshape成几行,但是你很肯定要reshape成4列,那不确定的地方就可以写成-1。

就比如上面的x共有5 3=15个数据,x.view(-1, 5) 明确要5列,便改变形状为3 5。

结果为:

torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])

注意:view()返回的新Tensor与源Tensor虽然可能有不同的size,但是是共享data的,也即更改其中的一个,另外一个也会跟着改变(view()得到的新张量依然共享内存)。

返回一个真正新的副本(即不共享data内存)的方法:

先用clone创造一个副本然后再使用view

(不推荐使用reshape()

x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)

此时x和x_cp不共享内存,结果为:

tensor([[ 0.6035,  0.8110, -0.0451],
        [ 0.8797,  1.0482, -0.0445],
        [-0.7229,  2.8663, -0.5655],
        [ 0.1604, -0.0254,  1.0739],
        [ 2.2628, -0.9175, -0.2251]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345, 1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])

使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor

使用item()将一个标量Tensor转换成一个Python number:

# 注意是转换的tensor需是标量
x = torch.randn(1)
print(x)
print(x.item()) 

输出:

tensor([2.3466])
2.3466382026672363

2.4 线性代数

一些好用的线性函数api:

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

更多可以参考官方文档:https://pytorch.org/docs/stable/tensors.html

3. 广播机制

当对两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个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]])

由于xy分别是1行2列和3行1列的矩阵,如果要计算x + y,那么x中第一行的2个元素被广播(复制)到了第二行和第三行,而y中第一列的3个元素被广播(复制)到了第二列。如此,就可以对2个3行2列的矩阵按元素相加。

4. 运算的内存开销

索引操作是不会开辟新内存的,而像y = x + y这样的运算是会新开内存的,然后将y指向新内存。

为了演示这一点,我们可以使用Python自带的id函数:如果两个实例的ID一致,那么它们所对应的内存地址相同;反之则不同。

(很重要)

注:虽然view返回的Tensor与源Tensor是共享data的,但是依然是一个新的Tensor(因为Tensor除了包含data外还有一些其他属性),二者id(内存地址)并不一致。

5. Tensor和NumPy相互转换

使用numpy()Tensor转换成NumPy数组:

a = torch.ones(5)
b = a.numpy()

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:

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)

结果代表使用from_numpy()转换后的tensor和源numpy数组共享同一个内存。

所有在CPU上的Tensor(除了CharTensor)都支持与NumPy数组相互转换。

直接用torch.tensor()将NumPy数组转换成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)

6. Tensor on GPU

用方法to()Tensor在CPU和GPU(需要硬件支持)之间相互移动:

# 以下代码只有在PyTorch 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()还可以同时更改数据类型

二. 自动求梯度

PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。

1. 概念

将其属性.requires_grad设置为True,它将开始追踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用.backward()来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。

大致就是如果设置Tensor.requires_gradTrue的话,在输出Tensor时,会输出这个Tensor是如何得到的(加法,乘法….)。

2.Tensor的追踪

创建一个Tensor并设置requires_grad=True:

x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)

输出:

(如果Tensor是直接创建的(称为叶子节点),那么它没有grad_fngrad_fnNone

tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
None

运算操作:

y = x + 2
print(y)
print(y.grad_fn)

输出:

(通过一个加法操作创建的,所以它有一个为<AddBackward>grad_fn

tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>

可以用is_leaf判断这个张量是不是叶子节点:

print(x.is_leaf, y.is_leaf) 
# True False

通过.requires_grad_()来用in-place的方式改变requires_grad属性:

a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)

输出:

False
True
<SumBackward0 object at 0x118f50cc0>

3.梯度

首先来一点前置知识:

1.函数值与自变量都为张量的函数求导

数学上,如果有一个函数值和自变量都为向量的函数 $\vec{y}=f(\vec{x})$, 那么 $\vec{y}$ 关于 $\vec{x}$ 的梯度就是一个雅可比矩阵(Jacobian matrix):

$$ J=\left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right) $$

2.链式法则

链式法則,亦称连锁法则(英语:chain rule),是求复合函数导数的一个法则。设$f$和$g$为两个关于$x$可导函数,则复合函数$(f\circ g)(x)$的导数$(f\circ g)'(x)$为:$(f\circ g)'(x)=f'(g(x))g'(x).$

如果out是一个标量,所以调用backward()时不需要指定求导变量:

out.backward(x)表示对x求导(x是与y同形的张量)。如果out为标量,则不需要指定求导变量

out.backward()
# 等价于 out.backward(torch.tensor(1.))

这时候out关于x的梯度 $\frac{d(out)}{dx}$为:

(使用x.grad来对$x$求导)

# print(x.grad)
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

现在来求证一下计算是否正确

我们令out为 $o$:

$$ o=\frac14\sum_{i=1}^4z_i=\frac14\sum_{i=1}^43(x_i+2)^2 $$

梯度为:

$$ \frac{\partial{o}}{\partial{x_i}}\bigr\rvert_{x_i=1}=\frac{9}{2}=4.5 $$

结果显然是正确的

如果是标量对张量进行求导,我们需要运用到链式法则:

数学上,如果有一个函数值和自变量都为向量的函数 $\vec{y}=f(\vec{x})$, 那么 $\vec{y}$ 关于 $\vec{x}$ 的梯度就是一个雅可比矩阵(Jacobian matrix):

$$ J=\left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right) $$

torch.autograd这个包就是用来计算一些雅克比矩阵的乘积的。例如,如果 $v$ 是一个标量函数的 $l=g\left(\vec{y}\right)$ 的梯度:

$$ v=\left(\begin{array}{ccc}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) $$

那么根据链式法则我们有 $l$ 关于 $\vec{x}$ 的雅克比矩阵就为:

$$ v J=\left(\begin{array}{ccc}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) \left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right)=\left(\begin{array}{ccc}\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}\end{array}\right) $$

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

# 再来反向传播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)

输出:

tensor([[5.5000, 5.5000],
        [5.5000, 5.5000]])
tensor([[1., 1.],
        [1., 1.]])

现在我们解释一个问题,为什么在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor?
简单来说就是为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导。举个例子,假设形状为 m x n 的矩阵 X 经过运算得到了 p x q 的矩阵 Y,Y 又经过运算得到了 s x t 的矩阵 Z。那么按照前面讲的规则,dZ/dY 应该是一个 s x t x p x q 四维张量,dY/dX 是一个 p x q x m x n的四维张量。问题来了,怎样反向传播?怎样将两个四维张量相乘???这要怎么乘???就算能解决两个四维张量怎么乘的问题,四维和三维的张量又怎么乘?导数的导数又怎么求,这一连串的问题,感觉要疯掉……
为了避免这个问题,我们不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量,举个例子,假设y由自变量x计算而来,w是和y同形的张量,则y.backward(w)的含义是:先计算l = torch.sum(y * w),则l是个标量,然后求l对自变量x的导数。
参考

举例:

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>)

现在 z 不是一个标量,所以在调用backward时需要传入一个和z同形的权重向量进行加权求和得到一个标量

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=<ThAddBackward>) True

可以看到,上面的y2是没有grad_fn而且y2.requires_grad=False的,而y3是有grad_fn。如果我们将y3x求梯度的话会是多少呢?

y3.backward()
print(x.grad)

输出:

tensor(2.)

为什么是2呢?$ y_3 = y_1 + y_2 = x^2 + x^3$,当 $x=1$ 时 $\frac {dy_3} {dx}$ 不应该是5吗?事实上,由于 $y_2$ 的定义是被torch.no_grad():包裹的,所以与 $y_2$ 有关的梯度是不会回传的,只有与 $y_1$ 有关的梯度才会回传,即 $x^2$ 对 $x$ 的梯度。

同时,你无法调用y2.backward(),报错如下:

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) # 但是已经是独立于计算图之外(False)

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.])
最后修改:2021 年 05 月 07 日 04 : 30 PM