dive-into-deep-learning-notes
Published in:2024-01-22 |

1. 预备知识

1.1 数据处理

Tensor数据类型和numpy中的ndarray类型相似,但是差异点在于

首先,GPU很好地支持加速计算,而NumPy仅支持CPU计算;

其次,张量类支持自动微分。 这些功能使得张量类更适合深度学习

对于任意具有相同形状的张量, 常见的标准算术运算符(+-*/**)都可以被升级为按元素运算。 我们可以在同一形状的任意两个张量上调用按元素操作。

我们也可以把多个张量连结(concatenate)在一起, 把它们端对端地叠起来形成一个更大的张量。

1
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)

对张量中的所有元素进行求和,会产生一个单元素张量。

1
torch.sum(x)

在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作。 这种机制的工作方式如下:

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

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

1
2
A = X.numpy()
B = torch.tensor(A)

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

1
2
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)

1.2 线性代数

  • Hadamard

两个矩阵的按元素乘法称为Hadamard积(Hadamard product)(数学符号⊙)
$$
A⊙B = \begin{bmatrix} a_{11}b_{11} & a_{12}b_{12} &a_{13}b_{13} \ a_{21}b_{21} & a_{22}b_{22} &a_{23}b_{23} \ a_{31}b_{31}&a_{32}b_{32} &a_{3,3}b_{33} \end{bmatrix}
$$

将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。

  • 降维求和

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

sum,mean都是同理的

  • 非降维求和

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

1
A.cumsum(axis=0)
  • 点积

给定两个向量$x,y$的点积$x^Ty$(或$<x,y>$)是相同位置的按元素乘积的和
$$
x^Ty = \sum_{i=1}^{d}x_iy_i
$$

将两个向量规范化得到单位长度后,点积表示它们夹角的余弦。

1
torch.dot(x, y)
  • 矩阵-向量积

$$
Ax = \begin{bmatrix} a_1^T \ a_2^T \ a_3^T \end{bmatrix}x = \begin{bmatrix} a_1^Tx \ a_2^Tx \ a_3^Tx \end{bmatrix}
$$

在代码中使用张量表示矩阵-向量积,我们使用mv函数。

1
torch.mv(A, x)
  • 矩阵-矩阵乘法

用行向量$A_i^T$表示矩阵$A$的第$i$行,列向量$b_j$作为矩阵$B$的第$j$列

看作简单地执行m次矩阵-向量积,并将结果拼接在一起,使用mm函数
$$
C=AB= \begin{bmatrix} a_1^T \ a_2^T \ a_3^T \end{bmatrix}\begin{bmatrix} b_1 & b_2 & b_3\ \end{bmatrix} = \begin{bmatrix} a_1^Tb_1 & a_1^Tb_2 & a_1^Tb_3\ a_2^Tb_1 & a_2^Tb_2 & a_2^Tb_3 \ a_3^Tb_1 & a_3^Tb_2 & a_3^Tb_3 \end{bmatrix}
$$

1
torch.mm(A, B)
  • 范数

欧几里得距离是一个$L_2$范数: 假设$n$维向量$x$中的元素是$x_1,x_2…x_n$,其$L_2$范数是向量元素平方和的平方根:

$||x||2 = \sqrt{\sum{i=1}^{n}x_i^2}$

1
2
3
u = torch.tensor([3.0, -4.0])
torch.norm(u)
tensor(5.)

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

$||x||1 = \sum{i=1}^{n}|x_i|$

1
torch.abs(u).sum()

矩阵$X$的Frobenius范数(Frobenius norm)是矩阵元素平方和的平方根

1.3 微积分

我们可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度(gradient)向量。 具体而言,设函数f:Rn→R的输入是 一个n维向量x=[x1,x2,…,xn]⊤,并且输出是一个标量。 函数f(x)相对于x的梯度是一个包含n个偏导数的向量:.
$$
\nabla_\mathbf{x}f(\mathbf{x})=\left[\frac{\partial f(\mathbf{x})}{\partial x_1},\frac{\partial f(\mathbf{x})}{\partial x_2},\ldots,\frac{\partial f(\mathbf{x})}{\partial x_n}\right]^\top
$$

1.4 自动微分

  • 计算图
  • 反向传播
1
2
3
4
5
6
7
x = torch.arange(4.0, requires_grad=True) #需要保存梯度
y = 2 * torch.dot(x, x)
print(x.grad) # None
y.backward()
print(x.grad == 4 * x) #tensor([True, True, True, True])
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()

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

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

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() #对y的sum反向传播,把张量变成标量
x.grad # tensor([0., 2., 4., 6.])

有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z则是作为yx的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数, 并且只考虑到xy被计算后发挥的作用。

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

1
2
3
4
5
6
7
x.grad.zero_()
y = x * x
u = y.detach() #分离y
z = u * x

z.sum().backward()
x.grad == u # tensor([True, True, True, True])
  • 深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。

1.5 概率

[TODO]

2. 线性神经网络

2.1 线性回归

定义$\vec{x} = \begin{bmatrix} x_1 & x_2 & x_3 & … \end{bmatrix}$,参数$\vec{w} = \begin{bmatrix} w_1 & w_2 & w_3 & … \end{bmatrix}$

$\hat{y} = Xw+b$

损失函数$J(w,b) = \frac{1}{2m} \sum_{i=1}^{m} {(y^i-\hat{y})}^2$

随机梯度下降SGD(stochastic gradient descent)

2.2 softmax

  • 独热编码(one-hot encoding)

    独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。

../_images/softmaxreg.svg

要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。 此外,我们需要一个训练的目标函数,来激励模型精准地估计概率。 例如, 在分类器输出0.5的所有样本中,我们希望这些样本是刚好有一半实际上属于预测的类别。 这个属性叫做校准(calibration)。

softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持 可导的性质。 为了完成这一目标,我们首先对每个未规范化的预测求幂,这样可以确保输出非负。 为了确保最终输出的概率值总和为1,我们再让每个求幂后的结果除以它们的总和。如下式:

$\hat{y} = softmax(o)$

其中,
$$
\hat{y}j = \frac{exp(o_j)}{\sum{k}{exp(o_k)}}
$$

在预测过程中,我们仍然可以用下式来选择最有可能的类别。

$argmax\ \hat{y}_j = argmax\ o_j$

尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。

其中,对于任何标签$y$和模型预测$\hat{y}$,损失函数为:

$l(y,\hat{y})=-\sum_{j=q}^{q}y_jlog\hat{y}_j$

通常被称为交叉熵损失(cross-entropy loss)

3. 多层感知机

3.1 激活函数

我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前$L-1$层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP

多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出。

../_images/mlp.svg

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。 大多数激活函数都是非线性的。 由于激活函数是深度学习的基础,下面简要介绍一些常见的激活函数。

  • ReLU

$ReLU(x) = max(x,0)$

../_images/output_mlp_76f463_21_0.svg

  • sigmoid

$sigmoid(x)=\frac{1}{1+exp(-x)}$

../_images/output_mlp_76f463_51_0.svg

  • tanh

$tanh(x) = \frac{1-exp(-2x)}{1+exp(-2x)}$

../_images/output_mlp_76f463_81_0.svg

3.2 误差

训练误差(training error)是指, 模型在训练数据集上计算得到的误差。 泛化误差(generalization error)是指, 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。

问题是,我们永远不能准确地计算出泛化误差。 这是因为无限多的数据样本是一个虚构的对象。 在实际中,我们只能通过将模型应用于一个独立的测试集来估计泛化误差, 该测试集由随机选取的、未曾在训练集中出现的数据样本构成。

../_images/capacity-vs-error.svg

3.3 权重衰退

在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为$L_2$正则化。

一种简单的方法是通过线性函数$f(x)=w^Tx$ 中的权重向量的某个范数来度量其复杂性, 例如$||w||^2$。 要保证权重向量比较小, 最常用方法是将其范数作为惩罚项加到最小化损失的问题中。 将原来的训练目标最小化训练标签上的预测损失, 调整为最小化预测损失和惩罚项之和。 现在,如果我们的权重向量增长的太大, 我们的学习算法可能会更集中于最小化权重范数$||w||^2$。 这正是我们想要的。

我们通过正则化常数$\lambda$来描述这种权衡, 这是一个非负超参数,我们使用验证数据拟合:

$L(w,b)+\frac{\lambda}{2}||w||_2$

此外,为什么我们首先使用$L_2$范数,而不是$L_1$范数。 事实上,这个选择在整个统计领域中都是有效的和受欢迎的。 $L_2$正则化线性模型构成经典的岭回归(ridge regression)算法, $L_1$正则化线性回归是统计学中类似的基本模型, 通常被称为套索回归(lasso regression)。 使用$L_2$范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。 这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。 在实践中,这可能使它们对单个变量中的观测误差更为稳定。 相比之下,$L_1$惩罚会导致模型将权重集中在一小部分特征上, 而将其他权重清除为零。 这称为特征选择(feature selection),这可能是其他场景下需要的。

$L_2$正则化回归的小批量随机梯度下降更新如下式:

$w = (1-\alpha\lambda) w - \frac{\alpha}{n}\sum_{i=1}^{n}(w^Tx^{(i)}+b-y^{(i)})$

我们仅考虑惩罚项,优化算法在训练的每一步衰减权重。 与特征选择相比,权重衰减为我们提供了一种连续的机制来调整函数的复杂度。 较小的$\lambda$值对应较少约束的$w$, 而较大的$\lambda$值对$w$的约束更大。

pytorch中,我们在实例化优化器时直接通过weight_decay指定weight decay超参数。 默认情况下,PyTorch同时衰减权重和偏移。 这里我们只为权重设置了weight_decay,所以偏置参数$b$不会衰减。

1
2
3
4
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}], lr=lr)
# wd为lambda
  • 正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。
  • 保持模型简单的一个特别的选择是使用$L_2$惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。
  • 权重衰减功能在深度学习框架的优化器中提供。
  • 在同一训练代码实现中,不同的参数集可以有不同的更新行为。

3.4 丢弃法

当面对更多的特征而样本不足时,线性模型往往会过拟合。 相反,当给出更多样本而不是特征,通常线性模型不会过拟合。 不幸的是,线性模型泛化的可靠性是有代价的。 简单地说,线性模型没有考虑到特征之间的交互作用。 对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。

那么关键的挑战就是如何注入这种噪声。 一种想法是以一种无偏向(unbiased)的方式注入噪声。 这样在固定住其他层时,每一层的期望值等于没有噪音时的值。

在每次训练迭代中,他将从均值为零的分布$\epsilon~N(0,\delta^2)$采样噪声添加到输入$x$, 从而产生扰动点$x’=x+\epsilon$, 预期是$E(x’)=x$。

在标准暂退法正则化中,通过按保留(未丢弃)的节点的分数进行规范化来消除每一层的偏差。 换言之,每个中间活性值ℎ以暂退概率$p$由随机变量ℎ′替换,如下所示:

$h’= 0 \ when\ p=0$

$h’=\frac{h}{1-p} \ otherwise$

../_images/dropout2.svg

对于深度学习框架的高级API,我们只需在每个全连接层之后添加一个Dropout层, 将暂退概率作为唯一的参数传递给它的构造函数。 在训练时,Dropout层将根据指定的暂退概率随机丢弃上一层的输出(相当于下一层的输入)。 在测试时,Dropout层仅传递数据。

1
2
3
4
5
6
7
8
9
10
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))
  • 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
  • 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。
  • 暂退法将活性值ℎ替换为具有期望值ℎ的随机变量。
  • 暂退法仅在训练期间使用。

3.5 正向传播、反向传播、计算图

  • 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量,它的顺序是从输入层到输出层。
  • 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。
  • 在训练深度学习模型时,前向传播和反向传播是相互依赖的。
  • 训练比预测需要更多的内存。
Prev:
Optuna搜参优化LightGBM预测催化反应产率
Next:
Machine Learning Notes