一个神经元就是一个计算单元,传入$n$个输入,产生一个输出,再应用于激活函数。记$n$维输入向量为$x$,$n$维权重矩阵向量是$w$,偏置项为$b$,激活函数为sigmoid,最终激活后的输出为$a$:
\begin{align*}a=\frac{1}{1+\exp(-(w^Tx+b))}\end{align*}
将权重和偏置项组合在一起,得到如下公式:
\begin{align*}a=\frac{1}{1+\exp(-[w^T\quadb]\cdot[x\quad1])}\end{align*}
图1更直观地描述了该公式:
图1单个神经元的输入及输出
将单个神经元扩展到一层,共$m$个神经元,每个神经元的输入都是$x$,权重记做$\{w^{(i)},\cdots,w^{(m)}\}$,偏置项记做$\{b^{(i)},\cdots,b^{(m)}\}$,则每个神经元激活后的输出:
\begin{align*}a_1&=\frac{1}{1+\exp(-((w^{(1)})^Tx+b_1))}\\&\vdots\\a_m&=\frac{1}{1+\exp(-((w^{(m)})^Tx+b_m))}\end{align*}
下面我们定义更抽象的形式,以便用于复杂的神经网络:
\begin{align*}&W=\begin{bmatrix}-&w^{(1)T}&-\\&\cdots&\\-&w^{(m)T}&-\end{bmatrix}\in\mathbb{R}^{m\timesn}\\&b=\begin{bmatrix}b_1\\\vdots\\b_m\end{bmatrix}\in\mathbb{R}^m\\&z=Wx+b\\&\sigma(z)=\begin{bmatrix}\frac{1}{1+\exp(-z_1)}\\\vdots\\\frac{1}{1+\exp(-z_m)}\end{bmatrix}\\&\begin{bmatrix}a^{(1)}\\\vdots\\a^{(m)}\end{bmatrix}=\sigma(z)=\sigma(Wx+b)\end{align*}
图2简单的前馈神经网络
如图2所示的神经网络,只有1个隐层,输出:
\begin{align*}s=U^Ta=U^Tf(Wx+b)\end{align*}
其中,$f$是激活函数。
维度分析:假设词向量维度为2,一次使用5个词作为输入,则输入$x\in\mathbb{R}^{20}$。如果隐层有8个sigmoid神经元,并在输出层产生1个未规范化的分值,那么$W\in\mathbb{R}^{8\times20},b\in\mathbb{R}^{8},U\in\mathbb{R}^{8\times1},s\in\mathbb{R}$。
\begin{align*}J=\max(s_c-s,0)\end{align*}
训练神经网络的目标是使得$J$最小。
为了得到一个更安全的边界,我们希望正样本分值比负样本分值大出$\Delta$(大于0),因此:
\begin{align*}J=\max(s_c-s+\Delta,0)\end{align*}
\begin{align*}J=\max(s_c-s+1,0)\end{align*}
我们需要求得损失函数关于每个参数的偏导数,然后使用梯度下降更新参数:
\begin{align*}\theta^{(t+1)}=\theta^{(t)}-\alpha\nabla_{\theta^{(t)}}J\end{align*}
反向传播使用链式求导法则,求得损失函数关于每个参数的偏导数。为了进一步理解这一技术,首先看一下图3的神经网络:
图3
上图的神经网络只有一个隐层,一个输出。为简单起见,定义以下概率:
如图4,如果要更新$W^{(1)}_{14}$,首先要意识到,只有在计算$z^{(2)}_1$时才会用到$W^{(1)}_{14}$。$z^{(2)}_1$仅仅用于计算了$a^{(2)}_1$,$a^{(2)}_1$与$W^{(2)}_1$用于计算最终的分值。首先有算是函数关于$s$和$s_c$的偏导数:
\begin{align*}\frac{\partialJ}{\partials}=-\frac{\partialJ}{\partials_c}=-1\end{align*}
为简单起见,我们只计算$\frac{\partials}{\partialw^{(1)}_{ij}}$:
\begin{align*}\frac{\partials}{\partialw^{(1)}_{ij}}&=\frac{\partialW^{(2)}a^{(2)}}{\partialw^{(1)}_{ij}}\tag{1}\\&=\frac{\partialW^{(2)}_ia^{(2)}_i}{\partialw^{(1)}_{ij}}\tag{2}\\&=W^{(2)}_i\frac{\partiala^{(2)}_i}{\partialw^{(1)}_{ij}}\tag{3}\\\end{align*}
第(1)步很直观,因为$s=W^{(2)}a^{(2)}$。第(2)步是因为,只有在计算标量$a^{(2)}_i$时,才会用到向量$W^{(1)}_i$。第(3)步也很直观,我们是在求关于$W^{(1)}_i$的偏导数,$W^{(2)}_i$直接看做常数。
然后应用链式法则:
\begin{align*}W^{(2)}_i\frac{\partiala^{(2)}_i}{\partialw^{(1)}_{ij}}&=W^{(2)}_i\frac{\partiala^{(2)}_i}{\partialz^{(2)}_i}\frac{\partialz^{(2)}_i}{\partialw^{(1)}_{ij}}\\&=W^{(2)}_i\frac{\partialf(z^{(2)}_i)}{\partialz^{(2)}_i}\frac{\partialz^{(2)}_i}{\partialw^{(1)}_{ij}}\\&=W^{(2)}_if'(z^{(2)}_i)\frac{\partialz^{(2)}_i}{\partialw^{(1)}_{ij}}\\&=W^{(2)}_if'(z^{(2)}_i)\frac{\partial}{\partialw^{(1)}_{ij}}(b^{(1)}_i+a^{(1)}_1W^{(1)}_{i1}+a^{(1)}_2W^{(1)}_{i2}+a^{(1)}_3W^{(1)}_{i3}+a^{(1)}_4W^{(1)}_{i4})\\&=W^{(2)}_if'(z^{(2)}_i)a^{(1)}_j\\&=\delta^{(2)}_i\cdota^{(1)}_j\end{align*}
$\delta^{(2)}_i$本质上是第2层第$i$个神经元反向传回的误差。
现在我们换一种方式,用误差分配和反向传播来讨论如何更新图4中的更新$W^{(1)}_{14}$:
以上我们用链式法则和误差分配反向传播得到的结果是一样的。
偏置项更新:偏置项也可以看成输入向量的一个维度,只不过这个维度始终为1(这种1.1小节中的第二个公式就可以看出)。因此,第$k$层第$i$个神经元偏置项的偏导数直接就是$\delta^{(k)}_i$。例如,在上面我们是要更新$b^{(1)}_1$,而不是$W^{(1)}_{14}$,那么梯度直接就是$f'(z^{(2)}_1)W^{(2)}_{1}$。
将$\delta^{(k)}$到$\delta^{(k-1)}$的误差计算一般化:
图5从$\delta^{(k)}$到$\delta^{(k-1)}$的误差传播
用向量化的代码取代for循环,有助于提高代码的执行效率(可以充分利用GPU加速吧?)。
上面我们给出了如何计算一个参数的梯度,现在我们介绍更一般化的方法,一次性地更新整个权重矩阵和偏置向量。这一简单的扩张有助于为我们建立一种直觉,误差传播可以抽象到矩阵-向量级别。
给出一个权重$W^{(k)}_{ij}$,我们定义其误差梯度为$\delta^{(k+1)}_i\cdota^{(k)}_j$。$W^{(k)}$是将$a^{(k)}$映射为$z^{(k+1)}$的权重矩阵。我们可以建立整个矩阵$W^{(k)}$的误差梯度:
\begin{align*}\nabla_{W^{(k)}}=\begin{bmatrix}\delta^{(k+1)}_1a^{(k)}_1&\delta^{(k+1)}_1a^{(k)}_2&\cdots\\\delta^{(k+1)}_2a^{(k)}_1&\delta^{(k+1)}_2a^{(k)}_2&\cdots\\\vdots&\vdots&\ddots\end{bmatrix}=\delta^{(k+1)}a^{(k)T}\end{align*}
下面我们来看如何计算误差向量$\delta^{(k)}$。在图5中我们已经知道,$\delta^{(k)}_j=f'(z^{(k)}_j)\sum_i\delta^{(k+1)}_iW^{(k)}_{ij}$,这可以一般化为如下的矩阵形式:
\begin{align*}\delta^{(k)}=f'(z^{(k)})\circ(W^{(k)T}\delta^{(k+1)}_i)\end{align*}
其中,$\circ$运算符是指矩阵点乘($\mathbb{R}^N\circ\mathbb{R}^N\rightarrow\mathbb{R}^N$)。
计算效率:我们探索了基于元素的更新和基于矩阵的更新。我们必须意识到,向量化的实现在科学运算环境里效率更高,比如MATLAB和Python的NumPy/SciPy包。因此,我们应该使用向量化的实现。更进一步,在反向传播时应该避免重复计算。比如,$\delta^{(k)}$直接依赖于$\delta^{(k+1)}$。我们应该确保,在使用$\delta^{(k+1)}$更新完$W^{(k)}$之后,不能丢弃,而是要保存训练,用于后面计算$\delta^{(k)}$。重复这一过程$(k-1)\cdots(1)$。最终得到了一个计算上还负担得起的递归过程。