深度学习入门笔记

4.9k 词

动手深度学习

1. 预备知识

1.1 数据操作

1.1.1 基础

数据操作的基础:

  1. 获取数据
  2. 将数据读入计算机后存储

数据操作的核心:张量(tensor), 即n维数组

一维张量称为向量(vector), 二维张量称为矩阵(matrix)

Pytorch提供了tensor类做张量操作:

1
2
3
4
import torch
x = torch.arange(12)
print(x)
# tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

可以用tensor类建立任意张量:

1
2
3
4
5
6
7
8
9
10
11
12
torch.zeros((2, 3, 4))
'''
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.]]])
即三维,分别为2 3 4的张量
点号代表都为浮点数即0.0
'''

torch.reshape((3,1))可以将原始张量转为3*1的矩阵,且可以写成(3,-1)的简写。

也可以从随机化初始值: 其中的 每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。

1
torch.randn(3, 4)

可以直接用嵌套列表赋值:最外层的列表对应于轴0(即),内层的列表对应于轴1(即)。

1
2
3
4
5
6
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]])
'''

1.1.2 运算符

对于任意具有相同形状的张量,常见的标准算术运算符(+、-、*、/和**)都可以被升级为按元素单独运算。

1
2
3
4
5
6
7
8
9
10
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # **运算符是求幂运算
'''
(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.]))
'''

我们也可以把多个张量连结(concatenate)在一起,把它们端对端地叠起来形成一个更大的张量。我们只需 要提供张量列表,并给出沿哪个轴连结。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
'''
dim=0即指明沿轴0连接
(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))

'''

1.1.3 广播机制

我们可以在相同形状的张量做元素操作,但不同形状的张量也可以通过广播机制执行元素操作:

  1. 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;
  2. 对生成的数组执行按元素操作。

沿着数组中长度为1的轴进行广播:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a + b
'''
a:
(tensor([[0],
[1],
[2]]),
b:
tensor([[0, 1]]))
a + b:
tensor([[0, 1],
[1, 2],
[2, 3]])

'''

矩阵a将复制列(即0 - 1 - 2变成0,0 - 1,1 - 2,2),矩阵b将复制行,然后再按元素相加。

1.1.4 索引和切片

就像在任何其他Python数组中一样,张量中的元素可以通过索引访问。与任何Python数组一样:第一个元素 的索引是0,最后一个元素索引是‐1;可以指定范围以包含第一个元素和最后一个之前的元素。

如果我们想为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值。例如,[0:2, :]访问 第1行和第2行,其中“:”代表沿轴1(列)的所有元素。虽然我们讨论的是矩阵的索引,但这也适用于向量 和超过2个维度的张量。

1
2
3
4
5
6
7
8
X[0:2, :] = 12
# 0:2代表0到1,即从0开始数两个元素
X
'''
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
'''

1.1.5 x += y比x = x + y更节省内存

如果在后续计算中没有重复使用X,我们也可以使用X[:] = X + YX += Y来减少操作的内存开销。

因为可以实现原地操作,而不会新开辟内存空间。

1.1.6 转换为其他Python对象

将深度学习框架定义的张量转换为NumPy张量(ndarray)很容易,反之也同样容易。torch张量和numpy数 组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

1
2
3
4
5
6
A = X.numpy() 
B = torch.tensor(A)
type(A), type(B)
'''
(numpy.ndarray, torch.Tensor)
'''

要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数强制转换。

1.2 预处理

• pandas软件包是Python中常用的数据分析工具(可用于读取各种数据, 例如.csv),pandas可以与张量兼容。

• 用pandas处理缺失的数据(NaN)时,我们可根据情况选择用插值法和删除法。

pandas转为张量:

1
2
3
4
5
6
7
import torch
import pandas as pd
inputs = pd.get_dummies(inputs, dummy_na=True)

X = torch.tensor(inputs.to_numpy(dtype=float))
y = torch.tensor(outputs.to_numpy(dtype=float))
X, y

1.3 线性代数

向量

向量只是一个数字数组,就像每个数组都有一个长度一样,每个向量也是如此。在数学表示法中,如果我们想 说一个向量x由n个实值标量组成,可以将其表示为x ∈ R n。向量的长度通常称为向量的维度(dimension)。

请注意,维度(dimension)这个词在不同上下文时往往会有不同的含义,这经常会使人感到困惑。为了清楚 起见,我们在此明确一下:向量或轴的维度被用来表示向量或轴的长度,即向量或轴的元素数量。然而,张 量的维度用来表示张量具有的轴数。在这个意义上,张量的某个轴的维数就是这个轴的长度。

矩阵

1
2
3
A = torch.arange(20).reshape(5, 4)
# 矩阵转置
A.T

我们可以通过行索引(i)和列索引(j)来访问矩阵中的标量元素.

矩阵是有用的数据结构:它们允许我们组织具有不同模式的数据。例如,我们矩阵中的行可能对应于不同的 房屋(数据样本),而列可能对应于不同的属性。曾经使用过电子表格软件或已阅读过 2.2节的人,应该对此 很熟悉。因此,尽管单个向量的默认方向是列向量,但在表示表格数据集的矩阵中,将每个数据样本作为矩 阵中的行向量更为常见。后面的章节将讲到这点,这种约定将支持常见的深度学习实践。例如,沿着张量的 最外轴,我们可以访问或遍历小批量的数据样本。

张量

矩阵是向量的推广一样,我们可以构建具有更多轴的数据结构。张量(本小节中的 “张量”指代数对象)是描述具有任意数量轴的n维数组的通用方法。例如,向量是一阶张量,矩阵是二阶张 量。张量用特殊字体的大写字母表示(例如,X、Y和Z),它们的索引机制与矩阵类似。

当我们开始处理图像时,张量将变得更加重要,图像以n维数组形式出现,其中3个轴对应于高度、宽度,以 及一个通道(channel)轴,用于表示颜色通道(红色、绿色和蓝色)。现在先将高阶张量暂放一边,而是专 注学习其基础知识。

运算

两个矩阵的按元素乘法称为Hadamard积(Hadamard product)

pir7MLV.png

降维

默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。

我们还可以指定张量沿哪一个轴来通过求和降低维度。以矩阵为例,为了通过求和所有行的元素来降维(轴0),可以在调用函数时指 定axis=0。由于输入矩阵沿0轴降维以生成输出向量,因此输入轴0的维数在输出形状中消失。

1
2
3
4
5
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
'''
torch.Size([5, 4]), tensor(190.) ==> (tensor([40., 45., 50., 55.]), torch.Size([4]))
'''

沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和。

1
A.sum(axis=[0, 1]) # 结果和A.sum()相同

非降维求和

1
2
3
4
5
6
7
8
9
10
11
sum_A = A.sum(axis=1, keepdims=True)
sum_A
'''
tensor([[ 6.],
[22.],
[38.],
[54.],
[70.]])
否则正常会降维成为:
(tensor([ 6., 22., 38., 54., 70.]), torch.Size([5]))
'''

如果我们想沿某个轴计算A元素的累积总和,比如axis=0(按行计算),可以调用cumsum函数。此函数不会沿 任何轴降低输入张量的维度。

点积

给定两个向量x, y ∈ R d,它 们的点积(dot product)⟨x, y⟩,是相同位置的按元素乘积的和。

1
2
3
4
5
y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)
'''
(tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))
'''

注意,我们可以通过执行按元素乘法,然后进行求和来表示两个向量的点积:torch.sum(x * y)

点积在很多场合都很有用。例如,给定一组由向量x ∈ R d表示的值,和一组由w ∈ R d表示的权重

x 中的值 根据权重w的加权和,可以表示为点积x ⊤w。当权重为非负数且和为1时,点积表示加权平均(weighted average)。将两个向量规范化得到单位长度后,点积表示它们夹角的余弦。本节后面的内 容将正式介绍长度(length)的概念。

矩阵-向量积

在代码中使用张量表示矩阵‐向量积,我们使用mv函数。当我们为矩阵A和向量x调用torch.mv(A, x)时,会执 行矩阵‐向量积。注意,A的列维数(沿轴1的长度)必须与x的维数(其长度)相同

我们可以把一个矩阵A ∈ R m×n乘法看作一个从R n到R m向量的转换。

这些转换是非常有用的,例如可以用方阵的乘法来表示旋转。后续章节将讲到,我们也可以使用矩阵‐向量积来描述在给定前一层的值时,求解神经网络每一层所需的复杂计算。

矩阵乘法

我们可以将矩阵‐矩阵乘法AB看作简单地执行m次矩阵‐向量积,并将结果拼接在一起,形成一个n × m矩阵。 在下面的代码中,我们在A和B上执行矩阵乘法。这里的A是一个5行4列的矩阵,B是一个4行3列的矩阵。两者 相乘后,我们得到了一个5行3列的矩阵。

1
2
3
4
5
6
7
8
9
B = torch.ones(4, 3)
torch.mm(A, B)
'''
tensor([[ 6., 6., 6.],
[22., 22., 22.],
[38., 38., 38.],
[54., 54., 54.],
[70., 70., 70.]])
'''

范数

向量的范数是表示一个向量有多大。这里考虑的大小(size)概念不涉及维度,而是分量的大小

pir71dU.png

范数听起来很像距离的度量。欧几里得距离和毕达哥拉斯定理中的非负性概念和三角不等式可能会给出一些 启发。事实上,欧几里得距离是一个L2范数

假设n维向量x中的元素是x_1, . . . , x_n,其L2范数是向量元素平方和的平方根

1
2
3
4
u = torch.tensor([3.0, -4.0])
torch.norm(u)
# tensor(5.)
# 即exp( 3*3 + (-4)*(-4) )

深度学习中更经常地使用L2范数的平方,也会经常遇到L1范数,它表示为向量元素的绝对值之和.

1
2
torch.abs(u).sum()
# tensor(7.)

1.4 微积分

在深度学习中,我们“训练”模型,不断更新它们,使它们在看到越来越多的数据时变得越来越好。通常情况下,变得更好意味着最小化一个损失函数(loss function),即一个衡量“模型有多糟糕”这个问题的分数。

最终,我们真正关心的是生成一个模型,它能够在从未见过的数据上表现良好。但“训练”模型只能将模型与我们实际能看到的数据相拟合。因此,我们可以将拟合模型的任务分解为两个关键问题:

• 优化(optimization):用模型拟合观测数据的过程;

• 泛化(generalization):数学原理和实践者的智慧,能够指导我们生成出有效性超出用于训练的数据集本身的模型。

梯度(重要)

我们可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度(gradient)向量

梯度是一个向量,它表示函数在某一点上的变化率和方向。在机器学习和优化中,梯度广泛应用于求解函数的最小值或最大值。

对于一个多元函数,其梯度由偏导数组成。例如,对于一个具有 n 个自变量(输入变量)的函数 f(x₁, x₂, …, xₙ),其梯度向量 ∇f 表示为 (∂f/∂x₁, ∂f/∂x₂, …, ∂f/∂xₙ)。梯度的每个分量表示函数在相应自变量方向上的变化率

梯度的方向指向函数在该点上的最陡增长方向,而梯度的大小表示变化率的强度。梯度越大,函数在该点上的变化越快。

在机器学习中,梯度在训练神经网络等模型时起着重要的作用。通过计算损失函数对模型参数的梯度,我们可以使用梯度下降等优化算法来更新参数,以使损失函数最小化梯度指导了参数更新的方向,使模型能够朝着更好的方向进行调整。

pir73oF.png

当我们计算一个张量的梯度时,需要执行以下步骤:

  1. 在进行反向传播之前,将计算图的梯度缓存清零。可以使用optimizer.zero_grad()或者直接调用张量的zero_()方法来实现。
  2. 对于想要计算梯度的张量,通过调用其backward()方法进行反向传播。这将根据计算图自动计算梯度。
  3. 访问张量的grad属性,即可获得计算得到的梯度值。

例如,假设有一个张量x,并且已经定义了一个损失函数loss。我们可以按照以下方式计算x的梯度:

1
2
3
4
5
6
7
8
9
10
import torch

x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x.sum()
loss = y**2
# 即y的平方 "**"="^"

loss.backward() # 执行反向传播

print(x.grad) # 输出张量x的梯度值

上述代码中,我们首先设置了xrequires_grad属性为True,以便PyTorch跟踪其梯度。然后,我们定义了计算图中的一系列操作,并计算了loss。通过调用backward()方法,我们执行了反向传播计算梯度。最后,我们通过访问x.grad属性获取了张量x的梯度值。

需要注意的是,只有具有requires_grad=True属性的张量才会计算梯度并存储在grad属性中。如果不需要计算某个张量的梯度,可以将其设置为requires_grad=False,以节省计算和内存资源。此外,在每次反向传播前都需要将梯度缓存清零,以避免梯度累积的影响。

1.5 自动微分(重要)

深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。

实际中,根据设计好的模型,系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播(back propagate)意味着跟踪整个计算图,填充关于每个参数的偏导数

例如,在我们计算y关于x的梯度之前,需要一个地方来存储梯度。重要的是,我们不会在每次对一个参数求导时都分配新的内存。因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗 尽。注意,一个标量函数关于向量x的梯度是向量,并且与x具有相同的形状。

1
2
3
4
import torch
x = torch.arange(4.0)
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
x.grad # 默认值是None

现在计算y。

1
2
y = 2 * torch.dot(x, x)
# tensor(28., grad_fn=<MulBackward0>)

x是一个长度为4的向量,计算x和x的点积,得到了我们赋值给y的标量输出。接下来,通过调用反向传播函数来自动计算y关于x每个分量的梯度,并打印这些梯度。

1
2
3
y.backward()
x.grad
# tensor([ 0., 4., 8., 12.])

即反向传播函数查询到y的数据操作引用了x,因此计算出y关于x张量的每个分量梯度。

注意:在默认情况下,PyTorch会累积梯度,我们需要清除之前的值 。

1
2
3
4
x.grad.zero_()
y = x.sum()
y.backward()
x.grad

非标量变量的反向传播

当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。对于高阶和高维的y和x,求导的结果可以 是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中),但当调用向量的反向计算时, 我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里,我们的目的不是计算微分矩阵, 而是单独计算批量中每个样本的偏导数之和。

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

分离计算

有时,我们希望将某些计算移动到记录的计算图之外。

例如,假设y是作为x的函数计算的,而z则是作为y和x的 函数计算的。想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数,并且只考虑 到x在y被计算后发挥的作用。

这里可以分离y来返回一个新变量u,该变量与y具有相同的值(仅有值是相同的,但不具有y的计算属性),但丢弃计算图中如何计算y的任何信息。换句话说,梯度不会向后流经u到x。因此,下面的反向传播函数计算z=u*x关于x的偏导数,同时将u作为常数处理, 而不是z=x*x*x关于x的偏导数。

1
2
3
4
5
6
7
8
9
x.grad.zero_()
y = x * x
u = y.detach()

z = u * x
z.sum().backward()
# z.sum()用于将z转为输出的标量,便于计算梯度
x.grad == u
# tensor([True, True, True, True])

由于记录了y的计算结果,我们可以随后在y上调用反向传播,得到y=x*x关于的x的导数,即2*x。

1
2
3
4
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
# tensor([True, True, True, True])

Python控制流的梯度计算

即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值

1
2
3
4
5
6
7
8
9
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

计算梯度:

1
2
3
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()

2. 线性神经网络

留言