线性回归到深度网络

Posted by Masutangu on August 25, 2022

线性回归与单层神经网络

我们可以用神经网络来描述线性回归模型:

上图所示的神经网络中,输入为 $ x_1, \ldots, x_d $,因此输入层中的输入数(或称为特征维度 feature dimensionality)为 $d$。 网络的输出为 $o_1$,因此输出层中的输出数是 $1$。通常计算神经网络的层数时不考虑输入层,因此上图的神经网络层数为 $1$。线性回归模型可以视为一个单层神经网络:

\[\hat{y} = \mathbf{w}^\top \mathbf{x} + b\]

对于线性回归,输出和输入的全部特征维度都相关联,通常将这种变换称为全连接层(fully-connected layer)或者稠密层(dense layer)。

使用 pytorch 或 tensorflow 可以很方便的创建一个全连接层,代码如下:

PyTorch 版本

1
2
3
4
# nn是神经网络的缩写
from torch import nn
# 2 为输入层的维度,1 为输出层的维度
net = nn.Sequential(nn.Linear(2, 1))

TensorFlow 版本

1
2
3
net = tf.keras.Sequential()
# Keras 无需指定输入层的维度
net.add(tf.keras.layers.Dense(1))

Softmax 回归

前面提到,线性回归一般解决的是单个或多个连续值的预测问题。如果想解决分类问题的预测,首先需要明确如何表示分类(即标签 label)。统计学中通常采用独热编码(one-hot encoding)来表示分类。 独热编码是一个向量,维数和类别一样多。 类别对应的分量设置为 $ 1 $,其他所有分量设置为 $ 0 $。

如果想用全连接层来解决分类问题,则需要输出单一值变成输出多个值:

输出值的个数对应类别的个数。上图同样为单层神经网络,可以用 $\mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b}$ 来表示。

我们需要将输出转换为类别的概率向量,则需要满足:每个输出的取值区间位于 $[0, 1]$,所有输出加起来和为 $1$。通常会采用 softmax 函数来对输出 $\mathbf{o}$ 进行转换:

\[\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}\]

求幂保证了输出非负,除以总和保证了输出之和为 $1$。同时 softmax 不会改变输出的大小顺序,因此预测时可以采用下式:

\[\operatorname*{argmax}_j \hat y_j = \operatorname*{argmax}_j o_j.\]

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

Log Softmax

softmax 函数会有数值不稳定的风险,有可能造成数值上溢(overflow)或下溢(underflow)。

数值上溢

前面提到 softmax 公式 $\hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}$,当 $o_k$ 数值很大时,$\exp(o_k)$ 可能会大于数据类型的上限,引发上溢。在 python 中这可能会导致分子或分母变成 inf,最后计算出 0、inf 或 nan 的 $\hat y_j$。

其中一个解决方法是,计算 softmax 前,先从所有 $o_k$ 中减去 $\max(o_k)$:

\[\begin{aligned} \hat y_j & = \frac{\exp(o_j - \max(o_k))\exp(\max(o_k))}{\sum_k \exp(o_k - \max(o_k))\exp(\max(o_k))} \\ & = \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}. \end{aligned}\]

数值下溢

减去 $\max(o_k)$ 可以解决上溢问题,但有可能引起下溢。如果 $o_j - \max(o_k)$ 是一个比较大的负值,$\exp(o_j - \max(o_k))$ 将会接近于 $0$,四舍五入后可能会导致 $\hat y_j$ 为 $0$,$\log(\hat y_j)$ 为 -inf(交叉熵损失函数需要计算 $\log(\hat y_j)$,具体见 线性回归、损失函数以及梯度下降 ),导致反向传播出现 nan。

log softmax

上面提到,计算交叉熵损失函数时,我们实际计算的是 $ \log(\hat y_j) $:

\[\begin{aligned} \log{(\hat y_j)} & = \log\left( \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}\right) \\ & = \log{(\exp(o_j - \max(o_k)))}-\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)} \\ & = o_j - \max(o_k) -\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)}. \end{aligned}\]

通过将 softmax 和 log 组合在一起,我们无需计算 $\exp(o_j - \max(o_k))$ 而是直接计算 $o_j - \max(o_k)$ ,规避了数值溢出的问题,这个属于 LogSumExp Trick,可以参考最底下的文章。

多层感知机(MLP)

隐藏层

线性意味着单调,可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。

这种架构通常称为多层感知机(multilayer perceptron),通常缩写为 MLP。注意,MLP 中每一层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。

线性到非线性

用 $\mathbf{H} \in \mathbb{R}^{n \times h}$ 表示隐藏层的输出,$\mathbf{O} \in \mathbb{R}^{n \times q}$ 表示多层感知机的输出,可得:

\[\begin{aligned} \mathbf{H} & = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)} \\ \mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)} \end{aligned}\]

可得:

\[\begin{aligned}\mathbf{O} & = (\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})\mathbf{W}^{(2)} + \mathbf{b}^{(2)} \\ & = \mathbf{X} \mathbf{W}^{(1)}\mathbf{W}^{(2)} + \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)} \\ & = \mathbf{X} \mathbf{W} + \mathbf{b}\end{aligned}\]

其中:

\[\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}\] \[\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}\]

可见添加隐藏层,得到的依然是线性模型,并没有带来额外的好处。我们还需要关键要素: 在仿射变换之后对每个隐藏单元应用非线性激活函数(activation function)$\sigma$。从而使线性模型进化成非线性模型:

\[\begin{aligned} \mathbf{H} & = \sigma(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}) \\ \mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}\\ \end{aligned}\]

激活函数

Sigmoid

定义

\[\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}\]

导数

优点

  • 对每个神经元的输出进行了归一化,输出范围是 0 到 1
  • 函数可微

缺点

  • 从图像可以看出,输入太大或太小都会使其梯度接近 0,在反向传播中梯度接近 0,参数就会难以更新,容易造成梯度消失。另外从图中可以看出,sigmoid 的导数取值阈值为 $[0, 0.25]$,即使取最大值 0.25,经过多层神经网络反向传播,0.25 连乘之后也会变得特别小,出现梯度消失。具体分析见下文“BP 算法”。
  • sigmoid 输出不是 0 均值的,会影响模型收敛速度。分析如下:

    梯度下降是通过求偏导来更新参数的:

    \[w \gets w - \eta\cdot\frac{\partial L}{\partial w}\]

    对于某个神经元来说,输入输出公式为,$\sigma$ 为激活函数:

    \[\sigma(\vec x; \vec w, b) = \sigma(z) = \sigma\Bigl(\sum_iw_ix_i + b\Bigr)\]

    可得:

    \[\frac{\partial L}{\partial w_i} = \frac{\partial L}{\partial \sigma}\frac{\partial \sigma}{\partial z}\frac{\partial z}{\partial w_i} = x_i \cdot \frac{\partial L}{\partial \sigma}\frac{\partial \sigma}{\partial z}\]

    参数更新公式如下:

    \[w_i \gets w_i - \eta x_i\cdot \frac{\partial L}{\partial \sigma}\frac{\partial \sigma}{\partial z}\]

    $\frac{\partial L}{\partial \sigma}\frac{\partial \sigma}{\partial z}$ 对于 $w_i$ 来说是常数,因此 $w_i$ 更新方向是由输入值 $x_i$ 决定。如果 $x_i$ 和 $x_j$ 符号相同,则 $w_i$ 和 $w_j$ 更新方向就相同,反之则相反。

    在 sigmoid 函数中,输出值始终为正,这意味着如果上一层采用了 sigmoid 作为激活函数,那参数 $w_i, w_j, …$ 只能往相同的方向更新,导致模型收敛的时间变慢。

  • 指数运算较慢

ReLU

定义

\[\operatorname{ReLU}(x) = \max(x, 0)\]

导数

优点

  • 在输入大于 0 的时候导数为 1,反向传播中导数连乘也不会出现梯度消失。
  • 计算速度快

缺点

  • 输出不是 0 的均值,和 sigmoid 一样的问题
  • Dead ReLU 问题,部分神经元永远不会被激活。ReLU 在输入为负数时,梯度为 0,这就导致反向传播时各层的梯度都为 0,无法再更新参数。

    产生这种现象的两个原因:

    • 参数初始化问题
    • learning rate 太高导致在训练过程中参数更新太大

    解决方案:

    • 采用 Xavier 初始化方法
    • 避免将 learning rate 设置太大或使用 adagrad 等自动调节 learning rate 的算法
    • Leaky ReLU/PReLU/ELU

Tanh

定义

\[\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}\]

导数

优缺点和 sigmoid 类似。

反向传播 BackPropagation

BackPropagation (BP),即“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练神经网络的常见方法。上一篇文章我们介绍了梯度下降和误差函数,这里介绍下在 BP 算法中如何结合以优化神经网络参数。

符号定义

定义 $w^l_{jk}$ 表示第 $(l-1)^{\rm th}$ 层第 $k^{\rm th}$ 个神经元和第 $l^{\rm th}$ 层第 $j^{\rm th}$ 个神经元之间的 weight,注意这里 $k$ 是输入层,$j$ 是输出层:

定义 $b^l_j$ 作为第 $l^{\rm th}$ 层第 $j^{\rm th}$ 个神经元的 bias,定义 $a^l_j$ 作为第 $l^{\rm th}$ 层第 $j^{\rm th}$ 个神经元的 activation(激活函数的输出):

可得:

\[a^{l}_j = \sigma\left( \sum_k w^{l}_{jk} a^{l-1}_k + b^l_j \right)\]

定义第 $l^{\rm th}$ 层的 weight matrix 为 $w^l$,其中第 $j^{\rm th}$ 行第 $k^{\rm th}$ 列的元素 $w^l_{jk}$ 表示第 $(l-1)^{\rm th}$ 层第 $k^{\rm th}$ 个神经元和第 $l^{\rm th}$ 层第 $j^{\rm th}$ 个神经元之间的 weight。类似的,定义第 $l^{\rm th}$ 层的 bias vector 为 $b^l$,activation vector 为 $a^l$。可以将上式重写为:

\[a^{l} = \sigma(w^l a^{l-1}+b^l)\]

上一层的输出作为这一层的输入,乘以 weight matrix,加上 bias vector 得到 activations,然后再作为下一层的输入,以此推进直到最后一层。

为方便下面的推导,定义 $z^l \equiv w^l a^{l-1}+b^l$,$z^l$ 指第 $l^{\rm th}$ 层的 weighted input,也即第 $l^{\rm th}$ 层的激活函数的输入,可得 $a^l = \sigma(z^l)$。

误差函数

这里使用 MSE 作为误差函数:

\[C = \frac{1}{2n} \sum_x \|y(x)-a^L(x)\|^2\]

单个样本的误差为:

\[C = \frac{1}{2} \|y-a^L\|^2 = \frac{1}{2} \sum_j (y_j-a^L_j)^2\]

可以看出,误差函数是关于 activations $a^L$ 的函数。注意这里输入 $x$ 和对应的 $y$ 都是固定的,因此误差函数不是关于 $y$ 的函数。

四个公式

假设在第 $l^{\rm th}$ 层第 $j^{\rm th}$ 个神经元的 weighted input 加上一个极小的偏移值 $\Delta z^l_j$,则该神经元的输出为 $\sigma(z^l_j+\Delta z^l_j)$。这个偏差会一直传递到最后一层,带给误差 $\frac{\partial C}{\partial z^l_j} \Delta z^l_j$ 的偏移。

而我们的目标是减小误差,如果 $\frac{\partial C}{\partial z^l_j}$ 非常大(正数或者负数),则 $\Delta z^l_j$ 取和 $\frac{\partial C}{\partial z^l_j}$ 相反的符号即可以大大减小误差。如果 $\frac{\partial C}{\partial z^l_j}$ 非常小接近零,则 $\Delta z^l_j$ 取正负对误差都影响不大。因此可以认为 $\frac{\partial C}{\partial z^l_j}$ 是对神经元误差的评估。定义 $\delta^l_j$ 为第 $l^{\rm th}$ 层第 $j^{\rm th}$ 个神经元的误差,$\delta^l_j \equiv \frac{\partial C}{\partial z^l_j}$。定义 $\delta^l$ 为第 $l^{\rm th}$ 层的误差向量。BP 算法会计算每一层的 $\delta^l$,并关联到 $\partial C / \partial w^l_{jk}$ 和 $\partial C / \partial b^l_j$,即通过 BP 算法计算出神经网络参数 $w$ 和 $b$ 的梯度,得以更新 $w$ 和 $b$ 以减少误差。

公式 1

输出层的误差 error in the output layer $\delta^L_j$ 定义如下:

\[\delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma'(z^L_j)\qquad\qquad\qquad\qquad(BP1)\]

如果是 MSE 误差函数 $C = \frac{1}{2} \sum_j (y_j-a^L_j)^2$,可得 $\partial C / \partial a^L_j = (a_j^L-y_j)$。

定义 $\nabla_a C$ 为 $\partial C / \partial a^L_j$ 构成的向量,矩阵形式的公式如下:

\[\delta^L = \nabla_a C \odot \sigma'(z^L)\qquad\qquad\qquad(BP1a)\]

代入 $\nabla_a C = (a^L-y)$ 可得:

\[\delta^L = (a^L-y) \odot \sigma'(z^L)\]

$BP1$ 证明

根据链式法则:

\[δ^L_j=\frac{∂C}{∂z^L_j}=∑\limits_k\frac{∂C}{∂a^L_k}\frac{∂a^L_k}{∂z^L_j}\]

$a^L_k$ 只取决于 $z^L_k$,因此当 $k≠j$ 时 $∂a^L_k/∂z^L_j$ 为 $0$,因此可得 $δ^L_j=\frac{∂C}{∂a^L_j}\frac{∂a^L_j}{∂z^L_j}=\frac{∂C}{∂a^L_j}σ′(z^L_j)$ 。

公式 2

由 $\delta^{l+1}$ 推导 $\delta^l$ 的公式如下:

\[\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)\qquad\qquad(BP2)\]

这里体现了 backward 的过程,将误差从后一层往前一层回溯。通过 $BP1$ 计算最后一层输出层的误差,再通过 $BP2$ 计算前一层的误差,以此类推,就可以计算出每一层的误差。

$BP2$ 证明

根据链式法则:

\[δ^l_j=\frac{∂C}{∂z^l_j}=∑\limits_k\frac{∂C}{∂z^{l+1}_k}\frac{∂z^{l+1}_k}{∂z^l_j}=∑\limits_k\frac{∂z^{l+1}_k}{∂z^l_j}{δ^{l+1}_k}\] \[z^{l+1}_k=∑\limits_jw^{l+1}_{kj}a^l_j+b^{l+1}_k=∑\limits_jw^{l+1}_{kj}σ(z^l_j)+b^{l+1}_k\] \[{∂z^{l+1}_k}{∂z^l_j}=w^{l+1}_{kj}σ′(z^l_j)\] \[δ^l_j=∑\limits_kw^{l+1}_{kj}δ^{l+1}_kσ′(z^l_j)\]

公式 3

\[\frac{\partial C}{\partial b^l_j} = \delta^l_j\qquad\qquad\qquad\qquad\qquad(BP3)\]

$BP3$ 证明

\[\frac{∂C}{∂b^l_j}=∑\limits_k\frac{∂C}{∂z^l_k}\frac{∂z^l_k}{∂b^l_j}=∑\limits_kδ^l_k\frac{∂(∑\limits_jw^l_{kj}a^{l−1}_j+b^l_k)}{∂b^l_j}=∑\limits_kδ^l_k\frac{∂(b^l_k)}{∂b^l_j}=δ^l_j∗1=δ^l_j\]

\(\frac{∂(∑\limits_jw^l_{kj}a^{l−1}_j+b^l_k)}{∂b^l_j}$项中 $∑\limits_jw^l_{kj}a^{l−1}_j$ 与 $b^l_j$ 无关,因此为 $0$。当 $k≠j$ 时,$\frac{∂(b^l_k)}{∂b^l_j}$为 $0$。只有当 $k=j$ 时,$\frac{∂(b^l_k)}{∂b^l_j}$ 才为 $1\)。

公式 4

\[\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j\qquad\qquad\qquad\qquad(BP4)\]

上式可以重写为:

\[\frac{\partial C}{\partial w} = a_{\rm in} \delta_{\rm out}\]

$BP4$ 证明

\[\frac{∂C}{∂w^l_{jk}}=∑\limits_a\frac{∂C}{∂z^l_a}\frac{∂z^l_a}{∂w^l_{jk}}=∑\limits_aδ^l_a\frac{∂(∑\limits_bw^l_{ab}a^{l−1}_b+b^l_a)}{∂w^l_{jk}}\]

右边部分只有在 $a=j,b=k$ 时才不为0,此时就可以去掉求和符号得到:

\[\frac{∂C}{∂w^l_{jk}}=a^{l−1}_kδ^l_j\]

BP 算法

  1. Input $x$: 计算出输入层对应的 activation $a^1$
  2. Feedforward: 对第 $l=2,3,…,L$ 层,依次计算每一层的 $z^l=w^la^{l−1}+b^l$ 和 $a^l=σ(z^l)$
  3. Output error $δ^L$: 得到输出层的结果后,计算输出层的误差 $δ^L=∇_aC⊙σ′(z^L)$
  4. Backpropagate the error: 对第 $l=L−1,L−2,…,2$ 层,从后往前依次计算每一层的误差 $δ^l=((w^{l+1})^Tδ^{l+1})⊙σ′(z^l)$
  5. Output: 得到每一层的误差后,就可以计算出每一层参数 $w$ 和 $b$ 的梯度 $\frac{∂C}{∂w^l_jk}=a^{l−1}_kδ^l_j$ 和$\frac{∂C}{∂b^l_j}=δ^l_j$

梯度消失

当采用 sigmoid 做激活函数时,输出接近 $0$ 或 $1$ 时,曲线变得很平, $\sigma’(z^L_j) \approx 0$:

从 $BP1$ 公式可知,$\sigma’(z^L_j) \approx 0$ 会导致 $δ^L_j \approx 0$,从 $BP3$ 和 $BP4$ 可知 $δ^L_j \approx 0$ 会导致输出层的参数 $w$ 和 $b$ 的梯度接近于 $0$,参数更新缓慢。

从 $BP2$ 公式可知,不止最后一层,每一层如果 $\sigma’(z^l_j) \approx 0$,且 $w^{l+1}_{kj}$ 不够大时,都会造成梯度消失,导致参数更新缓慢。且从 $BP2$ 还可以看出,误差会从输出层逐层往回连乘的,sigmoid 导数的最大值为 0.25,在深度神经网络中连乘 N 次之后会变得非常小,这就是为什么 sigmoid 激活函数容易造成梯度消失。在深度神经网络中 relu 是更常用的激活函数。

参考文献