无需反向传播的深度学习:DeepMind的合成梯度

在这篇博文中,我们将从起点(从零开始)学习 DeepMind 最近提出的一篇论文—使用合成梯度的解耦神经接口。读者可以点击下载此论文

合成梯度概述


通常,神经网络将其预测与数据集进行比较,以决定如何更新其权重。然后使用反向传播来确定每个权重应该如何移动,以使预测更加准确。然而,对于合成梯度来说,数据的「最佳预测」由各层完成,然后基于这个预测更新权重。这个「最佳预测」被称为合成梯度。数据仅用于帮助更新每个层的「预测器」或者合成梯度生成器。这使得(大部分情况下)单个层独立学习,提升了训练的速度。

640-39.jpeg

上图(论文中)对于所发生的事情(从左到右)给出了非常直观的解释。圆角的正方形是层,菱形物体是(我称其为)合成梯度生成器。让我们来看看一个常见的神经网络层是如何更新的。


使用合成梯度


我们不关注合成梯度(Synthetic Gradients)的创建方式,仅仅是关注其使用方法。最左边的框显示了如何更新神经网络的第一层。第一层前向传播到合成梯度生成器(M i+1),然后返回梯度。使用此梯度而不是实际梯度(这将需要一个完整的正向传播和反向传播来计算)。然后,权重正常更新,并认为该合成梯度是真实的梯度值。如果你需要了解如何使用梯度更新权重,请查看 A Neural Network in 11 Lines of Python (http://iamtrask.github.io/2015/07/12/basic-python-network/)或者也可以关注在 Gradient Descent 的后续帖子。

简而言之,合成梯度(Synthetic Gradients)就像是普通梯度一样被使用,并且因为某些神奇的原因,它们似乎是准确的(没有使用数据)!像是魔法吗?让我们看看它们是如何构建的。


生成合成梯度


好的,这部分真的非常巧妙,坦白说,它的工作原理非常精妙。你如何为一个神经网络生成合成梯度?当然是使用另一个网络!合成梯度生成器不过是一个训练良好的神经网络,在该网络中可以得到某一层的输出并预测可能发生在该层的梯度。


Geoffrey Hinton 相关的工作

实际上,这让我想起了几年前 Geoffrey Hinton 做的一些工作。虽然我找不到参考文献了,但是他的确做了一些工作,证明你可以通过随机生成的矩阵反向传播,并且仍然完成学习。此外,他表明其有一种正则化效应。这的确是一些有趣的工作。

好的,回到合成梯度。所以,现在我们知道合成梯度是由另一个神经网络训练的,该神经网络基于某一层的输出给出相应的梯度预测。论文还显示,任何其他相关的信息均可以被用作合成梯度生成器网络的输入,但在论文中,好像只有该层的输出用于正常的前馈网络。此外,论文甚至指出可以使用单线性层作为合成梯度生成器。太厉害了,我们要尝试一下。


我们如何学习生成合成梯度的网络?

那么问题就来了,我们如何学习产生合成梯度的神经网络?事实证明,当我们进行全部的正反向传播时,我们实际上得到了「正确的」梯度。我们可以用我们比较神经网络的输出和数据集的方法,将其与我们的「合成」梯度进行比较。因此,我们可以通过假设「真实梯度」来自于虚拟数据集来训练我们的合成神经网络。所以我们就可以像通常那样训练它们了。棒!


如果我们的合成梯度网络需要反馈,它有什么意义?

问得好!这个技术的全部意义是允许单个神经网络训练,而不用相互等待以完成前向与反向传播。如果我们的合成梯度网络需要等待完整的前向/反向传播过程的话,那么我们又回到了原点并且还需要更大的计算量(这更糟了!)。为了找到答案,让我们从论文中回顾一下。

640-40.jpeg

请观察左边的第二部分。看看梯度(Mi+2)是如何通过(fi+1)反向传播到达 M(i+1)的?正如你所看到的,每一个合成梯度生成器实际上只使用了来自下一层的合成梯度进行训练。因此,只有最后一层实际上是在数据上训练的。其他所有层,包括,合成梯度生成器网络,均基于合成梯度训练。因此,网络只需等待来自下一层的合成梯度就可以训练每个层。太棒了!


基线神经网络


编程时间到!为了开始(所以我们有一个更简单的参考框架),我将使用一个用反向传播训练的 vanilla 神经网络,风格与 A Neural Network in 11 Lines of Python 相似。(所以如果你没理解,去看那篇文章然后再回来)。然而, 我要添加一个附加层,但是这不应该妨碍理解。我只是在想,既然我们在减少依赖关系,更多的层可能会说明的更好。

就我们所训练的数据集而言,我将使用二进制加法来生成合成数据集。因此,神经网络将会采取两个随机二进制数并预测他们的和(也是二进制数)。好消息是,这使我们能够灵活地根据需要增加任务的维度(难度)。以下是生成数据集的代码。

640-41.jpeg

编译器无法在微信展示,读者朋友可以在原文运行


这里是关于在该数据集的训练 vanilla 神经网络的代码

640-42.jpeg

现在,在这一点上,我觉得非常有必要做一些我在学习中几乎从未做过的事情,增加一些面向对象的结构。通常,这会使网络模糊一点,并使其更难(从高级别)读懂正在进行什么(相对于只读一个 python 脚本而言)。然而,因为本文是关于「解耦神经接口(Decoupled Neural Interfaces)」及其优点,实际上很难解释这些接口是否合理解耦。所以,为了更易学习,我首先将上述网络转化为完全相同的网络,但会使用一个在后文中会转化为 DNI 的「Layer」类对象。让我们来看看这个 Layer 对象。

class Layer(object):
   
   def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv):
       
       self.weights = (np.random.randn(input_dim, output_dim) * 0.2) - 0.1
       self.nonlin = nonlin
       self.nonlin_deriv = nonlin_deriv
   
   def forward(self,input):
       self.input = input
       self.output = self.nonlin(self.input.dot(self.weights))
       return self.output
   
   def backward(self,output_delta):
       self.weight_output_delta = output_delta * self.nonlin_deriv(self.output)
       return self.weight_output_delta.dot(self.weights.T)
   
   def update(self,alpha=0.1):
       self.weights -= self.input.T.dot(self.weight_output_delta) * alpha

在这个 Layer 类中,我们有几个类变量。权重是我们用于从输入到输出的线性变换的矩阵(就像普通的线性层),根据需要,我们也可以包含一个非线性输出函数,它将非线性放置在我们的输出网络中。如果我们不想要非线性,我们可以简单地将此值设置为 lambda x:x。在我们的例子中,我们将传递给 "sigmoid" 函数。

我们传递的第二个函数是 nonlin_deriv,一个特殊的导函数。该函数需要从非线性中输出,并将其转化为导数。对于 sigmoid 来说,这非常容易(out * (1 - out)),其中「out」是 sigmoid 的输出值。这个特定的功能几乎存在于所有常见的神经网络非线性中。

现在,我们来看看这个类中的各种方法。forward 正如其名字所示。它通过层向前传播,首先通过一个线性变换,然后通过非线性函数。backward 接受了一个 output_delta 参数,它表示反向传播期间从下一层返回的「真实梯度」(而非合成梯度),然后我们使用它来计算 self.weight_output_delta,它是我们权重输出处的导数(仅在非线性内)。最后,它反向传播错误并传入至上一层,并将其返回。

update 可能是最简单的方法。它只需要在权重的输出中使用导数,并使用它来进行权重更新。如果你对这些步骤有任何疑问,再一次地,查看 A Neural Network in 11 Lines of Python 再回来。如果你能全部理解,那么让我们在训练中看看我们的层对象。

layer_1 = Layer(input_dim,layer_1_dim,sigmoid,sigmoid_out2deriv)
layer_2 = Layer(layer_1_dim,layer_2_dim,sigmoid,sigmoid_out2deriv)
layer_3 = Layer(layer_2_dim, output_dim,sigmoid, sigmoid_out2deriv)

for iter in range(iterations):
   error = 0

   for batch_i in range(int(len(x) / batch_size)):
       batch_x = x[(batch_i * batch_size):(batch_i+1)*batch_size]
       batch_y = y[(batch_i * batch_size):(batch_i+1)*batch_size]  
       
       layer_1_out = layer_1.forward(batch_x)
       layer_2_out = layer_2.forward(layer_1_out)
       layer_3_out = layer_3.forward(layer_2_out)

       layer_3_delta = layer_3_out - batch_y
       layer_2_delta = layer_3.backward(layer_3_delta)
       layer_1_delta = layer_2.backward(layer_2_delta)
       layer_1.backward(layer_1_delta)
       
       layer_1.update()
       layer_2.update()
       layer_3.update()

给定一个数据集 x 和 y,这就是我们使用我们的新层对象的方式。如果你把它与之前的脚本进行比较,几乎是一模一样。我只是更换了调用的神经网络的版本。

所以,我们所做是将先前神经网络的脚本代码在类中分成不同的函数。下面,我们在实际中看看这个层。

640-43.jpeg

如果你将以前的网络和此网络同时插入 Jupyter notebooks,你将会看到随机种子会使这些网络具有完全相同的值。看起来,Trinket.io 可能没有完美的随机选取种子点以使这些网络达到几乎相同的值。然而,我向你保证,网络是相同的。如果你认为这个网络毫无意义,不要进行下一步的学习。在进行后面的学习前确保对这个抽象网络的工作方式熟稔于心,因为下面将会变得更加复杂。


整合合成梯度


好的,所以现在我们将使用一个与上述非常相似的接口,唯一不同在于我们将所学到的关于合成梯度的知识整合入 Layer 对象中(并重命名为 DNI)。首先,我们要向你展示类,然后我会对其进行解释。一探究竟吧!

class DNI(object):
   
   def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv,alpha = 0.1):
       
       # same as before
       self.weights = (np.random.randn(input_dim, output_dim) * 0.2) - 0.1
       self.nonlin = nonlin
       self.nonlin_deriv = nonlin_deriv


       # new stuff
       self.weights_synthetic_grads = (np.random.randn(output_dim,output_dim) * 0.2) - 0.1
       self.alpha = alpha
   
   # used to be just "forward", but now we update during the forward pass using Synthetic Gradients :)
   def forward_and_synthetic_update(self,input):

    # cache input
       self.input = input

       # forward propagate
       self.output = self.nonlin(self.input.dot(self.weights))
       
       # generate synthetic gradient via simple linear transformation
       self.synthetic_gradient = self.output.dot(self.weights_synthetic_grads)

       # update our regular weights using synthetic gradient
       self.weight_synthetic_gradient = self.synthetic_gradient * self.nonlin_deriv(self.output)
       self.weights += self.input.T.dot(self.weight_synthetic_gradient) * self.alpha
       
       # return backpropagated synthetic gradient (this is like the output of "backprop" method from the Layer class)
       # also return forward propagated output (feels weird i know... )
       return self.weight_synthetic_gradient.dot(self.weights.T), self.output
   
   # this is just like the "update" method from before... except it operates on the synthetic weights
   def update_synthetic_weights(self,true_gradient):
       self.synthetic_gradient_delta = self.synthetic_gradient - true_gradient
       self.weights_synthetic_grads += self.output.T.dot(self.synthetic_gradient_delta) * self.alpha        

那么,第一个大的变化。我们有了一些新的类变量。其中唯一真正重要的是,self.weights_synthetic_grads 变量,这就是我们的合成生成器神经网络(只是一个线性层,其实,,,就是一个矩阵)

前向和合成更新:前向方法已经变成 forward_and_synthetic_update。记得我们是如何不需要网络的任何其他的部分来更新我们的权重吗?这就是神奇的地方了。首先,前向传播像正常一样发生(22 行)。然后,我们通过非线性传递我们的输出来生成我们的合成梯度。这部分可能是一个更复杂的网络,但是相反,我们决定保持简单,并只是用一个简单的线性层生成我们的合成梯度。在我们得到我们的梯度时,我们继续执行并更新我们的普通权重(28 和 29 行)。最终,我们从权重的输出反向传播我们的合成梯度至其输入,这样我们可以将其传入上一层。

更新合成梯度:好的,那么我们在「forward」方法的结尾处返回了梯度。这就是我们从下一层接受进 update_synthetic_gradient 方法的东西。那么,如果我们现在在第二层,那么第三层从其 forward_and_synthetic_update 方法中返回了一个梯度,并将其输入进第二层的 update_synthetic_weights。然后,我们只需要像更新普通的神经网络那样更新我们的合成梯度就好。我们将输入传入合成梯度层(self.output),然后用 output delta 执行均值外积运算(矩阵转置 → 矩阵乘法)。这与在一个普通的神经网络中学习没什么不同,我们只是在 leau of data 中得到一些特殊的输入和输出。

让我们在实践中看一看它

640-44.jpeg

训练有点不一样。较大的数据批量规模和较小的 alpha 值似乎性能更好,但积极的一面是,在训练中只迭代了一半次数!(这可能很容易调整,但是仍然。。不错)。一些结论。训练似乎有些混乱(不会下降)。我不知道在 hood 那发生了什么事,但是当它收敛时,肯定很快。

我通常会在 iamtrask 上发布新完成的 blogpost。如果你有兴趣阅读,欢迎关注并给予反馈! 机器之心icon.png


原文地址:https://iamtrask.github.io/2017/03/21/synthetic-gradients/

入门
1
暂无评论
暂无评论~
返回顶部