非线性假设与神经网络


非线性假设的引入

我们以计算机视觉(CV)中的一个典型问题为例。假设我们需要研究一种算法,使得其能够判断一张照片中的物体是不是一辆车,这实际上就是一个二分类问题。对于人而言,识别一辆汽车是很容易的,但是计算机并不能像人一样去思考。

具体而言,在计算机视觉中,这个问题通常被抽象为

  • 给定一张像素数为 N×MN\times M 的照片,每个像素被分配一个数值,称之为像素的强度。这个值可以直观表示为亮度、RGB色值等。这些数值代表一个样本的特征量
  • 现有训练集数据,以供机器训练
  • 基于此实现一套分类算法

我们考虑一个最简单的情况,假设图片尺寸为 50×5050\times 50,也就是一共有 25002500 个像素。虽然这在现实中意味着图片及极其模糊,但我们假定是可以识别出一辆车的。现在,每个样本的特征量有 n=2500n=2500 个。

如果要使用多项式回归、Logistic回归等算法,介于问题本身的非线性特点,我们不得不添加二次项、三次项等,以增加 n2,n3n^2,n^3 量级的参数项。这是很有可能导致过拟合的,并且极大地增加了算法的复杂度和运行效率。

为此,我们需要寻求一种新型算法,以更好引入这些非线性特征。

神经网络的背景

在上个世纪,科学家们尝试寻找一些能够模仿人类大脑思考方式的算法,即神经网络算法(Neural Network)。大脑的基本结构是神经元,他们错综复杂,通过传递电信号实现信息的传输,因此这种算法被称作神经网络。

大脑皮层具有不同的部分,以处理不同的感知内容。比如,听觉区负责处理人的听觉,视觉区负责人的视觉。现在的问题是,能否找到一种通用的算法,使得其可以对于不同类型感知器的输入进行信息处理。神经网络算法便基于此提出。

到了上世纪末,神经网络算法的发展已经颇为成熟,但是在当时没有引起较大的轰动。这主要归结于当时计算机算力和普及度的落后。到了21世纪初,相关算法理论再次引起机器学习领域的关注,并不断发展至今。

神经网络算法的表示


基础模型

我们先回顾一下生物知识。大脑中的神经元结构可以表示如下

在一个神经元细胞中,不同部位具有不同的作用。

  • 树突(Dendrite):接受其他神经元的信息
  • 细胞体:处理输入信息
  • 轴突(Axon):将信息传输给其他神经元

因此,一个神经元可以看做一个计算单元。其从一定数量的神经元接受电信号信息,进行处理后输出给其他神经元。我们可以把这个结构进行数学化表示

  1. 有若干个值 x1,x2,,xnx_1,x_2,\cdots,x_n 作为输入
  2. 对输入内容进行处理,如采用Logistic回归模型
  3. 处理后的 h(x)h(\boldsymbol{x}) 作为输出传输给下一个单元

其中,参数 θ\boldsymbol{\theta} 也被称作权重(Weight)。

一般而言,在神经网络的图表示中只显示 xi(i>0)x_i(i>0)。也可以写出 x0=1x_0=1,此时称 x0x_0偏置单元(Bias Unit)。特别地,采用了函数 g(θTx)=11+eθTxg(-\boldsymbol{\theta}^T\boldsymbol{x})=\dfrac{1}{1+e^{-\boldsymbol{\theta}^T\boldsymbol{x}}} 的神经元单元称作使用了Sigmoid(Logistic)激活函数的人工神经元。

神经网络通常具有多层结构,也就是有不同的(Layers)。以下图的三层神经网络结构为例,我们来介绍一下其组成部分,并规定相关的符号表示。

  • 链接规则:上一层的每个神经元都与下一层的每个神经元有链接
  • 输入层(Input Layer):有若干个初始输入 x1,x2,,xnx_1,x_2,\cdots,x_n
  • 输出层(Output Layer):输出最终结果 hΘ(x)h_{\Theta}(\boldsymbol{x})
  • 隐藏层(Hidden Layer):链接输入层和输出层的中间部分
    • ai(j)a_i^{(j)}:第 jj 层第 ii 个单元的激活项(Activition),即输出值
    • Θ(j)\boldsymbol{\Theta}^{(j)}:第 jj 层到第 j+1j+1 层的权重矩阵
  • jj 层的单元数:sjs_j

以上图为例,展开矩阵形式即有

a1(2)=g(Θ10(1)x0+Θ11(1)x1+Θ12(1)x2+Θ13(1)x3)a2(2)=g(Θ20(1)x0+Θ21(1)x1+Θ22(1)x2+Θ23(1)x3)a3(2)=g(Θ30(1)x0+Θ31(1)x1+Θ32(1)x2+Θ33(1)x3)hΘ(x)=a1(3)=g(Θ10(2)a0(2)+Θ11(2)a1(2)+Θ12(2)a2(2)+Θ13(2)a3(2))\begin{aligned} a_{1}^{(2)} & =g(\Theta_{10}^{(1)}x_{0}+\Theta_{11}^{(1)}x_{1}+\Theta_{12}^{(1)}x_{2}+\Theta_{13}^{(1)}x_{3}) \\ a_2^{(2)} & =g(\Theta_{20}^{(1)}x_{0}+\Theta_{21}^{(1)}x_{1}+\Theta_{22}^{(1)}x_{2}+\Theta_{23}^{(1)}x_{3}) \\ a_3^{(2)} & =g(\Theta_{30}^{(1)}x_0+\Theta_{31}^{(1)}x_1+\Theta_{32}^{(1)}x_2+\Theta_{33}^{(1)}x_3) \\ h_{\Theta}(x) & =a_{1}^{(3)}=g(\Theta_{10}^{(2)}a_{0}^{(2)}+\Theta_{11}^{(2)}a_{1}^{(2)}+\Theta_{12}^{(2)}a_{2}^{(2)}+\Theta_{13}^{(2)}a_{3}^{(2)}) \end{aligned}

其中,g()g(\cdot) 是Sigmoid函数。权重矩阵 Θ(j)\boldsymbol{\Theta}^{(j)} 的维度是 sj+1×(sj+1)s_{j+1}\times (s_j+1),因为还有偏置项。

前向传播与矩阵表示

为了让神经网络模型的表示更加简单,我们再来规定一些新符号。

  • zi(j)z^{(j)}_i:第 j1j-1 层到 ai(j)a^{(j)}_i 的线性部分。例如对于

a1(2)=g(Θ10(1)x0+Θ11(1)x1+Θ12(1)x2+Θ13(1)x3)a_{1}^{(2)} =g(\Theta_{10}^{(1)}x_{0}+\Theta_{11}^{(1)}x_{1}+\Theta_{12}^{(1)}x_{2}+\Theta_{13}^{(1)}x_{3})

则有 z1(2)=Θ10(1)x0+Θ11(1)x1+Θ12(1)x2+Θ13(1)x3z^{(2)}_1=\Theta_{10}^{(1)}x_{0}+\Theta_{11}^{(1)}x_{1}+\Theta_{12}^{(1)}x_{2}+\Theta_{13}^{(1)}x_{3},因此

a1(2)=g(z1(2))a_{1}^{(2)}=g(z_{1}^{(2)})

同理有 a2(2)=g(z2(2)),a3(2)=g(z3(2)),a1(3)=g(z1(3))a_{2}^{(2)}=g(z_{2}^{(2)}),a_{3}^{(2)}=g(z_{3}^{(2)}),a_{1}^{(3)}=g(z_{1}^{(3)})

  • a(1)\boldsymbol{a}^{(1)}:输入层 x0,x1,x2,,xnx_0,x_1,x_2,\cdots,x_n 组成的 (n+1)×1(n+1)\times 1 列向量
  • a(j)\boldsymbol{a}^{(j)}ai(j)a^{(j)}_i 组成的 sj×1s_{j}\times 1 列向量
  • z(j)\boldsymbol{z}^{(j)}zi(j)z^{(j)}_i 组成的 sj×1s_{j}\times 1 列向量

据此,可以把上一小节中的三层神经网络写成如下矩阵形式

  1. 输入层到隐藏层

z(2)=Θ(1)a(1)R3,a(2)=g(z(2))R3\boldsymbol{z}^{(2)}=\boldsymbol{\Theta}^{(1)}\boldsymbol{a}^{(1)}\in \mathbb{R}^{3},\quad \boldsymbol{a}^{(2)}=g(\boldsymbol{z}^{(2)})\in\mathbb{R}^3

  1. 添加偏置项

Add a0(2)=1 in a(2), so that a(2)R4\text{Add $a_{0}^{(2)}=1$ in $\boldsymbol{a}^{(2)}$, so that } \boldsymbol{a}^{(2)}\in\mathbb{R}^4

  1. 隐藏层到输出层

z(3)=Θ(2)a(2)R,hΘ(x)=a(3)=g(z(3))R\boldsymbol{z}^{(3)}=\boldsymbol{\Theta}^{(2)}\boldsymbol{a}^{(2)}\in \mathbb{R},\quad h_{\Theta}(x)=\boldsymbol{a}^{(3)}=g(\boldsymbol{z}^{(3)})\in\mathbb{R}

由于权重矩阵 Θ(j)\boldsymbol{\Theta}^{(j)} 的维度是 sj+1×(sj+1)s_{j+1}\times (s_j+1),而经过Sigmoid激活函数处理的 a(j)=g(z(j))\boldsymbol{a}^{(j)}=g(\boldsymbol{z}^{(j)}) 维度是 sj×1s_j\times 1,因此需要添加偏置项 a0(j)=1a_{0}^{(j)}=1,使得 a(j)\boldsymbol{a}^{(j)} 的维度变为 (sj+1)×1(s_j+1)\times 1

这种计算过程也被称作前向传播(Forward Propagaton),其核心在于把上一层的激活项输入给下一层,计算顺序从输入层经过隐藏层到输出层

观察最后一个隐藏层到输出层的过程,实际上很像 Logistic 回归法。然而,其使用的参数是经过多层处理的数据 ai(j)a^{(j)}_i,而不是初始输入 xix_i。神经网络模型因此可以在前几层中通过不同的权重矩阵 Θ\boldsymbol{\Theta},自己构造新的特征量,从而增强模型的预测和泛化能力。

举例:XNOR问题

假设有二进制特征量 x1,x2{0,1}x_1,x_2\in\{0,1\},定义异或运算(XOR)和同或运算(XNOR)

XOR: y=x1x2={0,x1=x21,x1x2\text{XOR: }y=x_1\oplus x_2=\begin{cases}0,&x_1=x_2\\1,& x_1\neq x_2 \end{cases}

XNOR: y=x1x2={1,x1=x20,x1x2\text{XNOR: }y=x_1\odot x_2=\begin{cases}1,&x_1=x_2\\0,& x_1\neq x_2 \end{cases}

现在想要构建一个二输入神经网络,使得其能够进行同或运算。这实际上是一个逻辑门问题,我们需要考虑能否用一些基本的逻辑门形式(如与、或、非)构造这个神经网络模型。

考虑构造一个与门(AND)神经网络

  • 输入层为 x0=1,x1,x2x_0=1,x_1,x_2
  • 无隐藏层
  • 输出层 hΘ(x)=g(Θ(1)x)=g(30x0+20x1+20x2)h_{\Theta}(\boldsymbol{x})=g(\boldsymbol{\Theta}^{(1)}\boldsymbol{x})=g(-30x_0+20x_1+20x_2)

已知 g(4.6)0.99,g(4.6)0.01g(4.6)\approx 0.99,g(-4.6)\approx 0.01,有下表

x1x_1 x2x_2 x1x2x_1\land x_2 hΘ(x)h_{\Theta}(\boldsymbol{x})
00 00 00 0\approx 0
00 11 00 0\approx 0
11 00 00 0\approx 0
11 11 11 1\approx 1

接着考虑构造一个或门(OR)神经网络

  • 输入层为 x0=1,x1,x2x_0=1,x_1,x_2
  • 无隐藏层
  • 输出层 hΘ(x)=g(Θ(1)x)=g(10x0+20x1+20x2)h_{\Theta}(\boldsymbol{x})=g(\boldsymbol{\Theta}^{(1)}\boldsymbol{x})=g(-10x_0+20x_1+20x_2)

根据取值有下表

x1x_1 x2x_2 x1x2x_1\lor x_2 hΘ(x)h_{\Theta}(\boldsymbol{x})
00 00 00 0\approx 0
00 11 11 1\approx 1
11 00 11 1\approx 1
11 11 11 1\approx 1

最后考虑构造一个非门(NOT)神经网络

  • 输入层为 x0=1,x1x_0=1,x_1
  • 无隐藏层
  • 输出层 hΘ(x)=g(Θ(1)x)=g(10x020x1)h_{\Theta}(\boldsymbol{x})=g(\boldsymbol{\Theta}^{(1)}\boldsymbol{x})=g(10x_0-20x_1)

根据取值有下表

x1x_1 ¬x1\lnot x_1 hΘ(x)h_{\Theta}(\boldsymbol{x})
00 11 1\approx 1
11 00 0\approx 0

接着,我们需要尝试用这三个基本逻辑门构造XNOR门。由逻辑学知

x1x2=(x1x2)(¬x1¬x2)x_1\odot x_2=(x_1\land x_2)\lor(\lnot x_1\land \lnot x_2)

因此整个神经网络模型可以表示为

a(2)=[a1(2)a2(2)][x1x2¬x1¬x2]\boldsymbol{a}^{(2)}=\begin{bmatrix}a_{1}^{(2)}\\a_{2}^{(2)}\end{bmatrix}\leftarrow \begin{bmatrix}x_1\land x_2\\\lnot x_1\land \lnot x_2\end{bmatrix}

hΘ(x)=a1(3)a1(2)a2(2)h_{\Theta}(x)=a_{1}^{(3)}\leftarrow a_1^{(2)}\lor a_2^{(2)}

应用:多类别分类

神经网络模型通常应用于多类别分类问题,比如前些年很流行的手写数字识别(点击跳转知乎专栏)。神经网络在多类别分类的应用可以视为一对多模型的拓展,比如下面这个例子。

我们令最后的输出层有多个单元,每一个单元对应一种类别,且其值看做模型预测结果是此类别的概率。因此。当某一个节点的值接近1,而其他的节点值接近0时,我们就断言识别结果是此类别。

反向传播


参数拟合与代价函数

对于神经网络模型这种具有大量参数的模型,我们需要找到一种合适的学习算法,使得机器自我更新参数,因此需要再次搬出代价函数的概念。接下来的讨论主要聚焦于多类别分类问题。对于此类神经网络算法,规定记号

  • (x(i),y(i)),i=1,2,m(x^{(i)},y^{(i)}),i=1,2\cdots,m:训练集数据样本
  • LL:神经网络的层数
  • sls_l:第 ll 层的单元个数
  • hΘ(x)RKh_{\Theta}(x)\in \mathbb{R}^K:输出层单元向量,这里 sL=Ks_L=K 表示类别数
  • hΘ(x)ih_{\Theta}(x)_i:输出层第 ii 个单元的值
  • yk(i)y^{(i)}_k:当 y(i)y^{(i)} 的类别是 kk 时为1,否则为0

一般的,称输出单元个数为 KK 的神经网络算法为 KK 分类算法

回顾一下,Logistic 回归模型的正则化代价函数形式为

J(θ)=1mi=1m[y(i)logh(x(i))+(1y(i))log(1h(x(i)))]+λ2mj=1nθj2=Cost+Penalty\begin{aligned}J(\boldsymbol{\theta})&=-\frac{1}{m}\sum_{i=1}^m\Big[y^{(i)}\log h(\boldsymbol{x^{(i)}})+(1-y^{(i)})\log(1-h(\boldsymbol{x^{(i)}}))\Big]+\frac{\lambda}{2m}\sum_{j=1}^n\theta_j^2\\&=\text{Cost}+\text{Penalty} \end{aligned}

在神经网络模型中,输出结果有 KK 个,因此需要改写代价函数为

Cost=1mi=1mk=1K[yk(i)loghΘ(x(i))k+(1yk(i))log(1hΘ(x(i))k)]\text{Cost}=-\frac{1}{m}\sum_{i=1}^m\sum_{k=1}^K\Big[y_k^{(i)}\log h_{\Theta}(\boldsymbol{x^{(i)}})_k+(1-y_k^{(i)})\log(1-h_{\Theta}(\boldsymbol{x^{(i)}})_k)\Big]

Penalty=l=1L1i=1slj=1sl+1(Θji(l))2\text{Penalty}=\sum_{l=1}^{L-1}\sum_{i=1}^{s_l}\sum_{j=1}^{s_{l+1}}(\Theta_{ji}^{(l)})^2

我们主要来解释一下为什么代价函数中的多重求和是这种形式:

  • 对于代价项:有 mm 个样本,每个样本有 KK 个输出
  • 对于惩罚项:LL 层共有 L1L-1 个权重矩阵,且 Θ(l)\boldsymbol{\Theta}^{(l)} 除掉偏置项,维度为 sl+1×sls_{l+1}\times s_l

单样本反向传播

上一节给出了神经网络模型的代价函数为

J(Θ)=1mi=1mk=1K[yk(i)loghΘ(x(i))k+(1yk(i))log(1hΘ(x(i))k)]+λ2ml=1L1i=1slj=1sl+1(Θji(l))2J(\boldsymbol{\Theta})=-\frac{1}{m}\sum_{i=1}^m\sum_{k=1}^K\Big[y_k^{(i)}\log h_{\Theta}(\boldsymbol{x^{(i)}})_k+(1-y_k^{(i)})\log(1-h_{\Theta}(\boldsymbol{x^{(i)}})_k)\Big]+\frac{\lambda}{2m}\sum_{l=1}^{L-1}\sum_{i=1}^{s_l}\sum_{j=1}^{s_{l+1}}(\Theta_{ji}^{(l)})^2

现在我们想要最小化 J(Θ)J(\boldsymbol{\Theta}),需要对 l=1L1sl×sl+1\displaystyle\sum_{l=1}^{L-1}s_{l}\times s_{l+1} 个参数 Θij(l)\Theta^{(l)}_{ij} 求偏导。因此接下来的讨论将围绕这一漫长且艰难的高数课后题数学问题进行推导与解释。我们先从最简单的情况看起。

设训练集只有一个样本 (x,y)(x,y),神经网络层数 L=4L=4,定义误差项

  • δ(l)\boldsymbol{\delta}^{(l)}:第 ll 层单元的误差向量
  • δj(l)\delta^{(l)}_j:第 ll 层第 jj 个单元的误差

现在我们需要规定什么叫做误差。以4层神经网络为例,当 l=4l=4 时显然有

δj(4)=(hΘ(x))jyj\delta^{(4)}_j=(h_{\Theta}(x))_j-y_j

因为 yjy_j 就是样本真实值,所以输出层的误差项被定义为上式。当 1<l<41<l<4 时,我们该如何确定隐藏层单元的误差呢?因此,我们或许需要重新对误差定义如下

δ(l)=Jz(l)\boldsymbol{\delta}^{(l)}=\frac{\partial J}{\partial \boldsymbol{z}^{(l)}}

由于 z(l+1)=Θ(l)a(l)\boldsymbol{z}^{(l+1)}=\boldsymbol{\Theta}^{(l)}\boldsymbol{a}^{(l)},根据链式法则有

δ(l)=Jz(l)=Jz(l+1)z(l+1)a(l)a(l)z(l)=(Θ(l))Tδ(l+1)a(l)z(l)\boldsymbol{\delta}^{(l)}=\frac{\partial J}{\partial \boldsymbol{z}^{(l)}}=\frac{\partial J}{\partial \boldsymbol{z}^{(l+1)}}\frac{\partial \boldsymbol{z}^{(l+1)}}{\partial \boldsymbol{a}^{(l)}}\frac{\partial \boldsymbol{a}^{(l)}}{\partial \boldsymbol{z}^{(l)}}=(\boldsymbol{\Theta}^{(l)})^T\boldsymbol{\delta}^{(l+1)}\circ \frac{\partial \boldsymbol{a}^{(l)}}{\partial \boldsymbol{z}^{(l)}}

由于 a(l)=g(z(l))\boldsymbol{a}^{(l)}=g(\boldsymbol{z}^{(l)}),由 Sigmoid 函数的导数性质有

a(l)z(l)=g(z(l))[1g(z(l))]=a(l)(1a(l))\frac{\partial \boldsymbol{a}^{(l)}}{\partial \boldsymbol{z}^{(l)}}=g(\boldsymbol{z}^{(l)})\circ[1-g(\boldsymbol{z}^{(l)})]=\boldsymbol{a}^{(l)}\circ(1-\boldsymbol{a}^{(l)})

其中 \circ 表示同维度向量的逐元素乘积,即对应位置元素相乘。

因此有

δ(l)=(Θ(l))Tδ(l+1)a(l)(1a(l))\boldsymbol{\delta}^{(l)} = (\boldsymbol{\Theta}^{(l)})^T \boldsymbol{\delta}^{(l+1)} \circ \boldsymbol{a}^{(l)} \circ (1-\boldsymbol{a}^{(l)})

这表明,上一层的误差项来源于下一层的误差项。这种反向计算误差的方法称作反向传播(Back Propagation)法。可以通过反向传播过程证明,对于所有的参数 Θij(l)\Theta^{(l)}_{ij},其偏导数为

JΘij(l)=aj(l)δi(l+1),Not using regularization\frac{\partial J}{\partial \Theta^{(l)}_{ij}}=a^{(l)}_j\delta^{(l+1)}_i,\quad \text{Not using regularization}

因此,神经网络的反向传播过程实际上是机器自主学习并通过计算误差调整参数的过程,其与前向传播过程是紧密相连的——前者根据已有参数负责计算各个激活值,后者通过激活值计算误差,得到各个参数的梯度。

多样本反向传播

假设现在有 mm 个训练样本,我们定义 L1L-1 个矩阵 Δ(l)\boldsymbol{\Delta}^{(l)} 表示 mm 个样本分别经过单样本反向传播后,参数梯度的和。用数学语言可以表示为

Δij(l)=p=1m(J(p)Θij(l)),where (p) represents the sample id\Delta^{(l)}_{ij}=\sum_{p=1}^m\left(\frac{\partial J_{(p)}}{\partial \Theta^{(l)}_{ij}}\right),\quad \text{where $_{(p)}$ represents the sample id}

也就是说,我们实际上就是分别对 mm 个样本应用单样本反向传播算法,然后把得到的 l=1L1sl×sl+1\displaystyle\sum_{l=1}^{L-1}s_{l}\times s_{l+1} 个参数 Θij(l)\Theta^{(l)}_{ij} 的梯度加起来,储存到对应角标的矩阵中。为了更方便表示 L1L-1 个参数矩阵 Θ(l)\boldsymbol{\Theta}^{(l)} 的梯度,我们再定义梯度矩阵 D(l)\boldsymbol{D}^{(l)}

D(l)=JΘ(l),so that Dij(l)=JΘij(l)\boldsymbol{D}^{(l)}=\frac{\partial J}{\partial \boldsymbol{\Theta}^{(l)}},\quad \text{so that }D^{(l)}_{ij}=\frac{\partial J}{\partial \Theta^{(l)}_{ij}}

因此,梯度就可以写成如下式子(考虑正则化)

Dij(l)={1m(Δij(l)+λΘij(l)),j01mΔij(l),j=0D^{(l)}_{ij}=\begin{cases} \dfrac{1}{m}\left(\Delta_{ij}^{(l)}+\lambda\Theta_{ij}^{(l)}\right),&j\neq 0\\\\ \dfrac{1}{m}\Delta_{ij}^{(l)},&j=0\end{cases}

对于非偏置项 j0j\neq 0,括号内的后一项可以直接由L2正则项的导数给出。

最后,我们来总结一下获取参数梯度的整个过程。

  1. 给定 L1L-1 个参数矩阵 Θ(l)\boldsymbol{\Theta}^{(l)}
  2. L1L-1 个矩阵 Δ(l):=Osj+1×(sj+1)\boldsymbol{\Delta}^{(l)}:=\boldsymbol{O}_{s_{j+1}\times(s_j+1)}
  3. 对第 pp 个样本 (x(p),y(p))(\boldsymbol{x}^{(p)},\boldsymbol{y}^{(p)}) 进行单样本反向传播操作
    i. 令输入层 a(1)=x(p)\boldsymbol{a}^{(1)}=\boldsymbol{x}^{(p)}
    ii. 前向传播计算所有的 a(l),l=2,3,,L\boldsymbol{a}^{(l)},l=2,3,\cdots,L
    iii. 计算输出层的误差 δ(L)=a(L)y(p)\boldsymbol{\delta}^{(L)}=\boldsymbol{a}^{(L)}-\boldsymbol{y}^{(p)}
    iv. 反向传播计算所有的 δ(l),l=L1,L2,,2\boldsymbol{\delta}^{(l)},l=L-1,L-2,\cdots,2
    v. 对得到的所有参数梯度累加 Δij(l):=Δij(l)+aj(l)δi(l+1)\Delta^{(l)}_{ij}:=\Delta^{(l)}_{ij}+a^{(l)}_j\delta^{(l+1)}_i
  4. 计算总的梯度矩阵 D(l),l=1,2,,L1\boldsymbol{D}^{(l)},l=1,2,\cdots,L-1

神经网络中的技巧


梯度检测

事实上,反向传播算法能计算出参数的梯度,但上文中能窥见其复杂度。在实际编写程序时,一些细节如矩阵维度、偏置项、逐参数乘积与矩阵乘积等很容易混淆。这些所谓的 BUG 很容易导致梯度计算的错误,从而影响参数拟合。但这些错误通常是不容易看出来的,于是我们需要一种机制,来检测反向传播的梯度计算是否正确

微积分学告诉我们,一元可导函数 f(x)f(x) 在某点 x0x_0 的导数等价于

f(x0)=limε0+f(x0+ε)f(x0ε)2εf'(x_0)=\lim_{\varepsilon\to 0^{+}}\frac{f(x_0+\varepsilon)-f(x_0-\varepsilon)}{2\varepsilon}

上极限式告诉我们,当 ε\varepsilon 取充分小时,有近似式

f(x)f(x+ε)f(xε)2εf'(x)\approx \frac{f(x+\varepsilon)-f(x-\varepsilon)}{2\varepsilon}

在数值微分方法中,上式也被称作中心差分公式,其给出了计算函数导数的近似式。对于一个 nn 元函数 f(x)=f(x1,x2,,xn)f(\boldsymbol{x})=f(x_1,x_2,\cdots,x_n),每一个变元的偏导数也可以近似计算为

fx1f(x1+ε,x2,,xn)f(x1ε,x2,,xn)2ε\frac{\partial f}{\partial x_1}\approx \frac{f(x_1+\varepsilon,x_2,\cdots,x_n)-f(x_1-\varepsilon,x_2,\cdots,x_n)}{2\varepsilon}

fx2f(x1,x2+ε,,xn)f(x1,x2ε,,xn)2ε\frac{\partial f}{\partial x_2}\approx \frac{f(x_1,x_2+\varepsilon,\cdots,x_n)-f(x_1,x_2-\varepsilon,\cdots,x_n)}{2\varepsilon}

\vdots

fxnf(x1,x2,,xn+ε)f(x1,x2,,xnε)2ε\frac{\partial f}{\partial x_n}\approx \frac{f(x_1,x_2,\cdots,x_n+\varepsilon)-f(x_1,x_2,\cdots,x_n-\varepsilon)}{2\varepsilon}

因此,梯度检测(Gradient Check)便是基于上式的一种检测梯度计算正确性的方法。一般只需要设置一个较小的阈值(如 e=107e=10^{-7}),然后对每个参数进行上述近似计算,判断得到的值和对应偏导数的误差是否小于阈值。若该检测对所有参数都成立,我们就断言该梯度计算是正确的。

随机初始化

和梯度下降算法一样,如果我们想要训练神经网络的模型参数,必须提供一组初始参数。不同的初始参数可能会影响模型训练的效率,这种选取合适初始参数的问题称为参数初始化。在神经网络模型中,有几种常用的参数初始化方法

  1. 零初始化:把所有参数设为 00
  2. 随机初始化:设定一个数 ε>0\varepsilon>0,随机生成 ε<Θij(l)<+ε-\varepsilon<\Theta_{ij}^{(l)}<+\varepsilon
  3. 其他方法:如高斯分布初始化

这些方法各有优缺点,具体选择需要考虑应用场景。

注意事项

本小节中,我们来介绍神经网络模型的构造与训练中的注意事项。

  • 选取合适的神经网络框架:确定层数 LL,每层单元数 sls_l
    • 输入层单元数 s1=ns_1=n
    • 输出层单元数 sL=Ks_L=K
    • 隐藏层单元数建议保持一致
  • 选取合适的初始化参数
  • 确保每一轮模型训练的流程顺序正确
    • 第一步:前向传播
    • 第二步:反向传播
    • 第三步:梯度检测
    • 第四步:优化参数,如梯度下降法

代码实现


以 XNOR 运算的神经网络为例,设每层单元数为 [2,4,1][2,4,1],一些准备工作如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 训练集
X = np.array([[0, 0],
[0, 1],
[1, 0],
[1, 1]])
Y = np.array([[1], [0], [0], [1]])

# Sigmoid 激活函数
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))

def sigmoid_deriv(z):
s = sigmoid(z)
return s * (1 - s)

# 随机初始化
def init_params(layer_dims, seed=42):
np.random.seed(seed)
params = {}
L = len(layer_dims) - 1
for l in range(1, L + 1):
fan_in = layer_dims[l - 1]
scale = np.sqrt(2.0 / fan_in) if l < L else np.sqrt(1.0 / fan_in)
params[f"W{l}"] = np.random.randn(layer_dims[l], fan_in) * scale
params[f"b{l}"] = np.zeros((layer_dims[l], 1))
return params

接着,需要实现前向传播与反向传播过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 前向传播
def forward(X, params):
W1, b1 = params["W1"], params["b1"]
W2, b2 = params["W2"], params["b2"]
Z1 = W1 @ X.T + b1
A1 = sigmoid(Z1)
Z2 = W2 @ A1 + b2
A2 = sigmoid(Z2)
cache = {"Z1": Z1, "A1": A1, "Z2": Z2, "A2": A2}
return A2, cache

# 代价函数
def compute_loss(A2, Y, params, lambd):
m = Y.shape[0]
eps = 1e-9
ce = -np.mean(Y.T * np.log(A2 + eps) + (1 - Y.T) * np.log(1 - A2 + eps))
l2 = sum(np.sum(v ** 2) for k, v in params.items() if k.startswith("W"))
return ce + (lambd / (2 * m)) * l2

# 反向传播
def backward(X, Y, params, cache, lambd):
m = X.shape[0]
W1, W2 = params["W1"], params["W2"]
Z1, A1, Z2, A2 = cache["Z1"], cache["A1"], cache["Z2"], cache["A2"]
dZ2 = A2 - Y.T
dW2 = (dZ2 @ A1.T) / m + (lambd / m) * W2
db2 = np.mean(dZ2, axis=1, keepdims=True)
dA1 = W2.T @ dZ2
dZ1 = dA1 * sigmoid_deriv(Z1)
dW1 = (dZ1 @ X) / m + (lambd / m) * W1
db1 = np.mean(dZ1, axis=1, keepdims=True)
return {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}

为了确保梯度计算的正确性,需要进行梯度检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def build(t):
p, off = {}, 0
for k, sh in zip(param_keys, shapes):
sz = int(np.prod(sh))
p[k] = t[off:off + sz].reshape(sh)
off += sz
return p

def gradient_check(X, Y, params, lambd, epsilon=1e-5):
param_keys = sorted([k for k in params if k.startswith(("W", "b"))])
theta = np.concatenate([params[k].ravel() for k in param_keys])
shapes = [params[k].shape for k in param_keys]

A2, cache = forward(X, params)
grads = backward(X, Y, params, cache, lambd)
grad_theta = np.concatenate([grads["d" + k].ravel() for k in param_keys])

num_grad = np.zeros_like(theta)
for i in range(len(theta)):
tp, tm = theta.copy(), theta.copy()
tp[i] += epsilon
tm[i] -= epsilon

A2p = forward(X, build(tp))[0]
Jp = compute_loss(A2p, Y, build(tp), lambd)

A2m = forward(X, build(tm))[0]
Jm = compute_loss(A2m, Y, build(tm), lambd)

num_grad[i] = (Jp - Jm) / (2 * epsilon)

rel_err = np.linalg.norm(grad_theta - num_grad) / (
np.linalg.norm(grad_theta) + np.linalg.norm(num_grad) + 1e-12)
return rel_err

紧接着,定义训练函数并通过输出可视化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def train(X, Y, layer_dims, lr=0.5, lambd=0.01, epochs=2000, print_every=1000):
params = init_params(layer_dims)
loss_history = []
w1_norms, w2_norms = [], []

print("=" * 55)
print(" Training XNOR Neural Network")
print(f" Architecture: {layer_dims} | LR={lr} λ={lambd}")
print("=" * 55)

for epoch in range(1, epochs + 1):
A2, cache = forward(X, params)
loss = compute_loss(A2, Y, params, lambd)
grads = backward(X, Y, params, cache, lambd)
for key in params:
params[key] -= lr * grads["d" + key]

loss_history.append(loss)
w1_norms.append(np.linalg.norm(params["W1"]))
w2_norms.append(np.linalg.norm(params["W2"]))

if epoch % print_every == 0 or epoch == 1:
print(f" Epoch {epoch:6d} | Loss: {loss:.6f}")

print("=" * 55)
return params, loss_history, w1_norms, w2_norms

# 验证训练结果
def evaluate(X, Y, params):
A2, _ = forward(X, params)
preds = (A2 >= 0.5).astype(int).T
print("\n XNOR Predictions")
print(" ┌──────┬──────┬────────┬──────────┬────────┐")
print(" │ x1 │ x2 │ True │ Raw Prob │ Pred │")
print(" ├──────┼──────┼────────┼──────────┼────────┤")
for i in range(len(X)):
x1, x2 = X[i]
mark = "✓" if preds[i, 0] == Y[i, 0] else "✗"
print(f" │ {x1}{x2}{Y[i,0]}{A2[0,i]:.4f}{preds[i,0]} {mark} │")
print(" └──────┴──────┴────────┴──────────┴────────┘")
acc = np.mean(preds == Y) * 100
print(f"\n Accuracy: {acc:.1f}%\n")
return acc

此代码仅作为一个示例。