本章主要说明如何使用TensorBoard进行可视化,以及部分的调参方法。

这是一篇dandelionmane在TensorFlow Dev Summit 2017关于TensorBoard介绍的总结教程。

转载请说明出处:Gaussic

在之前的章节中,几乎所有的性能评估都是通过打印中间结果字符串来完成的。使用更多的可视化的图表可以让人对模型有一个更加直观的认识。在本章中,我们将使用TensorBoard对模型进行可视化。

计算图可视化

要可视化TensorFlow的计算图,需要先构建网络。

网络层

本章的网络,依然使用之前几个章节对MNIST数据集使用的网络结构。为了方便实现,固定了其中的一部分参数。相关层如下:

# 简单卷积层,为方便本章教程叙述,固定部分参数
def conv_layer(input,
               channels_in,    # 输入通道数
               channels_out):  # 输出通道数

    weights = tf.Variable(tf.truncated_normal([5, 5, channels_in, channels_out], stddev=0.05))
    biases = tf.Variable(tf.constant(0.05, shape=[channels_out]))
    conv = tf.nn.conv2d(input, filter=weights, strides=[1, 1, 1, 1], padding='SAME')
    act = tf.nn.relu(conv + biases)
    return act

# 简化全连接层
def fc_layer(input, num_inputs, num_outputs, use_relu=True):
    weights = tf.Variable(tf.truncated_normal([num_inputs, num_outputs], stddev=0.05))
    biases = tf.Variable(tf.constant(0.05, shape=[num_outputs]))
    act = tf.matmul(input, weights) + biases

    if use_relu:
        act = tf.nn.relu(act)
    return act     

# max pooling 层
def max_pool(input):
    return tf.nn.max_pool(input, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

载入数据,构建网络

from tensorflow.examples.tutorials.mnist import input_data
data = input_data.read_data_sets('data/MNIST', one_hot=True)

x = tf.placeholder(tf.float32, shape=[None, 784])   # 固定这部分值
y = tf.placeholder(tf.float32, shape=[None, 10])
x_image = tf.reshape(x, [-1, 28, 28, 1])

conv1 = conv_layer(x_image, 1, 32)   # 增加了卷积核数目
pool1 = max_pool(conv1)

conv2 = conv_layer(pool1, 32, 64)
pool2 = max_pool(conv2)

flat_shape = pool2.get_shape()[1:4].num_elements()
flattened = tf.reshape(pool2, [-1, flat_shape])

fc1 = fc_layer(flattened, flat_shape, 1024)     # 增大神经元数目
logits = fc_layer(fc1, 1024, 10, use_relu=False)

交叉熵,优化器,准确率

# 计算交叉熵
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=logits))

# 使用Adam优化器来训练
optimizer = tf.train.AdamOptimizer(learning_rate=1e-4).minimize(cross_entropy)

# 计算准确率
correct_prediction = tf.equal(tf.argmax(y, axis=1), tf.argmax(logits, axis=1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

创建session,训练

session = tf.Session()
session.run(tf.global_variables_initializer())

train_batch_size = 100

for i in range(2001):
    x_batch, y_batch = data.train.next_batch(train_batch_size)

    feed_dict = {x: x_batch, y: y_batch}

    if i % 500 == 0:
        train_accuracy = session.run(accuracy, feed_dict=feed_dict)
        print("迭代轮次: {0:>6}, 训练准确率: {1:>6.4%}".format(i, train_accuracy))

    session.run(optimizer, feed_dict=feed_dict)
迭代轮次:      0, 训练准确率: 9.0000%
迭代轮次:    500, 训练准确率: 93.0000%
迭代轮次:   1000, 训练准确率: 97.0000%
迭代轮次:   1500, 训练准确率: 98.0000%
迭代轮次:   2000, 训练准确率: 100.0000%

可见训练效果比较理想。

可视化计算图

现在需要将计算图可视化,需要使用tf.summary.FileWriter来将计算图写入指定目录:

tensorboard_dir = 'tensorboard/mnist'   # 保存目录
if not os.path.exists(tensorboard_dir):
    os.makedirs(tensorboard_dir)

writer = tf.summary.FileWriter(tensorboard_dir)
writer.add_graph(session.graph)

以上代码运行结束后,在保存目录下生成了相应文件。在终端运行如下命令:

$ tensorboard --logdir tensorboard/mnist

浏览器中访问localhost:6006便可进入TensorBoard控制台。

tensorboard1-4

当前导航栏除了GRAPHS以外,其他均没有数据,点击进入GRAPHS,可查看如下计算图:

graph1-1

然而,目前来看,这个图实在过于复杂,因为它显示了所有的计算细节。我们需要对代码进行相应的调整。

命名范围

我们在之前的章节已经使用了为某个网络模块命名的方法。TensorFlow使用name scope来确定模块的作用范围。对代码进行相应的调整,添加部分名称和作用域:

# 简单卷积层,为方便本章教程叙述,固定部分参数
def conv_layer(input,
               channels_in,    # 输入通道数
               channels_out,   # 输出通道数
               name='conv'):   # 名称
    with tf.name_scope(name):    # 在该名称作用域下的子变量
        weights = tf.Variable(tf.truncated_normal([5, 5, channels_in, channels_out],
                                                  stddev=0.05), name='W')
        biases = tf.Variable(tf.constant(0.05, shape=[channels_out]), name='B')
        conv = tf.nn.conv2d(input, filter=weights, strides=[1, 1, 1, 1], padding='SAME')
        act = tf.nn.relu(conv + biases)
        return act

# 简化全连接层
def fc_layer(input, num_inputs, num_outputs, use_relu=True, name='fc'):
    with tf.name_scope(name):   # 在该名称作用域下的子变量
        weights = tf.Variable(tf.truncated_normal([num_inputs, num_outputs],
                                                  stddev=0.05), name='W')
        biases = tf.Variable(tf.constant(0.05, shape=[num_outputs]), name='B')
        act = tf.matmul(input, weights) + biases

        if use_relu:
            act = tf.nn.relu(act)
        return act     

# max pooling 层
def max_pool(input):
    return tf.nn.max_pool(input, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

给其他的部分同样添加名称和相关作用域:

x = tf.placeholder(tf.float32, shape=[None, 784], name='x')
y = tf.placeholder(tf.float32, shape=[None, 10], name='labels')
x_image = tf.reshape(x, [-1, 28, 28, 1])

conv1 = conv_layer(x_image, 1, 32, 'conv1')
pool1 = max_pool(conv1)

conv2 = conv_layer(pool1, 32, 64, 'conv2')
pool2 = max_pool(conv2)

flat_shape = pool2.get_shape()[1:4].num_elements()
flattened = tf.reshape(pool2, [-1, flat_shape])

fc1 = fc_layer(flattened, flat_shape, 1024, name='fc1')
logits = fc_layer(fc1, 1024, 10, use_relu=False, name='fc2')

# 计算交叉熵
with tf.name_scope("xent"):
    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=logits))

# 使用Adam优化器来训练
with tf.name_scope('optimizer'):
    optimizer = tf.train.AdamOptimizer(learning_rate=1e-4).minimize(cross_entropy)

# 计算准确率
with tf.name_scope('accuracy'):
    correct_prediction = tf.equal(tf.argmax(y, axis=1), tf.argmax(logits, axis=1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

先不训练,创建一个新的目录保存新的计算图,然后将计算图写入这个目录

tensorboard_dir = 'tensorboard/mnist2'   # 保存目录
if not os.path.exists(tensorboard_dir):
    os.makedirs(tensorboard_dir)

writer = tf.summary.FileWriter(tensorboard_dir)
writer.add_graph(session.graph)

运行tensorboard,将logdir指向新的目录,计算图如下:

graph2-2

现在的计算图变得更加直观容易理解,因为它将部分的细节藏在了一个个大的模块里面。点击某个模块可以查看它的内部细节:

graph2_detail-2

可以看到,定义的名称W和B是属于conv2内部的子名称。

点击左边的Trace inputs,可以查看数据到某一模块的流向,例如计算accuracy,是x经过了一系列网络层并比对label计算出来的。

trace_input-2

标量,直方图

除了画出模型的计算图外,TensorBoard还支持收集一些准确率、损失等标量信息,检查输入的图像,以及描绘变量的直方图信息等等,这些信息对于评判模型的性能有着重要作用。

我们需要对代码做一定修改,来收集这些信息。

卷积层直方图

使用tf.summary.histogram收集直方图信息。

# 简单卷积层,为方便本章教程叙述,固定部分参数
def conv_layer(input,
               channels_in,    # 输入通道数
               channels_out,   # 输出通道数
               name='conv'):   # 名称
    with tf.name_scope(name):
        weights = tf.Variable(tf.truncated_normal([5, 5, channels_in, channels_out],
                                                  stddev=0.05), name='W')
        biases = tf.Variable(tf.constant(0.05, shape=[channels_out]), name='B')
        conv = tf.nn.conv2d(input, filter=weights, strides=[1, 1, 1, 1], padding='SAME')
        act = tf.nn.relu(conv + biases)

        # 收集以下三个信息,统计直方图
        tf.summary.histogram('weights', weights)   
        tf.summary.histogram('biases', biases)     
        tf.summary.histogram('activations', act)
        return act

交叉熵,准确率,图像输入

使用tf.summary.scalar收集标量信息,使用tf.summary.image收集图像。

tf.summary.scalar('cross_entropy', cross_entropy)
tf.summary.scalar('accuracy', accuracy)
tf.summary.image('input', x_image, 3)

保存这些信息

使用tf.summary.merge_all(),喂入训练数据,可以收集以上定义的所有信息。

tensorboard_dir = 'tensorboard/mnist3'   # 保存到新的目录
if not os.path.exists(tensorboard_dir):
    os.makedirs(tensorboard_dir)

merged_summary = tf.summary.merge_all()   # 使用tf.summary.merge_all(),可以收集以上定义的所有信息
writer = tf.summary.FileWriter(tensorboard_dir)
writer.add_graph(session.graph)

通过训练进行数据收集

train_batch_size = 100

for i in range(2001):
    x_batch, y_batch = data.train.next_batch(train_batch_size)

    feed_dict = {x: x_batch, y: y_batch}

    if i % 5 == 0:   # 运行merger_summary,使用add_summary写入数据
        # 这里的feed_dict应该使用验证集,但是当前仅作为演示目的,在此不做修改
        s = session.run(merged_summary, feed_dict=feed_dict)
        writer.add_summary(s, i)

    if i % 500 == 0:
        train_accuracy = session.run(accuracy, feed_dict=feed_dict)
        print("迭代轮次: {0:>6}, 训练准确率: {1:>6.4%}".format(i, train_accuracy))

    session.run(optimizer, feed_dict=feed_dict)

运行tensorboard,指向 tensorboard/mnist3。点击导航栏SCALARS:

scalar_1-1

显示了准确率和交叉熵在迭代过程中的变化情况,准确率在稳步上升,交叉熵逐渐下降,可见该模型的效果还算理想。

点击导航栏HISTOGRAMS:

histogram_1-1

可以查看变量在不同迭代轮次的直方图分布情况。第一层卷积的权重随着迭代变化较为明显,第二层表现出平滑的趋势。

点击导航栏IMAGES,可以显示不同迭代轮次的3张图片:

image_1-1

参数搜索

以上的示例中,TensorBoard都只显示了一个模型的可视化数据。对于不同的参数,如何将多个模型显示在一张图中进行对比?TensorBoard对这一问题作了同样的支持。我们需要调整部分代码,并加入一些参数搜索的代码。

将 max_pooling 合并到卷积中,将relu从全连接抽离

# 简单卷积层,为方便本章教程叙述,固定部分参数
def conv_layer(input,
               channels_in,    # 输入通道数
               channels_out,   # 输出通道数
               name='conv'):   # 名称
    with tf.name_scope(name):
        weights = tf.Variable(tf.truncated_normal([5, 5, channels_in, channels_out],
                                                  stddev=0.05), name='W')
        biases = tf.Variable(tf.constant(0.05, shape=[channels_out]), name='B')
        conv = tf.nn.conv2d(input, filter=weights, strides=[1, 1, 1, 1], padding='SAME')
        act = tf.nn.relu(conv + biases)

        tf.summary.histogram('weights', weights)
        tf.summary.histogram('biases', biases)
        tf.summary.histogram('activations', act)

        return tf.nn.max_pool(act, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

# 简化全连接层
def fc_layer(input, num_inputs, num_outputs, name='fc'):
    with tf.name_scope(name):
        weights = tf.Variable(tf.truncated_normal([num_inputs, num_outputs],
                                                  stddev=0.05), name='W')
        biases = tf.Variable(tf.constant(0.05, shape=[num_outputs]), name='B')
        act = tf.matmul(input, weights) + biases

        tf.summary.histogram('weights', weights)
        tf.summary.histogram('biases', biases)
        tf.summary.histogram('activations', act)

        return act

保存到新的目录

tensorboard_dir = 'tensorboard/mnist4/'   # 保存目录
if not os.path.exists(tensorboard_dir):
    os.makedirs(tensorboard_dir)

根据不同参数构建模型

def mnist_model(learning_rate, use_two_fc, use_two_conv, hparam):
    tf.reset_default_graph()    # 重置计算图
    sess = tf.Session()

    x = tf.placeholder(tf.float32, shape=[None, 784], name="x")
    x_image = tf.reshape(x, [-1, 28, 28, 1])
    tf.summary.image('input', x_image, 3)
    y = tf.placeholder(tf.float32, shape=[None, 10], name="labels")

    if use_two_conv:    # 是否使用两个卷积
        conv1 = conv_layer(x_image, 1, 32, "conv1")
        conv_out = conv_layer(conv1, 32, 64, "conv2")
    else:
        conv1 = conv_layer(x_image, 1, 64, "conv")    # 如果使用一个卷积,则再添加一个max_pooling保证维度相通
        conv_out = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    flattened = tf.reshape(conv_out, [-1, 7 * 7 * 64])

    if use_two_fc:    # 是否使用两个全连接
        fc1 = fc_layer(flattened, 7 * 7 * 64, 1024, "fc1")
        relu = tf.nn.relu(fc1)
        tf.summary.histogram("fc1/relu", relu)
        logits = fc_layer(fc1, 1024, 10, "fc2")
    else:
        logits = fc_layer(flattened, 7*7*64, 10, "fc")

    with tf.name_scope("xent"):
        xent = tf.reduce_mean(
            tf.nn.softmax_cross_entropy_with_logits(
                logits=logits, labels=y), name="xent")
        tf.summary.scalar("xent", xent)

    with tf.name_scope("train"):
        train_step = tf.train.AdamOptimizer(learning_rate).minimize(xent)

    with tf.name_scope("accuracy"):
        correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(y, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        tf.summary.scalar("accuracy", accuracy)

    summ = tf.summary.merge_all()    # 收集所有的summary

    saver = tf.train.Saver()     # 保存训练过程

    sess.run(tf.global_variables_initializer())
    writer = tf.summary.FileWriter(tensorboard_dir + hparam)
    writer.add_graph(sess.graph)

    for i in range(2001):
        batch = data.train.next_batch(100)
        if i % 5 == 0:   # 每5轮写入一次
            # 同上,feed_dict应该使用验证集,但是当前仅作为演示目的,在此不做修改
            [train_accuracy, s] = sess.run([accuracy, summ], feed_dict={x: batch[0], y: batch[1]})
            writer.add_summary(s, i)

        if i % 100 == 0:    # 每100轮保存依存训练过程
            train_accuracy = sess.run(accuracy, feed_dict={x: batch[0], y: batch[1]})
            saver.save(sess, os.path.join(tensorboard_dir, "model.ckpt"), i)

            print("迭代轮次: {0:>6}, 训练准确率: {1:>6.4%}".format(i, train_accuracy))

        sess.run(train_step, feed_dict={x: batch[0], y: batch[1]})

以下函数用于生成超参数的字符串:

def make_hparam_string(learning_rate, use_two_fc, use_two_conv):
    conv_param = "conv=2" if use_two_conv else "conv=1"
    fc_param = "fc=2" if use_two_fc else "fc=1"
    return "lr_%.0E,%s,%s" % (learning_rate, conv_param, fc_param)

开始训练:

for learning_rate in [1E-3, 1E-4, 1e-5]:
    for use_two_fc in [False, True]:
        for use_two_conv in [False, True]:
            hparam = make_hparam_string(learning_rate, use_two_fc, use_two_conv)
            print('Starting run for %s' % hparam)

            mnist_model(learning_rate, use_two_fc, use_two_conv, hparam)

print('Done training!')  

在训练过程中即可直接打开tensorboard实时查看训练情况:

$ tensorboard --logdir tensorboard/mnist4

accuracy1-1

xent1-1

以上就显示了不同参数情况下的准确率和交叉熵变化情况,左下角区域可以选择显示几条线。中间的Horizontal Axis同样给了三种不同的显示,STEP按步长,RELATIVE按相对时间,WALL将它们分开显示。鼠标移动到图像上,会给出部分的详细信息:

accuracy1_detail-1

其他几个部分也是如此,不再详述。

Embeddings

Embeddings可能是TensorBoard最惊艳的部分。它显示了训练样本在三维空间的距离。如下图所示:

embedding1

但是目前我们无法确定某个样本的标签,因此无法确认。需要对代码做一定的修改。

这里只显示1024张图片,需要两个额外的文件,一个存储标签,一个存储每个点的缩略图。这两个文件可以在dandelionmane的GitHub下载。

LABELS = os.path.join(os.getcwd(), "labels_1024.tsv")
SPRITES = os.path.join(os.getcwd(), "sprite_1024.png")

def mnist_model(learning_rate, use_two_fc, use_two_conv, hparam):
    tf.reset_default_graph()    # 重置计算图
    sess = tf.Session()

    x = tf.placeholder(tf.float32, shape=[None, 784], name="x")
    x_image = tf.reshape(x, [-1, 28, 28, 1])
    tf.summary.image('input', x_image, 3)
    y = tf.placeholder(tf.float32, shape=[None, 10], name="labels")

    if use_two_conv:    # 是否使用两个卷积
        conv1 = conv_layer(x_image, 1, 32, "conv1")
        conv_out = conv_layer(conv1, 32, 64, "conv2")
    else:
        conv1 = conv_layer(x_image, 1, 64, "conv")    # 如果使用一个卷积,则再添加一个max_pooling保证维度相通
        conv_out = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    flattened = tf.reshape(conv_out, [-1, 7 * 7 * 64])

    if use_two_fc:    # 是否使用两个全连接
        fc1 = fc_layer(flattened, 7 * 7 * 64, 1024, "fc1")
        relu = tf.nn.relu(fc1)
        embedding_input = relu
        tf.summary.histogram("fc1/relu", relu)
        embedding_size = 1024
        logits = fc_layer(fc1, 1024, 10, "fc2")
    else:
        embedding_input = flattened   # 新添加的embedding_input和embedding_size
        embedding_size = 7*7*64
        logits = fc_layer(flattened, 7*7*64, 10, "fc")

    with tf.name_scope("xent"):
        xent = tf.reduce_mean(
            tf.nn.softmax_cross_entropy_with_logits(
                logits=logits, labels=y), name="xent")
        tf.summary.scalar("xent", xent)

    with tf.name_scope("train"):
        train_step = tf.train.AdamOptimizer(learning_rate).minimize(xent)

    with tf.name_scope("accuracy"):
        correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(y, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        tf.summary.scalar("accuracy", accuracy)

    summ = tf.summary.merge_all()    # 收集所有的summary

    # 添加embedding变量
    embedding = tf.Variable(tf.zeros([1024, embedding_size]), name="test_embedding")
    assignment = embedding.assign(embedding_input)
    saver = tf.train.Saver()     # 保存训练过程

    sess.run(tf.global_variables_initializer())
    writer = tf.summary.FileWriter(tensorboard_dir + hparam)
    writer.add_graph(sess.graph)

    # embedding的配置,详见官方文档
    config = tf.contrib.tensorboard.plugins.projector.ProjectorConfig()
    embedding_config = config.embeddings.add()
    embedding_config.tensor_name = embedding.name
    embedding_config.sprite.image_path = SPRITES
    embedding_config.metadata_path = LABELS
    # Specify the width and height of a single thumbnail.
    embedding_config.sprite.single_image_dim.extend([28, 28])
    tf.contrib.tensorboard.plugins.projector.visualize_embeddings(writer, config)

    for i in range(2001):
        batch = data.train.next_batch(100)
        if i % 5 == 0:   # 每5轮写入一次
            # 同样,最好使用验证集
            [train_accuracy, s] = sess.run([accuracy, summ], feed_dict={x: batch[0], y: batch[1]})
            writer.add_summary(s, i)

        if i % 100 == 0:    # 每100轮保存依存训练过程
            sess.run(assignment, feed_dict={x: data.test.images[:1024], y: data.test.labels[:1024]})
            train_accuracy = sess.run(accuracy, feed_dict={x: batch[0], y: batch[1]})
            saver.save(sess, os.path.join(tensorboard_dir, "model.ckpt"), i)

            print("迭代轮次: {0:>6}, 训练准确率: {1:>6.4%}".format(i, train_accuracy))

        sess.run(train_step, feed_dict={x: batch[0], y: batch[1]})

初始运行时,样本基本分散在空间中,没有什么特殊的规律:

tsne1-2

在经过多轮的迭代后,相同类别的样本聚集在了一起,不同类别的样本分散开来,呈现聚类趋势,虽然存在部分的误分样本。

tsne2-1

可见,Embedding能够反映聚类的属性,这对我们观察分类性能有很直观的帮助。Embedding常用在文本中,例如判断词向量的相似程度。

tsne4-min