当前位置: 首页 > 科技观察

这是我见过的解释TensorFlow的最透彻的文章!

时间:2023-03-18 22:01:19 科技观察

介绍:“我叫Jacob,是GoogleAIResidency项目的学者。2017年夏天进入这个项目时,我已经有了丰富的编程经验,对机器学习有了深刻的理解,但之前从未使用过。我体验过TensorFlow。当时我以为凭着自己的能力可以很快的掌握Tensorflow,没想到学习它的过程会如此坎坷。即使加入项目几个月后,我仍然偶尔会感到困惑,不知道如何用Tensorflow代码实现自己的新想法。这篇博文就像是我写给往日自己的一封瓶中信:回首当初,多希望开始学习的时候有这样的介绍。也希望本文能对同行有所帮助,提供参考。《AIFrontline》翻译了这位现任GoogleBrain工程师关于学习Tensorflow过程中遇到的所有困难的文章,希望对大家有所帮助。过去的教程遗漏了什么?发布三年后,Tensorflow成为了Tensorflow的基石深度学习生态系统。然而,它对初学者来说不是很简单,特别是与PyTorch或DyNet等按运行定义的神经网络库相比。Tensorflow上有很多入门教程,涵盖线性回归、MNIST分类,甚至机器翻译.这些具体、实用的指南可以帮助人们快速上手并运行一个Tensorflow项目,并作为类似项目的切入点。然而,一些开发者开发的应用并没有很好的教程参考,一些项目正在探索新的路线(在研究中很常见。对于这些开发人员来说,开始使用TensorFlow是非常混乱的。我写这篇文章是为了填补那个空白。本文不会研究特定任务,而是提出一种更通用的方法并分析Tensorflow的基本抽象。掌握了这些概念后,用Tensorflow进行深度学习会更加直观易懂。目标读者本教程面向具有一定编程和机器学习经验并希望开始使用Tensorflow的从业者。他们可能是:CS专业的学生,??想在深度学习课程的最后一个项目中使用Tensorflow;刚转入涉及深度学习项目的软件工程师;问好雅各布)。如果您需要从基础开始,请参阅下面的资源。如果你明白这一点,让我们开始吧!了解TensorflowTensorflow不是普通的Python库。大多数Python库都是作为Python的自然扩展编写的。当你导入一个库时,你得到的是一组变量、函数和类,它们补充和扩展了你的代码“工具箱”。当您使用这些库时,您知道它们会产生什么。我认为在谈论TensorFlow时应该摒弃这些看法,这些看法从根本上不符合TensorFlow的理念,也没有反映TF与其他代码交互的方式。Python和Tensorflow之间的联系可以比作Javascript和HTML之间的关系。Javascript是一种功能齐全的编程语言,可以做各种很棒的事情。HTML是一种框架,用于表示某种类型的有用计算抽象(在这种情况下,可以由Web浏览器呈现的内容)。Javascript在交互式网页中的作用是组装浏览器看到的HTML对象,然后在需要时通过用新的HTML更新它来与之交互。与HTML一样,Tensorflow是一种框架,用于表示称为“计算图”的某些类型的计算抽象。当我们用Python操作Tensorflow时,我们用Python代码做的第一件事就是组装一个计算图。我们之后的第二个任务是与之交互(使用Tensorflow的“会话”)。但重要的是要记住计算图不在变量内部,它在全局命名空间内部。莎士比亚曾经说过:“所有的RAM都是一个舞台,所有的变量都不过是指针”。第一个重点抽象:计算图当我们浏览Tensorflow文档时,有时会发现内容中会提到“图”和“节点”。如果您仔细阅读并深入挖掘,您甚至可能会发现我在其中以更精确和更专业的方式更详细地解释了所涵盖的内容。本节将从顶层开始,抓住关键的直观概念,同时跳过一些技术细节。那么什么是计算图呢?它本质上是一个全局数据结构:计算图是一个有向图,它捕获有关计算方法的指令。让我们看看如何构建示例。下图中,上半部分是我们运行的代码及其输出,下半部分是结果计算图。显然,仅仅导入Tensorflow并不能给我们一个有趣的计算图,而只是一个孤独的、空白的全局变量。但是当我们调用Tensorflow操作时会发生什么?看!我们得到一个节点,其中包含常量:2。我知道您对有一个名为tf.constant的函数感到惊讶。当我们打印这个变量时,我们看到它返回一个tf.Tensor对象,它是指向我们刚刚创建的节点的指针。为了强调这一点,这里有另一个例子:每次调用tf.constant时,我们都会在图中创建一个新节点。即使节点在功能上与现有节点相同,即使我们将节点重新分配给同一个变量,或者即使我们根本不将其分配给变量,结果也是一样的。相反,如果您创建一个新变量并将其设置为等于现有节点,您只需将该指针复制到该节点,而不会向图中添加任何内容:好的,让我们更进一步。现在让我们看看——这才是我们想要的真正的计算图!注意+操作在Tensorflow中是重载的,所以同时添加两个张量会在图中添加一个节点,虽然看起来不像Tensorflow操作。好吧,那么two_node指向包含2的节点,three_node指向包含3的节点,sum_node指向包含...+的节点?会发生什么?它不应该包含5个吗?事实证明,没有。计算图只包含计算步骤,不包含结果。至少……还没有!第二个关键抽象:session对TensorFlow抽象的误解还有MarchMadness比赛(美国大学篮球赛繁忙的冠军赛季),“session”会是每年排名最好的种子选手。这种令人尴尬的荣誉是由于会话的反直觉命名,但它们无处不在——几乎每个Tensorflow程序都至少调用一次tf.Session()。会话的作用是处理内存分配和优化,使我们能够实际执行图形指定的计算。将计算图视为我们要执行的计算的“模板”:它列出了所有步骤。为了使用这个图,我们还需要发起一个会话,它可以让我们真正完成任务。例如,遍历模板的所有节点,分配一组内存用于存储计算输出。为了使用Tensorflow执行各种计算,我们需要图形和会话。一个会话包含一个指向全局图的指针,它会不断更新所有节点的指针。这意味着在创建节点之前或之后创建会话并不重要。创建会话对象后,您可以使用sess.run(node)返回节点的值,Tensorflow将执行任何必要的计算以确定该值。杰出的!我们还可以传递一个列表,sess.run([node1,node2,...]),并让它返回多个输出:一般来说,sess.run()调用往往是最大的TensorFlow瓶颈之一,因此调用它尽可能少的次数。如果可能,在一次sess.run()调用中返回多个项目,而不是进行多次调用。到目前为止,我们所做的占位符和feed_dict计算一直很乏味:没有机会获得输入,所以它们总是输出相同的东西。一个实际的应用程序可能涉及构建一个计算图,该图接受输入、以某种(一致的)方式处理它并返回输出。最直接的方法是使用占位符。占位符是接受外部输入的节点。...这是一个不好的例子,因为它会引发异常。占位符应该被赋予一个值,但我们没有提供,所以Tensorflow崩溃了。为了提供一个值,我们使用sess.run()的feed_dict属性。好多了。注意传递给feed_dict的数值的格式。这些key应该是图中占位符节点对应的变量(前面说了,其实就是指向图中占位符节点的指针)。相应的值是要分配给每个占位符的数据元素(通常是标量或Numpy数组)。第三个关键抽象:计算的路径下面是另一个使用占位符的例子:为什么第二次调用sess.run()会失败?我们没有检查input_placeholder,为什么会引发与input_placeholder相关的错误?答案在于最终的关键Tensorflow抽象:计算路径。幸运的是,这种抽象非常直观。当我们在依赖于图中其他节点的节点上调用sess.run()时,我们还需要计算这些节点的值。如果这些节点有依赖关系,那么我们需要计算这些值(以此类推...),直到我们到达计算图的“顶部”,也就是所有节点都没有前驱的情况。查看sum_node的计算路径:三个节点都需要求值,计算sum_node的值。最重要的是,它包含我们未填充的占位符,并解释了异常!相反,看看three_node的计算路径:根据图的结构,我们不需要计算所有节点来评估我们想要的节点!由于我们不需要评估placeholder_node来评估three_node,因此运行sess.run(three_node)不会抛出异常。Tensorflow仅通过必要的节点自动路由计算这一事实是它的巨大优势。如果计算图很大并且有很多不必要的节点,它可以节省大量的运行时间。它允许我们构建大型“多用途”图,这些图使用单个共享的核心节点集合来根据所采用的计算路径执行不同的任务。对于几乎所有应用程序,重要的是要考虑如何根据采用的计算路径调用sess.run()。变量和副作用到目前为止,我们已经看到了两种类型的“无祖先”节点:tf.constant(每次运行都相同)和tf.placeholder(每次运行都不同)。还有第三种节点:通常具有相同值,但也可以更新为新值的节点。这时候就用到了变量。了解变量对于使用Tensorflow进行深度学习至关重要,因为模型的参数是变量。在训练期间,您希望通过梯度下降在每一步更新参数,但在计算期间,您希望保持参数不变,并将大量不同的测试输入输入模型。模型的所有可训练参数都可能是变量。要创建变量,请使用tf.get_variable()。tf.get_variable()的前两个参数是必需的,其余的是可选的。它们是tf.get_variable(name,shape)。name是唯一标识此变量对象的字符串。它在全局图中必须是唯一的,因此请确保不会出现重复名称。shape是与张量形状对应的整数数组,其语法很直观——每个维度一个整数,并相应地排序。例如,一个3×8矩阵的形状可能是[3,8]。要创建标量,请使用空列表作为形状:[]。又发现了一个异常。当一个变量节点在***创建时,它的值基本上是“null”,任何试图计算它的操作都会抛出这个异常。我们只有先给变量赋值后才能用它来计算。有两种主要方法可用于为变量赋值:initializers和tf.assign()。让我们先看看tf.assign():与我们目前看到的节点相比,tf.assign(target,value)有一些独特的属性:身份操作。tf.assign(target,value)不做计算,它总是等于值。副作用。当计算“流经”assign_node时,它??会对图中的其他节点产生副作用。在这种情况下,副作用是将count_variable的值替换为存储在zero_node中的值。独立的边缘。尽管count_variable节点和assign_node在图中是连接的,但都不依赖于其他节点。这意味着在计算任何节点时,计算不会通过该边回流。然而,assign_node依赖于zero_node,它需要知道分配什么。“副作用”节点填充了大多数Tensorflow深度学习工作流程,因此请确保您很好地理解它们。当我们调用sess.run(assign_node)时,计算路径会经过assign_node和zero_node。当计算流经图中的任何节点时,它还会导致由该节点控制的副作用(以绿色显示)生效。由于tf.assign的特殊副作用,与count_variable关联的内存(之前为“null”)现在绝对设置为0。这意味着,当我们下次调用sess.run(count_variable)时,不会抛出异常。相反,我们将得到0。接下来,让我们看看初始化程序:这里发生了什么?为什么初始化程序不起作用?问题是会话和图形之间的分离。我们已经将get_variable的initializer属性指向const_init_node,但它只是在图中的节点之间添加了一个新的连接。我们没有做任何与导致异常相关的事情:与变量节点关联的内存(保存在会话中,而不是图形中)仍然是“空”。我们需要让const_init_node通过会话更新变量。为此,我们添加了另一个特殊节点:init=tf.global_variables_initializer()。类似于tf.assign(),这是一个有副作用的节点。与tf.assign()不同,我们实际上不需要指定它的输入!tf.global_variables_initializer()将在创建全局图时查看它,自动将依赖项添加到上级图中的每个tf.initializer。当我们调用sess.run(init)时,它会告诉每个初始化器完成它们的工作,初始化变量,以便在调用sess.run(count_variable)时不会出现错误。变量共享您可能会遇到具有变量共享的Tensorflow代码,代码具有其范围和“reuse=True”设置。我强烈建议您不要在代码中使用变量共享。如果要在多个地方使用单个变量,只需取一个指向该变量节点的指针,并在需要时使用它。换句话说,对于您打算保留在内存中的每个参数,您应该只调用一次tf.get_variable()。优化器***:做真正的深度学习!如果您仍处于这种状态,那么其余的概念对您来说应该非常简单。在深度学习中,典型的“内循环”训练如下:取输入和true_output根据输入和参数计算“猜测”根据猜测和true_output之间的差异计算“损失”根据梯度更新参数loss让我们将所有内容放在一个脚本中,并解决一个简单的线性回归问题:如您所见,损失基本上没有变化,我们对真实参数有很好的估计。这段代码只有一两行对你来说是新的:既然你已经很好地理解了Tensorflow的基本概念,那么这段代码应该很容易解释!***Line,optimizer=tf.train.GradientDescentOptimizer(1e-3)不向图中添加节点。它只是创建一个包含一些有用函数的Python对象。第二行,train_op=optimizer.minimize(loss),向图中添加一个节点,并为train_op分配一个指针。train_op节点没有输出,但有一个非常复杂的副作用:train_op回溯其输入的计算路径,寻找变量节点。对于它找到的每个变量节点,它计算关于损失的变量梯度。然后它为该变量计算一个新值:当前值减去梯度乘以学习率。***,它执行赋值操作来更新变量的值。基本上,当我们调用sess.run(train_op)时,它会为我们对所有变量进行梯度下降操作。当然,我们还需要使用feed_dict来填充输入输出占位符,我们还要打印这些损失,因为这样方便调试。使用tf.Print进行调试当您开始使用Tensorflow执行更复杂的操作时,您需要进行调试。通常,很难检查计算图中发生了什么。您不能使用常规的Python打印语句,因为您永远无法访问要打印的值——它们被锁定在sess.run()调用中。例如,假设您要检查计算的中间值,该中间值在调用sess.run()之前尚不存在。然而,当sess.run()调用返回时,中间值就没有了!让我们看一个简单的例子。我们看到结果是5。但是如果我们要查看中间值two_node和three_node呢?检查中间值的一种方法是在sess.run()中添加一个返回参数,指向每个要检查的中间节点,然后在返回后打印出来。这样做通常没有问题,但是当代码变得更复杂时,它可能会有点笨拙。更方便的方法是使用tf.Print语句。令人困惑的是,tf.Print实际上是一种具有输出和副作用的Tensorflow类型的节点!它有两个必需的参数:要复制的节点和要打印的内容列表。“要复制的节点”可以是图中的任意节点,tf.Print是与“要复制的节点”相关的识别操作,即输出其输入的副本。但是,它有打印“打印列表”中所有值的副作用。关于tf.Print的一个重要但有点微妙的点:打印实际上只是它的一个副作用。与所有其他副作用一样,打印仅在计算流经tf.Print节点时发生。如果tf.Print节点不在计算路径中,则不会打印任何内容。即使tf.Print节点正在复制的原始节点在计算路径上,但tf.Print节点本身可能不在。当心这个问题!发生这种情况时,可能会非常令人沮丧,您需要费力地找出问题所在。一般来说,最好在创建要复制的节点后立即创建tf.Print节点。这里(https://wookayin.github.io/tensorflow-talk-debugging/#1)有一个很好的资源和更实用的调试建议。结论希望本文能帮助您更好地了解Tensorflow、它的工作原理以及使用方法。毕竟,这里介绍的概念对所有Tensorflow程序都很重要,但它们只是触及表面。在您的Tensorflow冒险中,您可能会遇到您想要使用的各种其他有趣的东西:条件、迭代、分布式Tensorflow、变量范围、保存和加载模型、多图、多会话和多核数据加载器队列等.