大数据文摘来源:eisenjulian编译:周家乐、钱天培用tensorflow、pytorch等深度学习库写神经网络并不少见。但是,你知道如何优雅地使用python和numpy构建神经网络吗?如今,有多种深度学习框架可供选择,具有自动微分、基于图的优化计算和硬件加速等各种重要特性。.对于人们来说,享受这些重要功能带来的便利似乎是理所当然的事情。但实际上,看看这些特性下隐藏着什么,可以更好地帮助你理解这些网络是如何工作的。那么今天,文摘菌就来教大家如何搭建神经网络。原材料就是简单的python和numpy代码!文章中的所有代码都可以在这里获取。https://colab.research.google.com/github/eisenjulian/slides/blob/master/NN_from_scratch/notebook.ipynb符号说明在计算反向传播时,我们可以选择使用函数符号和变量符号来记录推导过程。它们对应于计算图中的边和节点来表示它们。给定R^n→R和x∈R^n,那么梯度就是一个由偏导数?f/?j(x)组成的n维行向量。若f:R^n→R^m且x∈R^n,则雅可比矩阵是由以下函数组成的m×n矩阵。对于给定的函数f和向量a和b,如果a=f(b)那么我们用?a/?b来表示雅可比矩阵,当a是实数时,它表示梯度。链式法则给定属于不同向量空间的三个向量a∈A和c∈C以及两个可微函数f:A→B和g:B→C使得f(a)=b和g(b)=c,我们可以得到复合函数的雅可比矩阵是函数f和g的雅可比矩阵的乘积:这就是著名的链式法则。1960年代和70年代提出的反向传播算法应用链式法则来计算实函数相对于其不同参数的梯度。知道我们的最终目标是沿着梯度的反方向逐渐找到函数的最小值(当然***是全局最小值),因为这样做会让函数值逐渐减小,至少局部是这样。当我们有两个参数需要优化时,整个过程如图所示:ReverseModeDerivation假设函数fi(ai)=ai+1由两个以上的函数组成,我们可以重复套用公式推导和获取:可以通过多种方式计算此乘积,最常见的是从左到右或从右到左。如果an是标量,那么我们可以通过先计算?an/?an-1,然后逐渐右乘所有雅可比矩阵?ai/?ai-1来计算整个梯度。这种操作有时称为VJP或向量-雅可比积(Vector-JacobianProduct)。又因为我们从计算?an/?an-1开始逐步计算?an/?an-2、?an/?an-3到***的梯度,并保存中间值,所以这个过程就是这个称为反向模式推导。最后,我们可以计算an相对于所有其他变量的梯度。相对而言,正向模式的过程正好相反。它首先将雅可比矩阵计算为?a2/?a1并左乘?a3/?a2以计算?a3/?a1。如果我们一直乘以?ai/?ai-1并保存中间值,最终我们可以得到所有变量相对于?a2/?a1的梯度。当?a2/?a1为标量时,所有的积都是列向量,称为雅可比向量积(或JVP,Jacobian-VectorProduct)。你可能已经猜到了,对于反向传播,我们更喜欢应用反向模式——因为我们想逐渐获得损失函数相对于每一层参数的梯度。虽然前向模式也可以计算出所需的梯度,但由于重复计算太多,效率低下。计算梯度的过程看起来像很多高维矩阵乘法,但实际上,雅可比矩阵往往是稀疏的、分块的或对角矩阵的,而由于我们只关心将它右乘的结果行向量,所以不需要消耗太多的计算和存储资源。在本文中,我们的方法主要用于逐层顺序构建的神经网络,但同样的方法也适用于计算梯度的其他算法或计算图。反向和正向模式的详细描述可以在这里找到?:http://colah.github.io/posts/2015-08-Backprop/DeepNeuralNetwork在一个典型的监督机器学习算法中,我们通常使用一个非常A复杂函数,其输入是一个张量,其中包含标记样本的数值特征。此外,还有很多用于描述模型的权重张量。损失函数是样本和权重的标量函数,它衡量模型输出与预期标签的差距。我们的目标是找到最合适的权重来最小化损失。在深度学习中,损失函数表示为一堆易于区分的简单函数的组合。所有这些简单的函数(除了最后一个),就是我们所说的层,每一层通常有两组参数:输入(可以是前一层的输出)和权重。虽然最后一个函数表示损失度量,但它也有两组参数:模型输出y和真实标签y^。例如,如果损失度量l是平方误差,则?l/?y是2avg(y-y^)。损失度量的梯度将是应用反向模式微分的起始行向量。Autograd自动推导背后的思想已经相当成熟。它可以在运行时或编译期间完成,但它的实现方式会对性能产生巨大影响。建议大家仔细阅读HIPSautograd的Python实现,真正理解autograd。核心理念从未改变。当我们在学校学习如何求导时,我们就应该知道这一点。如果我们可以追踪最终导致标量输出的计算,并且我们知道如何区分简单的运算(例如加法、乘法、求幂、指数、对数等),我们就可以计算输出的梯度。假设我们有一个线性中间层f,由矩阵乘法表示(暂时忽略偏置):为了用梯度下降调整w的值,我们需要计算梯度?l/?w。在这里我们可以观察到改变y以影响l是一个关键。每一层必须满足以下条件:如果给定损失函数相对于本层输出的梯度,则损失函数相对于本层输入(即上一层的输出)的梯度可以得到。现在两次应用链式法则得到损失函数相对于w的梯度:相对于x为:因此,我们既可以向后传递一个梯度来更新上一层,也可以更新层间的权重来优化损失,即它!动手实践看一下代码,或者直接试试ColabNotebook:https://colab.research.google.com/github/eisenjulian/slides/blob/master/NN_from_scratch/notebook.ipynb我们封装了一个类张量及其梯度开始。现在我们可以创建一个层类,关键思想是在前向传播过程中,我们返回这一层的输出和一个可以接受输出梯度和输入梯度的函数,并在这个过程中更新权重梯度。然后,训练过程将分为三个步骤,计算前向传播,然后计算反向传播,最后更新权重。这里的重点是先更新权重,因为权重可以在多个层中重复使用,我们更愿意在需要的时候更新它。classLayer:def__init__(self):self.parameters=[]defforward(self,X):"""Overrideme!"""param=Parameter(tensor)self.parameters.append(param)returnparamdefupdate(self,optimizer):forparaminself.parameters:optimizer.update(param)标准做法是将更新参数的工作交给优化器,优化器会在每批之后接收一个参数的实例。最简单和最著名的优化方法是小批量随机梯度下降。classSGDOptimizer():def__init__(self,lr=0.1):self.lr=lrdefupdate(self,param):param.tensor-=self.lr*param.gradientparam.gradient.fill(0)在此框架下,并使用After前面计算的结果,线性层看起来像这样:(1/inputs)selfself.weights=self.build_param(tensor)selfself.bias=self.build_param(np.zeros(outputs))defforward(self,X):defbackward(D):self.weights.gradient+=X.T@Dself.bias.gradient+=D.sum(axis=0)returnD@self.weights.tensor.TreturnX@self.weights.tensor+self.bias.tensor,backward接下来我们看另外一个常用的层,激活层.它们是逐点非线性函数。逐点函数的雅可比行列式是对角线的,这意味着当乘以梯度时,它是逐点相乘的。classReLu(Layer):defforward(self,X):mask=X>0returnX*mask,lambdaD:D*mask计算Sigmoid函数的梯度有点难度,也是逐点计算的:classSigmoid(Layer):defforward(self,X):S=1/(1+np.exp(-X))defbackward(D):returnD*S*(1-S)returnS,backward当我们依次构建很多层时,我们可以遍历它们并依次得到每一层的输出,我们可以将backward函数存储在一个列表中,在计算反向传播时使用它,这样我们就可以直接得到相对于输入层的损失梯度。太神奇了:classSequential(Layer):def__init__(self,*layers):super().__init__()self.layers=layersforlayerinlayers:self.parameters.extend(layer.parameters)defforward(self,X):backprops=[]Y=Xforlayerinself.layers:Y,backprop=layer.forward(Y)backprops.append(backprop)defbackward(D):forbackpropinreversed(backprops):D=backprop(D)returnDreturnY,backward如前所述,我们将需要为这批样本定义损失函数和梯度。一个典型的例子是MSE,它常用于回归问题。我们可以这样实现它:defmse_loss(Yp,Yt):diff=Yp-Ytreturnp.square(diff).mean(),2*diff/len(diff)就差不多了!现在,我们已经定义了两层,以及合并它们的方法,下面如何训练呢?我们可以使用类似于scikit-learn或Keras的API。classLearner():def__init__(self,model,loss,optimizer):self.model=modelself.loss=lossself.optimizer=optimizerdefit_batch(self,X,Y):Y_,backward=self.model.forward(X)L,D=self.loss(Y_,Y)backward(D)self.model.update(self.optimizer)returnLdeffit(self,X,Y,epochs,bs):losses=[]forepochinrange(epochs):p=np。随机排列(len(X))X,Y=X[p],Y[p]loss=0.0foriinrange(0,len(X),bs):loss+=self.fit_batch(X[i:i+bs],Y[i:i+bs])losses.append(loss)returnlosses就是这样!如果你顺着我的思路走,你可能会发现其实有几行代码是可以省略的。这段代码有效吗?现在我们可以用一些数据测试我们的代码。X=np.random.randn(100,10)w=np.random.randn(10,1)b=np.random.randn(1)Y=X@W+Bmodel=Linear(10,1)learner=Learner(model,mse_loss,SGDOptimizer(lr=0.05))learner.fit(X,Y,epochs=10,bs=10)一共训练了10轮。我们还可以检查学习到的权重是否与真实权重一致。print(np.linalg.norm(m.weights.tensor-W),(m.bias.tensor-B)[0])>1.848553648022619e-055.69305886743976e-06嗯,就这么简单。让我们再次尝试使用非线性数据集,比如y=x1x2,并通过添加一个sigmoid非线性层和另一个线性层来使我们的模型更加复杂。像这样:X=np.random.randn(1000,2)Y=X[:,0]*X[:,1]losses1=Learner(Sequential(Linear(2,1)),mse_loss,SGDOptimizer(lr=0.01)).fit(X,Y,epochs=50,bs=50)losses2=Learner(顺序(线性(2,10),Sigmoid(),线性(10,1)),mse_loss,SGDOptimizer(lr=0.3)).fit(X,Y,epochs=50,bs=50)plt.plot(losses1)plt.plot(losses2)plt.legend(['1Layer','2Layers'])plt.show()比较训练具有sigmoid激活函数的单层与两层模型的损失。***希望通过搭建这个简单的神经网络,你已经掌握了用python和numpy实现神经网络的基本思路。在这篇文章中,我们只定义了三种类型的层和一种损失函数,因此还有很多工作要做,但基本原理都是相似的。有兴趣的同学可以尝试实现更复杂的神经网络!参考资料:Thinc深度学习库:https://github.com/explosion/thincPyTorch教程:https://pytorch.org/tutorials/beginner/nn_tutorial。htmlCalculusonComputationalGraphs:http://colah.github.io/posts/2015-08-Backprop/HIPSAutograd:https://github.com/HIPS/autograd相关报告:https://eisenjulian.github.io/deep-learning-in-100-lines/【本文为专栏组织大数据文摘微信公众号“大数据文摘(id:BigDataDigest)”原创文章】点此查看更多此文好文作者
