一、概述
机器学习如此复杂,训练模型的时候,摸不清背后到底是如何运行的。自己设置的参数和关键变量,如果能看到在训练时的变化情况,可以为后面的参数调优阶段提供很大的便利。
Tensorboard
就是这样一个工具。
它刻意将模型抽象成图像,tensor
每一步是如何流动的,一目了然。
通过适当的代码设置,还能将指定的关键变量在训练时的变化情况绘制成曲线图,以便训练完成后观察变量的变化情况,来更加准确定位问题。
这篇文章简单介绍一下tensorboard
的基本用法。
二、Tensorboard使用
tensorboard
(以下简称tb
)的操作,从创建一个FileWriter
开始。
在接下来的代码中,我参照CS231N课程的数据集例子,用tensorflow
(以下简称tf
)写了一个Logistic Regression
,并以此来说明tb
的基本用法。
用到的notebook
在我的github上可以找到。使用之前,请确保执行
conda env create -f environment_tf.yml
来创建和我一样的运行环境。如有问题,可以留言或者ISSUE。
创建好环境之后,运行
source activate tb-lab
激活conda
环境,然后运行
jupyter notebook
来使用本例中的notebook
。
下面的示例程序用tf
做了一个两层的分类网络。将下图中的数据集分类。
最终分类效果是这样的。
tb
的使用,大致归纳为三步:
- 调用
tf
中的FileWriter
将自己关注的数据写到磁盘 - 在命令行使用
tensorboard --logdir /path/to/log
启动tb
的web app
- 然后在本地浏览器输入
localhost:6006
来使用tb
下面具体看一下怎么使用。
1.生成模型图
生成模型图只需要一句话就行。比如说,现在已经初始化好了变量,处理好了数据,部分代码如下:
# 数据初始化
N = 100 # number of points per class
D = 2 # dimensionality
K = 3 # number of classes
X = np.zeros((N * K, D)) # data matrix (each row = single example)
y = np.zeros(N * K, dtype='uint8') # class labels
for j in range(K):
ix = range(N * j, N * (j + 1))
r = np.linspace(0.0, 1, N) # radius
t = np.linspace(j * 4, (j + 1) * 4, N) + np.random.randn(N) * 0.2 # theta
X[ix] = np.c_[r * np.sin(t), r * np.cos(t)]
y[ix] = j
# lets visualize the data:
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.show()
# 输入数据
inputs = tf.placeholder(tf.float32, [N * K, D])
# 数据标签
targets = tf.placeholder(tf.float32, [None, K])
softmax_w_1 = tf.Variable(tf.truncated_normal((D, h), stddev=0.1), dtype=tf.float32, name='weight_1')
softmax_b_1 = tf.Variable(tf.zeros((1, h)), dtype=tf.float32)
softmax_w_2 = tf.Variable(tf.truncated_normal((h, K), stddev=0.1), dtype=tf.float32, name='weight_2')
softmax_b_2 = tf.Variable(tf.zeros((1, K)), dtype=tf.float32)
省略部分代码......
prediction = tf.nn.softmax(logits_2)
准备开始训练的时候,加上一句
tf.summary.FileWriter('./logs/summary', session.graph)
例如,在session
开始的时候添加就好。
with tf.Session() as session:
session.run(init)
merged = tf.summary.merge_all()
writer = tf.summary.FileWriter('./logs/summary', session.graph)
我们要看整个模型的图像,因此传入session.graph
对象。
这时,来到命令行,切换到notebook
所在的目录,然后执行
tensorboard --logdir logs/summary
logs/summary
就是代码中定义的目录路径。tb
启动后会提示到浏览器用localhost:6006
去打开tb
应用。
在浏览器打开后,切换到GRAPH
标签,看到的模型图时这样的。
这个时候其他标签还没有内容,因为还没有在代码中进行添加。
上面的图片,就是现在这个模型的原始图像,没有进行任何分组和加工。看起来很乱,有一些标签,例如slice
什么的,都不知道是什么意思。
图中,
- 每条曲线代表
tensor
的流向 - 每个椭圆代表一个操作,如
add
,matmul
- 每个圆角矩形代表一组操作,可以双击放大,看到这个组里面的细节
- 整张图片可以放大缩小,随意拖动;点开每个节点,右上角都会有这个节点的详细信息
下面来加工一下,为模型图分组,让图像更加清晰有条理。
2.使用name_scope分组
调用tf.name_scope()
方法来为graph
分组。
我想清楚看到
- 输入
Inputs
- 标签
Targets
- 两组
Weight
和bias
变量 - 两个隐藏层的输出
Logits_1
和Logits_2
- 损失函数
loss
- 训练准确率
Accuracy
以及 - 整个训练过程
Train
示例代码如下。
输入Inputs
with tf.name_scope('Inputs') as scope:
inputs = tf.placeholder(tf.float32, [N * K, D], name='inputs')
标签
Targets
with tf.name_scope('Targets') as scope:
targets = tf.placeholder(tf.float32, [None, K], name='targets')
两组Weight和bias变量
# 创建第一层的weights和bias
with tf.name_scope('Weight_Set_1') as scope:
softmax_w_1 = tf.Variable(tf.truncated_normal((D, h), stddev=0.1), dtype=tf.float32, name='weight_1')
softmax_b_1 = tf.Variable(tf.zeros((1, h)), dtype=tf.float32, name='bias_1')
# 创建第二层的weights和bias
with tf.name_scope('Weight_Set_2') as scope:
softmax_w_2 = tf.Variable(tf.truncated_normal((h, K), stddev=0.1), dtype=tf.float32, name='weight_2')
softmax_b_2 = tf.Variable(tf.zeros((1, K)), dtype=tf.float32, name='bias_2')
两个隐藏曾的输出Logits_1和Logits_2
with tf.name_scope('Logits_1') as scope:
# 第一层的输出logits_1,使用ReLU作为activation function
logits_1 = tf.nn.relu(tf.add(tf.matmul(X, softmax_w_1), softmax_b_1), name='logits_1')
with tf.name_scope('Logits_2') as scope:
logits_2 = tf.add(tf.matmul(logits_1, softmax_w_2), softmax_b_2, name='logits_2')
# 计算最终的预测分数,使用softmax计算最后的分数
prediction = tf.nn.softmax(logits_2, name='prediction')
损失函数loss
with tf.name_scope('Loss') as scope:
loss = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=prediction, name='loss'))
训练准确率Accuracy
with tf.name_scope('Accuracy') as scope:
# Determine if the predictions are correct
is_correct_prediction = tf.equal(tf.argmax(prediction, 1), tf.argmax(y_, 1))
# Calculate the accuracy of the predictions
accuracy = tf.reduce_mean(tf.cast(is_correct_prediction, tf.float32), name='accuracy')
训练过程Train
with tf.name_scope('Train') as scope:
# 使用adam最为optimizer
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss)
设置好这些代码之后,就可以跟第一步中一样,使用
with tf.Session() as session:
session.run(init)
writer = tf.summary.FileWriter('./logs/summary', session.graph)
......
来生成模型图。
训练完成后,再次执行tensorboard --logdir logs/summary
,在浏览器中看到的模型图就是这样的了。
跟刚才相比,是不是清晰了很多,我所需的所有关注点,这个图上都有了。
这时如何为图中的节点分组。
3.添加变量详情
添加变量详情,刻意在训练结束后,直接看到变量的变化情况。上面两步中,只有GRAPH
标签下面有内容,其他标签下面都没有。这一步就是往其他标签下面添加内容。
添加变量详情,使用
tf.summary.histogram()
tf.summary.scalar()
这两个方法。
比如,我想关注:
- 两组Weight和bias的变化情况
- 每一次的预测结果的变化情况
- 训练过程中loss的变化情况
- 训练过程中准确率的变化情况
我可以通过上述两个方法来添加代码。
注意在使用这两个方法之前,要为变量都加上name
关键字参数。
看上面的代码中,有
softmax_w_1 = tf.Variable(tf.truncated_normal((D, h), stddev=0.1), dtype=tf.float32, name='weight_1')
最后这个name='weight_1'
就是用来标识softmax_w_1
的名称。
为了使结果更加准确,务必为分组中的变量都添加相应的名称标识。
名称都添加了之后,就可以使用下面的代码来添加我们想要的结果了。
# 两组Weight和bias的直方图,用histogram
w_1_hist = tf.summary.histogram('weight_1', softmax_w_1)
b_1_hist = tf.summary.histogram('bias_1', softmax_b_1)
w_2_hist = tf.summary.histogram('weight_2', softmax_w_2)
b_2_hist = tf.summary.histogram('bias_2', softmax_b_2)
# 每次预测值的直方图
logits = tf.summary.histogram('prediction', prediction)
# 损失loss和准确率accuracy的变化,这里用scalar就好,具体原因还没有深究
# 用histogram应该也没有问题的,用了scalar之后,scalar标签下面就有内容了
loss_hist = tf.summary.scalar('loss', loss)
acc_hist = tf.summary.scalar('accuracy', accuracy)
然后,在训练的时候,要调用merge_all()
,并用session
去执行merge
,最后将merge_all()
返回的对象(本例中叫summary
)添加到FileWriter
中才行。看着复杂,一步步看代码还是很清晰的。
# 初始化Variable
init = tf.global_variables_initializer()
# 打开session
with tf.Session() as session:
# 运行初始化
session.run(init)
# 调用merge_all(),将前面添加的所有histogram和scalar合并到一起,方便观察
merged = tf.summary.merge_all()
# 同样写入到logs中
writer = tf.summary.FileWriter('./logs/summary', session.graph)
training_feed_dict = {inputs: X, targets: y_.eval()}
for i in range(epochs):
# 训练的时候,第一个传入merged对象,返回summary
summary, _, l = session.run(
[merged, optimizer, loss],
feed_dict=training_feed_dict)
if not i % 50:
print('Epoch {}/{} '.format(i + 1, epochs),
'Training loss: {:.4f}'.format(l))
# 每50步,将summary对象添加到writer写入磁盘,最后来观察变化
writer.add_summary(summary, i)
在这里,就可以通过观察这些变量的行为来找问题所在了。
训练完成后,同理打开tb
。
可以看到SCALAR
标签下,就有刚才添加的loss
和accuracy
了。
看这两个变量的行为不错,loss
一直降低,accuracy
最后达到了%99
左右。
这个图片时可以通过选定指定一个区域来放大的,放大之后如下图。
这是accuracy
放大之后的效果图,可以更加清晰。
接下来到DISTRIBUTION
和HISTOGRAM
里看看。
上图是两组Weights
在训练过程中的行为,这里分布均匀,挺正常。如果有问题的话,会保持不变,也就是说模型没有进行学习。
这里是两组bias
的行为。这里可以看出点问题了,这个实例模型在训练的时候,虽然准确率能到%99
,但是loss
降到0.5
左右就下不去了。
如果在notebook
中多次尝试运行整个模型,会发现最后分类完成的那张图片,无法很精准的分类。
看上图,bias_2
的分布有点可疑,我还不确定,但是这样异常的不均匀的情况可以给予一定的方向。我现在要做的就是就去看看什么影响了bias_2
的正常更新,并且这个影响是不是引起了最终的loss
瓶颈。
上图是HISTOGRAM
中Weights
的分布,看起来还是比较正常的,分布很均匀。
再来看看bias
的。
这是两个bias
的分部情况,第一个看起来还不错。但是第二个就异常了。刻意点一下图像左下角那个按钮,就可以将图像放大。我放大了第二个bias
的图像,如下。
简直是找不到规律,虽然不是很明白为什么会这样,但是感觉问题就出在这里了。
这里看到的是2D图像,点左边栏中的OFFSET
,就能看到和Weights
那一样的3D图像了。
三、总结
就像上面的例子,如果没有tb
,我并不能准确知道到底是哪一个变量有异常。tb
的可视化功能大大提高了训练网络的效率。
这里是tb
的基本使用方式,还有很多有待研究。