机器学习基础(10)-神经网络
前馈神经网络
模型描述
神经元是神经网络的基本单元,本质上是一个非线性函数,有如下的定义: \[ y = f(x_1, x_2, ..., x_n) = a(\sum_{i=1}^n w_ix_i+b) \] 其中\(a(\cdot)\)是特定的非线性函数,称为激活函数。常见的激活函数有:
sigmoid函数: \[ \begin{aligned} a(z) &= \text{sigmoid}(z) = \frac{1}{1+e^{-z}} \\ a^{'}(z) &= a(z)(1-a(z)) \end{aligned} \] 双曲正切tanh函数(hyperbolic tangent function): \[ \begin{aligned} a(z) &= \text{tanh}(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} \\ a^{'}(z) &= 1 - a(z)^2 \end{aligned} \] 整流线性ReLU函数(rectified linear unit): \[ \begin{aligned} a(z) &= \text{relu}(z) = max(0, z) \\ a^{'}(z) &= \begin{cases} 1, & \text{if } z > 0 \\ 0, & \text{if else} \\ \end{cases} \end{aligned} \] 其中sigmoid和tanh有一定的转化关系,直观上双曲正切tanh函数将sigmoid函数放大两倍,并向下平移一个单位长度: \[ \text{tanh}(z) = 2 \cdot \text{sigmoid}(2z) - 1 \] 而前馈神经网络则由多层神经元组成,层间的神经元相互连接,前一层神经元的输出是后一层神经元的输入。整个前馈神经网络整体就表示从输入信号到输出信号的多次非线性转换。前馈神经网络模型可以看作是矩阵与向量乘积的非线性变换的多次重复,其基本结构是非常简单的。通常神经网络可以分为输入层,隐藏层和输出层,隐藏层和输出层的激活函数通常有不同的定义。在这里,我们定义以含有神经元的层作为神经网络的层,即不考虑输入层。
前馈神经网络与我们之前学过的logistic回归、感知机和支持向量机等模型有密切的联系。
对于多分类的单层神经网络,如果输出层激活函数是softmax函数的时候,模型等价于多项logistic回归模型,其中softmax函数定义如下。因此前馈神经网络可以看作是logistic回归模型的扩展。 \[ \text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}} \] 对于二分类的单层神经网络,如果输出层激活函数是tanh函数的时候,模型可以与感知机相对应。可以认为前馈神经网络是感知机的扩展,有说法也称前馈神经网络为多层感知机。而对于二分类的多层神经网络,当输出层激活函数是tanh函数的时候,模型也可以与非线性支持向量机对应。
实际上,前馈神经网络具有非常强大的表示能力,对于任意的连续函数,都存在一个二层的前馈神经网络能够对其进行近似。当然理论存在性并不代表现实可行性,通常要达到近似精度,二层前馈神经网络中所需要的神经元的个数可能是非常大的,甚至是指数级别的,具有同等表征能力的深度神经网络和浅层神经网络,往往浅层神经网络的复杂度会指数级别地提高,因此在实际操作过程中通常会考虑另一个方向,即将模型变深,而非将模型变宽。这也是为什么神经网络对应了深度学习。
优化算法
反向传播
神经网络的优化算法使用梯度下降的算法,其中最为关键的是梯度的计算,我们采用反向传播(back propagation)算法来计算梯度。反向传播算法也称为误差反向传播算法,它提供了一个高校的梯度计算以及参数更新的方式,只需要依照网络结构进行一次正向传播(forward)和一次反向传播(backward),就可以完成梯度下降的一次迭代。其中正向传播旨在基于当前的参数重新计算神经网络的所有变量,并将神经元的输出进行存储;反向传播旨在利用当前的变量重新计算损失函数对所有参数的梯度。
反向传播利用的核心思想为求导中的链式法则。下面举例演示的是如何用一笔data来进行反向传播。
回到梯度下降的过程中,我们定义一个损失函数\(L(\theta) = \underset{N}{\sum}C^n(\theta)\)。其中,函数\(C^n(\theta)\)表示衡量第n个输出与第n个label之间的距离,距离越大代表损失函数的值越大,代表效果越不好。这里真实值y有N个维度。这里函数接受参数\(\theta\)作为输入,参数\(\theta\)先通过一系列过程影响神经网络的输出,然后再影响距离衡量函数。
在梯度下降的过程中,我们需要计算$L() \(。考虑\)\(中的某一个参数\)w$,计算偏导则有: \[ \frac{\partial L(\theta)}{\partial w} = \sum \limits_N \frac{\partial C^n(\theta)}{\partial w} \] 为了更新这个参数,我们需要计算等式左边的值。为此我们只需要计算求和号后面的每一项,再进行求和即可。于是我们考虑其中一项\(\partial C(\theta) / \partial w\)。
考虑如下图的网络结构:
其中,a代表一个神经元的输出,z代表一个神经元的输入,w表示网络中需要更新(需要计算梯度)的参数,并且假设神经元的激活函数为\(a = \sigma (z)\)。这里的w在意义上涵盖了网络中所有的参数,分别为后一层即为输出层的参数和后一层还是隐藏层的参数。
根据链式法则,我们可以得到以下表达式: \[ \frac{\partial C}{\partial w} = \frac{\partial z}{\partial w} \frac{\partial C}{\partial z} \tag{backpropagation} \] 前项很容易求得,从网络中容易看出,\(\partial z /\partial w\)就等于前一个神经元对应的输出,在上图中的网络中,有$z_1 /w_1 = a_1 \(,\)z_1 /w_1 = a_1 \(,\)z_2 /w_2 = a_2 \(,\)z_1 /w_6 = a_2 \(,等等。这一项求取的过程,称为Forward。意思是只需要沿着网络的正向逐步前进,就可以计算出所有的\)z /w$。
后项看起来比较困难,因为由\(z\)到达\(C\),中间需要经过很多乱七八糟的过程。这一项求取的过程称为Backward。后面我们可以看到,只需要沿着网络的逆向逐步前进,就可以计算出所有的\(\partial C /\partial z\)。
现在考虑两种不同的参数\(w\)。
假设现在需要更新的参数\(w\)处在最后一个隐藏层中,如图中的\(w_1,w_2,w_5,w_6\)。下面考虑参数\(w_1\),那么有关系式\(y^{'}_1 = \sigma (z_1)\)。这样,我们需要求的(backpropagation)中的后项有: \[ \frac{\partial C}{\partial z_1} = \frac{\partial y^{'}_1}{\partial z_1} \frac{\partial C}{\partial y^{'}_1} \] 在这个式子中,等式右边的前项,即为\(\sigma ^{'}(z_1)\)。
而后项的求取,只需要知道函数\(C\)的表达式后也很容易得到,即为\(C^{'}(y^{'}_1)\)。而这个表达式也是事先给定的。
下面假设现在需要更新的参数\(w\)处于其他的隐藏层中,如图中的\(w_3,w_4,w_7,w_8\)。下面考虑参数\(w_3\),那么有需要求的(backpropagation)中的后项如下: \[ \begin{aligned} \frac{\partial C}{\partial z_3} &= \frac{\partial a_1}{\partial z_3} \frac{\partial C}{\partial a_1} \\ &= \sigma ^{'}(z_3) (\frac{\partial C}{\partial z_1} \frac{\partial z_1}{\partial a_1} + \frac{\partial C}{\partial z_2} \frac{\partial z_2}{\partial a_1}) \\ &= \sigma ^{'}(z_3) (w_1 \frac{\partial C}{\partial z_1} + w_5 \frac{\partial C}{\partial z_2}) \\ \end{aligned} \] 并且观察到,其中的\(\partial C / \partial z_1\)和\(\partial C / \partial z_2\),都是已经可以求出来的。这样,逐步向前推广,就可以求出所有的\(\partial C / \partial z\)。而在求取的过程中,如果采用网络逆向的方式,计算的复杂度也将降低为与Forward一样的复杂度。并且也可以将求取的过程看成是经过另一个网络的模式。
其中,a元素在该网络结构中没有起到作用,经过每一个三角形单元表示整个式子乘上对应的\(\sigma^{'}(z_i)\),而到达对应\(z_i\)的位置,得到的就是对应的\(\partial C / \partial z_i\)。
综上所述,整个反向传播算法的过程,只需要先进行一次Forward计算得出所有的\(\partial z / \partial w\),再进行一次Backward计算得出所有的\(\partial C / \partial z\),再对应相乘即可得到所有的\(\partial C / \partial w\)。并且在这个过程中,Forward和Backward的复杂度是相同的。
算法实现技巧
深度神经网络学习是一个复杂的非凸优化问题,实际操作过程中可能会产生一些优化上的困难,例如会出现梯度消失和梯度爆炸、内部协变量偏移等。
梯度消失和爆炸
在深度神经网络学习的过程中,首先通过正向传播计算各层的输出,然后通过反向传播计算各层的误差和梯度。在这个过程中,各层的梯度,尤其是靠前层的梯度,有时候会接近0(梯度消失),或者接近无穷(梯度爆炸)。梯度消失会导致参数更新停止,梯度爆炸则会导致参数溢出,两者都会使得学习无法有效地进行。
当然也有一些防止梯度消失和梯度爆炸的技巧:
- 进行恰当的随机参数初始化:一个经验的方法是对于每个神经元的权重\(\mathbf{w} = (w_1, w_2, ..., w_n)^T\),根据正态分布\(\mathscr{N}(0, 1/n)\)进行随机取值;
- 使用ReLU作为激活函数,可以一定程度上防止梯度消失;
- 使用特定的网络架构,避免反向传播时只依赖矩阵连乘。例如使用ResNet、LSTM等
内部协变量偏移
在机器学习包括深度学习中存在一个普遍现象,如果将输入向量\(\mathbf{x}\)的每一维的数值进行归一化,使其框定在一定范围内例如0到1之间,那么就可以加快基于梯度下降的学习收敛速度。这是因为梯度下降是以相同的学习率对每个维度进行最小化的,如果各个维度之间取值范围相差很大,那么学习就很难在各个维度上同时收敛。当然如果学习率取得很小的话,可以避免这个问题,但是这样反过来会降低学习效率。
对于深度神经网络来说,网络中的每一层,它的输入依赖于前面的层,前面层的参数在学习过程中会不断改变,因此该层的输入也会不断变化,这不利于该层以及后面层的学习。这种现象在神经网络的各层都会发生,称作内部协变量偏移。上面提到的归一化就可以解决这个问题。
批量归一化
批量归一化(batch normalization)指的是对每一层的输入,针对每一个Batch的数据进行归一化后再输入该层。
原理上在每一层,对于原始输入\(\mathbf{x}\)和净输入\(\mathbf{z}\)都可以进行归一化(两者的关系是\(\mathbf{z} = \mathbf{W}^T \cdot \mathbf{x} + \mathbf{b}\))。在实际操作过程中,发现对净输入\(\mathbf{z}\)进行归一化可以取得更好的效果。
批量归一化操作如下,假设批量数据在当前层的净输入是\(\{z_1, z_2, ..., z_n\}\),其中\(n\)为批量大小,首先计算当前层净输入的均值和方差: \[ \begin{aligned} \mu &= \frac{1}{n} \sum_{j=1}^n z_j \\ \sigma^2 &= \frac{1}{n-1} \sum_{j=1}^n (z_j - \mu)^2 \end{aligned} \] 这里\(\mu\)和\(\sigma^2\)分别表示均值向量和方差向量,之后对于每个样本的净输入向量进行归一化: \[ \bar{z_j} = \frac{z_j - \mu}{\sqrt{\sigma^2 + \varepsilon }},\quad j=1,2,...,n \] 其中,\(\varepsilon\)是一个每个元素都是很小正数的向量,主要用来保证分母不为0。归一化之后,再进行仿射变换: \[ \tilde{z_j} = \gamma \odot\bar{z_j} + \beta , \quad j=1,2,...,n \] 其中,\(\gamma\)和\(\beta\)都是参数向量,\(\odot\)是向量逐元素乘积。最后将经过归一化和仿射变换的结果\(\tilde{z_j}\)作为批量数据在这一层的净输入。一方面,每个批量在各层具有各自的均值\(\mu\)和方差\(\sigma^2\),当输入样本和网络参数确定的时候,\(\mu\)和\(\sigma^2\)就确定了。另一方面,每一层具有各自的参数\(\gamma\)和\(\beta\)。
经过批量归一化之后,使得神经网络的每一层净输入的均值是0,方差是1。这样使得每一层输出的各个维度都不会有太大数量级的差别,使得学习收敛速度提高。
层归一化
层归一化(layer normalization)是另一种防止内部协变量偏移的方法,其思想与批量归一化相同,不过是在每一层的神经元上进行归一化,而不是在每个批量的数据上进行归一化。
假设当前层的所有神经元,净输入为\(\mathbf{z} =(z_1, z_2, ...,z_m)^T\),其中\(z_j\)表示第\(j\)个神经元的净输入,\(m\)是该层神经元的个数。之后有类似的操作,首先计算该层净输入的均值和方差: \[ \begin{aligned} \mu &= \frac{1}{m} \sum_{j=1}^m z_j \\ \sigma^2 &= \frac{1}{m-1} \sum_{j=1}^m (z_j - \mu)^2 \end{aligned} \] 之后进行归一化: \[ \bar{z_j} = \frac{z_j - \mu}{\sqrt{\sigma^2 + \varepsilon }} , \quad j=1, 2, ..., m \] 之后再进行仿射变换: \[ \tilde{z_j} = \gamma \odot\bar{z_j} + \beta, \quad j = 1, 2, ..., m \]
正则化
正则化的目的是提高学习的泛化能力,深度学习中的正则化包括L1正则化、L2正则化、early stopping和dropout等。其中前三种方法是机器学习通用的方法,最后一种则是深度学习特有。
early stopping指的是在学习过程中,使用验证集来进行评估,判断训练的终止点,从而进行模型的选择,是一种隐式的正则化方法。
在early stopping中将数据分为训练集、验证集和测试集。在学习过程中,持续使用训练集训练模型,得到在训练集上的误差,同时使用验证集来进行评估,得到在验证集上的误差。通常随着训练轮数的增加,训练误差会逐渐减少并趋近于0,而验证误差会先降低后升高。early stopping选择那个验证误差最小的点作为训练的终止点,将此时的模型作为最终的模型输出。
early stopping的优点是简单有效,缺点是需要将一部分标注数据用作训练的评估而不是训练。
dropout指的是在训练过程中每一步随机选择一些神经元,让它们不参与训练。这是一种经验性的方法,在实际使用过程中确实很有效,但是目前没有严格的理论证明。
具体来说,对于神经网络的输入层和隐藏层,每一层都有一个保留概率\(p\),当然各层的保留概率不一定相同。在每次训练的时候,每层的神经元每个神经元以\(p\)的概率保留。