在理解了建构神经网络的大致函数用途,且熟悉了神经网络原理后,我们已经大致具备可以编写神经网络的能力了,在涉及比较复杂的神经网络结构前,还有两件重要的事情需要了解,那就是中途存档和事后读取的函数,它攸关到庞大的算力和时间投入后产出的结果是否能够被再次使用,是一个绝对必须弄清楚的环节。另外是 Tensorflow 提供的的一个工具名为 Tensorboard,它可以很有效率的为我们呈现数据流图可视化的过程,包含了计算的结果和数据分布的状态,让我们在寻找错误的时候有一个更为清晰的逻辑脉络,因此本节主要围绕这两个主题展开:
Checkpoint
tf.train.Saver().save()
tf.train.Saver().restore()
Tensorboard
前者如同会议记录一般,可以针对性的把训练过程记录下来,除了避免前功尽弃之外,还可以让我们有机会一窥训练过程的究竟,从演变过程中寻找改善算法的方案;而后者提供一个在浏览器梳理计算过程的核心工具,提升了整体的开发效率与优化参数的过程。
p.s. 关于设备如果手边没有,非常建议直接使用云端的计算服务,如 AWS, FloydHub 等平台
在初期一般训练模型简单且训练速度极快,对于参数中间变化的过程我们也不会特别在意,但是到了复杂的神经网络训练过程时,为参数训练过程中途存档这件事情就会变得非常重要,这就像我们玩电玩游戏闯关的时候,希望最好能够中途存档,如果死在半路上可以直接从存档的地方恢复游戏。
同理深度学习训练过程,一般训练耗费时间约为几天乃至一周,如果中途发生机器停机或是任何意外导致训练终止,我们可以从检查点记录的地方重新开始。抑或者如果我们要分析训练过程中参数的变化走势,检查点也非常实用。使用的类为:
tf.train.Saver(max_to_keep=None) 档名: 「.ckpt」
.Saver({’save_w‘: weight}) 括弧中可以用字典的方式指定只要储存哪一个参数
max_to_keep=None: 最多有几个检查点被保存下来,如果是 None 或是 0 则表示全保存
keep_checkpoint_every_n_hours=1: 设置几个小时保存一次检查点
变量以二进制的方式被存在名为 .ckpt 的档案中,内容包含了变量的名字和对应张量的数值,创建一个该类的示例,就可以呼叫里面储存与载入储存文件内容的函数方法:
tf.train.Saver().save(sess, './file_directory', global_step=int(num))
sess: 表示要储存哪个绘话里面的参数
'./file_directory/file_name': 储存的路径沿着执行训练的 .py 文档路径位置继续指定路径,如果文件夹不存在指定目录的话,它会自行创建。官网教程中建议档名后面连同后缀一起加上,如下代码…
global_step:指定一个数字,将一起被纳入检查点文件命名中
!!! 储存这些参数的时候特别需要注意申明清楚参数的数据类型非常重要,它攸关到之后要呼叫回这些参数的时候是否顺利,如果没有事先申明清楚,大概率上会有错误发生。
下面代码展示如何保存检查点:
1import numpy as np
2import tensorflow as tf
3
4x_data = np.random.rand(100).astype(np.float32)
5y_data = x_data * 0.1 + 0.3
6
7weight = tf.Variable(tf.random_uniform(shape=[1], minval=-1.0, maxval=1.0),
8 dtype=np.float32, name='weight')
9bias = tf.Variable(tf.zeros(shape=[1]), dtype=np.float32, name='bias')
10y = weight * x_data + bias
11
12loss = tf.reduce_mean(tf.square(y - y_data))
13optimizer = tf.train.GradientDescentOptimizer(0.5)
14training = optimizer.minimize(loss)
15
16sess = tf.Session()
17init = tf.global_variables_initializer()
18sess.run(init)
19
20# The instance is created to call the method saving checkpoint
21saver = tf.train.Saver(max_to_keep=2)
22save_w = tf.train.Saver({'a_name': weight, 'b_name': bias})
23
24for step in range(101):
25 sess.run(training)
26 if step % 10 == 0:
27 print('Round {}, weight: {}, bias: {}'
28 .format(step, sess.run(weight[0]), sess.run(bias[0])))
29 saver.save(sess=sess, save_path='./checkpoint/linear.ckpt', global_step=step)
30 save_w.save(sess=sess, save_path='./weight/linear.ckpt', global_step=step)
31
32saver.save(sess, './checkpoint/linear.ckpt')
33sess.close()
1Round 0, weight: -0.4444888234138489, bias: 0.8327240943908691
2Round 10, weight: -0.20793604850769043, bias: 0.46674099564552307
3Round 20, weight: -0.0472208596765995, bias: 0.37971681356430054
4Round 30, weight: 0.0296153761446476, bias: 0.3381116986274719
5Round 40, weight: 0.06634990125894547, bias: 0.3182207942008972
6Round 50, weight: 0.08391226083040237, bias: 0.3087111711502075
7Round 60, weight: 0.09230863302946091, bias: 0.30416470766067505
8Round 70, weight: 0.09632284939289093, bias: 0.3019911050796509
9Round 80, weight: 0.09824200719594955, bias: 0.30095192790031433
10Round 90, weight: 0.09915953129529953, bias: 0.30045512318611145
11Round 100, weight: 0.09959817677736282, bias: 0.3002175986766815
检查点的路径设置需要使用 「./…/…/…」 的格式去写路径,尤其是开头的 ./ 必须加上,否则在某些平台上会出现错误,等代码运行完毕后在下面 .py 文档执行路径下出现我们设置的储存文件夹和文件名称,如下图:
文件存好之后接下来就是读取上图中储存的文件,储存在文件里面的数据是一个原封不动的 tf.Variable() 物件,有着与储存前一模一样的名字和属性,甚至在呼叫回该储存的变量时也不用初始化,是一个非常全面的保存结果, 只是需要记得: 「同样变量名的物件需要事先存在在代码中, 并且数据类型和长相必须一模一样。」
读取的方式也很直观,同样的创建一个 tf.train.Saver() 示例,并用该示例里面的方法 .restore() 完成读取,读取完毕后储存的参数就回像起死回生一般重新回到我们的代码中。
tf.train.Saver().restore(sess, 'file_directory')
sess: 表示我们希望把该储存的内容重新叫回哪一个绘话中
'./file_directory/file_name': 表示我们要呼叫的该存档文件
p.s. 如果在储存过程中有加上 global_step 参数,呼叫文档名的时候就必须一起把数字也加上去,如下代码。
呼叫储存文件的时候有以下三种情况:
最直接: 使用 tf.train.Saver() 创建示例后,呼叫 .restore() 方法配合对应名字,成功回到训练中途的记录
第一个方法受阻: 绕道使用 .meta 储存文件,并使用 tf.import_meta_graph() 示例的 .restore() 方法,同样可以成功回到训练中途的记录
呼叫只储存部分参数的记录档: 创建一个示例前先在 tf.train.Saver() 括弧中使用字典形式声明好当时部分储存的时候对应一模一样名字的字典键和参数名,再用 .restore() 方法成功回到训练中途的记录
详细代码如下演示:
1import tensorflow as tf
2
3# tf.reset_default_graph()
4weight = tf.Variable([33], dtype=tf.float32, name='weight')
5bias = tf.Variable([3], dtype=tf.float32, name='bias')
6
7saver_1 = tf.train.Saver()
8# saver = tf.train.import_meta_graph('./checkpoint/linear.ckpt-100.meta')
9saver_2 = tf.train.Saver({'b_name': bias})#, 'a_name': weight})
10# init = tf.global_variables_initializer()
11
12sess = tf.Session()
13# sess.run(init)
14# path1 = saver_1.restore(sess, save_path=tf.train.latest_checkpoint('./checkpoint'))
15path1 = saver_1.restore(sess, './checkpoint/linear.ckpt-100')
16path2 = saver_2.restore(sess, './weight/linear.ckpt-80')
17print(sess.run(weight))
18print(sess.run(bias))
19sess.close()
20
21
22# print(sess.run(bias))
23
24# ### ----- Result as follow ----- ###
25# FailedPreconditionError:
26# Attempting to use uninitialized value Variable
27# [[Node: _retval_Variable_0_0 = _Retval[T=DT_FLOAT, index=0,
28# _device='/job:localhost/replica:0/task:0/device:CPU:0'](Variable)]]
1/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/importlib/_bootstrap.py:205: RuntimeWarning: compiletime version 3.5 of module 'tensorflow.python.framework.fast_tensor_util' does not match runtime version 3.6
2 return f(*args, **kwds)
3/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
4 from ._conv import register_converters as _register_converters
5
6
7INFO:tensorflow:Restoring parameters from ./checkpoint/linear.ckpt-100
8INFO:tensorflow:Restoring parameters from ./weight/linear.ckpt-80
9[0.10062683]
10[0.2989355]
可以观察到,如果没有成功导入内容, sess.run() 执行一个参数的时候就会被通知该参数没有初始化,需要特别注意。另外如果重复导入同样的值到该代码中,那么该值以最后一次导入为主,如上面代码中的 weight,最近导入的 60 个回合训练的 weight 值比训练 90 个回合的 bias 值还要不准得多。
花了大把的时间才找出在回传参数的时候发生错误的症结点,最后原因还是在于 tf.Variable() 的格式没有完全一样,前面只专注在数据格式上面,但是其节点名称必须也完全一致才可以! 如果表明名称 name='a_name', 那么就都不要写,如果表明了名称,那就必须完全一致才行!
下面提炼三个有关储存和导出的要点:
储存的时候官网建议我们呢加上 .ckpt 后缀的目的仅仅只是为了区隔该储存文件与别的文件之间的不同,其实并非真正去修改其后档名,因此没有加上此后缀的情况下,储存的参数照样可以被导入
储存后的文件状态有些电脑中是三个,有些是四个,他们的全名甚至没有与当时储存文件时的名字完全相同,但是实际上不用担心,在回传参数的时候只要想着当时储存的档名是什么即可,完全可以忽视不同档名的影响
回传参数的时候必须与法官一样严格的审视要被导入的框架容器是否跟当时储存参数的时候完全一样,包含命名节点的名字,和每个节点设置的数据类型
p.s. 如果是使用 Jupyter Notebook 启动代码的话,切记在使用 .restore 回传参数之前确定没有先启动了训练的过程,需要该变量的值是空的情况下才能顺利传参
回传的是该目录下最近一次被储存的 checkpoint 文件完整位置,数据类型是字符串,此类方法放入的路径切记是文件夹目录,而非文件本身的目录,因此通常只要找到存放储存点的文件夹目录,用此方法回传一个字符串结果后,放入 .restore() 内就可以顺利呼叫最新的存档参数内容。
在使用 Tensorflow 之初,我们首先了解的两个观念肯定是节点 node 和边 edge,借由在这两者里面添加张量和运算单元等方法,我们最终可以驱动计算机完成我们期望的运算结果。然而这些运算过程都是在我们脑子里面抽象概念,放到计算机中也只是一行一行的代码,并没有办法提供给我们太直观的感受,而 Tensorboard 就是中间的润滑剂,它良好的构建一个硬邦邦的代码与直观感受之间的桥梁,让如同一个黑箱般的神经网络运算过程透出一线光亮,只要在现有的代码中添加一些 作用域 Scope
的设置后,导出文件并使用终端的 Tensorboard 指令执行,Tensorflow 就会自动为我们建构出一个完整的数据布告栏在浏览器中使用。
而这些额外添加的代码这里,目的就是用来给不同的节点和边之间设置各自归属的作用域,分为下面两种:
node names
name scopes
继续沿用上面的代码,加上作用域的方法如下:
1import numpy as np
2import tensorflow as tf
3
4# Create a graph and set this graph as the default one.
5# By doing this, the original default graph would not be called
6graph = tf.Graph()
7with graph.as_default():
8 x_data = np.random.rand(100).astype(np.float32)
9 y_data = x_data * 0.1 + 0.3
10
11 with tf.name_scope('linear'):
12 weight = tf.Variable(tf.random_uniform(shape=[1], minval=-1.0, maxval=1.0),
13 dtype=np.float32, name='weight')
14 bias = tf.Variable(tf.zeros(shape=[1]), dtype=np.float32, name='bias')
15 y = weight * x_data + bias
16
17 with tf.name_scope('gradient_descent'):
18 loss = tf.reduce_mean(tf.square(y - y_data), name='loss')
19 optimizer = tf.train.GradientDescentOptimizer(0.5)
20 training = optimizer.minimize(loss)
21 init = tf.global_variables_initializer()
22
23
24sess = tf.Session(graph=graph)
25
26writer = tf.summary.FileWriter('/Users/kcl/Documents/Python_Projects/01_AI_Tutorials/tb')
27writer.add_graph(graph)
28
29sess.run(init)
30
31for step in range(101):
32 sess.run(training)
33
34writer.close()
35sess.close()
1Round 0, weight: 0.3617558479309082, bias: 0.21680830419063568
2Round 10, weight: 0.22135570645332336, bias: 0.23605786263942719
3Round 20, weight: 0.1615721732378006, bias: 0.26755768060684204
4Round 30, weight: 0.1312398761510849, bias: 0.283539742231369
5Round 40, weight: 0.11585018038749695, bias: 0.2916485667228699
6Round 50, weight: 0.10804189741611481, bias: 0.2957627475261688
7Round 60, weight: 0.1040802150964737, bias: 0.2978501617908478
8Round 70, weight: 0.10207019001245499, bias: 0.2989092469215393
9Round 80, weight: 0.10105035454034805, bias: 0.29944658279418945
10Round 90, weight: 0.10053293406963348, bias: 0.2997192144393921
11Round 100, weight: 0.10027039051055908, bias: 0.29985755681991577
代码的运行结果会在指定的目录下创建一个文件,该文件只能够在终端 (Terminal or CMD) 使用下面指令开启:tensorboard --logdir='/Users/01_AI_Tutorials/tb'
或者 tensorboard --logdir /Users/01_AI_Tutorials/tb
的方式开启,之后终端里面会创建一个本地伺服器和一个对应网址,用来让使用者在浏览器中开启 Tensorboard 的页面,下图即为开启上面代码背后的 Tensorboard 模样:
p.s. 但是值得我们注意的一点,只有属于 tf 的节点与运算子,或者是跟节点产生关联的算式才能够被记录在 Tensorboard 上面,如果跟 tf 一点关系都没有的算式如 x_data,y_data 就不会显示在数据流图中。
在建立数据流图的上面过程中,实际上完整的步骤如下:
创建一个 tf.Graph() 对象
使用对象的方法 .as_default() 设定该图为指定的图
在该图中设定需要的节点内容并给予名称
使用 tf.name_scope() 等方法设定更大作用域的名称,名称注意不能够有空格
使用 tf.summary.FileWriter() 方法在指定路径下创建一个文件
把步骤五的方法指向一个对象,然后使用 .add_graph() 添加指定 session 创建好的 graph
完成后,才可以如上述步骤去终端指定路径下开启 Tensorboard 观察数据流图,一般代码中之所以没那么复杂,原因是在创建数据流图的时候,Tensorflow 框架本身已经为我们预设了一个数据流图,因此省去许多麻烦。
我们在使用 Tensorflow 创建自己的数据流图的时候,都是基于 一张图
的构建的,此图就如同一张无边际的画布让我们在上面自由的创建张量和运算子,即便在我们不特别去设定一个数据流图的时候,tf 框架本身也已经自动帮使用者生成好了,因此可以确保每个节点和运算子都落在这张图里面。
但是如果如上图的情况一样,是我们自己从头到尾设定的数据流图作用域,那就必须考虑到是否把全部的节点和运算子全部都建立在图上,一旦某个东西落在了图外面,到了执行的时候 tf 框架就会不明白该对象是谁家的孩子。
把哪个节点和运算子归类到了哪个作用域这件事情,说到底其实也就只是在 Tensorboard 上面呈现的画面不同而已,并不会对运算的功能产生任何影响。
另外,Tensorboard 只会打印数据流图本身的结构,并不会参与 sess.run() 执行运算的结果输出,因此不论储存文档的代码放在 sess.run() 的前还是后,结果都会是一样的结构呈现,但并没有运算数值。
直到目前为止我们在 '图' 上创建的数据流图也仅仅只是数据流图,并没有数据实际上在 Tensorboard 中被参与进来,把参数写入到 Tensorboard 的方法不外乎 tf.summary
系列的函数。然而,用这些函数写入数据到 Tensorboard 的过成类似于 sess.run() 的过程,对同一个对象而言一次只会写一个数字,完成图之所以会有一个连贯的结果,那是因为使用 Python 建构一个回圈运行出来的结果导致,系列函数包含了如下几个:
tf.summary.scalar()
tf.summary.image()
tf.summary.audio()
tf.summary.histogram()
tf.summary.tensor()
上面陈列方法的参数项内容都是一样的,皆为 …('a_name/node', object),第一个参数是一个字符串,其内容是什么,在图上显示出来坐标的名称就是什么,而第二项则是一个经过运算后的对象,谁被放到这里的话,谁就能够在图上的坐标轴中显示方程式经过训练的变化过程。
面对简单的数据流图,我们尚且可以数得出来一共有多少个数值需要被放入 Tensorboard 中,但是如果是像 Inception 这类的巨大神经网络,那我们就需要一个工具统合所有需要被写入的对象,一次性的完成写入的动作:tf.summary.merge_all()
,我们可以进一步想像上面陈列的五种方法就像是剪刀,负责剪下我们期望截取的数值,然而一次一次贴上 board 实在太麻烦,因此先全部把这些剪下来的值融为一体后,再使用 .FileWriter() 方法一次贴上,并以 .add_summary() 方法逐步更新。函数方法使用方式如下代码:
1import numpy as np
2import tensorflow as tf
3
4graph = tf.Graph()
5with graph.as_default():
6 x_data = np.random.rand(100).astype(np.float32)
7 y_data = x_data * 0.1 + 0.3
8
9 with tf.name_scope('linear'):
10 weight = tf.Variable(tf.random_uniform(shape=[1], minval=-1.0, maxval=1.0),
11 dtype=np.float32, name='weight')
12 # In order to watch the changes of weight, use histogram method
13 tf.summary.histogram('linear_weight', weight)
14
15 bias = tf.Variable(tf.zeros(shape=[1]), dtype=np.float32, name='bias')
16 # Mind that we don't need to assign the method to an object
17 tf.summary.histogram('linear_bias', bias)
18 y = weight * x_data + bias
19
20 with tf.name_scope('gradient_descent'):
21 loss = tf.reduce_mean(tf.square(y - y_data), name='loss')
22 # The changes of loss would be updated in each iteration of optimization
23 tf.summary.scalar('linear_loss', loss)
24 optimizer = tf.train.GradientDescentOptimizer(0.5)
25 training = optimizer.minimize(loss)
26
27 ### --! Mind the graph scope if we are using the graph set by ourselves !-- ###
28 init = tf.global_variables_initializer()
29 # As we can see above, we have total 3 values needed to be merged
30 merge = tf.summary.merge_all()
31
32# If we are using the default graph, 'graph=graph' would not need to be emphasized
33sess = tf.Session(graph=graph)
34
35# A graph should only be created once outside of the for loop
36writer = tf.summary.FileWriter('/Users/kcl/Documents/Python_Projects/01_AI_Tutorials/tb')
37# If the default graph is used, 'graph' should be replaced by 'sess.graph'
38writer.add_graph(graph)
39
40sess.run(init)
41
42for step in range(101):
43 if step%2 == 0:
44 # In order to update the value throughout training iteration,
45 # we should use add_summary method to record the values into the graph.
46 # But before the recording, we have to merge all summary nodes again first.
47 m = sess.run(merge)
48 writer.add_summary(m, step)
49
50 sess.run(training)
51 if step % 10 == 0:
52 print('Round {}, weight: {}, bias: {}'
53 .format(step, sess.run(weight[0]), sess.run(bias[0])))
54
55writer.close()
56sess.close()
1Round 0, weight: 0.14186275005340576, bias: 0.3834246098995209
2Round 10, weight: 0.09872959554195404, bias: 0.3006848394870758
3Round 20, weight: 0.09932643920183182, bias: 0.3003629744052887
4Round 30, weight: 0.09964289516210556, bias: 0.3001924455165863
5Round 40, weight: 0.09981069713830948, bias: 0.30010202527046204
6Round 50, weight: 0.09989965707063675, bias: 0.30005407333374023
7Round 60, weight: 0.09994679689407349, bias: 0.3000286817550659
8Round 70, weight: 0.09997180849313736, bias: 0.3000152111053467
9Round 80, weight: 0.09998505562543869, bias: 0.30000805854797363
10Round 90, weight: 0.0999920666217804, bias: 0.3000042736530304
11Round 100, weight: 0.09999579191207886, bias: 0.3000022768974304
p.s. 需要非常注意如果是自己定义的 Graph,就必须非常小心作用域错误位造成的程序崩溃。
代码当中,我们可以设定要在经历几个 step 之后,才 merge 一次上面的所有值并把这些值写入到文件当中,成为 Tensorboard 里面显示坐标图的精度控制,下面是经过不同的 tf.summary 方法回传值到文件中并用浏览器显示出来的结果:
利用 Tensorboard 提供的诸多工具可以让我们在训练模型的过程中更容易找到问题所在,并根据过程的变化来判断优化模型的方向。此工具是 Google 话费大量人力所共同完成的一项厉害的作品,同时也是其他深度学习框架中没有的工具之一,如果使用其他的模型同时需要观察例如损失函数的数值变化,那么使用者只能够自己从头建构坐标轴,使用如 matplotlib 等工具一点一点的把数值记录到图上,但这么一来又将降低代码的执行效率,是一个鱼与熊掌的关系。反观 Tensorboard 是一个嵌入在 tf 的工具,有非常好的效率和兼容性,值得让我们花时间一探究竟。
联系客服