动手学深度学习笔记

1. 深度学习的运算基本单位

深度学习的本质就是通过一堆网络和参数形成一个映射 f ,来拟合各种任务的输入和输出之间的关系。输入转换为输出的关键就在于大量的计算,其中的计算基本单位是张量类。

根据深度学习框架的不同,有不同的张量类。如:MXNet 中的 ndarray,Pytorch 和 TensorFlow 中的 Tensor。张量和 Numpy 中的 ndarray 很相似,可以看做一个数组(根据维度可以分别看作向量、高维矩阵)。但是张量类支持 GPU 加速计算,Numpy 只支持 CPU 计算,并且张量类还支持自动微分,非常适合深度学习。

张量可以具有任意维度,最简单的张量是一维的,可以看成一个向量。

1
2
3
import torch
x = torch.arange(12)
print(x)

tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

高维的张量可以看作矩阵,如通过 reshape 函数将 上面的 x 转换为 (3,4) 的矩阵。

1
2
X = x.reshape(3,4)
print(X)

tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])

2. 张量的基本操作

2.1 张量形状

在深度模型的搭建过程中,弄清当前输入、输出张量的形状很重要。访问张量的形状可以很多方法实现:

1
2
3
4
5
#通过.shape属性或size()函数访问张量形状
print(X.shape)
print(X.size())
#通过numel()函数获取张量中的元素个数
print(X.numel())

torch.Size([3, 4])

torch.Size([3, 4])

12

2.2 转换形状

知道了张量的形状,我们可以通过 reshape 函数将张量转换为我们喜欢的形状。

1
2
3
4
5
6
#将(3,4)的X转换为(4,3)的形状
print(X.reshape(4,3))

#通过设置-1参数值,可以让系统自动判断第二维度的值
print(X.reshape(2,-1))
#print(X.reshape(-1,12))

tensor([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]])

tensor([[ 0, 1, 2, 3, 4, 5], [ 6, 7, 8, 9, 10, 11]])

2.3 张量创建

张量的创建可以通过函数生成,也可以通过现有的 ndarray 或列表数据转换而成。

1
2
3
4
5
6
#通过zeros函数创建全零张量
print(torch.zeros((2,3,4)))
#通过ones函数创建全1张量
print(torch.ones((2,3,4)))
#通过randn函数随机采样(均值为1,标准差为1的标准高斯分布)
print(torch.randn(3,4))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tensor([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],

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

[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
tensor([[-0.4038, 1.2483, -0.3773, -3.5461],
[ 1.0424, -0.0109, 0.2068, -1.2183],
[ 0.7477, -1.1807, -0.3766, -0.2664]])
1
2
#通过tensor函数将列表或ndarray转换
print(torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]))

tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

2.4 张量计算

张量的运算是按元素方式进行的,即单元运算会对所有元素进行运算,多元运算会对同一位置的元素进行运算。

1
2
3
4
5
6
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
# **运算符是求幂运算
print(x + y, x - y, x * y, x / y, x ** y)
#单元运算 求e^x
print(torch.exp(x))
1
2
3
4
5
6
(tensor([ 3., 4., 6., 10.]),
tensor([-1., 0., 2., 6.]),
tensor([ 2., 4., 8., 16.]),
tensor([0.5000, 1.0000, 2.0000, 4.0000]),
tensor([ 1., 4., 16., 64.]))
tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])

张量运算还有一个重要的性质是广播机制。当两个形状不一致的张量进行运算时,会将两个张量都扩展到一样的形状然后进行运算。

1
2
3
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
print(a+b) #将a,b都扩展成(3,2)的向量

tensor([[0, 1], [1, 2], [2, 3]])

2.5 张量索引与切片

张量和数组的索引一样,第一个元素为0,最后一个元素为-1。

1
2
x = torch.arange(12).reshape(3,4)
print(x[-1],x[1:3]) #选取x最后一行,选取x第2到第3行

tensor([ 8, 9, 10, 11])

tensor([[ 4, 5, 6, 7], [ 8, 9, 10, 11]])

张量通过 X[ 行索引 ,列索引 ] 的方式进行索引,行列之间用逗号隔开,而行索引和列索引可以使用 : 来指定具体的行列范围。其中行列索引也可以使用 index 列表进行索引。

1
2
3
4
5
6
7
8
9
10
x = torch.arange(12).reshape(3,4)
print(x)
print(x[0:2,1:3])

index_row = [0,1]
index_col = [1,2]
print(x[index_row,index_col]) #索引(0,1) (1,2)
index_row = [[0,0],[1,1]]
index_col = [[1,2],[1,2]]
print(x[index_row,index_col])#索引(0,1)(0,2)(1,1)(1,2)

tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
tensor([[1, 2], [5, 6]])

tensor([1, 6])

tensor([[1, 2], [5, 6]])

2.6 向量-矩阵运算

该节主要包括三种运算:向量间的点积、向量矩阵之间的积、矩阵之间的积。

向量点积在线性表示中有重要的意义,可以简化公式。向量点积产生的结果为一个标量(是一个数值)。

1
2
3
4
5
x = torch.arange(4, dtype = torch.float32)
y = torch.ones(4, dtype = torch.float32)
print(x)
print(y)
print(torch.dot(x,y))

tensor([0., 1., 2., 3.]) tensor([1., 1., 1., 1.]) tensor(6.)

矩阵与向量之间的积是通过 mv 函数进行的,可以用于将矩阵变为向量。

1
2
3
4
5
x = torch.arange(4, dtype = torch.float32)
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
print(x)
print(A)
print(torch.mv(A,x))

tensor([0., 1., 2., 3.]) tensor([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.], [16., 17., 18., 19.]]) tensor([ 14., 38., 62., 86., 110.])

矩阵之间的积分为矩阵-矩阵乘法和 Hadamard 积。

1
2
3
4
5
6
7
#Hadamard积是两个矩阵的对应元素相乘,两个矩阵形状相同
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = torch.ones(5,4)
print(A*B)
#矩阵-矩阵乘法为线性代数乘积前一个矩阵列数等于后一矩阵行数
B = torch.ones(4,5)
print(torch.mm(A,B))
1
2
3
4
5
6
7
8
9
10
11
tensor([[ 0.,  1.,  2.,  3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])

tensor([[ 6., 6., 6., 6., 6.],
[22., 22., 22., 22., 22.],
[38., 38., 38., 38., 38.],
[54., 54., 54., 54., 54.],
[70., 70., 70., 70., 70.]])

3. 梯度

这一节学起来比较迷糊的是不知道各种函数的梯度结果是什么结构。

  • 对于 ,来说,如果 都是标量,则导数是标量。

  • 如果 是标量, 是向量,则梯度(导数)是向量,梯度向量与 同形状。梯度的分量为对 的各分量 求的偏导。

  • 如果 是向量, 是向量,则梯度是矩阵,矩阵元素为 的分量 对 的分量求偏导。
  • 同理可以推出,若 是向量, 是矩阵,则梯度应该为3维张量。

掌握了梯度结果的结构,还需要知道一些求梯度的运算法则,一些常用的求梯度法则如下:

图3.1 常用求梯度法则

4. 自动微分

理解第3部分的梯度结构之后,学习自动微分处的代码就可以更好地理解了。

第一个例子是对向量点积求梯度,从上述的求梯度法则可以看出来,梯度结果应该是 2*X 。因此下述代码通过自动微分求出来的梯度正好是两倍的 X。

1
2
3
4
5
import torch
x = torch.arange(4.0,requires_grad=True)
y = 2 * torch.dot(x,x)
print(x)
print(y)

tensor([0., 1., 2., 3.])

tensor(28., grad_fn=<MulBackward0>)

1
2
3
y.backward()
print(x.grad)
print(x.grad == 4 * x)

tensor([ 0., 4., 8., 12.])

tensor([True, True, True, True])

第二个例子是对和函数求梯度。这个梯度结构仍然是向量,但是没有求导法则,因此需要我们自己推导。

推导结果如下:

梯度向量推导

因此最终梯度应该为全1的向量,我感觉这里作者可能只是想让我们知道对于和函数求梯度,结果是全1的向量。根据链式法则,我们将求和函数作为外部函数时,最终的梯度需要乘以和函数的梯度。但由于和函数的梯度全为1,因此最终梯度大小不会因为和函数的加入而改变大小。

1
2
3
4
5
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum()
y.backward()
print(x.grad)

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

虽然和函数不会改变最终梯度的大小,但他可以改变梯度结构,由矩阵转换为向量。

在这个例子中,如果不使用和函数,最终的梯度是一个矩阵,使用了和函数,梯度则变为了一个向量。

如果不求和,梯度矩阵推导出来的结果如下:

梯度矩阵推导

同时,在深度学习中,我们一般是批次化训练模型,将一批数据送进模型,然后输出一批损失函数的值(大小为批次大小的损失向量,包含各个样本的损失值)。

参数优化需要通过求损失函数的梯度进行,因此此时有两种办法,一种是将所有损失求和,另一种是将损失求平均,获取能表征整个批次样本的损失值。前者会导致损失很大,参数更新会比较慢,因此一般会将学习率放大 batch_size 倍以作平衡。

因此我猜测,这里举例进行求和,是想告诉我们,求和不但可以获取整个批次的损失表征,还可以将梯度结构转化为向量。

1
2
3
4
5
6
7
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
print(x.grad)

tensor([0., 2., 4., 6.])