当前位置: 主页 > N人生活 >100 行 Python,神经网路轻鬆搞定 >

100 行 Python,神经网路轻鬆搞定

作者: 分类: N人生活 发布于:2020-06-03 浏览(668)


100 行 Python,神经网路轻鬆搞定

用 tensorflow,pytorch 这类深度学习库来写一个神经网路早就不稀奇了。

可是,你知道怎幺用 python 和 numpy 来优雅地搭一个神经网路吗?

现如今,有多种深度学习框架可供选择,他们带有自动微分、基于图的优化计算和硬体加速等各种重要特性。对人们而言,似乎享受这些重要特性带来的便利已经是理所当然的事了。但其实,瞧一瞧隐藏在这些特性下的东西,能更好的帮助你理解这些网路究竟是如何工作的。

所以今天,文摘菌就来手把手教大家搭一个神经网路。原料就是简单的 python 和 numpy !

文章中的所有代码都在这》

在计算反向传播时,我们可以选择使用函数符号、变量符号去记录求导过程。它们分别对应了计算图中的边和节点来表示它们。

给定 R^n→R 和 x∈R^n,那幺梯度是由偏导 ∂f/∂ j (x) 组成的 n 维行向量

如果 f:R^n→R^m 和 x∈R^n,那幺 Jacobian 矩阵是下列函数组成的一个 m×n 的矩阵。

100 行 Python,神经网路轻鬆搞定

对于给定的函数 f 和向量 a 和 b 如果 a=f(b) 那幺我们用 ∂a/∂b 表示 Jacobian 矩阵,当 a 是实数时则表示梯度

给定三个分属于不同向量空间的向量 a∈A 及 c∈C 和两个可微函数 f:A→B 及 g:B→C 使得 f(a)=b 和 g(b)=c,我们能得到复合函数的 Jacobian 矩阵是函数 f 和 g 的 jacobian 矩阵的乘积:

100 行 Python,神经网路轻鬆搞定

这就是大名鼎鼎的鍊式法则。提出于上世纪 60、70 年代的反向传播算法就是应用了鍊式法则来计算一个实函数相对于其不同参数的梯度的。

要知道我们的最终目标是通过沿着梯度的相反方向来逐步找到函数的最小值(当然最好是全局最小值),因为至少在局部来说,这样做将使得函数值逐步下降。当我们有两个参数需要优化时,整个过程如图所示:

100 行 Python,神经网路轻鬆搞定

假设函数 f i (a i )=a i +1  由多于两个函数複合而成,我们可以反覆应用公式求导并得到:

100 行 Python,神经网路轻鬆搞定

可以有很多种方式计算这个乘积,最常见的是从左向右或从右向左。

如果 a n  是一个标量,那幺在计算整个梯度的时候我们可以通过先计算 ∂a n /∂a n-1  并逐步右乘所有的 Jacobian 矩阵 ∂ai/∂ai-1  来得到。这个操作有时被称作 VJP 或向量-Jacobian 乘积(Vector-Jacobian Product)。

又因为整个过程中我们是从计算 ∂a n /∂a n-1  开始逐步计算 ∂a n /∂a n-2,∂a n /∂a n-3 等梯度到最后,并保存中间值,所以这个过程被称为反向模式求导。最终,我们可以计算出 a n  相对于所有其他变量的梯度。

100 行 Python,神经网路轻鬆搞定

相对而言,前向模式的过程正相反。它从计算 Jacobian 矩阵如 ∂a 2 /∂a 1  开始,并左乘 ∂a 3 /∂a 2  来计算 ∂a 3 /∂a1。如果我们继续乘上 ∂a i /∂a i-1  并保存中间值,最终我们可以得到所有变量相对于 ∂a 2 /∂a 1  的梯度。当 ∂a2/∂a1 是标量时,所有乘积都是列向量,这被称为 Jacobian 向量乘积(或者 JVP,Jacobian-Vector Product)。

100 行 Python,神经网路轻鬆搞定

你大概已经猜到了,对于反向传播来说,我们更偏向应用反向模式——因为我们想要逐步得到损失函数对于每层参数的梯度。正向模式虽然也可以计算需要的梯度,但因为重複计算太多而效率很低。

计算梯度的过程看起来像是有很多高维矩阵相乘,但实际上,Jacobian 矩阵常常是稀疏、块或者对角矩阵,又因为我们只关心将其右乘行向量的结果,所以就不需要耗费太多计算和存储资源。

在本文中,我们的方法主要用于按顺序逐层搭建的神经网路,但同样的方法也适用于计算梯度的其他算法或计算图。

关于反向和正向模式的详尽描述可以参考这里》

损失函数是关于样本和权重的标量函数,它是衡量模型输出与预期标籤的差距的指标。我们的目标是找到最合适的权重让损失最小。在深度学习中,损失函数被表示为一串易于求导的简单函数的複合。所有这些简单函数(除了最后一个函数),都是我们指的层,而每一层通常有两组参数:输入(可以是上一层的输出)和权重。

而最后一个函数代表了损失度量,它也有两组参数:模型输出 y 和真实标籤 y^。例如,如果损失度量 l 为平方误差, 则 ∂l/∂y 为 2 avg(yy^)。损失度量的梯度将是应用反向模式求导的起始行向量。

自动求导背后的思想已是相当成熟了。它可以在运行时或编译过程中完成,但如何实现会对性能产生巨大影响。我建议你能认真阅读 HIPS autograd 的 Python 实现,来真正了解 autograd。

核心想法其实始终未变。从我们在学校学习如何求导时,就应该知道这一点了。如果我们能够追蹤最终求出标量输出的计算,并且我们知道如何对简单操作求导(例如加法、乘法、幂、指数、对数等等),我们就可以算出输出的梯度。

假设我们有一个线性的中间层 f,由矩阵乘法表示(暂时不考虑偏置):

100 行 Python,神经网路轻鬆搞定

为了用梯度下降法调整 w 值,我们需要计算梯度 ∂l/∂w。这里我们可以观察到,改变 y 从而影响 l 是一个关键。

每一层都必须满足下面这个​​条件: 如果给出了损失函数相对于这一层输出的梯度,就可以得到损失函数相对于这一层输入(即上一层的输出)的梯度。

现在应用两次鍊式法则得到损失函数相对于 w 的梯度:

100 行 Python,神经网路轻鬆搞定

相对于 x 的是:

100 行 Python,神经网路轻鬆搞定

因此,我们既可以后向传递一个梯度,使上一层得到更新并更新层间权重,以优化损失,这就行啦!

先来看看代码,或者直接试试 Colab Notebook

我们从封装了一个张量及其梯度的类(class)开始。

现在我们可以创建一个 layer 类,关键的想法是,在前向传播时,我们返回这一层的输出和可以接受输出梯度和输入梯度的函数,并在过程中更新权重梯度。

然后,训练过程将有三个步骤,计算前向传递,然后后向传递,最后更新权重。这里关键的一点是把更新权重放在最后, 因为权重可以在多个层中重用,我们更希望在需要的时候再更新它。

标準的做法是将更新参数的工作交给优化器,优化器在每一批(batch)后都会接收参数的实例。最简单和最广为人知的优化方法是 mini-batch 随机梯度下降。

在此框架下,并使用前面计算的结果后,线性层如下所示:

接下来看看另一个常用的层,启用层。它们属于点式(pointwise)非线性函数。点式函数的 Jacobian 矩阵是对角矩阵,这意味着当乘以梯度时,它是逐点相乘的。

计算 Sigmoid 函数的梯度略微有一点难度,而它也是逐点计算的:

当我们按序构建很多层后,可以遍历它们并先后得到每一层的输出,我们可以把 backward 函数存在一个列表内,并在计算反向传播时使用,这样就可以直接得到相对于输入层的损失梯度。就是这幺神奇:

正如我们前面提到的,我们将需要定义批样本的损失函数和梯度。一个典型的例子是 MSE,它被常用在回归问题里,我们可以这样实现它:

就差一点了!现在,我们定义了两种层,以及合併它们的方法,下面如何训练呢?我们可以使用类似于 scikit-learn 或者 Keras 中的 API。

这就行了!如果你跟随着我的思路,你可能就会发现其实有几行代码是可以被省掉的。

现在可以用一些数据测试下我们的代码了。

100 行 Python,神经网路轻鬆搞定

我们还能检查学到的权重和真实的权重是否一致。

好了,就这幺简单。让我们再试试非线性数据集,例如 y=x 1 x 2,并且再加上一个 Sigmoid 非线性层和另一个线性层让我们的模型更複杂些。像下面这样:

100 行 Python,神经网路轻鬆搞定

希望通过搭建这个简单的神经网路,你已经掌握了用 python 和 numpy 实现神经网路的基本思路。

在这篇文章中,我们只定义了三种类型的层和一个损失函数,所以还有很多事情可做,但基本原理都相似。感兴趣的同学可以试着实现更複杂的神经网路哦!