TensorFlow的设计原则为:高性能,易开发,可移植
高性能
TensorFlow中的算子,设计过程中已经针对底层硬件架构进行了充分的优化.
针对生成的计算图,TensorFlow又提供了一系列的优化操作,以提升计算图的运行效率.
TensorFlow调度器可以根据网络结构特点,并发运行没有数据依赖的节点.
例如对于下面的例子:
import tensorflow as tf
a = tf.constant(1.0)
b = tf.constant(2.0)
c = tf.sin(a)
d = tf.cos(b)
e = tf.add(c, d)
with tf.Session() as sess:
sess.run(e)
上面例子中c
和d
没有依赖关系,可以并发执行.
易开发
TensorFlow针对现有的多种深度学习算法,提取了大的共性运算,并封装成算子.
用户使用TensorFlow进行算法开发时,能够直接调用这些算子,很方便的实现算法.
可移植
深度学习中通常采用梯度下降法来更新模型参数.
梯度计算比较直观,但对于复杂模型,手动计算梯度非常困难
目前大部分深度学习框架均提供自动梯度计算功能
用户只需描述前向计算的过程,由编程框架自动推导反向计算图,完成导数计算
常用的求导方法包括: 手动求解法,数值求导法,符号求导法,自动求导法.
即传统的反向传播算法: 手动用链式法则求解出梯度公式,代入数值,得到最终梯度值.
缺点:
利用导数的原始定义求解
f ′ ( x ) = lim h → 0 f ( x + h ) − f ( x ) h f'(x) = \lim_{h \rarr 0} \frac{f(x+h) - f(x)}{h} f′(x)=h→0limhf(x+h)−f(x)
优点:
缺点:
利用求导规则来对表达式进行自动操作,从而获得导数.
常见求导规则:
d d x ( f ( x ) + g ( x ) ) = d d x f ( x ) + d d x g ( x ) d d x ( f ( x ) g ( x ) ) = ( d d x f ( x ) ) g ( x ) + f ( x ) ( d d x g ( x ) ) d d x f ( x ) g ( x ) = f ′ ( x ) g ( x ) − f ( x ) g ′ ( x ) g ( x ) 2 \frac{d}{dx}(f(x)+g(x)) = \frac{d}{dx}f(x) + \frac{d}{dx}g(x) \\ \frac{d}{dx}(f(x) \, g(x)) = (\frac{d}{dx}f(x))g(x) + f(x)(\frac{d}{dx}g(x)) \\ \frac{d}{dx} \frac{f(x)}{g(x)} = \frac{f'(x)g(x) - f(x)g'(x)}{g(x)^2} dxd(f(x)+g(x))=dxdf(x)+dxdg(x)dxd(f(x)g(x))=(dxdf(x))g(x)+f(x)(dxdg(x))dxdg(x)f(x)=g(x)2f′(x)g(x)−f(x)g′(x)
缺点: 表达式膨胀问题
上述求导方法的对比:
计算图结构天然适用于自动求导:
计算图将多输入的复杂计算表达成了由多个基本二元计算组成的有向图,并保留了所有中间变量,有助于程序自动利用链式法则进行求导.
优点:
下面例子展示TensorFlow中注册Sin(x)函数反向求导方法:
@ops.RegisterGradient("Sin")
def _SinGrad(op, grad):
"""Returns grad * cos(x) """
x = op.inputs[0]
with ops.control_dependencies([grad]):
x = math_ops.conj(x)
return grad * math_ops.cos(x)
TensorFlow会自动生成对应的反向计算节点,并将其加入到计算图中.
v1 = tf.Variable(0.0, name="v1")
v2 = tf.Variable(0.0, name="v2")
loss = tf.add(tf.sin(v1), v2)
sgd = tf.train.GradientDescentOptimizer(0.01)
grads_and_vars = sgd.compute_gradient(loss)
计算过程: 分两步执行
原始函数建立计算图,数据正向传播,计算出中间节点 x i x_i xi,并记录计算图中的节点依赖关系.
反向遍历计算图,计算输出对于每个节点的导数
x i ˉ = ∂ y j ∂ x i \bar{x_i} = \frac{\partial{y_j}}{\partial{x_i}} xiˉ=∂xi∂yj
对于前向计算中一个数据( x i x_i xi)连接多个输出数据( y j y_j yj, y k y_k yk)的情况,自动求导中,将这些输出数据相对于该数据的导数累加.
x i ˉ = y j ˉ ∂ y j ∂ x i + y k ˉ ∂ y k ∂ x i \bar{x_i} = \bar{y_j} \frac{\partial{y_j}}{\partial{x_i}} + \bar{y_k} \frac{\partial{y_k}}{\partial{x_i}} xiˉ=yjˉ∂xi∂yj+ykˉ∂xi∂yk
方法 | 对图的遍历次数 | 精度 | 备注 |
---|---|---|---|
手动求解法 | N A N_A NA | 高 | 实现复杂 |
数值求导法 | n i + 1 n_i+1 ni+1 | 低 | 计算量大,速度慢 |
符号求导法 | N A N_A NA | 高 | 表达式膨胀 |
自动求导法 | n o + 1 n_o+1 no+1 | 高 | 对输入维度较大的情况优势明显 |
其中:
在模型训练过程中,使用tf.train.Saver()
来保存模型中的所有变量.
# 实例化Saver对象
saver = tf.train.Saver()
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(1000000):
# 执行模型训练
sess.run(training_op)
if step % 1000 == 0:
# 将训练得到的变量值保存到检查点文件中
saver.save(sess, './ckpt/nfiy-model')
当需要基于某个checkpoint继续训练模型参数时,需要从.ckpt文件中恢复出已保存的变量.
同样使用tf.train.Saver()
来恢复变量,恢复变量时不需要先初始化变量.
# 实例化Saver对象
saver = tf.train.Saver()
with tf.Session() as sess:
# 找到存储变最值的位置
ckpt = tf.train.latest_checkpoint(model_path)
# 恢复变量
saver.restore(sess, ckpt)
print(sess.run(weights))
TensorFlow通过向计算图中插入Save节点及其关联节点来完成保存模型的功能;在恢复模型时,也是通过在计算图中插入Restore节点及其关联节点来完成.
TensorFlow中使用控制流算子来实现不同复杂控制流场景.
通过引入少量简单基础操作,为多样的TensorFlow应用提供丰富的控制流表达.
在TensorFlow中,每一个操作都会在一个执行帧中北执行,控制流操作负责创建和管理这些执行帧.
Switch: 一个Switch操作根据控制输入p的布尔值,将一个输入张量d
推进到某一个输出(二选一).
Merge: Merge操作将它的其中一个输入推向输出.当一个Merge操作的任意一个输入准备好之后,Merge操作就会执行.
Enter(name): Enter操作将它的输入推向名为name的执行帧.
Exit: Exit操作,将一个张量从一个子执行帧推向它的辅执行帧.它的作用时将张量从子执行帧返回给父执行帧.
NextIteration: NextIteration操作将一个张量从当前执行帧的一轮迭代传递到下一轮迭代.
在一个执行帧内部可能有多个NextIteration操作.当执行帧的第N轮执行的第一个NextIteration操作开始执行时,TensorFlow的运行时开始执行第N+1轮的迭代.
当更多的张量通过了NextIteration操作进入新的执行轮次时,新执行伦茨中更多的操作就会开始运行.当输入准备完成之后,NextIteration操作开始执行.
条件表达式tf.cond(pred, true_fn, false_fn)
:
以条件表达式tf.cond(x > y, lambda: tf.subtract(x, y), lambda: tf.add((x, y)))
为例:
# 添加Switch节点
switch_f, switch_t = Switch(pred, pred)
# 创建Switch为真时的环境
ctx_t = MakeCondCtx(pred, switch_t, branch=1)
# 创建Switch为真时的计算图
res_t = ctx_t.Parse(true_fn)
# 创建Switch为假时的环境
ctx_f = MakeCondCtx(pred, switch_f, branch=0)
# 创建Switch为假时的计算图
res_f = ctx_tf.Parse(false_fn)
# 将两个分支结果合并到一起
rets = [Merge([f, t]) for(f, t) in zip(res_f, res_t)]
循环操作tf.while_loop_v2(cond, body, loop_vars)
:
以循环操作tf.while_loop(lambda i: i < 16, lambda i: tf.multiply(i, 2), [4])
# 创建环境
while_ctx = WhileContext()
while_ctx.Enter()
# 为每个循环变暈添加一个Enter节点
enters = [Enter(x, frame_name) for x in loop_vars]
# 添加Merge节点,Merge节点的第二个输入稍后会被更新
merges = [Merge([x, x]) for x in enters]
# 构建循环子图
pred_results = pred(*merges)
# 添加Switch节点
switchs = [Switch(x, pred_result) for x in merges]
# 构建循环体
body_res = body(*[x[1] for x in switchs])
# 添加Nextltreation节点
nexts = [Nextlteration(x) for x in body_res]
# 构建循环迭代
for m, n in zip(merge_vars, nexts):
m.op.update(l, n)
# 添加退出节点
exits = [Exit(x[0]) for x in switchs]
while_ctx.Exit()
client: 通过session接口与master和worke接口通信.
worker可以是一个,也可以是多个.
master: 控制所有的worker按照计算图执行.
worker: 每一个worker负责一个或多个计算设备的仲裁访问,并根据master的指令,执行这些计算设备中 的计算图节点.
设备: 可以是CPU核或GPU卡.
简单示例:
import tensorflow as tf
x = tf.constant(8)
y = tf.constant(9)
z = tf.multiply(x, y)
with tf.Session() as sess:
put_z = sess.run(z)
具体的执行模式有: 本地单设备执行, 本地多设备执行, 分布式执行.
本地单设备执行:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiZz7Mwx-1590138801492)(image-20200522122257519 - 副本.png)]
最简单的执行场景:适用于一个worker进程中仅包含一个设备的情况.在该情况下:
本地多设备执行:
模型训练过程:
分布式执行:
该模式下,client、master和worker可以工作于不同机器上的不同进程中
该模式兼容本地多设备执行模式.
计算图本地执行的过程包括: 计算图剪枝,计算图分配,计算图优化,计算图切分.
目的: 得到本地运行的最小子图.
包括:
为输入输出建立与外界的交互:
这种函数调用帧类似于函数调用时的栈帧.
Source和Sink是两个特殊的节点,Source节点的入度为0,Sink节点的出度为0.
去除与最终输出节点无关的节点和边:
计算图分配解决了多设备运行环境中,对计算图中的每个节点如何分配计算设备的问题.
使用开销模型cost model算法分配计算图中的节点,该算法考虑图中每个节点的输入输出tensor的数据量、每个节点的预计计算时间.
算法执行过程:
计算图优化的手段包括:常量折叠ConstFold,算数优化Arithmetic,布局优化Layout,算子融合Remapper.
常量折叠ConstFold: 有的常数节点可以被提前计算,用得到的结果生成新的节点 来代替原来的常数节点.主要由三个关键函数组成:
算数优化Arithmetic: 包括公共子表达式消除,算术简化.
布局优化Layout:
TensorFlow中默认采用NHWC格式,而GPU中使用NCHW格式.
两个连续的GPU计算节点之间的连续NCHW2NHWC和NHWC2NCHW转换应互相抵消去除.
分为两类: 点到点通信(Point-to-Point Communication)和集合通信(Collective Communication).
TensorFlow中实现了集合通信的基本算子: