为了成为高效,灵活且可立即投入生产的库,TensorFlow使用数据流图来表示各个操作之间的关系。数据流是广泛用于并行计算的编程模型,在数据流图中,节点表示计算单元,而边沿表示计算单元消耗或产生的数据。
这篇文章摘自 Ekta Saraogi和Akshat Gupta撰写的《使用TensorFlow 2.0的动手神经网络》。Packt Publishing的这本书解释了TensorFlow的工作原理,从基础到高级,都采用了基于案例研究的方法。
使用图表示计算的优点是能够通过梯度下降来运行训练参数化机器学习模型所需的前向和后向遍历,将链式规则应用为每个节点的局部过程来计算梯度;但是,这并不是使用图形的唯一优点。
降低抽象级别并考虑使用图形表示计算的实现细节将带来以下优点:
并行性:TensorFlow使用节点表示操作和表示其依赖关系的边,从而能够识别可以并行执行的操作。
计算优化:作为图形,众所周知的数据结构,可以以优化执行速度为目标对其进行分析。例如,可以检测图中的未使用节点并将其删除,从而针对大小进行优化。还可以检测到冗余操作或次优图形,并用最佳替代方法进行替换。
可移植性:图形是计算的语言无关和平台无关的表示。TensorFlow使用协议缓冲区(Protobuf),这是一种简单的语言无关,平台无关且可扩展的机制,用于序列化结构化数据以存储图。实际上,这意味着可以将使用TensorFlow在Python中定义的模型保存为与语言无关的表示形式(Protobuf),然后在用另一种语言编写的另一个程序中使用。
分布式执行:每个图的节点都可以放置在独立的设备和不同的机器上。TensorFlow将负责节点之间的通信,并确保图形的执行正确。此外,TensorFlow本身能够在多个设备上划分图,知道某些操作在某些设备上的性能更好。
主要结构– tf.Graph
Python简化了图描述阶段,因为它甚至可以创建图而无需显式定义它。实际上,定义图形有两种不同的方法:
隐式:只需使用*方法定义图。如果未明确定义图,TensorFlow始终定义默认值tf.Graph,可通过调用来访问tf.get_default_graph。隐式定义限制了TensorFlow应用程序的表达能力,因为它仅限于使用单个图形。
明确的:可以明确定义一个计算图,因此每个应用程序可以包含多个图。该选项具有更强的表达能力,但通常不需要,因为需要多个图形的应用程序并不常见。
为了明确定义图,TensorFlow允许创建tf.Graph对象,这些对象通过as_default方法创建上下文管理器;在上下文中定义的每个操作都放在关联的图内。实际上,一个tf.Graph对象tf.Operation为其包含的对象定义一个名称空间。
tf.Graph结构的第二个特点是它的图形集合。每个人都tf.Graph使用收集机制来存储与图结构关联的元数据。集合由键唯一标识,并且其内容是对象/操作的列表。用户通常不需要担心集合的存在,因为TensorFlow本身会使用它们来正确定义图形。
图形定义–从tf.Operation到tf.Tensor
数据流图是计算的表示,其中节点表示计算单位,边缘表示计算消耗或产生的数据。
在的上下文中tf.Graph,每个API调用都定义tf.Operation(节点),该节点可以具有多个输入和输出tf.Tensor(边)。例如,参考我们的主要示例,在调用时tf.constant([[1
由于图中的每个节点都是唯一的,因此如果图中已经有一个名为Const的节点(这是所有常量的默认名称),TensorFlow将通过添加后缀'_1','_ 2',以此类推。如果未提供名称,例如在我们的示例中,TensorFlow将为每个添加的操作提供默认名称,并添加后缀以使它们在这种情况下也唯一。
输出tf.Tensor具有与关联的名称相同的名称tf.Operation,并带有:ID后缀。该ID是一个渐进的数字,表示操作多少输出产生。在的情况下tf.constant,输出只是一个张量,因此ID = 0;但是可以有多个输出的操作,在这种情况下,后缀:0,:1等将添加到tf.Tensor该操作生成的名称中。
也可以在上下文(由tf.name_scope调用定义的上下文)内创建的所有操作中添加名称范围前缀。缺省名称作用域前缀是所有活动tf.name_scope上下文管理器名称的/分隔列表。为了保证在范围内定义的操作的唯一性和范围本身的唯一性,使用与tf.Operation保留相同的后缀附加规则。
以下代码段显示了如何将基线示例包装到单独的图形中,如何在同一Python脚本中创建第二个独立图形,以及如何使用来更改节点名称,添加前缀tf.name_scope。首先,我们导入TensorFlow库:
(tf1)
将tensorflow作为tf导入
然后,我们定义两个tf.Graph对象(作用域系统使您可以轻松使用多个图):
g1 = tf.Graph()
g2 = tf.Graph(),
带有g1.as_default():
A = tf.constant([[[1,2],[3,4]],dtype = tf.float32)
x = tf .constant([[0,10],[
0,0.5 ]])b = tf.constant([[1,-1]],dtype = tf.float32)
y = tf.add(tf.matmul(A, x),b,name =“ result”)
和g2.as_default():
与tf.name_scope(“ scope_a”):
x = tf.constant(1,name =“ x”)
print(x)
与tf.name_scope (“ scope_b”):
x = tf.constant(10,name =“ x”)
print(x)
y = tf.constant(12)
z = x * y
然后,我们定义了两个摘要作者。我们需要使用两个不同的tf.summary.FileWriter对象来记录两个单独的图。
writer = tf.summary.FileWriter(“ log / two_graphs / g1”,g1)
writer = tf.summary.FileWriter(“ log / two_graphs / g2”,g2)
writer.close()
运行示例,并使用TensorBoard可视化两个图形,使用TensorBoard上的左侧列在“运行”之间切换。
具有相同名称(在示例中为x)的节点可以一起生活在同一图中,但是它们必须处于不同范围内。实际上,处于不同范围内会使节点完全独立且对象完全不同。实际上,节点名称不仅是传递给操作定义的参数名称,而且是包含所有前缀的完整路径。
实际上,运行脚本时,输出如下:
Tensor(“ scope_a / x:0”,shape =(),dtype = int32)
Tensor(“ scope_b / x:0”,shape =(),dtype = int32)
正如我们所看到的,全名是不同的,并且我们还具有有关生成的张量的其他信息。通常,每个张量都有一个名称,类型,等级和形状:
该名称在计算图中唯一标识张量。使用name_scope,我们可以为张量名称添加前缀,从而更改其完整路径。我们还可以使用每个tf.*API调用的name属性来指定名称。
的类型是张量的数据类型; 例如,float32,tf.int8等。
TensorFlow世界中的等级(与严格的数学定义不同)仅是张量的维数。例如,标量的等级为0,向量的等级为1,矩阵的等级为2,依此类推。
的形状是在每一维的元素的数目; 例如,标量的等级为0,空形状为(),矢量的等级为1,形状为(D0),矩阵的等级为2,形状为(D0,D1),依此类推。
作为C ++库,TensorFlow严格是静态类型的。这意味着必须在图定义时知道每个操作/张量的类型。此外,这还意味着不可能在不兼容的类型之间执行操作。
仔细观察基线示例,可能会发现矩阵乘法和加法运算都是在具有相同类型tf.float32的张量上执行的。已经定义了由Python变量A和b标识的张量,从而在操作定义中使类型清晰,而张量x具有相同的tf.float32类型;但是在这种情况下,它是通过Python绑定来推断的,该绑定能够查看常量值的内部并推断创建操作时要使用的类型。
Python绑定的另一个特殊之处是使用操作符重载简化了一些常见数学运算的定义。最常见的数学运算为tf.Operation:因此,使用运算符重载来简化图形定义是很自然的。
下表显示了TensorFlow Python API中超载的可用运算符:
Python运算子
操作名称
__neg__
一元 -
__abs__
abs()
__invert__
一元 ~
__add__
二元 +
__sub__
二元 -
__mul__
二元元素 *
__floordiv__
二元 //
__truediv__
二元 /
__mod__
二元 %
__pow__
二元 **
__and__
二元 &
__or__
二元 |
__xor__
二元 ^
__le__
二元 /kbd>
__lt__
二元 <=
__gt__
二元 >
__ge__
二元 <=
__matmul__
二元 @
运算符重载允许更快的图形定义,并且完全等同于其tf.*API调用(例如,__add__using与tf.add函数使用相同)。在仅一种情况下,使用TensorFlow API调用而不是关联的运算符重载是有益的:需要操作的名称时。
图形放置– tf.device
tf.device创建与设备匹配的上下文管理器。该功能允许用户请求将在其创建的上下文中创建的所有操作放置在同一设备上。所标识的设备tf.device不仅仅是物理设备;实际上,它能够识别诸如远程服务器,远程设备,远程工作器以及不同类型的物理设备(GPU,CPU和TPU)之类的设备。需要遵循设备规范以正确指示框架使用所需的设备。设备规范具有以下形式:
/job:<JOB_NAME>/task:<TASK_INDEX>/device:<DEVICE_TYPE>:<DEVICE_INDEX>
细分如下:
<JOB_NAME>是不以数字开头的字母数字字符串
<DEVICE_TYPE>是已注册的设备类型(例如GPU或CPU)
<TASK_INDEX>是一个非负整数,表示名为的作业中任务的索引 <JOB_NAME>
<DEVICE_NAME>是表示设备索引的非负整数;例如,/ GPU:0是第一个GPU
无需指定设备规范的每个部分。例如,当使用单个GPU运行单机配置时,您可能会用于tf.device将某些操作固定到CPU和GPU。
因此,我们可以扩展基线示例,以将操作放置在我们选择的设备上。因此,可以将矩阵乘法放在GPU上,因为它是针对此类操作进行硬件优化的,同时将所有其他操作保留在CPU上。
请注意,由于这仅是图形描述,因此不需要物理上具有GPU或使用tensorflow-gpu程序包。首先,我们导入TensorFlow库:
(tf1)
将tensorflow作为tf导入
现在,使用上下文管理器将操作放在不同的设备上,首先,在本地计算机的第一个CPU上:
with tf.device("/CPU:0"):
A = tf.constant([[1
x = tf.constant([[0
b = tf.constant([[1
然后,在本地计算机的第一个GPU上:
with tf.device("/GPU:0"):
mul = A @ x
当设备没有受到示波器的强制时,TensorFlow会决定哪个设备最好在以下位置进行操作:
y = mul + b
然后,我们定义摘要编写器:
writer = tf.summary.FileWriter("log/matmul_optimized"
writer.close()
如果查看生成的图,就会发现它与基线示例生成的图相同,但有两个主要区别:
我们没有为输出张量取一个有意义的名称,而只有默认名称
单击矩阵乘法节点,可以看到(在TensorBoard中)此操作必须在本地计算机的第一个GPU中执行
所述matmul节点被放置在本地机器的第一GPU,而任何其他操作在CPU中执行。TensorFlow以透明的方式处理不同设备之间的通信:
图执行– tf.Session
tf.Session TensorFlow提供了一个类来表示Python程序和C ++运行时之间的连接。
该tf.Session对象是唯一能够与硬件直接通信(通过C ++运行时),使用本地和分布式TensorFlow运行时在指定设备上进行操作的对象,目的是具体构建定义的图形。该tf.Session对象经过高度优化,并且一旦正确构建,就进行缓存tf.Graph以加快其执行速度。
作为物理资源的所有者,tf.Session必须将对象用作文件描述符以执行以下操作:
通过创建一个Session(相当于open操作系统调用)来获取资源
使用资源(相当于read/write在文件描述符上使用操作)
使用tf.Session.close(相当于close调用)释放资源
通常,通过上下文管理器使用会话,而不是手动定义会话并负责创建和销毁会话,上下文管理器会在块退出时自动关闭会话。
的构造函数tf.Session相当复杂且可高度自定义,因为它用于配置和创建计算图的执行。
在最简单和最常见的情况下,我们只想使用当前的本地硬件来执行上述计算图,如下所示:
(tf1)
# The context manager opens the session
with tf.Session() as sess:
# Use the session to execute operations
sess.run(...)
# Out of the context
在更复杂的场景中,我们不想使用本地执行引擎,而是使用远程TensorFlow服务器,该服务器可以访问它控制的所有设备。这可能是通过指定target parameter的tf.Session使用URL(只是grpc://服务器):
(tf1)
# the IP and port of the TensorFlow server
ip = "192.168.1.90"
port = 9877
with tf.Session(f"grpc://{ip}:{port}") as sess:
sess.run(...)
默认情况下,tf.Session将捕获并使用默认tf.Graph对象,但是当使用多个图形时,可以通过使用graph参数指定要使用的图形。很容易理解为什么使用多个图形是不寻常的,因为即使该tf.Session对象一次只能处理一个图形。
对象的第三个也是最后一个参数tf.Session是通过config参数指定的硬件/网络配置。该配置是通过tf.ConfigProto对象指定的,该对象能够控制会话的行为。该tf.ConfigProto对象相当复杂,并且带有选项,最常用和广泛使用的是以下两个(所有其他选项都是在分布式,复杂环境中使用的选项):
allow_soft_placement:如果设置为True,它将启用软设备放置。并非所有操作都可以无差别地放在CPU和GPU上,因为例如可能缺少该操作的GPU实现,并且使用此选项时,TensorFlow可以忽略通过device制定的设备规范,并将该操作放置在正确的设备上。在图形定义时指定了不支持的设备。
gpu_options.allow_growth:如果设置为True,它将更改TensorFlow GPU内存分配器;默认分配器在tf.Session创建后立即分配所有可用的GPU内存,而allow_growth为True时使用的分配器会逐渐增加分配的内存量。默认分配器以这种方式工作,因为在生产环境中,物理资源完全专用于tf.Session执行,而在标准研究环境中,资源通常是共享的(GPU是可以供其他进程使用的资源,而TensorFlow tf.Session正在执行)。
现在可以将基线示例扩展为不仅定义图,而且可以继续进行有效的构造和执行:
import tensorflow as tf
import numpy as np
A = tf.constant([[1
x = tf.constant([[0
b = tf.constant([[1
y = tf.add(tf.matmul(A
writer = tf.summary.FileWriter("log/matmul"
writer.close()
with tf.Session() as sess:
A_value
y_value = sess.run(y)
# Overwrite
y_new = sess.run(y
print(f"A: {A_value}\nx: {x_value}\nb: {b_value}\n\ny: {y_value}")
print(f"y_new: {y_new}")
第一次sess.run调用会评估这三个tf.Tensor对象,A
第二个呼叫,sess.run(y)以以下方式工作:
是操作的输出节点:回溯到其输入
递归地回溯遍历每个节点,直到找到所有没有父节点的节点
评估输入;在这种情况下,A
遵循依赖关系图:乘法运算必须在将其结果与相加之前执行 b
执行矩阵乘法
执行添加
加法运算是图形分辨率(Python变量y)的入口,计算结束。
因此,第一个打印调用将产生以下输出:
A: [[1. 2.]
[3. 4.]]
x: [[ 0. 10. ]
[ 0. 0.5]]
b: [[ 1. -1.]]
y: [[ 1. 10.]
[ 1. 31.]]
第三次sess.run调用显示了如何从外部以numpy数组形式将计算图值注入覆盖节点。该feed_dict参数允许您执行此操作:通常,使用feed_dict参数将输入传递给图形,并通过覆盖tf.placeholder为此目的而精确创建的操作。
tf.placeholder只是创建一个占位符,目的是当外部值没有注入到图形内部时引发错误。但是,该feed_dict参数不仅仅是馈送占位符的一种方式。实际上,前面的示例显示了如何使用它来覆盖任何节点。由Python变量标识的节点的覆盖产生的结果如下所示b,该numpy数组必须在类型和形状方面都与覆盖的变量兼容:
y_new: [[ 0. 11.]
[ 0. 32.]]
基线示例已更新,以显示以下内容:
如何建立图表
如何保存图形的图形表示
如何创建会话并执行定义的图
到目前为止,我们已经使用了具有恒定值的图,并使用feed_dict了sess.run调用的参数来覆盖节点参数。但是,由于TensorFlow旨在解决复杂问题,因此tf.Variable引入了的概念:可以使用TensorFlow定义和训练每个参数化机器学习模型。
在这篇文章中,我们讨论了数据流图如何在TensorFlow中工作。要知道,在TensorFlow中实现卷积神经网络以通过Churn预测案例研究和从X射线案例研究中检测肺炎,请阅读Packt出版的《使用TensorFlow 2.0进行动手神经网络》一书。

关注 CDA人工智能学院 ,回复“录播”获取更多人工智能精选直播视频!


雷达卡



京公网安备 11010802022788号







