1.前言
1.2.关键组件
- 我们可以学习的数据(data)。
- 如何转换数据的模型(model)。
- 一个目标函数(objective function),用来量化模型的有效性。
- 调整模型参数以优化目标函数的算法
2.预备知识
2.1.数据操作
2.1.1.入门
- 张量:n维数组;
- 无论使用哪个深度学习框架,它的张量类都与Numpy的ndarray类似,但又比Numpy的ndarray多一些重要功能:
首先,GPU很好地支持加速计算,而NumPy仅支持CPU计算;其次,张量类支持自动微分。这些功能使得张量类更适合深度学习。 - 表示由一个数值组成的数组,这个数组可能有多个维度。具有一个轴的张量对应数学上的向量(vector)。
具有两个轴的张量对应数学上的矩阵(matrix)。具有两个轴以上的张量没有特殊的数学名称。
- 无论使用哪个深度学习框架,它的张量类都与Numpy的ndarray类似,但又比Numpy的ndarray多一些重要功能:
- 可以使用arange创建一个行向量x。它们被默认创建为浮点数。除非额外指定,否则新的张量将存储在内存中,并采用基于CPU的计算。
x = torch.arange(12)
x
tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
- shape属性来访问张量的形状 (沿每个轴的长度)
x.shape
torch.Size([12])
- 如果只想知道张量中元素的总数,即形状的所有元素乘积,可以检查它的大小(size)
x.numel()
12
- 要改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数。
X = x.reshape(3, 4)
X
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
不需要通过手动指定每个维度来改变形状。可以通过在希望张量自动推断的维度放置-1来调用此功能
6. 创建一个形状为(2,3,4)的张量,其中所有元素都设置为0
torch.zeros((2, 3, 4))
创建一个形状为(2,3,4)的张量,其中所有元素都设置为1
torch.ones((2, 3, 4))
- 创建一个形状为(3,4)的张量。其中的每个元素都从均值为0、标准差为1的标准高斯(正态)分布中随机采样
torch.randn(3, 4)
tensor([[-0.9464, 0.7712, -0.0070, 1.0236],
[-2.1246, -0.7854, -1.9674, -0.1727],
[ 0.0397, -0.0477, -0.0160, -0.0113]])
8.为所需张量中的每个元素赋予确定值
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.1.2.运算
1.计算
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 # **运算符是求幂运算
torch.exp(x)
2.张量连结(concatenate)
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)
(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.]]))
2.二元张量
X == Y
tensor([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
3.张量元素求和,产生只有一个元素的张量
X.sum()
tensor(66.)
2.1.3.广播机制
1.广播机制(broadcasting mechanism)
- 首先,通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状。
- 其次,对生成的数组执行按元素操作。
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
由于a和b分别是 3×1 和 1×2 矩阵,如果我们让它们相加,它们的形状不匹配。我们将两个矩阵广播为一个更大的 3×2 矩阵,如下所示:矩阵a将复制列,矩阵b将复制行,然后再按元素相加
a + b
tensor([[0, 1],
[1, 2],
[2, 3]])
2.1.4.索引和切片
和python一样
2.1.5.节省内存
运行一些操作可能会导致为新结果分配内存。
before = id(Y)
Y = Y + X
id(Y) == before
False
执行原地操作非常简单。我们可以使用切片表示法将操作的结果分配给先前分配的数组 zeros_like来分配一个全 0 的块
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 140272150341696
id(Z): 140272150341696
也可以使用X += Y来减少操作的内存开销。
before = id(X)
X += Y
id(X) == before
True
2.1.6.转换为其他Python对象
1.转换后的结果不共享内存
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
(numpy.ndarray, torch.Tensor)
2.要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
(tensor([3.5000]), 3.5, 3.5, 3)
2.2.数据预处理
2.2.1.读取数据集
2.2.2.处理缺失值
- iloc()
- fillna()
- get_dummies()
2.2.3.转换为张量格式
具体见 2.2数据预处理.py
2.3.线性代数
2.3.1.标量
2.3.2.向量
2.3.3.矩阵
A = torch.arange(20).reshape(5, 4)
A
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
矩阵转置 A.T
2.2.4.张量
见上文
2.3.5.张量算法的基本性质
- 给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量
- 两个矩阵的按元素乘法称为哈达玛积(Hadamard product)(数学符号 ⊙)
A * B
- 将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘
2.3.6.降维
1.求和降维
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和
A.sum(axis=[0, 1]) # Same as `A.sum()`
2.平均值
A.mean(), A.sum() / A.numel()
(tensor(9.5000), tensor(9.5000))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(tensor([ 8., 9., 10., 11.]), tensor([ 8., 9., 10., 11.])
2.3.6.1.非降维求和
- 例如,由于sum_A在对每行进行求和后仍保持两个轴,我们可以通过广播将A除以sum_A。
sum_A = A.sum(axis=1, keepdims=True)
sum_A
tensor([[ 6.],
[22.],
[38.],
[54.],
[70.]])
A / sum_A
tensor([[0.0000, 0.1667, 0.3333, 0.5000],
[0.1818, 0.2273, 0.2727, 0.3182],
[0.2105, 0.2368, 0.2632, 0.2895],
[0.2222, 0.2407, 0.2593, 0.2778],
[0.2286, 0.2429, 0.2571, 0.2714]])
- 沿某个轴计算A元素的累积总和,比如axis=0(按行计算),我们可以调用cumsum函数。此函数不会沿任何轴降低输入张量的维度
A.cumsum(axis=0)
tensor([[ 0., 1., 2., 3.],
[ 4., 6., 8., 10.],
[12., 15., 18., 21.],
[24., 28., 32., 36.],
[40., 45., 50., 55.]])
2.3.7.点积
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.))
2.3.8.矩阵-向量积
A.shape, x.shape, torch.mv(A, x)
(torch.Size([5, 4]), torch.Size([4]), tensor([ 14., 38., 62., 86., 110.]))
2.3.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.]])
2.3.10.范数
- 范数:在线性代数中,向量范数是将向量映射到标量的函数 f
- 如果我们按常数因子 α 缩放向量的所有元素,其范数也会按相同常数因子的绝对值缩放
- 三角不等式
- 简单地说范数必须是非负的
- L2 范数是向量元素平方和的平方根
u = torch.tensor([3.0, -4.0])
torch.norm(u)
tensor(5.)
- L1 范数,它表示为向量元素的绝对值之和
torch.abs(u).sum()
tensor(7.)
- 矩阵 X∈Rm×n 的弗罗贝尼乌斯范数(Frobenius norm)是矩阵元素平方和的平方根
torch.norm(torch.ones((4, 9)))
tensor(6.)
2.3.10.1.范数和目标
通常,目标,或许是深度学习算法最重要的组成部分(除了数据),被表达为范数。
2.4.微分
- 拟合模型
- 优化(optimization)
- 泛化(generalization)
2.4.3梯度
- 假设x为n维向量,在微分多元函数时经常使用以下规则:
- 对于所有 A ∈ R m ∗ n A\in R^{m*n} A∈Rm∗n,都有 ▽ x A x = A T \bigtriangledown_x Ax=A^T ▽xAx=AT
- 对于所有 A ∈ R m ∗ n A\in R^{m*n} A∈Rm∗n,都有 ▽ x x T A = A \bigtriangledown_x x^T A=A ▽xxTA=A
- 对于所有 A ∈ R m ∗ n A\in R^{m*n} A∈Rm∗n,都有 ▽ x x T A x = ( A + A T ) x \bigtriangledown_x x^TAx=(A+A^T)x ▽xxTAx=(A+AT)x
-
▽
x
∥
x
∥
2
=
▽
x
x
T
x
=
2
x
\bigtriangledown_x \lVert x \rVert ^2=\bigtriangledown_x x^T x=2x
▽x∥x∥2=▽xxTx=2x
同样,对于任何矩阵X,我们都有 ▽ x ∥ x ∥ F 2 = 2 x \bigtriangledown_x \lVert x \rVert ^2_F=2x ▽x∥x∥F2=2x。
2.5.自动求导
深度学习框架通过自动计算导数,即自动求导(automatic differentiation),来加快这项工作。实际中,根据我们设计的模型,
系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动求导使系统能够随后反向传播梯度。
这里,反向传播(backpropagate)只是意味着跟踪整个计算图,填充关于每个参数的偏导数。
2.5.1.一个简单的例子
import torch
x = torch.arange(4.0)
print(x)
x.requires_grad_(True) # 等价于 `x = torch.arange(4.0, requires_grad=True)`
print(x.grad) # 默认值是None
y = 2 * torch.dot(x, x)
print(y)
y.backward()
print(x.grad)
print(x.grad == 4 * x)
2.5.2.非标量变量的反向传播
-
当
y
不是标量时,向量y
关于向量X
的导数的最自然解释是一个矩阵。对于高阶和高维的y
和x
,求导的结果可以是一个高阶张量# 对非标量调用`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.])
2.5.3.分离计算
-
希望将某些计算移动到记录的计算图之外,即视为常数,反向传播时梯度不会从此经过
x.grad.zero_() y = x * x u = y.detach() z = u * x z.sum().backward() x.grad == u tensor([True, True, True, True])
2.5.4. Python控制流的梯度计算
2.6.概率
2.7.查阅文档
help()
3.线性神经网络
3.1.线性回归
- 仿射变换的特点是通过加权和对特征进行 线性变换 (linear transformation),并通过偏置项来进行 平移 (translation)。
3.1.5.小结
- 机器学习模型中的关键要素是训练数据,损失函数,优化算法,还有模型本身。
- 矢量化使数学表达上更简洁,同时运行的更快。
- 最小化目标函数和执行最大似然估计等价。
- 线性回归模型也是神经网络。
3.3.线性回归的简洁实现
3.3.1. 生成数据集
3.3.2.读取数据集
3.3.3.定义模型
3.3.4.初始化模型参数
3.3.5.定义损失函数
3.3.6.定义优化算法
3.3.7.训练
在每个迭代周期里,我们将完整遍历一次数据集(rain_data
),不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量,我们会进行以下步骤:
- 通过调用
net(X)
生成预测并计算损失l
(正向传播)。 - 通过进行反向传播来计算梯度。
- 通过调用优化器来更新模型参数。
代码见3.3.线性回归.py
3.4.softmax回归
3.4.1.分类问题
独热编码 (one-hot encoding)。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。
3.4.2.网络结构
全连接层:每个输出取决于全部输入。
3.4.4.softmax运算
-
在分类器输出0.5的所有样本中,我们希望这些样本有一半实际上属于预测的类。 这个属性叫做校准 (calibration)。
-
softmax函数:为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。
$$
\hat{y}=softmax(o) ,其中, \hat{y_j}=\frac{exp(o_j)}{\sum_k exp(o_k)}$$
3.4.5. 小批量样本的矢量化
3.4.6. 损失函数
3.4.6.1. 对数似然
交叉熵损失 (cross-entropy loss)
3.4.7.2. 惊异
如果我们不能完全预测每一个事件,那么我们有时可能会感到惊异。当我们赋予一个事件较低的概率时,我们的惊异会更大。克劳德·香农决定用 log 1 P ( j ) = − log P ( j ) \log \frac{1}{P(j)}=-\log P(j) logP(j)1=−logP(j)来量化一个人的 惊异 (surprisal)。在观察一个事件j,并赋予它(主观)概率P(j)。定义的熵是当分配的概率真正匹配数据生成过程时的 预期惊异 (expected surprisal)。
3.4.7.3. 重新审视交叉熵
所以,如果熵是知道真实概率的人所经历的惊异程度,那么你可能会想知道,什么是交叉熵? 交叉熵从P到Q,记为H(P,Q),是主观概率为Q的观察者在看到根据概率P实际生成的数据时的预期惊异。当P=Q时,交叉熵达到最低。在这种情况下,从P到Q的交叉熵是 H ( P , P ) = H ( P ) H(P,P)=H(P) H(P,P)=H(P)。
简而言之,我们可以从两方面来考虑交叉熵分类目标:(i)最大化观测数据的似然;(ii)最小化传达标签所需的惊异。
3.4.9. 小结
- softmax运算获取一个向量并将其映射为概率。
- softmax回归适用于分类问题。它使用了softmax运算中输出类别的概率分布。
- 交叉熵是一个衡量两个概率分布之间差异的很好的度量。它测量给定模型编码数据所需的比特数
3.7.softmax回归的简洁实现
3.7.1.初始化模型参数
- PyTorch不会隐式地调整输入的形状。
因此,我们在线性层前定义了展平层(flatten),来调整网络输入的形状。
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
3.7.2. 重新审视Softmax的实现
- oj 是未归一化的预测 o 的第 j 个元素。如果 ok 中的一些数值非常大,那么 exp(ok) 可能大于数据类型容许的最大数字(即**上溢(overflow))**。
这将使分母或分子变为inf(无穷大),我们最后遇到的是0、inf或nan(不是数字)的 y^j 。在这些情况下,我们不能得到一个明确定义的交叉熵的返回值。 - 解决这个问题的一个技巧是,在继续softmax计算之前,先从所有 ok 中减去 max(ok) 。你可以证明每个 ok 按常数进行的移动不会改变softmax的返回值。
在减法和归一化步骤之后,可能有些 oj 具有较大的负值。由于精度受限, exp(oj) 将有接近零的值,即**下溢(underflow)**。
这些值可能会四舍五入为零,使 y^j 为零,并且使得 log(y^j) 的值为-inf。反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan结果。 - 尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。
通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。
loss = nn.CrossEntropyLoss()
3.7.3. 优化算法
在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。这与我们在线性回归例子中的相同,这说明了优化器的普适性。
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
3.7.4. 训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
4.多层感知机
4.1.1.隐藏层
- 仿射变换中的线性是一个很强的假设。
4.1.1.1.线性模型可能会出错
4.1.1.2.在网络中加入隐藏层
4.1.1.3.从线性到非线性
- 上面的隐藏单元由输入的仿射函数给出,而输出(softmax操作前)只是隐藏单元的仿射函数。
仿射函数的仿射函数本身就是仿射函数。但是我们之前的线性模型已经能够表示任何仿射函数。 - 为了发挥多层结构的潜力,我们还需要一个额外的关键要素:在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function) σ 。
激活函数的输出(例如, σ(⋅) )被称为激活值(activations)。一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型。 - 为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,一层叠一层,从而产生更有表达能力的模型。
4.1.1.4.通用近似定理
- 多层感知机可以通过隐藏神经元捕捉到我们输入之间复杂的相互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意计算。
例如,在一对输入上进行基本逻辑操作。多层感知机是通用近似器。 - 即使是网络只有一个隐藏层,给定足够的神经元(可能非常多)和正确的权重,我们可以对任意函数建模,尽管实际中学习该函数是很困难的。
- 虽然一个单隐层网络能学习任何函数,但并不意味着应该尝试使用单隐藏层网络来解决所有问题。
事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。
4.1.2.激活函数
- 激活函数是将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。
4.1.2.1.ReLU函数
-
线性整流单元(Rectified linear unit,ReLU)
- ReLU提供了一种非常简单的非线性变换。 给定元素 x ,ReLU函数被定义为该元素与 0 的最大值
$$
ReLU(x)=max(x,0)$$
- ReLU函数通过将相应的激活值设为0来仅保留正元素并丢弃所有负元素
4.1.2.2.sigmoid函数
- 对于一个定义域在 R 中的输入,sigmoid函数将输入变换为区间(0, 1)上的输出。
因此,sigmoid通常称为挤压函数(squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:
$$
sigmoid(x)=\frac{1}{1+exp(-x)}
$$
4.1.2.3.tanh函数
- 与sigmoid函数类似,tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。tanh函数的公式如下:
$$
tanch(x)= \frac{1-exp(-2x)}{1+exp(-2x)}
$$
4.4.模型选择、欠拟合和过拟合
- 作为机器学习科学家,我们的目标是发现模式(pattern)。但是,我们如何才能确定模型是真正发现了一种泛化的模式,而不是简单地记住了数据呢?
- 更正式地来说,我们的目标是发现模式,这些模式捕捉到了我们训练集所来自的潜在总体的规律。
- 如果成功做到了这点,即使是对我们以前从未遇到过的个体,我们也可以成功地评估风险。如何发现可以泛化的模式是机器学习的根本问题。
- 将模型在训练数据上拟合得比在潜在分布中更接近的现象称为***过拟合(overfitting)***,用于对抗过拟合的技术称为***正则化(regularization)***
4.4.1.训练误差和泛化误差
- ***训练误差training error***是指,我们的模型在训练数据集上计算得到的误差。
- 泛化误差(generalization error) 是指,当我们将模型应用在同样从原始样本的分布中抽取的无限多的数据样本时,我们模型误差的期望。
4.4.1.1.统计学习理论
- 在我们目前已探讨、并将在之后继续探讨的标准的监督学习中,我们假设训练数据和测试数据都是从相同的分布中独立提取的。
这通常被称为***独立同分布假设(i.i.d. assumption)***
4.4.1.2. 模型复杂性
- 很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。
就目前而言,一条简单的经验法则相当有用:统计学家认为,能够轻松解释任意事实的模型是复杂的,而表达能力有限但仍能很好地解释数据的模型可能更有现实用途. - 在本节中,为了给你一些直观的印象,我们将重点介绍几个倾向于影响模型泛化的因素:
- 可调整参数的数量。当可调整参数的数量(有时称为*度)很大时,模型往往更容易过拟合。
- 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
- 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
4.4.2. 模型选择
- 在机器学习中,我们通常在评估几个候选模型后选择最终的模型。这个过程叫做***模型选择***。
4.4.2.1. 验证集
4.4.2.2. K 折交叉验证
4.4.3. 欠拟合还是过拟合?
- 训练误差和验证误差都很严重,但它们之间仅有一点差距。如果模型不能降低训练误差,这可能意味着我们的模型过于简单(即表达能力不足),
无法捕获我们试图学习的模式。此外,由于我们的训练和验证误差之间的泛化误差很小,我们有理由相信可以用一个更复杂的模型降低训练误差。
这种现象被称为***欠拟合(underfitting)***
4.4.3.1. 模型复杂性
- 高阶多项式函数比低阶多项式函数复杂得多。高阶多项式的参数较多,模型函数的选择范围较广。因此在固定训练数据集的情况下,
高阶多项式函数相对于低阶多项式的训练误差应该始终更低(最坏也是相等)。
事实上,当数据样本包含了 x 的不同值时,函数阶数等于数据样本数量的多项式函数可以完美拟合训练集。
4.4.3.2. 数据集大小
另一个需要牢记的重要因素是数据集的大小。训练数据集中的样本越少,我们就越有可能(且更严重地)遇到过拟合。 随着训练数据量的增加,泛化误差通常会减小。
此外,一般来说,更多的数据不会有什么坏处。 对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。
给出更多的数据,我们可能会尝试拟合一个更复杂的模型。能够拟合更复杂的模型可能是有益的。如果没有足够的数据,简单的模型可能更有用。
对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。从一定程度上来说,深度学习目前的成功要归功于互联网公司、廉价存储、互联设备以及数字化经济带来的海量数据集。
4.4.4. 多项式回归
见4.4.多项式回归.py
4.5. 权重衰减
4.5.1. 范数与权重衰减
- L2 范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。
在实践中,这可能使它们对单个变量中的观测误差更为鲁棒。 - 相比之下, L1 惩罚会导致模型将其他权重清除为零而将权重集中在一小部分特征上。这称为特征选择(feature selection),这可能是其他场景下需要的。
- 根据之前章节所讲的,我们根据估计值与观测值之间的差异来更新 w 。然而,我们同时也在试图将 w 的大小缩小到零。这就是为什么这种方法有时被称为***权重衰减***
4.5.2. 高维线性回归
4.6. Dropout
- 在 4.5节 中,我们介绍了通过惩罚权重的L2L2范数来正则化统计模型的经典方法。在概率角度看,我们可以通过以下论证来证明这一技术的合理性:我们已经假设了一个先验,即权重的值取自均值为0的高斯分布。更直观的是,我们可能会说,我们鼓励模型将其权重分散到许多特征中,而不是过于依赖少数潜在的虚假关联。
4.6.1. 重新审视过拟合
- 在传统说法中,泛化性和灵活性之间的这种基本权衡被描述为 偏差-方差权衡 (bias-variance tradeoff)。线性模型有很高的偏差:它们只能表示一小类函数。然而,这些模型的方差很低:它们在不同的随机数据样本上给出了相似的结果。
4.6.2. 扰动的鲁棒性
-
dropout在正向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的标准技术。这种方法之所以被称为 dropout ,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元。 在整个训练过程的每一次迭代中,dropout包括在计算下一层之前将当前层中的一些节点置零。
-
在标准dropout正则化中,通过按保留(未丢弃)的节点的分数进行归一化来消除每一层的偏差。换言之,每个中间激活值hh以丢弃概率p由随机变量h′替换,如下所示:
-
$$
h^"=
\begin{cases}
0,概率为P\
\frac{h}{1-p},其他情况
\end{cases}$$
根据设计,期望值保持不变,即$ E [ h " ] = h E [ h " ] = h E[h"]=hE[h^"]=h E[h"]=hE[h"]=h
4.6.3. 实践中的dropout
- 通常,我们在测试时禁用dropout。给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。然而,也有一些例外:一些研究人员使用测试时的dropout作为估计神经网络预测的“不确定性”的启发式方法:如果预测在许多不同的dropout掩码上都是一致的,那么我们可以说网络更有自信心。
4.7. 正向传播、反向传播和计算图
4.7.1. 正向传播
- 正向传播 (forward propagation或forward pass)指的是:按顺序(从输入层到输出层)计算和存储 神经网络中每层的结果。
4.7.3. 反向传播
- 反向传播指的是计算神经网络参数梯度的方法。简言之,该方法根据微积分中的 链式规则 ,按相反的顺序从输出层到输入层遍历网络。该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。假设我们有函数Y=f(X)和Z=g(Y),其中输入和输出X,Y,Z是任意形状的张量。利用链式法则,我们可以计算Z关于X的导数
4.7.4. 训练神经网络
- 在训练神经网络时,在初始化模型参数后,我们交替使用正向传播和反向传播,利用反向传播给出的梯度来更新模型参数。注意,反向传播复用正向传播中存储的中间值,以避免重复计算。带来的影响之一是我们需要保留中间值,直到反向传播完成。这也是为什么训练比单纯的预测需要更多的内存(显存)的原因之一。此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。因此,使用更大的批量来训练更深层次的网络更容易导致 内存(显存)不足 (out of memory)错误。
4.8. 数值稳定性和模型初始化
- 初始化方案的选择在神经网络学习中起着非常重要的作用,它对保持数值稳定性至关重要。此外,这些选择可以与非线性激活函数的选择以有趣的方式结合在一起。我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快。糟糕选择可能会导致我们在训练时遇到***梯度爆炸***或***梯度消失***。
4.8.1. 梯度消失和梯度爆炸
- 不稳定梯度带来的风险不止在于数值表示。不稳定梯度也威胁到我们优化算法的稳定性。我们可能面临一些问题。
- 要么是 梯度爆炸 (gradient exploding)问题:参数更新过大,破坏了模型的稳定收敛;
- 要么是 梯度消失 (gradient vanishing)问题:参数更新过小,在每次更新时几乎不会移动,导致无法学习。
4.8.1.1. 梯度消失
- 导致梯度消失问题的一个常见的原因是跟在每层的线性运算之后的激活函数σ
- 正如你所看到的,当它的输入很大或是很小时,sigmoid函数的梯度都会消失。此外,当反向传播通过许多层时,除非我们在刚刚好的地方,这些地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。当我们的网络有很多层时,除非我们很小心,否则在某一层可能会切断梯度。事实上,这个问题曾经困扰着深度网络的训练。因此,更稳定(但在神经科学的角度看起来不太合理)的ReLU系列函数已经成为从业者的默认选择。
4.8.1.2. 梯度爆炸
- 相反的问题,当梯度爆炸时,可能同样令人烦恼。
- 当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛。
4.8.1.3. 打破对称性
- 神经网络设计中的另一个问题是其参数化所固有的对称性。假设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元。在这种情况下,我们可以对第一层的权重W(1)进行重排列,并且同样对输出层的权重进行重排列,可以获得相同的函数。第一个隐藏单元与第二个隐藏单元没有什么特别的区别。换句话说,我们在每一层的隐藏单元之间具有排列对称性。
- 这不仅仅是理论上的麻烦。考虑前述具有两个隐藏单元的单隐藏层多层感知机。为便于说明,假设输出层将两个隐藏单元转换为仅一个输出单元。想象一下,如果我们将隐藏层的所有参数初始化为W(1)=,c为常量,会发生什么情况。在这种情况下,在正向传播期间,两个隐藏单元采用相同的输入和参数,产生相同的激活,该激活被送到输出单元。在反向传播期间,根据参数W(1)对输出单元进行微分,得到一个梯度,其元素都取相同的值。因此,在基于梯度的迭代(例如,小批量随机梯度下降)之后,W(1)的所有元素仍然采用相同的值。这样的迭代永远不会打破对称性,我们可能永远也无法实现网络的表达能力。隐藏层的行为就好像只有一个单元。请注意,虽然小批量随机梯度下降不会打破这种对称性,但dropout正则化可以。
4.8.2. 参数初始化
- 解决(或至少减轻)上述问题的一种方法是仔细地进行初始化。优化期间的注意和适当的正则化可以进一步提高稳定性
4.8.2.1. 默认初始化
- 如果我们不指定初始化方法,框架将使用默认的随机初始化方法,对于中等规模的问题,这种方法通常很有效
4.8.2.2. Xavier初始化
-
通常,Xavier初始化从均值为零,方差 σ 2 = 2 n ( i n ) + n ( o u t ) \sigma^2=\frac{2}{n_(in)+n_(out)} σ2=n(in)+n(out)2的高斯分布中采样权重。
-
我们也可以利用Xavier的直觉来选择从均匀分布中抽取权重时的方差,注意均匀分布 U ( − a , a ) U(-a,a) U(−a,a)的方差为 a 2 3 \frac{a^2}{3} 3a2,将 a 2 3 \frac{a^2}{3} 3a2代入到 σ 2 \sigma^2 σ2条件中,将得到初始化的建议:
-
$$
U(-\sqrt{\frac{6}{n_(in)+n_(out)}},\sqrt{\frac{6}{n_(in)+n_(out)}}$$
4.8.3. 小结
- 梯度消失和爆炸是深度网络中常见的问题。在参数初始化时需要非常小心,以确保梯度和参数可以得到很好的控制。
- 需要用启发式的初始化方法来确保初始梯度既不太大也不太小。
- ReLU激活函数缓解了梯度消失问题,这样可以加速收敛。
- 随机初始化是保证在进行优化前打破对称性的关键。
- Xavier初始化表明,对于每一层,输出的方差不受输入数量的影响,任何梯度的方差不受输出数量的影响。
5. 深度学习计算
5.1. 层和块
- 为了实现这些复杂的网络,我们引入了神经网络块的概念。块可以描述单个层、由多个层组成的组件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的。
5.1.1. 自定义块
-
在实现我们自定义块之前,我们简要总结一下每个块必须提供的
- 将输入数据作为其正向传播函数的参数。
- 通过正向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收任意维的输入,但是返回一个维度256的输出。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
- 存储和访问正向传播计算所需的参数。
- 根据需要初始化模型参数。
-
注意,除非我们实现一个新的运算符,否则我们不必担心反向传播函数或参数初始化,系统将自动生成这些。让我们试一下。
-
块抽象的一个主要优点是它的多功能性。我们可以子类化块以创建层(如全连接层的类)、整个模型(如上面的
<span class="pre">MLP</span>
类)或具有中等复杂度的各种组件。
5.1.2. 顺序块
现在我们可以更仔细地看看Sequential
类是如何工作的。回想一下Sequential
的设计是为了把其他模块串起来。为了构建我们自己的简化的MySequential
,我们只需要定义两个关键函数:
- 一种将块逐个追加到列表中的函数。
- 一种正向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for block in args:
# 这里,`block`是`Module`子类的一个实例。我们把它保存在'Module'类的成员变量
# `_modules` 中。`block`的类型是OrderedDict。
self._modules[block] = block
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
5.1.3. 在正向传播函数中执行代码
-
Sequential
类使模型构造变得简单,允许我们组合新的结构,而不必定义自己的类。然而,并不是所有的架构都是简单的顺序结构。当需要更大的灵活性时,我们需要定义自己的块。 - 我们可以混合搭配各种组合块的方法。
5.1.5. 小结
- 层也是块。
- 一个块可以由许多层组成。
- 一个块可以由许多块组成。
- 块可以包含代码。
- 块负责大量的内部处理,包括参数初始化和反向传播。
- 层和块的顺序连接由
Sequential
块处理。
5.2. 参数管理
5.2.1. 参数访问
我们从已有模型中访问参数。当通过Sequential
类定义模型时,我们可以通过索引来访问模型的任意层。这就像模型是一个列表一样。每层的参数都在其属性中。如下所示,我们可以检查第二个全连接层的参数。
print(net[2].state_dict())
5.2.1.1. 目标参数
-
注意,每个参数都表示为参数(parameter)类的一个实例。要对参数执行任何操作,首先我们需要访问底层的数值。有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。下面的代码从第二个神经网络层提取偏置,提取后返回的是一个参数类实例,并进一步访问该参数的值。
print(type(net[2].bias)) print(net[2].bias) print(net[2].bias.data) <class 'torch.nn.parameter.Parameter'> Parameter containing: tensor([0.1629], requires_grad=True) tensor([0.1629])
-
参数是复合的对象,包含值、梯度和额外信息。这就是我们需要显式请求值的原因
-
除了值之外,我们还可以访问每个参数的梯度。由于我们还没有调用这个网络的反向传播,所以参数的梯度处于初始状态。
net[2].weight.grad == None True
5.2.1.2. 一次性访问所有参数
-
下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。
print(*[(name, param.shape) for name, param in net[0].named_parameters()]) print(*[(name, param.shape) for name, param in net.named_parameters()]) ('weight', torch.Size([8, 4])) ('bias', torch.Size([8])) ('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
-
这为我们提供了另一种访问网络参数的方式,如下所示。
net.state_dict()['2.bias'].data tensor([0.1629])
5.2.1.3. 从嵌套块收集参数
-
可以定义一个生成块的函数(可以说是块工厂),然后将这些块组合到更大的块中。
5.2.2. 参数初始化
- 深度学习框架提供默认随机初始化。然而,我们经常希望根据其他规则初始化权重。深度学习框架提供了最常用的规则,也允许创建自定义初始化方法。
- 默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵,这个范围是根据输入和输出维度计算出的。
- PyTorch的
nn.init
模块提供了多种预置初始化方法。
5.2.2.1. 内置初始化
-
让我们首先调用内置的初始化器。下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。
def init_normal(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, mean=0, std=0.01) nn.init.zeros_(m.bias) net.apply(init_normal) net[0].weight.data[0], net[0].bias.data[0] (tensor([-0.0004, 0.0054, 0.0049, 0.0013]), tensor(0.))
-
我们还可以将所有参数初始化为给定的常数(比如1)
def init_constant(m): if type(m) == nn.Linear: nn.init.constant_(m.weight, 1) nn.init.zeros_(m.bias) net.apply(init_constant) net[0].weight.data[0], net[0].bias.data[0] (tensor([1., 1., 1., 1.]), tensor(0.))
-
我们还可以对某些块应用不同的初始化方法。例如,下面我们使用Xavier初始化方法初始化第一层,然后第二层初始化为常量值42
def xavier(m): if type(m) == nn.Linear: nn.init.xavier_uniform_(m.weight) def init_42(m): if type(m) == nn.Linear: nn.init.constant_(m.weight, 42) net[0].apply(xavier) net[2].apply(init_42) print(net[0].weight.data[0]) print(net[2].weight.data) tensor([-0.1367, -0.2249, 0.4909, -0.6461]) tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
5.2.2.2. 自定义初始化
-
我们实现了一个
my_init
函数来应用到net
。def my_init(m): if type(m) == nn.Linear: print("Init", *[(name, param.shape) for name, param in m.named_parameters()][0]) nn.init.uniform_(m.weight, -10, 10) m.weight.data *= m.weight.data.abs() >= 5 net.apply(my_init) net[0].weight[:2]
5.2.3. 参数绑定
-
有时我们希望在多个层间共享参数。
-
这个例子表明第二层和第三层的参数是绑定的。它们不仅值相等,而且由相同的张量表示。因此,如果我们改变其中一个参数,另一个参数也会改变。
-
你可能会想,当参数绑定时,梯度会发生什么情况?答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层和第三个隐藏层的梯度会加在一起。
# 我们需要给共享层一个名称,以便可以引用它的参数。 shared = nn.Linear(8, 8) net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1)) net(X) # 检查参数是否相同 print(net[2].weight.data[0] == net[4].weight.data[0]) net[2].weight.data[0, 0] = 100 # 确保它们实际上是同一个对象,而不只是有相同的值。 print(net[2].weight.data[0] == net[4].weight.data[0]) tensor([True, True, True, True, True, True, True, True]) tensor([True, True, True, True, True, True, True, True])
5.5. 读写文件
5.5.1. 加载和保存张量
-
对于单个张量,我们可以直接调用
load
和save
函数分别读写它们。这两个函数都要求我们提供一个名称,save
要求将要保存的变量作为输入。import torch from torch import nn from torch.nn import functional as F x = torch.arange(4) torch.save(x, 'x-file')
-
我们现在可以将存储在文件中的数据读回内存.
x2 = torch.load('x-file') x2
-
我们可以存储一个张量列表,然后把它们读回内存。
y = torch.zeros(4) torch.save([x, y],'x-files') x2, y2 = torch.load('x-files') (x2, y2)
-
我们甚至可以写入或读取从字符串映射到张量的字典。当我们要读取或写入模型中的所有权重时,这很方便。
mydict = {'x': x, 'y': y} torch.save(mydict, 'mydict') mydict2 = torch.load('mydict') mydict2
5.5.2. 加载和保存模型参数
-
深度学习框架提供了内置函数来保存和加载整个网络。
-
需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。
- 例如,如果我们有一个3层多层感知机,我们需要单独指定结构。因为模型本身可以包含任意代码,所以模型本身难以序列化。
- 因此,为了恢复模型,我们需要用代码生成结构,然后从磁盘加载参数。
-
让我们从熟悉的多层感知机开始尝试一下。
class MLP(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(20, 256) self.output = nn.Linear(256, 10) def forward(self, x): return self.output(F.relu(self.hidden(x))) net = MLP() X = torch.randn(size=(2, 20)) Y = net(X)
-
接下来,我们将模型的参数存储为一个叫做“mlp.params”的文件。
torch.save(net.state_dict(), 'mlp.params')
-
为了恢复模型,我们实例化了原始多层感知机模型的一个备份。我们没有随机初始化模型参数,而是直接读取文件中存储的参数。
clone = MLP() clone.load_state_dict(torch.load('mlp.params')) clone.eval() MLP( (hidden): Linear(in_features=20, out_features=256, bias=True) (output): Linear(in_features=256, out_features=10, bias=True) )
-
由于两个实例具有相同的模型参数,在输入相同的
<span class="pre">X</span>
时,两个实例的计算结果应该相同。让我们来验证一下。Y_clone = clone(X) Y_clone == Y tensor([[True, True, True, True, True, True, True, True, True, True], [True, True, True, True, True, True, True, True, True, True]])
5.6. GPU
5.6.1. 计算设备
-
我们可以指定用于存储和计算的设备,如CPU和GPU。默认情况下,张量是在内存中创建的,然后使用CPU计算它。
-
在PyTorch中,CPU和GPU可以用
torch.device('cpu')
和torch.cuda.device('cuda')
表示。应该注意的是,cpu
设备意味着所有物理CPU和内存。这意味着PyTorch的计算将尝试使用所有CPU核心。然而,gpu
设备只代表一个卡和相应的显存。如果有多个GPU,我们使用torch.cuda.device(f'cuda:{i}')
来表示第ii块GPU(ii从0开始)。另外,cuda:0
和cuda
是等价的。import torch from torch import nn torch.device('cpu'), torch.cuda.device('cuda'), torch.cuda.device('cuda:1') (device(type='cpu'), <torch.cuda.device at 0x7f0bb457cf40>, <torch.cuda.device at 0x7f0bb459d550>)
-
我们可以查询可用gpu的数量
torch.cuda.device_count() 2
5.6.2. 张量与gpu
-
默认情况下,张量是在CPU上创建的。我们可以查询张量所在的设备
x = torch.tensor([1, 2, 3]) x.device device(type='cpu')
-
需要注意的是,无论何时我们要对多个项进行操作,它们都必须在同一个设备上。例如,如果我们对两个张量求和,我们需要确保两个张量都位于同一个设备上,否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。
5.6.2.1. 存储在GPU上
-
有几种方法可以在GPU上存储张量。例如,我们可以在创建张量时指定存储设备。
-
接下来,我们在第一个
gpu
上创建张量变量X<
。 -
在GPU上创建的张量只消耗这个GPU的显存。我们可以使用
nvidia-smi
命令查看显存使用情况。一般来说,我们需要确保不创建超过GPU显存限制的数据。X = torch.ones(2, 3, device=try_gpu()) X tensor([[1., 1., 1.], [1., 1., 1.]], device='cuda:0')
-
假设你至少有两个GPU,下面的代码将在第二个GPU上创建一个随机张量
Y = torch.rand(2, 3, device=try_gpu(1)) Y
5.6.2.2. 复制
-
如果我们要计算
X+Y
,我们需要决定在哪里执行这个操作。 不要简单地x
加上Y
, 因为这会导致异常。运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。 -
由于
Y
位于第二个GPU上,所以我们需要将X
移到那里,然后才能执行相加运算。Z = X.cuda(1) print(X) print(Z) tensor([[1., 1., 1.], [1., 1., 1.]], device='cuda:0') tensor([[1., 1., 1.], [1., 1., 1.]], device='cuda:1')
-
现在数据在同一个GPU上(Z和Y`都在),我们可以将它们相加。
-
假设变量
Z
已经存在于第二个GPU上。如果我们还是调用Z.cuda(1)
怎么办?它将返回Z
,而不会复制并分配新内存。 -
Z.cuda(1) is Z True
5.6.2.3. 旁注
- 人们使用GPU来进行机器学习,因为他们希望运行速度快。但是在设备之间传输变量是缓慢的。所以我们希望你百分之百确定你想做一些缓慢的事情。如果深度学习框架只是自动复制而没有崩溃,那么你可能不会意识到你已经编写了一些缓慢的代码。
- 此外,在设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。这也使得并行化变得更加困难,因为我们必须等待数据被发送(或者接收),然后才能继续进行更多的操作。这就是为什么拷贝操作要格外小心。根据经验,多个小操作比一个大操作糟糕得多。此外,除非你知道自己在做什么,否则,一次执行几个操作比代码中散布的许多单个操作要好得多。如果一个设备必须等待另一个设备才能执行其他操作,那么这样的操作可能会阻塞。这有点像排队订购咖啡,而不像通过电话预先订购时,当你在的时候发现咖啡已经准备好了。
- 最后,当我们打印张量或将张量转换为NumPy格式时,如果数据不在内存中,框架会首先将其复制到内存中,这会导致额外的传输开销。更糟糕的是,它现在受制于可怕的全局解释器锁,这使得一切都得等待Python完成。
5.6.3. 神经网络与GPU
-
类似地,神经网络模型可以指定设备。下面的代码将模型参数放在GPU上。
net = nn.Sequential(nn.Linear(3, 1)) net = net.to(device=try_gpu())
-
当输入为GPU上的张量时,模型将在同一GPU上计算结果。
-
模型参数存储在同一个GPU上
5.6.4. 小结
- 我们可以指定用于存储和计算的设备,例如CPU或GPU。默认情况下,数据在主内存中创建,然后使用CPU进行计算。
- 深度学习框架要求计算的所有输入数据都在同一设备上,无论是CPU还是GPU。
- 不经意地移动数据可能会显著降低性能。一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy
ndarray
中)时,将触发全局解释器锁,从而使所有GPU阻塞。最好是为GPU内部的日志分配内存,并且只移动较大的日志。
6. 卷积神经网络
- 卷积神经网络 (convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。
6.1.1. 不变性
- 卷积神经网络正是将 空间不变性 (spatial invariance)的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
- 平移不变性 (translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
- 局部性 (locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,在后续神经网络,整个图像级别上可以集成这些局部特征用于预测。
6.1.2. 限制多层感知机
6.1.2.1. 平移不变性
6.1.2.2. 局部性
6.1.3. 卷积
6.1.4.1. 通道
6.1.5. 小结
- 图像的平移不变性使我们以相同的方式处理局部图像,而不在乎它的位置。
- 局部性意味着计算相应的隐藏表示只需一小部*部图像像素。
- 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型。
- 卷积神经网络(CNN)是一类特殊的神经网络,它可以包含多个卷积层。
- 多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征。
6.2. 图像卷积
6.2.1. 互相关运算
- 严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是 互相关运算 (cross-correlation),而不是卷积运算。
6.2.2. 卷积层
- 卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
6.2.3. 图像中目标的边缘检测
6.2.4. 学习卷积核
- 现在让我们看看是否可以通过仅查看“输入-输出”对来学习由
X
生成Y
的卷积核。 我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y
与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。
6.2.5. 互相关和卷积
- 为了与深度学习文献中的标准术语保持一致,我们将继续把“互相关运算”称为卷积运算,尽管严格地说,它们略有不同。 此外,对于卷积核张量上的权重,我们称其为 元素 。
6.2.6. 特征映射和感受野
- 输出的卷积层有时被称为 特征映射 (Feature Map),因为它可以被视为一个输入映射到下一层的空间维度的转换器。
- 在CNN中,对于某一层的任意元素 x ,其 感受野 (Receptive Field)是指在前向传播期间可能影响 x计算的所有元素(来自所有先前层)。
6.2.7. 小结
- 二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。
- 我们可以设计一个卷积核来检测图像的边缘。
- 我们可以从数据中学习卷积核的参数。
- 学习卷积核时,无论用严格卷积运算或互相关运算,卷积层的输出不会受太大影响。
- 当需要检测输入特征中更广区域时,我们可以构建一个更深的卷积网络。
6.3. 填充和步幅
- 假设输入形状为 n h ∗ n w n_h * n_w nh∗nw,卷积核形状为 k h ∗ k w k_h * k_w kh∗kw,那么输出形状将是 ( n h − k h + 1 ) ∗ ( n w − k w + 1 ) (n_h -k_h +1)* (n_w -k_w+1) (nh−kh+1)∗(nw−kw+1)。 因此,卷积的输出形状取决于输入形状和卷积核的形状
6.3.1. 填充
- 在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。
- 解决这个问题的简单方法即为 填充 (padding):在输入图像的边界填充元素(通常填充元素是 0)。
- 通常,如果我们添加 p h p_h ph行填充(大约一半在顶部,一半在底部)和 p w p_w pw列填充(左侧大约一半,右侧一半),则输出形状将为 ( n h − k h + p h + 1 ) ∗ ( n w − k w + p w + 1 ) (n_h -k_h +p_h+1)* (n_w -k_w+p_w+1) (nh−kh+ph+1)∗(nw−kw+pw+1)
- 在许多情况下,我们需要设置 p h = k h − 1 p_h=k_h-1 ph=kh−1和 p w = k w − 1 p_w=k_w-1 pw=kw−1,使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。
- 卷积神经网络中卷积核的高度和宽度通常为奇数,例如 1、3、5 或 7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
6.3.2. 步幅
-
在计算互相关时,卷积窗口从输入张量的左上角开始,向下和向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
-
我们将每次滑动元素的数量称为 步幅 (stride)
-
通常,当垂直步幅为 s h s_h sh 、水平步幅为 s w s_w sw 时,输出形状为:
-
$$
[(n_h -k_h +p_h+s_h)/s_h]* [(n_w -k_w+p_w+s_w)/s_w]$$
6.3.3. 小结
- 填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
- 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 1/n1/n( nn 是一个大于 11 的整数)。
- 填充和步幅可用于有效地调整数据的维度。
6.4. 多输入多输出通道
- 当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有 3×h×w 的形状。我们将这个大小为 3的轴称为 通道 (channel) 维度。在本节中,我们将更深入地研究具有多输入和多输出通道的卷积核。
6.4.1. 多输入通道
- 当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数目的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为 cici,那么卷积核的输入通道数也需要为 cici
6.4.2. 多输出通道
- 在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。
- 直观地说,我们可以将每个通道看作是对不同特征的响应。
- 而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
6.4.3. 1×1卷积层
- 卷积的本质是有效提取相邻像素间的相关特征
- 因为使用了最小窗口,1×1 卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。 其实 1×1 卷积的唯一计算发生在通道上.
6.4.4. 小结
- 多输入多输出通道可以用来扩展卷积层的模型。
- 当以每像素为基础应用时,1×1 卷积层相当于全连接层。
- 1×1卷积层通常用于调整网络层的通道数量和控制模型复杂性。
6.5. 汇聚层
- 通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率,聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
- 而我们的机器学习任务通常会跟全局图像的问题有关, 所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
- 本节将介绍 池化 (pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
6.5.1. 最大汇聚层和平均汇聚层
- 与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为 池化窗口 )遍历的每个位置计算一个输出。
- 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。 相反,池运算符是确定性的,我们通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为 最大汇聚层 (maximum pooling)和 平均汇聚层 (average pooling)。
6.5.2. 填充和步幅
- 与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。
6.5.3. 多个通道
- 在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。
6.5.4. 小结
- 对于给定输入元素,最大汇聚层会输出该窗口内的最大值,平均汇聚层会输出该窗口内的平均值。
- 汇聚层的主要优点之一是减轻卷积层对位置的过度敏感。
- 我们可以指定汇聚层的填充和步幅。
- 使用最大汇聚层以及大于 1 的步幅,可减少空间维度(如高度和宽度)。
- 汇聚层的输出通道数与输入通道数相同。
6.6. 卷积神经网络(LeNet)
- 我们已经掌握了卷积层的处理方法,我们可以在图像中保留空间结构。
- 同时,用卷积层代替全连接层的另一个好处是:更简洁的模型所需的参数更少。
6.6.1. LeNet
- 总体来看,LeNet(LeNet-5)由两个部分组成:
- 卷积编码器:由两个卷积层组成;
- 全连接层密集块:由三个全连接层组成。
6.6.3. 小结
- 卷积神经网络(CNN)是一类使用卷积层的网络。
- 在卷积神经网络中,我们组合使用卷积层、非线性激活函数和汇聚层。
- 为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。
- 在传统的卷积神经网络中,卷积块编码得到的表征在输出之前需由一个或多个全连接层进行处理。
- LeNet是最早发布的卷积神经网络之一。
7. 现代卷积神经网络
- AlexNet。它是第一个在大规模视觉竞赛中击败传统计算机视觉模型的大型神经网络;
- 使用重复块的网络(VGG)。它利用许多重复的神经网络块;
- 网络中的网络(NiN)。它重复使用由卷积层和 1×1 卷积层(用来代替全连接层)来构建深层网络;
- 含并行连结的网络(GoogLeNet)。它使用并行连结的网络,通过不同窗口大小的卷积层和最大汇聚层来并行抽取信息;
- 残差网络(ResNet)。它通过残差块构建跨层的数据通道,是计算机视觉中最流行的体系结构;
- 稠密连接网络(DenseNet)。它的计算成本很高,但给我们带来了更好的效果。
7.1. 深度卷积神经网络(AlexNet)
- 因此,与训练端到端(从像素到分类结果)系统不同,经典机器学习的流水线看起来更像下面这样:
- 获取一个有趣的数据集。在早期,收集这些数据集需要昂贵的传感器(在当时最先进的图像也就100万像素)。
- 根据光学、几何学、其他知识以及偶然的发现,手工对特征数据集进行预处理。
- 通过标准的特征提取算法(如SIFT(尺度不变特征变换) 、SURF(加速鲁棒特征) 或其他手动调整的流水线来输入数据。
- 将提取的特征放到最喜欢的分类器中(例如线性模型或其它核方法),以训练分类器。
7.1.1. 学习表征
- 深度卷积神经网络的突破出现在2012年。突破可归因于两个关键因素。
7.1.1.1. 缺少的成分:数据
- 包含许多特征的深度模型需要大量的有标签数据,才能显著优于基于凸优化的传统方法(如线性方法和核方法)。
7.1.1.2. 缺少的成分:硬件
深度学习对计算资源要求很高,训练可能需要数百个迭代周期,每次迭代都需要通过代价高昂的许多线性代数层传递数据。这也是为什么在20世纪90年代至21世纪初,优化凸目标的简单算法是研究人员的首选。然而,用GPU训练神经网络改变了这一格局。图形处理器 (Graphics Processing Unit,GPU)早年用来加速图形处理,使电脑游戏玩家受益。GPU可优化高吞吐量的 4×4 矩阵和向量乘法,从而服务于基本的图形任务。幸运的是,这些数学运算与卷积层的计算惊人地相似。由此,英伟达(NVIDIA)和ATI已经开始为通用计算操作优化gpu,甚至把它们作为 通用GPU(general-purpose GPUs,GPGPU)来销售。
那么GPU比CPU强在哪里呢?
首先,我们深度理解一下*处理器(Central Processing Unit,CPU)的核心。 CPU的每个核心都拥有高时钟频率的运行能力,和高达数MB的三级缓存(L3 Cache)。 它们非常适合执行各种指令,具有分支预测器、深层流水线和其他使CPU能够运行各种程序的功能。 然而,这种明显的优势也是它的致命弱点:通用核心的制造成本非常高。 它们需要大量的芯片面积、复杂的支持结构(内存接口、内核之间的缓存逻辑、高速互连等等),而且它们在任何单个任务上的性能都相对较差。 现代笔记本电脑最多有4核,即使是高端服务器也很少超过64核,因为它们的性价比不高。
相比于CPU,GPU由 100∼1000 个小的处理单元组成(NVIDIA、ATI、ARM和其他芯片供应商之间的细节稍有不同),通常被分成更大的组(NVIDIA称之为warps)。 虽然每个GPU核心都相对较弱,有时甚至以低于1GHz的时钟频率运行,但庞大的核心数量使GPU比CPU快几个数量级。 例如,NVIDIA最近一代的Ampere GPU架构为每个芯片提供了高达312 TFlops的浮点性能,而CPU的浮点性能到目前为止还没有超过1 TFlops。 之所以有如此大的差距,原因其实很简单:首先,功耗往往会随时钟频率呈二次方增长。 对于一个CPU核心,假设它的运行速度比GPU快4倍,你可以使用16个GPU内核取代,那么GPU的综合性能就是CPU的 16×1/4=4 倍。 其次,GPU内核要简单得多,这使得它们更节能。 此外,深度学习中的许多操作需要相对较高的内存带宽,而GPU拥有10倍于CPU的带宽。
7.1.2. AlexNet
AlexNet和LeNet的设计理念非常相似,但也存在显著差异。 首先,AlexNet比相对较小的LeNet5要深得多。 AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。 其次,AlexNet使用ReLU而不是sigmoid作为其激活函数。 下面,让我们深入研究AlexNet的细节。
7.1.2.1. 模型设计
在AlexNet的第一层,卷积窗口的形状是 11×11 。 由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 第二层中的卷积窗口形状被缩减为 5×5 ,然后是 3×3 。 此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为 3×3 、步幅为2的最大汇聚层。 而且,AlexNet的卷积通道数目是LeNet的10倍。
在最后一个卷积层后有两个全连接层,分别有4096个输出。
7.1.2.2. 激活函数
此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。 一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。 另一方面,当使用不同的参数初始化方法时,ReLU激活函数使训练模型更加容易。 当sigmoid激活函数的输出非常接近于0或1时,这些区域的梯度几乎为0,因此反向传播无法继续更新一些模型参数。 相反,ReLU激活函数在正区间的梯度总是1。 因此,如果模型参数没有正确初始化,sigmoid函数可能在正区间内得到几乎为0的梯度,从而使模型无法得到有效的训练。
7.1.2.3. 容量控制和预处理
AlexNet通过dropout( 4.6节 )控制全连接层的模型复杂度,而LeNet只使用了权重衰减。 为了进一步扩充数据,AlexNet在训练时增加了大量的图像增强数据,如翻转、裁切和变色。 这使得模型更健壮,更大的样本量有效地减少了过拟合。
7.1.5. 小结
- AlexNet的结构与LeNet相似,但使用了更多的卷积层和更多的参数来拟合大规模的ImageNet数据集。
- 今天,AlexNet已经被更有效的结构所超越,但它是从浅层网络到深层网络的关键一步。
- 尽管AlexNet的代码只比LeNet多出几行,但学术界花了很多年才接受深度学习这一概念,并应用其出色的实验结果。这也是由于缺乏有效的计算工具。
- Dropout、ReLU和预处理是提升计算机视觉任务性能的其他关键步骤。