没有反向传播的深度学习

原文: http://iamtrask.github.io/2017/03/21/synthetic-gradients/

作者:Andrew Trask

翻译:@Chen Quan

教程:DeepMind的合成梯度

TLDR: 在本博文中,我们将(从头开始)进行原型设计,并了解DeepMind最近提出的使用合成梯度去耦神经接口的论文背后的直觉。

第1部分:合成梯度概述

通常,神经网络会将其预测与数据集进行比较,以决定如何更新其权重。然后,它使用反向传播来找出每个权重应如何移动以使预测更加准确。但是,使用“合成梯度”,各个图层将对他们认为数据会说的内容做出“最佳猜测”,然后根据此猜测更新权重。这种“最佳猜测”称为“合成梯度”。数据仅用于帮助更新每个图层的“猜测器”或“合成渐变”生成器。这允许(大部分时间)单独学习各个层,从而提高了训练速度。

**编辑:**该论文还增加了对伟大的直觉如何/为什么合成梯度非常有效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qfzTQrX-1572668141685)(img/synthetic_grads_paper-1571921126716.png)]资料来源:使用合成梯度的解耦神经接口

上面的图形(来自论文)提供了非常直观的画面(从左到右)。具有圆角的正方形是图层,菱形对象是(我所说的)合成梯度生成器。让我们从如何更新常规神经网络层开始。

第2部分:使用合成梯度

让我们先忽略合成梯度的创建方式,而只看如何使用它们。最左端的框显示了它们如何工作以更新神经网络中的第一层。第一层向前传播到“合成梯度”生成器(Mi+ 1)中,然后生成一个梯度。使用此梯度代替实际的梯度(这将需要完整的前向传播和反向传播进行计算)。然后按正常方式更新权重,假装此“合成梯度”是实际的梯度。如果您需要重新学习如何使用梯度更新权重,请查看11行Python中的神经网络,以及有关Gradient Descent的后续文章。

因此,简而言之,合成梯度的用法与普通梯度一样,并且出于某些不可思议的原因,它们似乎是准确的(无需查阅数据)!看起来像魔术?让我们看看它们是如何制成的。

第3部分:生成合成梯度

好的,这部分真的很聪明,坦率地说,它的工作原理令人惊讶。您如何为神经网络生成合成梯度?好吧,您当然会使用另一个网络!合成梯度生成器不过是经过训练的神经网络,用于获取图层的输出并预测该图层可能发生的渐变。

旁注:杰弗里·欣顿的相关作品

这实际上使我想起了杰弗里·欣顿(Geoffrey Hinton)几年前所做的一些工作,其中他证明了随机反馈权重支持深度神经网络中的学习。基本上,您可以反向传播随机生成的矩阵,并且仍然可以完成学习。此外,他表明它具有某种正则化影响。当然,这是一些有趣的工作。

好,回到合成梯度。因此,现在我们知道合成梯度是由另一个神经网络训练的,该神经网络在给定步骤的输出的情况下学会了预测该步骤的梯度。该论文还说,任何其他相关信息都可以用作“合成梯度”生成器网络的输入,但在该论文中,似乎该层的输出仅用于常规前馈网络。此外,论文甚至指出可以将单个线性层用作“合成梯度”生成器。惊人。我们将尝试一下。

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

这就引出了一个问题,我们如何学习生成合成梯度的神经网络?好吧,事实证明,当我们执行完整的正向和反向传播时,我们实际上得到了“正确的”梯度。我们可以将其与我们的“合成”梯度进行比较,就像我们通常将神经网络的输出与数据集进行比较一样。因此,我们可以通过假装我们的“真实梯度”来自虚拟的数据集来训练合成梯度网络,因此我们像平时一样训练它们。整齐!

等一下…如果我们的合成梯度网络需要反向传播…那有什么意义?

很好的问题!该技术的全部目的是允许训练单个神经网络,而无需等待对方完成正向和反向传播。如果我们的“合成梯度”网络需要等待完整的前进/后退步骤,那么我们将返回开始的位置,但正在进行更多的计算(甚至更糟!)。对于答案,让我们从本文中重新查看该可视化。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-46IgrUnD-1572668141686)(img/synthetic_grads_paper-1571920706626.png)]资料来源:使用合成梯度的解耦神经接口

专注于左侧的第二部分。看到梯度( M i + 2 M_{i + 2} Mi+2)如何反向传播通过( f i + 1 f_{i + 1} fi+1)并进入 M i + 1 M_{i + 1} Mi+1吗?如您所见,每个合成梯度生成器实际上仅使用从下一层生成的合成梯度进行训练。因此,只有最后一层实际上在数据上训练。所有其他层(包括“合成梯度”生成器网络)均基于“合成梯度”进行训练。因此,网络可以训练每一层而只需要等待下一层的合成梯度(没有其他依赖项)。很酷!

第4部分:基准神经网络

是时候开始编码了!为了让事情开始(因此我们有一个更简单的参考框架),我将从经过反向传播训练的香草神经网络开始,其样式与Python 11行中的神经网络相同。(因此,如果没有意义,请阅读该帖子然后再回来)。但是,我将添加一个附加层,但这对于理解应该不是一个问题。我只是认为,既然我们都在减少依赖关系,那么增加图层可能会更好地说明问题。

至于我们正在训练的数据集,我将使用二进制加法生成综合数据集(har!har!)。因此,网络将采用两个随机的二进制数并预测它们的总和(也是一个二进制数)。令人高兴的是,这使我们可以根据需要灵活地增加任务的维度(难度)。这是用于生成数据集的代码。

import numpy as np
import sys

def generate_dataset(output_dim = 8,num_examples=1000):
    def int2vec(x,dim=output_dim):
        out = np.zeros(dim)
        binrep = np.array(list(np.binary_repr(x))).astype('int')
        out[-len(binrep):] = binrep
        return out

    x_left_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    x_right_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    y_int = x_left_int + x_right_int

    x = list()
    for i in range(len(x_left_int)):
        x.append(np.concatenate((int2vec(x_left_int[i]),int2vec(x_right_int[i]))))

    y = list()
    for i in range(len(y_int)):
        y.append(int2vec(y_int[i]))

    x = np.array(x)
    y = np.array(y)
    
    return (x,y)
    


num_examples = 1000
output_dim = 12
iterations = 1000

x,y = generate_dataset(num_examples=num_examples, output_dim = output_dim)
print("Input: two concatenated binary values:")
print(x[0])
print("\nOutput: binary value of their sum:")
print(y[0])

这是在该数据集上进行普通神经网络训练的代码。

import numpy as np
import sys

def generate_dataset(output_dim = 8,num_examples=1000):
    def int2vec(x,dim=output_dim):
        out = np.zeros(dim)
        binrep = np.array(list(np.binary_repr(x))).astype('int')
        out[-len(binrep):] = binrep
        return out

    x_left_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    x_right_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    y_int = x_left_int + x_right_int

    x = list()
    for i in range(len(x_left_int)):
        x.append(np.concatenate((int2vec(x_left_int[i]),int2vec(x_right_int[i]))))

    y = list()
    for i in range(len(y_int)):
        y.append(int2vec(y_int[i]))

    x = np.array(x)
    y = np.array(y)
    
    return (x,y)
    
np.random.seed(1)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

num_examples = 1000
output_dim = 12
iterations = 1000

x,y = generate_dataset(num_examples=num_examples, output_dim = output_dim)

batch_size = 10
alpha = 0.1

input_dim = len(x[0])
layer_1_dim = 128
layer_2_dim = 64
output_dim = len(y[0])

weights_0_1 = (np.random.randn(input_dim,layer_1_dim) * 0.2) - 0.1
weights_1_2 = (np.random.randn(layer_1_dim,layer_2_dim) * 0.2) - 0.1
weights_2_3 = (np.random.randn(layer_2_dim,output_dim) * 0.2) - 0.1


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_0 = batch_x
        layer_1 = sigmoid(layer_0.dot(weights_0_1))
        layer_2 = sigmoid(layer_1.dot(weights_1_2))
        layer_3 = sigmoid(layer_2.dot(weights_2_3))

        layer_3_delta = (layer_3 - batch_y) * layer_3  * (1 - layer_3)
        layer_2_delta = layer_3_delta.dot(weights_2_3.T) * layer_2 * (1 - layer_2)
        layer_1_delta = layer_2_delta.dot(weights_1_2.T) * layer_1 * (1 - layer_1)

        weights_0_1 -= layer_0.T.dot(layer_1_delta) * alpha
        weights_1_2 -= layer_1.T.dot(layer_2_delta) * alpha
        weights_2_3 -= layer_2.T.dot(layer_3_delta) * alpha

        error += (np.sum(np.abs(layer_3_delta)))

    sys.stdout.write("\rIter:" + str(iter) + " Loss:" + str(error))
    if(iter % 100 == 99):
        print("")

现在,在这一点上,我真的觉得有必要做一些我在学习中几乎从未做过的事情,添加一些面向对象的结构。通常,这会使网络有点混乱,并且(从较高的层次)更难以看到正在发生的事情(相对于仅阅读python脚本而言)。但是,由于这篇文章是关于“去耦神经接口”及其提供的好处的,所以在没有使这些接口合理地去耦的情况下很难解释事情。因此,为了使学习更简单,我首先开始将上面的网络转换为*完全相同的网络,*但带有一个“ Layer”类对象,我们很快将其转换为DNI(解耦的神经接口)。让我们看一下这个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,这是一个特殊的派生函数。此函数需要从非线性中获取输出并将其转换为导数。对于S形,这就是简单的(out *(1-out)),其中“ out”是S形的输出。几乎所有常见的神经网络非线性都存在此特定功能。

现在,让我们看一下该类中的各种方法。向前做了它的名字所暗示的。它向前传播通过层,首先通过线性变换,然后通过非线性函数。向后接受output_delta参数,该参数表示反向传播期间从下一层返回的实际梯度(与合成梯度相反)。然后,我们使用它来计算self.weight_output_delta,它是权重输出(仅在非线性范围内)的导数。最后,它会将错误反向传播以发送到上一层并返回。

更新也许是所有方法中最简单的方法。它只是在权重的输出中获取导数,并使用它执行权重更新。如果这些步骤中的任何步骤对您没有意义,请再次查阅11行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,这就是我们使用新图层对象的方式。如果将它与以前的脚本进行比较,几乎所有事情都会在几乎相同的地方发生。我只是将神经网络的脚本版本换成方法调用

因此,我们实际上所做的就是从以前的神经网络中执行脚本中的步骤,并将它们拆分为类内部的不同功能。在下面,我们可以看到这一层的作用。

import numpy as np
import sys

def generate_dataset(output_dim = 8,num_examples=1000):
    def int2vec(x,dim=output_dim):
        out = np.zeros(dim)
        binrep = np.array(list(np.binary_repr(x))).astype('int')
        out[-len(binrep):] = binrep
        return out

    x_left_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    x_right_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    y_int = x_left_int + x_right_int

    x = list()
    for i in range(len(x_left_int)):
        x.append(np.concatenate((int2vec(x_left_int[i]),int2vec(x_right_int[i]))))

    y = list()
    for i in range(len(y_int)):
        y.append(int2vec(y_int[i]))

    x = np.array(x)
    y = np.array(y)
    
    return (x,y)


def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_out2deriv(out):
    return out * (1 - out)

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

np.random.seed(1)

num_examples = 1000
output_dim = 12
iterations = 1000

x,y = generate_dataset(num_examples=num_examples, output_dim = output_dim)

batch_size = 10
alpha = 0.1

input_dim = len(x[0])
layer_1_dim = 128
layer_2_dim = 64
output_dim = len(y[0])

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()
        
        error += (np.sum(np.abs(layer_3_delta * layer_3_out * (1 - layer_3_out))))

    sys.stdout.write("\rIter:" + str(iter) + " Loss:" + str(error))
    if(iter % 100 == 99):
        print("")

如果将先前的网络和该网络都拉进Jupyter笔记本,您会发现随机种子会导致这些网络具有完全相同的值。似乎Trinket.io(译者注:指的是原文中运行Python代码的平台)可能没有完美的随机种子,因此这些网络达到几乎相同的值。但是,我向您保证网络是相同的。如果这个网络对您没有意义,请不要继续。在继续进行之前,请确保您对这种抽象的工作方式感到满意,因为在下面它将变得更加复杂。

第6部分:基于图层输出的合成渐变

好的,所以现在我们将使用与该接口非常相似的接口,将我们从合成梯度中学到的知识集成到我们的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变量,它是我们的Synthetic Generator神经网络(只是线性层,即,只是矩阵)。

向前传播和综合更新: forward 方法已更改为forward_and_synthetic_update。还记得我们不需要网络的任何其他部分来进行权重更新吗?这就是魔术发生的地方。首先,向前传播像正常情况一样发生(第22行)。然后,我们通过将输出传递给非线性来生成合成梯度。这部分可能是一个更复杂的神经网络,但我们决定保持简单,仅使用简单的线性层来生成合成梯度。获得梯度后,我们继续更新常规权重(第28和29行)。最后,我们将合成梯度从权重的输出反向传播到输入,以便可以将其发送到上一层。

更新合成梯度: 好的,所以我们在“前进”方法末尾返回的梯度。这就是我们将从下一层接受到update_synthetic_gradient方法中的内容。因此,如果我们在第2层,则第3层从其forward_and_synthetic_update方法返回一个梯度,然后将其输入到第2层的update_synthetic_weights中。然后,我们只需像常规神经网络一样简单地更新合成权重即可。我们将输入带到合成梯度层(self.output),然后使用输出增量执行平均外部乘积(矩阵转置->矩阵点乘)。这与在普通的神经网络中学习没什么不同,我们只是在数据量较低的情况下得到了一些特殊的输入和输出

好!让我们来看看它的作用。


import numpy as np
import sys

def generate_dataset(output_dim = 8,num_examples=1000):
    def int2vec(x,dim=output_dim):
        out = np.zeros(dim)
        binrep = np.array(list(np.binary_repr(x))).astype('int')
        out[-len(binrep):] = binrep
        return out

    x_left_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    x_right_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    y_int = x_left_int + x_right_int

    x = list()
    for i in range(len(x_left_int)):
        x.append(np.concatenate((int2vec(x_left_int[i]),int2vec(x_right_int[i]))))

    y = list()
    for i in range(len(y_int)):
        y.append(int2vec(y_int[i]))

    x = np.array(x)
    y = np.array(y)
    
    return (x,y)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_out2deriv(out):
    return out * (1 - out)

class DNI(object):
    
    def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv,alpha = 0.1):
        
        self.weights = (np.random.randn(input_dim, output_dim) * 2) - 1
        self.bias = (np.random.randn(output_dim) * 2) - 1
        
        self.weights_0_1_synthetic_grads = (np.random.randn(output_dim,output_dim) * .0) - .0
        self.bias_0_1_synthetic_grads = (np.random.randn(output_dim) * .0) - .0
    
        self.nonlin = nonlin
        self.nonlin_deriv = nonlin_deriv
        self.alpha = alpha
    
    def forward_and_synthetic_update(self,input,update=True):
        
        self.input = input
        self.output = self.nonlin(self.input.dot(self.weights)  + self.bias)
        
        if(not update):
            return self.output
        else:
            self.synthetic_gradient = (self.output.dot(self.weights_0_1_synthetic_grads) + self.bias_0_1_synthetic_grads)
            self.weight_synthetic_gradient = self.synthetic_gradient * self.nonlin_deriv(self.output)
        
            self.weights -= self.input.T.dot(self.weight_synthetic_gradient) * self.alpha
            self.bias -= np.average(self.weight_synthetic_gradient,axis=0) * self.alpha
        
        return self.weight_synthetic_gradient.dot(self.weights.T), self.output
    
    def normal_update(self,true_gradient):
        grad = true_gradient * self.nonlin_deriv(self.output)
        
        self.weights -= self.input.T.dot(grad) * self.alpha
        self.bias -= np.average(grad,axis=0) * self.alpha
        
        return grad.dot(self.weights.T)
    
    def update_synthetic_weights(self,true_gradient):
        self.synthetic_gradient_delta = (self.synthetic_gradient - true_gradient)
        self.weights_0_1_synthetic_grads -= self.output.T.dot(self.synthetic_gradient_delta) * self.alpha
        self.bias_0_1_synthetic_grads -= np.average(self.synthetic_gradient_delta,axis=0) * self.alpha
        
np.random.seed(1)

num_examples = 100
output_dim = 8
iterations = 100000

x,y = generate_dataset(num_examples=num_examples, output_dim = output_dim)

batch_size = 10
alpha = 0.01

input_dim = len(x[0])
layer_1_dim = 64
layer_2_dim = 32
output_dim = len(y[0])

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

for iter in range(iterations):
    error = 0
    synthetic_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_and_synthetic_update(batch_x)
        layer_1_delta, layer_2_out = layer_2.forward_and_synthetic_update(layer_1_out)
        layer_3_out = layer_3.forward_and_synthetic_update(layer_2_out,False)

        layer_3_delta = layer_3_out - batch_y
        layer_2_delta = layer_3.normal_update(layer_3_delta)
        layer_2.update_synthetic_weights(layer_2_delta)
        layer_1.update_synthetic_weights(layer_1_delta)
        
        error += (np.sum(np.abs(layer_3_delta)))
        synthetic_error += (np.sum(np.abs(layer_2_delta - layer_2.synthetic_gradient)))
    if(iter % 100 == 99):
        sys.stdout.write("\rIter:" + str(iter) + " Loss:" + str(error) + " Synthetic Loss:" + str(synthetic_error))
    if(iter % 10000 == 9999):
        print("")

嗯…事情并没有像我最初想要的那样融合。我的意思是,它正在收敛,但并不是很快。经过进一步的查询,所有隐藏的表示开始都是非常平坦和随机的(我们将其用作渐变生成器的输入)。换句话说,两个不同的训练示例最终在不同层具有几乎相同的输出表示。这似乎使目标生成器确实很难完成工作。在本文中,解决方案是批量归一化,它将所有图层输出缩放为0均值和单位方差。这给原本相当简单的玩具神经网络增加了很多复杂性。此外,本文还提到您可以对gradietn生成器使用其他形式的输入。我将尝试使用输出数据集。这仍然使事物保持解耦(DNI的精神),但为网络提供了真正强大的功能,可从一开始就使用它来生成渐变。让我们来看看。


import numpy as np
import sys

def generate_dataset(output_dim = 8,num_examples=1000):
    def int2vec(x,dim=output_dim):
        out = np.zeros(dim)
        binrep = np.array(list(np.binary_repr(x))).astype('int')
        out[-len(binrep):] = binrep
        return out

    x_left_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    x_right_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
    y_int = x_left_int + x_right_int

    x = list()
    for i in range(len(x_left_int)):
        x.append(np.concatenate((int2vec(x_left_int[i]),int2vec(x_right_int[i]))))

    y = list()
    for i in range(len(y_int)):
        y.append(int2vec(y_int[i]))

    x = np.array(x)
    y = np.array(y)
    
    return (x,y)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_out2deriv(out):
    return out * (1 - out)

class DNI(object):
    
    def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv,alpha = 0.1):
        
        self.weights = (np.random.randn(input_dim, output_dim) * 0.2) - 0.1
        self.weights_synthetic_grads = (np.random.randn(output_dim,output_dim) * 0.2) - 0.1
        self.nonlin = nonlin
        self.nonlin_deriv = nonlin_deriv
        self.alpha = alpha
    
    def forward_and_synthetic_update(self,input):
        self.input = input
        self.output = self.nonlin(self.input.dot(self.weights))
        
        self.synthetic_gradient = self.output.dot(self.weights_synthetic_grads)
        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 self.weight_synthetic_gradient.dot(self.weights.T), self.output
    
    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
        
np.random.seed(1)

num_examples = 1000
output_dim = 12
iterations = 1000

x,y = generate_dataset(num_examples=num_examples, output_dim = output_dim)

batch_size = 1000
alpha = 0.0001

input_dim = len(x[0])
layer_1_dim = 128
layer_2_dim = 64
output_dim = len(y[0])

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

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_and_synthetic_update(batch_x)
        layer_1_delta, layer_2_out = layer_2.forward_and_synthetic_update(layer_1_out)
        layer_2_delta, layer_3_out = layer_3.forward_and_synthetic_update(layer_2_out)

        layer_3_delta = layer_3_out - batch_y
        layer_3.update_synthetic_weights(layer_3_delta)
        layer_2.update_synthetic_weights(layer_2_delta)
        layer_1.update_synthetic_weights(layer_1_delta)
        
        error += (np.sum(np.abs(layer_3_delta * layer_3_out * (1 - layer_3_out))))

    if(error < 0.1):
        sys.stdout.write("\rIter:" + str(iter) + " Loss:" + str(error))
        break       
        
    sys.stdout.write("\rIter:" + str(iter) + " Loss:" + str(error))
    if(iter % 100 == 99):
        print("")

而且训练的速度快了很多!考虑什么可能为梯度生成器提供良好的输入是一个非常有趣的概念。输入数据,输出数据和批处理归一化层输出之间的某种组合也许是最佳的(请尝试一下!)希望您喜欢本教程!

你可能感兴趣的:(深度学习,人工智能)