神经网络

0x00 神经元

神经元和感知器本质上时一样的,只不过神经元内部用的激活函数不是阶跃函数而是sigmoid或者tanh这样的激活函数。sigmoid函数的定义如下:

sigmoid(x)=11+ex\text{sigmoid}(x)=\frac{1}{1+e^{-x}}

这是一个非线性函数,其值域为(0, 1),其导数为:

y=y(1y)y'=y(1-y)

我们先来看一个神经网络:

左边红色的一层为输入层,用于接收输入的数据,最右边绿色的一层为输出层,外界从此获取预测值的输出。二者之间即为隐藏层,对外部而言是不可见的。在上图中, w41w_{41} 表示从神经元1到神经元4的权值, a4a_4 表示隐藏层神经元4的输出,其余的标号同理可得。由此我们可以有如下神经网络输出的形成过程:

a4=sigmoid(w41x1+w42x2+w43x3+b4)a_4=\text{sigmoid}(w_{41}x_1+w_{42}x_2+w_{43}x_3+b_4)

在前文线性单元和梯度下降一节,我们已经提到可以将后面的偏差项b看做是一个输入值永远为1的特殊的w,由此我们把b改写成 w4bw_{4b} 可以有:

a4=sigmoid(w41x1+w42x2+w43x3+w4b)a_4=\text{sigmoid}(w_{41}x_1+w_{42}x_2+w_{43}x_3+w_{4b})

由此将其转换成矩阵形式有:

a4=sigmoid([w41,w42,w43,w4b][x1x2x31])w4=[w41,w42,w43,w4b],x=[x1x2x31]a4=sigmoid(w4x)a_4=\text{sigmoid}([w_{41},w_{42},w_{43},w_{4b}]\begin{bmatrix}x_1\\x_2\\x_3\\1\end{bmatrix}) \\ 令\vec{w_4}=[w_{41},w_{42},w_{43},w_{4b}], \vec{x}=\begin{bmatrix}x_1\\x_2\\x_3\\1\end{bmatrix}\\ 则有a_4=\text{sigmoid}(\vec{w_4}\cdot \vec{x})

将上图中我们隐藏层的4个神经元写成矩阵的形式,可有:

a=[a4a5a6a7]=sigmoid([w41w42w43w4bw51w52w53w5bw61w62w63w6bw71w72w73w7b][x1x2x31])\vec{a}=\begin{bmatrix} a_4 \\ a_5 \\ a_6 \\a_7 \end{bmatrix}=\text{sigmoid}\left( \begin{bmatrix} w_{41}&w_{42}&w_{43}&w_{4b} \\ w_{51}&w_{52}&w_{53}&w_{5b} \\ w_{61}&w_{62}&w_{63}&w_{6b} \\ w_{71}&w_{72}&w_{73}&w_{7b} \end{bmatrix} \begin{bmatrix}x_1\\x_2\\x_3\\1\end{bmatrix} \right)

将前面一组满是w的矩阵记为 W1W_1 ,由此,我们就可以得到:

a=sigmoid(W1x)\vec{a}=\text{sigmoid}(W_1\cdot \vec{x})

同理,继续向下一层,我们可以得到整个神经网络的输出y:

每一层的算法都是一样的,上一层的输出就是下一层的输入,由此我们可以推广一个有更多层的神经网络的算法,如下:

上图中神经网络每一层的输出向量计算即为:

其中f(x)即为激活函数。

0x01 反向传播算法

反向传播算法主要用于训练神经网络。按照之前所说,我们取所有输出层结点的误差平方和作为误差函数:

Ed12ioutputs(tiyi)2E_d\equiv\frac{1}{2}\sum_{i\in \text{outputs}}(t_i-y_i)^2

依据随机梯度下降法,我们需要对误差函数进行优化:

wjiwjiηEdwjiw_{ji}\gets w_{ji}-\eta\frac{\partial{E_d}}{\partial{w_{ji}}}

由此我们需要求得 EdE_d 对每个权重值wjiw_{ji} 的偏导数。

wjiw_{ji} 表示从神经元 ii 到下一层神经元 jj 的连接的权重矩阵。由上图可以看出wjiw_{ji} 仅能通过影响结点 jj 的输入来影响网络的其他部分。例如上图中w41w_{41} 只能通过影响结点4的输入值(也就是结点1的输出值)来影响网络的其他部分。由此,我们记 netj\text{net}_j 为结点 jj 的加权输入,由此可得:

netj=iwjixji\text{net}_j=\sum_i w_{ji}\cdot x_{ji}

由此,我们可以有:

注意在上式中第二步到第三步的推算,对那个和式求偏导数的时候,不是 wjiw_{ji} 的那一项,比如 wj(i1)w_{j(i-1)} ,对 wjiw_{ji} 求偏导数就等于0了,就没有了,所以整个和式的最终求导结果为 xjix_{ji}

对于上式中 Ednetj\frac{\partial{E_d}}{\partial{\text{net}_j}} 的推导,则需要分为2种情况,输出层和隐藏层。对于输出层来说,由误差值的计算方法可得,最终的误差 EdE_d 直接与输出层 netj\text{net}_j 的输出值 yjy_j 有关:

Ed12ioutputs(tiyi)2E_d\equiv\frac{1}{2}\sum_{i\in \text{outputs}}(t_i-y_i)^2

EdE_dyiy_i 的函数,而且yiy_inetj\text{net}_j的函数,于是我们可以有:

对于第一项:

对于第二项:

由此:

Ednetj=yj(tjyj)(1yj)\frac{\partial{E_d}}{\partial{\text{net}_j}}=-y_j(t_j-y_j)(1-y_j)

此后我们将上面2式代入 Edwji\frac{\partial{Ed}}{\partial{w{ji}}} ,解得:

δj=Ednetj\delta_j=-\frac{\partial{E_d}}{\partial{\text{net}_j}} ,也就是一个结点的误差项 δ\delta 是整个网络的误差 a=ba = b 对这个结点输入 netj\text{net}_j 的偏导数的相反数。则有:

δj=yj(tjyj)(1yj)\delta_j=y_j(t_j-y_j)(1-y_j)

将上述推导带入随机梯度下降公式,可有:

对于隐藏层节点来说,首先我们来明确一个概念——下游节点(Downstream)。节点 jj 的下游节点就是下一层与节点 jj 直接相连的节点。在上图中,对于节点4来说,它的直接下游节点是节点8和节点9。由此我们可以发现,隐藏层的节点4的输出只能影响到节点8和9,然后再间接影响到 EdE_d 。由此我们定义节点 jj 的所有直接下游节点集合 Downstream(j)\text{Downstream}(j) ,设 netk\text{net}_k为节点j下游节点的输入,要注意的是这个输入不光节点 jj 的输出哦,还有与 jj 同层的其他节点的输出。由此我们可以说 netk\text{net}_knetj\text{net}_j的函数,而 EdE_d 又是 netk\text{net}_k的函数,由此我们可以有:

注:由0x00 神经元一节所述, aja_j 表示隐藏层神经元 jj 的输出:

aj=sigmoid(Wx)a_j= \text{sigmoid}(Wx)

上式中的x为上一个隐藏层的输出,也是当前神经元的输入。所以实则:

aj=sigmoid(netj)a_j=\text{sigmoid}(\text{net}_j)

对于隐藏层的下一层来说,下一层的输入就是上一层的输出,所以下一层的输入 netk\text{net}_{k} 则有:

netk=wkjaj\text{net}_{k}=w_{kj}\cdot a_j

依据sigmoid函数的导数公式,有了上述第3个等号到第4个等号的推算。

δj=Ednetj\delta_j=-\frac{\partial{E_d}}{\partial{\text{net}_j}} 代入上式可有:

δj=aj(1aj)kDownstream(j)δkwkj\delta_j=a_j(1-a_j)\sum_{k\in \text{Downstream}(j)}\delta_kw_{kj}

由此我们可以总结出对于某个节点的误差计算公式:

我们再回过头来看这幅图:

对于输出层节点8来说,其误差值 δ8\delta_8 即为:

对于隐藏层节点4来说,其误差值 δ4\delta_4 即为:

δ4=a4(1a4)(δ8w84+δ9w94)\delta_4=a_4(1-a_4)(\delta_8w_{84}+\delta_9w_{94})

最后依据下式更新w的值:

wjiwji+ηδjxjiw_{ji}\gets w_{ji}+\eta\delta_jx_{ji}

0x02 检查神经网络是否出错

对于梯度下降算法:

wjiwjiηEdwjiw_{ji}\gets w_{ji}-\eta\frac{\partial{E_d}}{\partial{w_{ji}}}

我们真正要保证的是这个偏导数 Edwji\frac{\partial{E_d}}{\partial{w_{ji}}} 的正确性,依据导数的定义:

f(x)=limϵ0f(x+ϵ)f(xϵ)2ϵf'(x)=\lim_{\epsilon \to 0}\frac{f(x+\epsilon)-f(x-\epsilon)}{2\epsilon}

对于任意的导数值我们都可以用右边的等式来进行计算,我们没法求解极限,但是我们只要把 ϵ\epsilon 取得足够小就行了,让其大致地在这个区间范围内,我们就可以认为其值是正确的:

0x03 训练MNIST

代码过长,参见我的GitHub Gist:

Implement a neron network from very beginning

0x04 为什么没有TensorFlow跑得快

  • 全部使用python编写,语言本身效率低,而TensorFlow底层计算全部基于C++编写的动态链接库,TensorFlow python端仅是一个用于与底层库交互的接口,实际的计算工作全部都是在底层库中完成的。

  • 全部基于CPU计算,TensorFlow GPU版可以完成基于GPU的计算,众所周知,GPU的浮点计算速度远高于CPU。

  • 没有使用向量化编程,没必要定义太多复杂的对象,直接把数学计算实现了就可以了。

  • 没有使用SGD(随机梯度下降算法)