学一招:教你用 Python 从零搭建神经网络

1,442 阅读7分钟

原创: James Loy 优达学城Udacity

导语: 今晚我们的目标是:让初学者看得懂的神经网络内部原理!

文/ James Loy

翻译/ kevin

编辑/ 龙哥


众所周知,我们有许多著名的深度学习库,如 Tensorflow。so,为什么还需要自己重复造轮子,用原生 Python 来搭神经网络呢?因为我的个人学习经验表明,通过亲手来搭建一个神经网络,能更好地理解神经网络的的运算原理,这对有志成为数据科学家的小伙伴而言非常重要。

本文是个人学习的心得体会,希望也对各位读者有所启发。

什么是神经网络?

许多介绍神经网络的文章都喜欢把它类比成大脑神经元。但我发现,把神经网络理解为一个把输入向量映射成输出向量的函数更易于初学者理解。

一个神经网络包含了以下元素:

  • 一个输入层(input layer), $x$

  • 任意数量的隐藏层(hidden layers)

  • 一个输出层(output layer), $\hat y$

  • 每个层与层之间都有一组权重(weights) $W$ 与偏移量(biases) $b$

  • 除了输入层外,每一层隐藏层与输出层都会有一个对应的激活函数 $\sigma$,本文默认以 $sigmoid$ 函数作为激活函数

下图是一个2层深度的神经网络(注意在计算神经网络层数时,通常不包括输入层):

用原生的Python来模拟这个神经网络其实很简单:

import numpy as npclass NeuralNetwork:    def __init__(self, x, y):        self.input      = x        self.weights1   = np.random.rand(self.input.shape[1],4)         self.weights2   = np.random.rand(4,1)                         self.y          = y        self.output     = np.zeros(y.shape)

训练神经网络

对于以上这个简单的2层神经网络,它的输出层 $\hat y$ 可以表示为: ?\hat y = \sigma (W2 \sigma (W1x + b1) + b2)?
也许你已经留意到,在以上等式中,权重 $W$ 与 偏移量 $b$ ,是影响输出层 $\hat y$ 的唯二因素。
因此,等式右边的权重与偏移量,决定了估算值 $\hat y$ 的准确度。而通过微调权重与偏移量、使得估算值越来越准确的过程,称为训练神经网络

每个迭代的训练过程都包含了以下步骤:

  • 计算估算值 $\hat y$,称为前向传播;

  • 更新权重与偏移量,称为反向传播

以下流程图展示了前向传播与反向传播的全过程:

前向传播

正如上图所示,所谓的前向传播,实质上就是在进行两次激活函数代入计算的过程:?\hat y = \sigma (W2 \sigma (W1x + b1) + b2)? 让我们把这个前向传播的计算加入之前的Python代码。注意,为了简化问题,我们假设所有的偏移量都等于零。

import numpy as npclass NeuralNetwork:    def __init__(self, x, y):        self.input      = x        self.weights1   = np.random.rand(self.input.shape[1],4)         self.weights2   = np.random.rand(4,1)                         self.y          = y        self.output     = np.zeros(self.y.shape)    def feedforward(self):        self.layer1 = sigmoid(np.dot(self.input, self.weights1))        self.output = sigmoid(np.dot(self.layer1, self.weights2))

然而,我们还需要一个衡量估算值的准确度的指标(比如可以量化估算值与真实值之间的差距大小)。

这时,损失函数(Loss Function)就是我们想要的指标。

损失函数

事实上,损失函数可以有很多种计算方式。而具体使用哪一种,则视具体的问题而定。在本例的问题,我们会使用方差作为我们的损失函数:?Mean Square Error = \frac{1}{N} \sum_{i = 1}^n{(y - \hat y)^2}? 方差就是指所有的估算值与真实值的偏差的平方和均值。对偏差取平方,使我们无需考虑符号(偏差方向)问题。需要说明的是,该公式有时候为了简化计算,可以把其中的常部分 $\frac{1}{N}$ 替换为任意常数值。
我们的目标是,训练网络、找到使损失函数的值最小的那组权重与偏移量。

反向传播

现在我们已经有方法衡量估算值的偏差,那么我们只需要找到一个方法来把这些误差传播回去,从而可以更新我们的权重与偏移量。

为了得到合适的权重与偏移量的更新值,我们需要求得损失函数对于权重与偏移量的偏导数。

回顾微积分可知,一个函数的导数可以看作是该函数曲线上某个点的斜率。

梯度下降算法(图例说明)

  • y轴为误差

  • x轴为权重

  • 左一说明:斜率的梯度(导数)告诉我们找到函数最小值的移动方向

  • 右上说明:损失函数的值告诉我们,某组权重所造成的误差是多少

  • 右下说明:令损失函数的输出值最小的那组权重,就是我们要找的那组权重值

如果我们获得了损失函数某个点的导数,我们就能用它来增加/减少原有的权重及偏移量(参考上图)。这就是梯度下降。

然而,我们无法直接算出损失函数对于权重与偏移量的偏导数。因为在等式中没有包括权重与偏移量。于是,我们需要用到链式法则来辅助计算。

看公式看得有点头大是吧。不想看推导过程的小伙伴只需要看公式最后一行就可以,这就是损失函数对于权重的偏导数(别忘了,我们之前假设了偏移量为零,所以暂且忽略它),有了这个函数梯度,我们就能更新权重了。

现在,我们把反向传播函数也加到我们的神经网络代码中去:

import numpy as npclass NeuralNetwork:    def __init__(self, x, y):        self.input      = x        self.weights1   = np.random.rand(self.input.shape[1],4)         self.weights2   = np.random.rand(4,1)                         self.y          = y        self.output     = np.zeros(self.y.shape)    def feedforward(self):        self.layer1 = sigmoid(np.dot(self.input, self.weights1))        self.output = sigmoid(np.dot(self.layer1, self.weights2))    def backprop(self):        # application of the chain rule to find derivative of the loss function with respect to weights2 and weights1        d_weights2 = np.dot(self.layer1.T, (2*(self.y - self.output) * sigmoid_derivative(self.output)))        d_weights1 = np.dot(self.input.T,  (np.dot(2*(self.y - self.output) * sigmoid_derivative(self.output), self.weights2.T) * sigmoid_derivative(self.layer1)))        # update the weights with the derivative (slope) of the loss function        self.weights1 += d_weights1        self.weights2 += d_weights2    def sigmoid_derivative(x):        return x * (1- x)

想要更深入地了解微积分与链式法则在反向传播中的应用,我强烈推荐这个视频:https://youtu.be/tIeHLnjs5U8

组装成一个完整的神经网络

现在我们有了前向传播与反向传播的全部代码,让我们试试用它来解决一些实际问题,看看效果如何吧。

上图是一个从三维向量x到一维向量y的映射。我们的神经网络理论上应该能够学习一组理想的权重来表达这个映射函数。

我们把神经网络训练1500个迭代,看看会发生什么。我们可以从下面的训练图表可以看到,误差随着训练的迭代数增加而呈单调递减的趋势,最终会收敛在某个最小值。这与我们前面提到的梯度下降算法是一致的。

我们再来看看经过1500个迭代训练后,神经网络的预测结果:

成功了!通过正向传播与反向传播算法,我们成功地训练了神经网络并且使预估值收敛于真实值附近。

需要注意的是,我们看到预估值与真实值之间有一定的差距,这是符合期望的。因为这样能避免过拟合,可以使神经网络能更好地泛化于一些没见过的数据样本。

然后呢?

尽管我们已经用Python搭建出了一个神经网络,并且成功训练它使其能得出一个非常接近真实值的映射结果。但对于入门神经网络与深度学习领域,我们还有许多需要去研究的细节。例如:

  • 除了 $sigmoid$ 激活函数,还有哪些其他的激活函数可以使用?

  • 在训练神经网络的过程中,引入学习率

  • 使用卷积层来处理图像识别任务

我会在未来继续介绍这些相关的主题,欢迎持续关注哦!

反思

在以上这个“重新造轮子”的过程中,我从中更深刻地了解了神经网络的运算原理。尽管坊间有许多现成的深度学习库,比如Tensorflow、Keras等,使神经网络的搭建与计算更容易更高效,即便你对神经网络的内部原理不是太清楚也能无碍于使用;但我发现对于有志成为数据科学家的小伙伴而言,更深入地了解神经网络的内部原理是相当有帮助的。


以上这个小实践对我而言是相当受益的,希望对各位也有所启发吧!