随着对Tensorflow的不断深入使用,愈加觉得对Tensorflow设计理念和细节了解越多越有好处,特写一篇总结方便后续回顾和后续进一步深入学习
声明,以下内容主要参考两篇精品博客,推荐有空可以好好读一下:
以线性回归为例,深入理解tensorflow的Operation、Tensor、Node的区别
Tensorflow基本概念
一、概念总述
深度学习(神经网络)之所以具备智能,就是因为它具有反馈机制。深度学习具有一套对输出所做的评价函数(损失函数),损失函数在对神经网络做出评价后,会通过某种方式(梯度下降法)更新网络的组成参数,以期望系统得到更好的输出数据。
由此可见,神经网络的系统主要由以下几个方面组成:
- 输入
- 系统本身(神经网络结构),以及涉及到系统本身构建的问题:如网络构建方式、网络执行方式、变量维护、模型存储和恢复等等问题
- 损失函数
- 反馈方式:训练方式
- 定义好以上的组成部分,我们就可以用流程化的方式将其组合起来,让系统对输入进行学习,调整参数。因为该系统的反馈机制,所以,组成的方式肯定需要循环。
而对于Tensorflow来说,其设计理念肯定离不开神经网络本身。所以,学习Tensorflow之前,对神经网络有一个整体、深刻的理解也是必须的。如下图:Tensorflow的执行示意。
上图展示了Tensorflow的核心工作原理,即训练闭环由 Graph, target(output)、Loss、Variable 变量(Tensorflow中也是Variable)
那么对于以上所列的几点,什么才是最重要的呢?我想肯定是有关系统本身所涉及到的问题。即如何构建、执行一个神经网络? 在Tensorflow中,用计算图来构建网络,用会话来具体执行网络。深入理解了这两点,我想,对于Tensorflow的设计思路,以及运行机制,也就略知一二了。
- 图(tf.Graph):计算图,主要用于构建网络,本身不进行任何实际的计算。计算图的设计启发是高等数学里面的链式求导法则的图。可以将计算图理解为是一个计算模板或者计划书。
- 会话(tf.session):会话,主要用于执行网络。所有关于神经网络的计算都在这里进行,它执行的依据是计算图或者计算图的一部分,同时,会话也会负责分配计算资源和变量存放,以及维护执行过程中的变量。
另外,我们需要对tensorflow内部API有一个整体了解,tensorflow API 结构一览:
在上图中, 左边的部分是高层API, 高层API在牺牲部分灵活性的情况下可以帮助你快速的搭建模型; 右边的部分属于底层API, 灵活性较高,但模型的实现较为复杂,一个简单模型往往需要上千行代码。
接下来,我们主要从计算图开始,看一看Tensorflow是如何构建、执行网络的。
数据流图
tensorflow 定义的数据流图如下所示:
从上面可以看出,tensorflow的Graph明确定义为:由节点和有向边描述数学运算的有向无环图
。例如
- 调用
tf.constant(42.0)
可创建单个tf.Operation
,该操作可以生成值 42.0,将该值添加到默认图中,并返回表示常量值的 tf.Tensor。 - 调用
tf.matmul(x, y)
可创建单个 tf.Operation,该操作会将 tf.Tensor 对象 x 和 y 的值相乘,将其添加到默认图中,并返回表示乘法运算结果的 tf.Tensor。 - 执行
v = tf.Variable(0)
可向图添加一个 tf.Operation,该操作可以存储一个可写入的张量值,该值在多个 tf.Session.run 调用之间保持恒定。tf.Variable 对象会封装此操作,并可以像张量一样使用,即读取已存储值的当前值。tf.Variable
对象也具有assign
和assign_add
等方法,这些方法可创建 tf.Operation对象,这些对象在执行时将更新已存储的值。(请参阅变量了解关于变量的更多信息。) - 调用
tf.train.Optimizer.minimize
可将操作和张量添加到计算梯度的默认图中,并返回一个 tf.Operation,该操作在运行时会将这些梯度应用到一组变量上。
这里有一个很重要的概念:什么是节点,什么事Tensor?
前向图中的节点分为三类:
Operation
,主要为数学函数和表达式。比如图中的MatMul,BiasAdd和Softmax,绝大多数节点都属于此类。- 存储模型参数的张量(
Variable
):比如图中的和。 - 占位符(
placeholder
):比如图中的Input和Class Labels,通常用来描述输入、输出数据的类型和形状。
后向图中的节点同样分为三类
梯度值
:经过前向图计算出的模型参数的梯度,比如图中的Gradients更新模型参数的操作
:比如图中和,它们定义了如何将梯度值更新到对应的模型参数。更新后的模型参数
,比如图中SGD Trainer内的W和b,与前向图中的模型参数一一对应,但参数值得到了更新,用于模型的下一轮训练。
有向边:定义了节点之间的关系。分为两类:一类用于传输数据,绝大部分流动着张量的边都是此类,在图中使用实线表示,简称数据边。还有一种特殊边,一般画为虚线边,称为控制依赖,可以用于控制操作的运算,这被用来确保happens-before关系,这类边上没有数据流过,但源节点必须在目的节点开始执行前完成执行。例如tf.global_variables_initializer()
形成的边。
二、线性回归的完整实例
本文以一个两层神经网络来实现线性回归,代码如下:
下面,以线性回归为例,深入理解tensorflow的Operation、Tensor、Node的区别
前言:在使用tensorflow的时候,常常会被Operation、Tensor、Op_name、tensor_name
等等概念搞混淆,本文专门通过一个简单的例子来深入讲解他们之间的区别于本质,并且如何在tensorboard中进行查看。
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
#使用numpy生成200个随机点
x_data=np.linspace(-0.5,0.5,200)[:,np.newaxis]
noise=np.random.normal(0,0.02,x_data.shape)
y_data=np.square(x_data)+noise
#定义两个placeholder存放输入数据
x=tf.placeholder(tf.float32,[None,1],name="model_input")
y=tf.placeholder(tf.float32,[None,1],name="model_output")
#定义神经网络中间层
Weights_L1=tf.Variable(tf.random_normal([1,10]),name="Dense1_weights")
biases_L1=tf.Variable(tf.zeros([1,10]),name="Dense1_bias") #加入偏置项
Wx_plus_b_L1=tf.matmul(x,Weights_L1)+biases_L1
L1=tf.nn.tanh(Wx_plus_b_L1,name="Dense1_output") #加入激活函数
#定义神经网络输出层
Weights_L2=tf.Variable(tf.random_normal([10,1]),name="Dense2_weights")
biases_L2=tf.Variable(tf.zeros([1,1]),name="Dense2_bias") #加入偏置项
Wx_plus_b_L2=tf.matmul(L1,Weights_L2)+biases_L2
prediction=tf.nn.tanh(Wx_plus_b_L2,name="Dense2_ouput") #加入激活函数
#定义损失函数(均方差函数)
loss=tf.reduce_mean(tf.square(y-prediction),name="loss_function")
#定义反向传播算法(使用梯度下降算法训练)
optimizer = tf.train.GradientDescentOptimizer(0.1).minimize(loss,name="optimizer")
# tensorboard
loss_scaler = tf.summary.scalar('loss',loss)
merge_summary = tf.summary.merge_all()
# 模型保存
saver = tf.train.Saver()
gpu_options = tf.GPUOptions()
gpu_options.visible_device_list = "1"
gpu_options.allow_growth = True
config = tf.ConfigProto(gpu_options = gpu_options)
with tf.Session(config=config) as sess:
#变量初始化
sess.run(tf.global_variables_initializer())
writer=tf.summary.FileWriter('./tensorboard/linear_regression',graph=sess.graph)
#训练2000次
for i in range(2000):
opti,loss_,merge_summary_ = sess.run([optimizer,loss,merge_summary],feed_dict={x:x_data,y:y_data})
writer.add_summary(merge_summary_,global_step=i)
if i%100==0:
print(f"now is training {i+1} batch,and loss is {loss_} ")
save_path = saver.save(sess,"./linear_model/linear_model.ckpt")
print(f"model have saved in {save_path} ")
运行之后,会得到权重保存文件checkpoint,以及tensorboard日志文件,现在打开tensorboard,我们可以看到如下面的graph结构:
三、底层概念与实现
这一节主要讨论一下一些东西,即Graph、GraphDef、Node、NodeDef、Op、OpDef等概念
3.1 Graph与GraphDef
(1)tf.Graph类的定义
关键属性:
collections
关键方法:
add_to_collection
add_to_collections
as_default
as_graph_def
clear_collection
control_dependencies
create_op
with g.device
finalize
get_all_collection_keys
get_collection
get_collection_ref
get_name_scope
get_name_scope
get_operation_by_name
get_operations
get_tensor_by_name
在我们恢复模型的时候,要预测模型,需要知道模型的输入与输出的名称,就需要用到两个方法
get_operation_by_name
get_tensor_by_name
那怎么区分“tensor_name”和“operation_name”这两个概念呢?后面会讲到。
(2)tf.GraphDef类的定义——仅在tensorflow1.x中有
四个属性
library: FunctionDefLibrary library
node: repeated NodeDef node(graph中所有的节点定义)
version: int32 version
versions: VersionDef versions
四、如何获取node_name和tensor_name
前面说了,本质上graph的组成是节点(node,即operation)和边(tensor),而tensor是依赖于每一个节点的输出值的,到底怎么去获取节点名称和张量名称呢?
例子:
import tensorflow as tf
print(tf.__version__)
a = tf.constant([1], name = 'a') # 创建两个自己命名的常量
b = tf.constant([2], name = 'b')
aa = tf.constant([3]) # 创建两个使用默认名称的常量
bb = tf.constant([4])
x = tf.Variable(initial_value=[1,2,3],name="x") # 创建两个命名的变量
y = tf.Variable(initial_value=[3,2,1],name="y")
# 创建三个操作
a_b = tf.add(a, b, name = "a_add_b")
aa_bb = tf.add(aa, bb)
x_y = tf.add(x,y,name="x_add_y")
# 会话GPU的相关配置
gpu_options = tf.GPUOptions()
gpu_options.visible_device_list = "1"
gpu_options.allow_growth = True
config = tf.ConfigProto(gpu_options = gpu_options)
with tf.Session(config=config) as sess:
print('a tensor name is : %s, Op name is : %s' %(a.name, a.op.name))
print('b tensor name is : %s, Op name is : %s' % (b.name, b.op.name))
print('aa tensor name is : %s, Op name is : %s' %(aa.name, aa.op.name))
print('bb tensor name is : %s, Op name is : %s' % (bb.name, bb.op.name))
print('x tensor name is : %s, Op name is : %s' % (x.name, x.op.name))
print('y tensor name is : %s, Op name is : %s' % (y.name, y.op.name))
print('a_b tensor name is : %s, Op name is : %s' % (a_b.name, a_b.op.name))
print('aa_bb tensor name is : %s, Op name is : %s' % (aa_bb.name, aa_bb.op.name))
print('x_y tensor name is : %s, Op name is : %s' % (x_y.name, x_y.op.name))
# 每个操作节点(Op Node)是一个 NodeDef 对象,包含 name、op、input、device、attr 等属性
print("======================================================================")
# 遍历图中的所有节点
node_name_list = [node.name for node in tf.get_default_graph().as_graph_def().node]
for node_name in node_name_list:
print(node_name)
运行结果:
a tensor name is : a:0, Op name is : a
b tensor name is : b:0, Op name is : b
aa tensor name is : Const:0, Op name is : Const
bb tensor name is : Const_1:0, Op name is : Const_1
x tensor name is : x:0, Op name is : x
y tensor name is : y:0, Op name is : y
a_b tensor name is : a_add_b:0, Op name is : a_add_b
aa_bb tensor name is : Add:0, Op name is : Add
x_y tensor name is : x_add_y:0, Op name is : x_add_y
======================================================================
a
b
Const
Const_1
x/initial_value # x是一个变量,变量在graph中是圆角矩形,里面可以展开成一个字图subgraph,里面又包含一些子节点node
x
x/Assign
x/read
y/initial_value
y
y/Assign
y/read
a_add_b
Add
x_add_y
为什么tensor_name的格式为 “<op_name>:<output_index>”
实际上是因为,TensorFlow中自己所创建的节点node完全可以同名称,如果是两个节点名称相同,那我就没办法光通过node_name来区分不同的tensor了,如下所示
import tensorflow as tf
print(tf.__version__)
data = tf.placeholder(tf.float32, [None, 28*28],name="data")
label = tf.placeholder(tf.float32, [None, 10],name="data")
with tf.Session() as sess:
print('data tensor name is : %s, Op name is : %s' %(data.name, data.op.name))
print('label tensor name is : %s, Op name is : %s' % (label.name, label.op.name))
print("======================================================================")
# 遍历图中的所有节点
node_name_list = [node.name for node in tf.get_default_graph().as_graph_def().node]
for node_name in node_name_list:
print(node_name)
'''
data tensor name is : data:0, Op name is : data
label tensor name is : data_1:0, Op name is : data_1
======================================================================
data
data_1
'''
五、全文总结:
(1)注意区分node(operation)与tensor的本质区别
(2)注意tensor_name与node_name的区别,注意tensor_name的格式:“node_name:index”
(3)注意tensor_name与node_name的命名规则,特别是出现了同名operation的时候怎么处理
(4)建议:编写神经网络的时候,为了便于管理各个变量,权重、偏置、输入、输出等等,最好起一个易于辨识的名称,不要使用默认名称,这样跟方便实用。
(5)模型的核心是图,而图的核心是节点和Tensor,那么如果能理解这个原理就能对图的加载和切分做很灵活的操作