Tensorflow-2-Tensorboard使用

一、概述

机器学习如此复杂,训练模型的时候,摸不清背后到底是如何运行的。自己设置的参数和关键变量,如果能看到在训练时的变化情况,可以为后面的参数调优阶段提供很大的便利。

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启动tbweb 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的流向
  • 每个椭圆代表一个操作,如addmatmul
  • 每个圆角矩形代表一组操作,可以双击放大,看到这个组里面的细节
  • 整张图片可以放大缩小,随意拖动;点开每个节点,右上角都会有这个节点的详细信息

下面来加工一下,为模型图分组,让图像更加清晰有条理。

2.使用name_scope分组

调用tf.name_scope()方法来为graph分组。

我想清楚看到

  • 输入Inputs
  • 标签Targets
  • 两组Weightbias变量
  • 两个隐藏层的输出Logits_1Logits_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标签下,就有刚才添加的lossaccuracy了。

看这两个变量的行为不错,loss一直降低,accuracy最后达到了%99左右。

这个图片时可以通过选定指定一个区域来放大的,放大之后如下图。

这里写图片描述

这是accuracy放大之后的效果图,可以更加清晰。

接下来到DISTRIBUTIONHISTOGRAM里看看。

这里写图片描述

上图是两组Weights在训练过程中的行为,这里分布均匀,挺正常。如果有问题的话,会保持不变,也就是说模型没有进行学习。

这里写图片描述

这里是两组bias的行为。这里可以看出点问题了,这个实例模型在训练的时候,虽然准确率能到%99,但是loss降到0.5左右就下不去了。

如果在notebook中多次尝试运行整个模型,会发现最后分类完成的那张图片,无法很精准的分类。

看上图,bias_2的分布有点可疑,我还不确定,但是这样异常的不均匀的情况可以给予一定的方向。我现在要做的就是就去看看什么影响了bias_2的正常更新,并且这个影响是不是引起了最终的loss瓶颈。

这里写图片描述

上图是HISTOGRAMWeights的分布,看起来还是比较正常的,分布很均匀。

再来看看bias的。

这里写图片描述

这是两个bias的分部情况,第一个看起来还不错。但是第二个就异常了。刻意点一下图像左下角那个按钮,就可以将图像放大。我放大了第二个bias的图像,如下。

这里写图片描述

简直是找不到规律,虽然不是很明白为什么会这样,但是感觉问题就出在这里了。

这里看到的是2D图像,点左边栏中的OFFSET,就能看到和Weights那一样的3D图像了。

三、总结

就像上面的例子,如果没有tb,我并不能准确知道到底是哪一个变量有异常。tb的可视化功能大大提高了训练网络的效率。

这里是tb的基本使用方式,还有很多有待研究。