在开始之前,先来看看最终的代码:1.分支和特征后端(https://github.com/OneRaynyDay/autodiff/tree/eigen)2.OnlyAbranchthatsupportsscalars(https://github.com/OneRaynyDay/autodiff/tree/master)这个项目是我和MinhLe一起完成的。为什么?如果您是计算机科学(CS)专业人士,您可能听过“不要自己动手____”这句话一千遍。它包括密码学、标准库、解析器等。我现在认为它还应该包括ML库。不管现实如何,这个有力的教训都值得学习。人们现在认为TensorFlow和类似的库是理所当然的。他们将其视为一个黑匣子并让它发挥作用,但没有多少人知道它在幕后是如何工作的。这只是一个非凸优化问题!请停止修改代码-只是为了让它看起来正确。TensorFlow在TensorFlow代码中有一个重要的组件,它允许您将计算串在一起以形成称为“计算图”的东西。这个计算图是一个有向图G=(V,E),其中在一些节点u1,u2,…,un,v∈V和e1,e2,…,en∈E,ei=(ui,v)。我们知道存在某种将u1,...,un映射到vv的计算图。例如,如果我们有x+y=z,则(x,z),(y,z)∈E。这对于计算算术表达式非常有用,我们可以在计算图的汇下找到结果。一个汇是一个像v∈V,?e=(v,u)这样的顶点。另一方面,这些顶点没有从它们自身到其他顶点的方向边界。同样,输入源为v∈V,?e=(u,v)。对于我们来说,我们总是将值放在输入源上,该值也会传播到接收器。反向模式下的微分如果你觉得我的解释不正确,可以参考这些幻灯片的解释。微分是Tensorflow中许多模型的核心要求,因为我们需要它来运行梯度下降。每个高中毕业的人都应该知道分化意味着什么。如果是基于基本函数的复杂函数,只需要求函数的导数,然后应用链式法则即可。超级简洁的概述如果我们有这样一个函数:关于x的导数:关于y的导数:其他例子:它的导数是:所以它的梯度是:链式法则,例如应用于f(g(h(x))):在5分钟内反转模式所以现在请记住,我们正在运行一个计算图,其中包含有向无环图(DAG/DirectedAcyclicGraph)和前面示例中使用的链式法则。如下图形式:x->h->g->f,可以得到f处的答案,也可以反过来:dx<-dh<-dg<-df,这样看起来像链式法则!我们需要将沿路径的导数相乘才能得到最终的结果。这是计算图的示例:这将其简化为图遍历问题。有没有人注意到这是拓扑排序和深度优先搜索/广度优先搜索?是的,为了在两个路径上都支持拓扑排序,我们需要包括一组父组和一组子组,而另一个方向的汇是源。反之亦然。执行在开学前,MinhLe和我开始设计这个项目。我们决定使用Eigen库后端进行线性代数运算。这个库有一个叫做MatrixXd的矩阵类,在我们的项目中用到了:,conststd::vector&);...//访问/修改当前节点值MatrixXdgetValue()const;voidsetValue(constMatrixXd&);op_typegetOp()const;voidsetOp(op_type);//访问内部(nomodify)std::vector&getChildren()const;std::vectorgetParents()const;...private://PImpliomrequiresforwarddeclarationoftheclass:std::shared_ptrpimpl;};structvar::impl{public:impl(constMatrixXd&);impl(op_type,conststd::vector&);MatrixXdval;op_typeop;std::vectorchildren;std::vector>parents;};在这里,我们使用一种称为“pImpl”的语法,意思是“指向执行的指针”。它有很多用途,比如接口的解耦实现,当栈上有本地接口时在内存堆上实例化东西。“pImpl”的一些副作用是运行时间稍慢,但编译时间快得多。这使我们能够在多个函数调用/返回中保持数据结构的持久性。像这样的树数据结构应该是持久的。我们有一些枚举来告诉我们当前正在进行哪些操作:enumclassop_type{plus,minus,multiply,divide,exponent,log,polynomial,dot,...none//nooperators.leaf.};执行这棵树的评估实际的类称为表达式:classexpression{public:expression(var);...//递归评估树.doublepropagate();...//Computesthederivativefortheentiregraph.//Performsatop-downevaluationofthetree.voidbackpropagate(std::unordered_map&leaves);...private:varroot;};在反向传播中,我们的代码可以做类似的事情:backpropagate(node,dprev):derivative=differentiate(node)*dprevforchildinnode.children:backpropagate(child,derivative)这几乎是在进行深度优先搜索(DFS),您发现了吗?为什么是C++?实际上,C++可能不太适合这种事情。我们可以花更少的时间用像“Oaml”这样的函数式语言进行开发。现在明白机器学习为什么用“Scala”了,主要是因为“Spark”。但是,使用C++有很多好处。Eigen(库名)例如,我们可以直接使用一个名为“Eigen”的TensorFlow线性代数库。这是一个线性代数库,不用考虑就可以使用。有一种类似于我们的表达式树的风格,我们在其中构建仅在真正需要时才计算的表达式。但是,使用“Eigen”可以在编译时决定何时使用模板,这意味着减少了运行时间。我非常尊重编写“Eigen”的人,因为看着模板错误几乎让我失明!他们的代码如下所示:MatrixA(...),B(...);autolazy_multiply=A.dot(B);typeid(lazy_multiply).name();//类名有点像Dot_Matrix_Matrix.Matrix(lazy_multiply);//函数式风格强制转换这个矩阵的评估。这个特征库非常强大,是TensortFlow的主要后端之一,原因是除了这种惰性评估技术之外,还有其他优化。运算符重载很适合用Java开发这个库——因为没有shared_ptrs、unique_ptrs、weak_ptrs;我们得到了一个真正有用的图形计算器(GC=GraphingCalculator)。这节省了大量的开发时间,更不用说更快的执行速度了。但是,Java不允许运算符重载,所以他们不能这样做://These3linescodeupanentireneuralnetwork!varsigm1=1/(1+exp(-1*dot(X,w1)));varsigm2=1/(1+exp(-1*dot(sigm1,w2)));varloss=sum(-1*(y*log(sigm2)+(1-y)*log(1-sigm2)));对了,上面是实际使用的代码。是不是很美?我会说这比TensorFlow中的Python包装器更优雅!我只是想证明它们也是矩阵。在Java中,拥有一个add()、divide()等链非常丑陋。更重要的是,这将使用户更加关注“PEMDAS”,同时C++运算符有非常好的表现。功能,而不是这个库中的一连串失败,可以肯定的是TensorFlow没有定义明确的API,或者有但我不知道。例如,如果我们只想训练一个特定的权重子集,我们可以只对我们感兴趣的特定来源进行反向传播。这对于卷积神经网络的迁移学习非常有用,因为很多时候,可以截断像VGG19这样的大型网络,然后附加一些额外的层,并使用来自新域的样本训练这些层的权重。基准在Python的TensorFlow库中,在10,000个“epochs”上训练iris数据集进行分类,并使用相同的超参数,我们有:TensorFlow的神经网络:23812.5ms“Scikit”神经网络:22412.2ms“Autodiff”神经网络,迭代,优化:25397.2毫秒“Autodiff”神经网络,迭代,无优化:29052.4毫秒“Autodiff”神经网络递归,无优化:28121.5毫秒令人惊讶的是,Scikit是其中最快的。这可能是因为我们没有做巨大的矩阵乘法。也可能是TensorFlow需要额外的编译步骤,如变量初始化等。或者我们可能必须在python而不是C中运行循环(Python循环真的非常糟糕!)我自己也不太确定。我完全理解这绝不是一个全面的基准,因为它只适用于特定情况下的单个数据点。但是这个库的性能并不能代表当前的state-of-art,希望读者能和我们一起改进。