From 2615072a3e46a321b163410dea5eeaf43e9272be Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Fri, 6 Oct 2017 19:33:14 -0600 Subject: [PATCH] Copy Beholder into TensorBoard repo (#613) --- tensorboard/plugins/beholder/BUILD | 54 +++ tensorboard/plugins/beholder/__init__.py | 0 tensorboard/plugins/beholder/beholder.py | 178 +++++++ .../plugins/beholder/client_side/BUILD | 31 ++ .../client_side/beholder-dashboard.html | 434 ++++++++++++++++++ .../beholder/client_side/beholder-info.html | 89 ++++ .../beholder/client_side/beholder-video.html | 90 ++++ tensorboard/plugins/beholder/demos/demo/BUILD | 6 + .../plugins/beholder/demos/demo/__init__.py | 0 .../plugins/beholder/demos/demo/demo.py | 225 +++++++++ .../plugins/beholder/file_system_tools.py | 54 +++ tensorboard/plugins/beholder/im_util.py | 262 +++++++++++ .../beholder/resources/arrays-missing.png | Bin 0 -> 3903 bytes .../plugins/beholder/resources/colormaps.npy | Bin 0 -> 24656 bytes .../beholder/resources/frame-missing.png | Bin 0 -> 3817 bytes .../plugins/beholder/resources/no-data.png | Bin 0 -> 3331 bytes .../plugins/beholder/server_side/BUILD | 20 + .../beholder/server_side/beholder_plugin.py | 160 +++++++ tensorboard/plugins/beholder/shared_config.py | 25 + tensorboard/plugins/beholder/video_writing.py | 0 tensorboard/plugins/beholder/visualizer.py | 293 ++++++++++++ 21 files changed, 1921 insertions(+) create mode 100644 tensorboard/plugins/beholder/BUILD create mode 100644 tensorboard/plugins/beholder/__init__.py create mode 100644 tensorboard/plugins/beholder/beholder.py create mode 100644 tensorboard/plugins/beholder/client_side/BUILD create mode 100644 tensorboard/plugins/beholder/client_side/beholder-dashboard.html create mode 100644 tensorboard/plugins/beholder/client_side/beholder-info.html create mode 100644 tensorboard/plugins/beholder/client_side/beholder-video.html create mode 100644 tensorboard/plugins/beholder/demos/demo/BUILD create mode 100644 tensorboard/plugins/beholder/demos/demo/__init__.py create mode 100644 tensorboard/plugins/beholder/demos/demo/demo.py create mode 100644 tensorboard/plugins/beholder/file_system_tools.py create mode 100644 tensorboard/plugins/beholder/im_util.py create mode 100644 tensorboard/plugins/beholder/resources/arrays-missing.png create mode 100644 tensorboard/plugins/beholder/resources/colormaps.npy create mode 100644 tensorboard/plugins/beholder/resources/frame-missing.png create mode 100644 tensorboard/plugins/beholder/resources/no-data.png create mode 100644 tensorboard/plugins/beholder/server_side/BUILD create mode 100644 tensorboard/plugins/beholder/server_side/beholder_plugin.py create mode 100644 tensorboard/plugins/beholder/shared_config.py create mode 100644 tensorboard/plugins/beholder/video_writing.py create mode 100644 tensorboard/plugins/beholder/visualizer.py diff --git a/tensorboard/plugins/beholder/BUILD b/tensorboard/plugins/beholder/BUILD new file mode 100644 index 00000000000..43fe5c8fe40 --- /dev/null +++ b/tensorboard/plugins/beholder/BUILD @@ -0,0 +1,54 @@ +# Description: +# TensorBoard plugin for tensors and tensor variance for an entire graph. + +package(default_visibility = ["//visibility:public"]) +licenses(["notice"]) # Apache 2.0 +exports_files(["LICENSE"]) + +py_library( + name = "file_system_tools", + data = ["resources"], + srcs = ["file_system_tools.py"], + srcs_version = "PY2AND3", +) + +py_library( + name = "im_util", + data = ["resources"], + srcs = ["im_util.py"], + deps = [":file_system_tools"], + srcs_version = "PY2AND3", +) + +py_library( + name = "visualizer", + srcs = ["visualizer.py", "shared_config.py"], + deps = [ + ":im_util", + ":file_system_tools", + ], + srcs_version = "PY2AND3", +) + +py_library( + name = "video_writing", + srcs = ["video_writing.py"], + deps = [ + ":im_util" + ], + srcs_version = "PY2AND3" +) + +py_library( + name = "beholder", + data = ["resources"], + srcs = ["beholder.py", "shared_config.py"], + deps = [ + ":im_util", + ":visualizer", + ":file_system_tools", + ":video_writing", + "//tensorboard/backend/event_processing:plugin_asset_util", + ], + srcs_version = "PY2AND3", +) diff --git a/tensorboard/plugins/beholder/__init__.py b/tensorboard/plugins/beholder/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tensorboard/plugins/beholder/beholder.py b/tensorboard/plugins/beholder/beholder.py new file mode 100644 index 00000000000..88a3eec3bad --- /dev/null +++ b/tensorboard/plugins/beholder/beholder.py @@ -0,0 +1,178 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import time + +import tensorflow as tf + +from tensorboard.plugins.beholder import im_util +from tensorboard.plugins.beholder.file_system_tools import read_pickle,\ + write_pickle, write_file +from tensorboard.plugins.beholder.shared_config import PLUGIN_NAME, TAG_NAME,\ + SUMMARY_FILENAME, DEFAULT_CONFIG, CONFIG_FILENAME +from tensorboard.plugins.beholder import video_writing +from tensorboard.plugins.beholder.visualizer import Visualizer + +class Beholder(object): + + def __init__(self, session, logdir): + self.video_writer = None + + self.PLUGIN_LOGDIR = logdir + '/plugins/' + PLUGIN_NAME + self.SESSION = session + + self.frame_placeholder = None + self.summary_op = None + + self.last_image_shape = [] + self.last_update_time = time.time() + self.config_last_modified_time = -1 + self.previous_config = dict(DEFAULT_CONFIG) + + if not tf.gfile.Exists(self.PLUGIN_LOGDIR + '/config.pkl'): + tf.gfile.MakeDirs(self.PLUGIN_LOGDIR) + write_pickle(DEFAULT_CONFIG, '{}/{}'.format(self.PLUGIN_LOGDIR, + CONFIG_FILENAME)) + + self.visualizer = Visualizer(self.PLUGIN_LOGDIR) + + + def _get_config(self): + '''Reads the config file from disk or creates a new one.''' + filename = '{}/{}'.format(self.PLUGIN_LOGDIR, CONFIG_FILENAME) + modified_time = os.path.getmtime(filename) + + if modified_time != self.config_last_modified_time: + config = read_pickle(filename, default=self.previous_config) + self.previous_config = config + else: + config = self.previous_config + + self.config_last_modified_time = modified_time + return config + + + def _write_summary(self, frame): + '''Writes the frame to disk as a tensor summary.''' + summary = self.SESSION.run(self.summary_op, feed_dict={ + self.frame_placeholder: frame + }) + path = '{}/{}'.format(self.PLUGIN_LOGDIR, SUMMARY_FILENAME) + write_file(summary, path) + + + + def _get_final_image(self, config, arrays=None, frame=None): + if config['values'] == 'frames': + if frame is None: + final_image = im_util.get_image_relative_to_script('frame-missing.png') + else: + frame = frame() if callable(frame) else frame + final_image = im_util.scale_image_for_display(frame) + + elif config['values'] == 'arrays': + if arrays is None: + final_image = im_util.get_image_relative_to_script('arrays-missing.png') + # TODO: hack to clear the info. Should be cleaner. + self.visualizer._save_section_info([], []) + else: + final_image = self.visualizer.build_frame(arrays) + + elif config['values'] == 'trainable_variables': + arrays = [self.SESSION.run(x) for x in tf.trainable_variables()] + final_image = self.visualizer.build_frame(arrays) + + return final_image + + + def _enough_time_has_passed(self, FPS): + '''For limiting how often frames are computed.''' + if FPS == 0: + return False + else: + earliest_time = self.last_update_time + (1.0 / FPS) + return time.time() >= earliest_time + + + def _update_frame(self, arrays, frame, config): + final_image = self._get_final_image(config, arrays, frame) + + if self.summary_op is None or self.last_image_shape != final_image.shape: + self.frame_placeholder = tf.placeholder(tf.uint8, final_image.shape) + self.summary_op = tf.summary.tensor_summary(TAG_NAME, + self.frame_placeholder) + self._write_summary(final_image) + self.last_image_shape = final_image.shape + + return final_image + + + def _update_recording(self, frame, config): + '''Adds a frame to the video using ffmpeg if possible. If not, writes + individual frames as png files in a directory. + ''' + # pylint: disable=redefined-variable-type + is_recording = config['is_recording'] + filename = self.PLUGIN_LOGDIR + '/video-{}.mp4'.format(time.time()) + + if is_recording: + if self.video_writer is None or frame.shape != self.video_writer.size: + try: + self.video_writer = video_writing.FFMPEG_VideoWriter(filename, + frame.shape, + 15) + except OSError: + message = ('Either ffmpeg is not installed, or something else went ' + 'wrong. Saving individual frames to disk instead.') + print(message) + self.video_writer = video_writing.PNGWriter(self.PLUGIN_LOGDIR, + frame.shape) + self.video_writer.write_frame(frame) + elif not is_recording and self.video_writer is not None: + self.video_writer.close() + self.video_writer = None + + + # TODO: blanket try and except for production? I don't someone's script to die + # after weeks of running because of a visualization. + def update(self, arrays=None, frame=None): + '''Creates a frame and writes it to disk. + + Args: + arrays: a list of np arrays. Use the "custom" option in the client. + frame: a 2D np array. This way the plugin can be used for video of any + kind, not just the visualization that comes with the plugin. + + frame can also be a function, which only is evaluated when the + "frame" option is selected by the client. + ''' + new_config = self._get_config() + + if self._enough_time_has_passed(self.previous_config['FPS']): + self.visualizer.update(new_config) + self.last_update_time = time.time() + final_image = self._update_frame(arrays, frame, new_config) + self._update_recording(final_image, new_config) + + + ############################################################################## + + @staticmethod + def gradient_helper(optimizer, loss, var_list=None): + '''A helper to get the gradients out at each step. + + Args: + optimizer: the optimizer op. + loss: the op that computes your loss value. + + Returns: the gradient tensors and the train_step op. + ''' + if var_list is None: + var_list = tf.trainable_variables() + + grads_and_vars = optimizer.compute_gradients(loss, var_list=var_list) + grads = [pair[0] for pair in grads_and_vars] + + return grads, optimizer.apply_gradients(grads_and_vars) diff --git a/tensorboard/plugins/beholder/client_side/BUILD b/tensorboard/plugins/beholder/client_side/BUILD new file mode 100644 index 00000000000..c03ce116832 --- /dev/null +++ b/tensorboard/plugins/beholder/client_side/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("@org_tensorflow_tensorboard//tensorboard/defs:web.bzl", "ts_web_library") + +ts_web_library( + name = "dashboard", + srcs = [ + "beholder-dashboard.html", + "beholder-video.html", + "beholder-info.html", + ], + path = "/beholder", + deps = [ + "@org_tensorflow_tensorboard//tensorboard/components/tf_backend", + "@org_tensorflow_tensorboard//tensorboard/components/tf_card_heading", + "@org_tensorflow_tensorboard//tensorboard/components/tf_categorization_utils", + "@org_tensorflow_tensorboard//tensorboard/components/tf_color_scale", + "@org_tensorflow_tensorboard//tensorboard/components/tf_dashboard_common", + "@org_tensorflow_tensorboard//tensorboard/components/tf_imports:d3", + "@org_tensorflow_tensorboard//tensorboard/components/tf_imports:lodash", + "@org_tensorflow_tensorboard//tensorboard/components/tf_imports:polymer", + "@org_tensorflow_tensorboard//tensorboard/components/tf_runs_selector", + "@org_polymer_paper_radio_group", + "@org_polymer_paper_button", + "@org_polymer_paper_dialog", + "@org_polymer_paper_icon_button", + "@org_polymer_paper_slider", + "@org_polymer_paper_spinner", + "@org_polymer_paper_tooltip", + ], +) diff --git a/tensorboard/plugins/beholder/client_side/beholder-dashboard.html b/tensorboard/plugins/beholder/client_side/beholder-dashboard.html new file mode 100644 index 00000000000..19dbb95a6cd --- /dev/null +++ b/tensorboard/plugins/beholder/client_side/beholder-dashboard.html @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + diff --git a/tensorboard/plugins/beholder/client_side/beholder-info.html b/tensorboard/plugins/beholder/client_side/beholder-info.html new file mode 100644 index 00000000000..64fa5b2b0eb --- /dev/null +++ b/tensorboard/plugins/beholder/client_side/beholder-info.html @@ -0,0 +1,89 @@ + + + + + + + + + + diff --git a/tensorboard/plugins/beholder/client_side/beholder-video.html b/tensorboard/plugins/beholder/client_side/beholder-video.html new file mode 100644 index 00000000000..2bf3a822387 --- /dev/null +++ b/tensorboard/plugins/beholder/client_side/beholder-video.html @@ -0,0 +1,90 @@ + + + + + + + + + + diff --git a/tensorboard/plugins/beholder/demos/demo/BUILD b/tensorboard/plugins/beholder/demos/demo/BUILD new file mode 100644 index 00000000000..022b7ba0f11 --- /dev/null +++ b/tensorboard/plugins/beholder/demos/demo/BUILD @@ -0,0 +1,6 @@ +py_binary( + name = "demo", + deps = ["//tensorboard/plugins/beholder"], + srcs = ["demo.py"], + srcs_version = "PY2AND3", +) diff --git a/tensorboard/plugins/beholder/demos/demo/__init__.py b/tensorboard/plugins/beholder/demos/demo/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tensorboard/plugins/beholder/demos/demo/demo.py b/tensorboard/plugins/beholder/demos/demo/demo.py new file mode 100644 index 00000000000..223e792c633 --- /dev/null +++ b/tensorboard/plugins/beholder/demos/demo/demo.py @@ -0,0 +1,225 @@ +# Copyright 2015 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""A simple MNIST classifier which displays summaries in TensorBoard. + +This is an unimpressive MNIST model, but it is a good example of using +tf.name_scope to make a graph legible in the TensorBoard graph explorer, and of +naming summary tags so that they are grouped meaningfully in TensorBoard. + +It demonstrates the functionality of every TensorBoard dashboard. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import sys + +import numpy as np +import tensorflow as tf + +from tensorflow.examples.tutorials.mnist import input_data + +from beholder.beholder import Beholder + +FLAGS = None + +LOG_DIRECTORY = '/tmp/beholder-demo' + +def train(): + mnist = input_data.read_data_sets(FLAGS.data_dir, + one_hot=True, + fake_data=FLAGS.fake_data) + + sess = tf.InteractiveSession() + + with tf.name_scope('input'): + x = tf.placeholder(tf.float32, [None, 784], name='x-input') + y_ = tf.placeholder(tf.float32, [None, 10], name='y-input') + + with tf.name_scope('input_reshape'): + image_shaped_input = tf.reshape(x, [-1, 28, 28, 1]) + tf.summary.image('input', image_shaped_input, 10) + + def weight_variable(shape): + """Create a weight variable with appropriate initialization.""" + initial = tf.truncated_normal(shape, stddev=0.01) + return tf.Variable(initial) + + def bias_variable(shape): + """Create a bias variable with appropriate initialization.""" + initial = tf.constant(0.1, shape=shape) + return tf.Variable(initial) + + def variable_summaries(var): + """Attach a lot of summaries to a Tensor (for TensorBoard visualization).""" + with tf.name_scope('summaries'): + mean = tf.reduce_mean(var) + tf.summary.scalar('mean', mean) + with tf.name_scope('stddev'): + stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean))) + tf.summary.scalar('stddev', stddev) + tf.summary.scalar('max', tf.reduce_max(var)) + tf.summary.scalar('min', tf.reduce_min(var)) + tf.summary.histogram('histogram', var) + + def nn_layer(input_tensor, input_dim, output_dim, layer_name, act=tf.nn.relu): + """Reusable code for making a simple neural net layer. + + It does a matrix multiply, bias add, and then uses ReLU to nonlinearize. + It also sets up name scoping so that the resultant graph is easy to read, + and adds a number of summary ops. + """ + # Adding a name scope ensures logical grouping of the layers in the graph. + with tf.name_scope(layer_name): + # This Variable will hold the state of the weights for the layer + with tf.name_scope('weights'): + weights = weight_variable([input_dim, output_dim]) + variable_summaries(weights) + with tf.name_scope('biases'): + biases = bias_variable([output_dim]) + variable_summaries(biases) + with tf.name_scope('Wx_plus_b'): + preactivate = tf.matmul(input_tensor, weights) + biases + tf.summary.histogram('pre_activations', preactivate) + activations = act(preactivate, name='activation') + tf.summary.histogram('activations', activations) + return activations + + #conv1 + kernel = tf.Variable(tf.truncated_normal([5, 5, 1, 10], dtype=tf.float32, + stddev=1e-1), name='conv-weights') + conv = tf.nn.conv2d(image_shaped_input, kernel, [1, 1, 1, 1], padding='VALID') + biases = tf.Variable(tf.constant(0.0, shape=[kernel.get_shape().as_list()[-1]], dtype=tf.float32), + trainable=True, name='biases') + out = tf.nn.bias_add(conv, biases) + conv1 = tf.nn.relu(out, name='relu') + + #conv2 + kernel2 = tf.Variable(tf.truncated_normal([3, 3, 10, 20], dtype=tf.float32, + stddev=1e-1), name='conv-weights2') + conv2 = tf.nn.conv2d(conv1, kernel2, [1, 1, 1, 1], padding='VALID') + biases2 = tf.Variable(tf.constant(0.0, shape=[kernel2.get_shape().as_list()[-1]], dtype=tf.float32), + trainable=True, name='biases') + out2 = tf.nn.bias_add(conv2, biases2) + conv2 = tf.nn.relu(out2, name='relu') + + flattened = tf.contrib.layers.flatten(conv2) + + + # hidden1 = nn_layer(x, x.get_shape().as_list()[1], 10, 'layer1') + hidden1 = nn_layer(flattened, flattened.get_shape().as_list()[1], 10, 'layer1') + + with tf.name_scope('dropout'): + keep_prob = tf.placeholder(tf.float32) + tf.summary.scalar('dropout_keep_probability', keep_prob) + dropped = tf.nn.dropout(hidden1, keep_prob) + + y = nn_layer(dropped, 10, 10, 'layer2', act=tf.identity) + + with tf.name_scope('cross_entropy'): + diff = tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y) + with tf.name_scope('total'): + cross_entropy = tf.reduce_mean(diff) + tf.summary.scalar('cross_entropy', cross_entropy) + + with tf.name_scope('train'): + optimizer = tf.train.AdamOptimizer(FLAGS.learning_rate) + gradients, train_step = Beholder.gradient_helper(optimizer, cross_entropy) + + with tf.name_scope('accuracy'): + with tf.name_scope('correct_prediction'): + correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1)) + with tf.name_scope('accuracy'): + accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) + tf.summary.scalar('accuracy', accuracy) + + merged = tf.summary.merge_all() + train_writer = tf.summary.FileWriter(LOG_DIRECTORY + '/train', sess.graph) + test_writer = tf.summary.FileWriter(LOG_DIRECTORY + '/test') + tf.global_variables_initializer().run() + + visualizer = Beholder(session=sess, + logdir=LOG_DIRECTORY) + + + def feed_dict(train): + if train or FLAGS.fake_data: + xs, ys = mnist.train.next_batch(100, fake_data=FLAGS.fake_data) + k = FLAGS.dropout + else: + xs, ys = mnist.test.images, mnist.test.labels + k = 1.0 + return {x: xs, y_: ys, keep_prob: k} + + for i in range(FLAGS.max_steps): + # if i % 10 == 0: # Record summaries and test-set accuracy + summary, acc = sess.run([merged, accuracy], feed_dict=feed_dict(False)) + test_writer.add_summary(summary, i) + print('Accuracy at step %s: %s' % (i, acc)) + # else: # Record train set summaries, and train + # if i % 100 == 99: # Record execution stats + # run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE) + # run_metadata = tf.RunMetadata() + # summary, _ = sess.run([merged, train_step], + # feed_dict=feed_dict(True), + # options=run_options, + # run_metadata=run_metadata) + # train_writer.add_run_metadata(run_metadata, 'step%03d' % i) + # train_writer.add_summary(summary, i) + # print('Adding run metadata for', i) + # else: # Record a summary + print('i', i) + feed_dictionary = feed_dict(True) + summary, gradient_arrays, activations, _ = sess.run([merged, gradients, [image_shaped_input, conv1, conv2, hidden1, y], train_step], feed_dict=feed_dictionary) + first_of_batch = sess.run(x, feed_dict=feed_dictionary)[0].reshape(28, 28) + + visualizer.update( + arrays=activations + [first_of_batch] + gradient_arrays, + frame=first_of_batch, + ) + train_writer.add_summary(summary, i) + + train_writer.close() + test_writer.close() + +def main(_): + if not tf.gfile.Exists(LOG_DIRECTORY): + tf.gfile.MakeDirs(LOG_DIRECTORY) + train() + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--fake_data', nargs='?', const=True, type=bool, + default=False, + help='If true, uses fake data for unit testing.') + parser.add_argument('--max_steps', type=int, default=1000000, + help='Number of steps to run trainer.') + parser.add_argument('--learning_rate', type=float, default=0.001, + help='Initial learning rate') + parser.add_argument('--dropout', type=float, default=0.9, + help='Keep probability for training dropout.') + parser.add_argument( + '--data_dir', + type=str, + default='/tmp/tensorflow/mnist/input_data', + help='Directory for storing input data') + parser.add_argument( + '--log_dir', + type=str, + default='/tmp/tensorflow/mnist/logs/mnist_with_summaries', + help='Summaries log directory') + FLAGS, unparsed = parser.parse_known_args() + tf.app.run(main=main, argv=[sys.argv[0]] + unparsed) diff --git a/tensorboard/plugins/beholder/file_system_tools.py b/tensorboard/plugins/beholder/file_system_tools.py new file mode 100644 index 00000000000..2c7634eeea2 --- /dev/null +++ b/tensorboard/plugins/beholder/file_system_tools.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import pickle + +from google.protobuf import message +import tensorflow as tf + +def write_file(contents, path, mode='wb'): + with tf.gfile.Open(path, mode) as new_file: + new_file.write(contents) + + +def read_tensor_summary(path): + with tf.gfile.Open(path, 'rb') as summary_file: + summary_string = summary_file.read() + + if not summary_string: + raise message.DecodeError('Empty summary.') + + summary_proto = tf.Summary() + summary_proto.ParseFromString(summary_string) + tensor_proto = summary_proto.value[0].tensor + array = tf.make_ndarray(tensor_proto) + + return array + + +def write_pickle(obj, path): + with tf.gfile.Open(path, 'wb') as new_file: + pickle.dump(obj, new_file) + + +def read_pickle(path, default=None): + try: + with tf.gfile.Open(path, 'rb') as pickle_file: + result = pickle.load(pickle_file) + + except (IOError, EOFError, ValueError, tf.errors.NotFoundError) as e: + # TODO: log this somehow? Could swallow errors I don't intend. + if default is not None: + result = default + else: + raise e + + return result + + +def resources_path(): + script_directory = os.path.dirname(__file__) + filename = os.path.join(script_directory, 'resources') + return filename diff --git a/tensorboard/plugins/beholder/im_util.py b/tensorboard/plugins/beholder/im_util.py new file mode 100644 index 00000000000..b1d87542066 --- /dev/null +++ b/tensorboard/plugins/beholder/im_util.py @@ -0,0 +1,262 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import threading + +import numpy as np +import tensorflow as tf + +from tensorboard.plugins.beholder.file_system_tools import resources_path + +# pylint: disable=not-context-manager + +def global_extrema(arrays): + return min([x.min() for x in arrays]), max([x.max() for x in arrays]) + + +def scale_sections(sections, scaling_scope): + ''' + input: unscaled sections. + returns: sections scaled to [0, 255] + ''' + new_sections = [] + + if scaling_scope == 'layer': + for section in sections: + new_sections.append(scale_image_for_display(section)) + + elif scaling_scope == 'network': + global_min, global_max = global_extrema(sections) + + for section in sections: + new_sections.append(scale_image_for_display(section, + global_min, + global_max)) + return new_sections + + +def scale_image_for_display(image, minimum=None, maximum=None): + image = image.astype(float) + + minimum = image.min() if minimum is None else minimum + image -= minimum + + maximum = image.max() if maximum is None else maximum + + if maximum == 0: + return image + else: + image *= 255 / maximum + return image.astype(np.uint8) + + +def pad_to_shape(array, shape, constant=245): + padding = [] + + for actual_dim, target_dim in zip(array.shape, shape): + start_padding = 0 + end_padding = target_dim - actual_dim + + padding.append((start_padding, end_padding)) + + return np.pad(array, padding, mode='constant', constant_values=constant) + +# New matplotlib colormaps by Nathaniel J. Smith, Stefan van der Walt, +# and (in the case of viridis) Eric Firing. +# +# This file and the colormaps in it are released under the CC0 license / +# public domain dedication. We would appreciate credit if you use or +# redistribute these colormaps, but do not impose any legal restrictions. +# +# To the extent possible under law, the persons who associated CC0 with +# mpl-colormaps have waived all copyright and related or neighboring rights +# to mpl-colormaps. +# +# You should have received a copy of the CC0 legalcode along with this +# work. If not, see . +colormaps = np.load('{}/colormaps.npy'.format(resources_path())) +magma_data, inferno_data, plasma_data, viridis_data = colormaps + + +def apply_colormap(image, colormap='magma'): + if colormap == 'grayscale': + return image + + data_map = { + 'magma': magma_data, + 'inferno': inferno_data, + 'plasma': plasma_data, + 'viridis': viridis_data, + } + + colormap_data = data_map[colormap] + return (colormap_data[image]*255).astype(np.uint8) + +# Taken from https://github.com/tensorflow/tensorboard/blob/ +# /28f58888ebb22e2db0f4f1f60cd96138ef72b2ef/tensorboard/util.py + +# Modified by Chris Anderson to not use the GPU. +class PersistentOpEvaluator(object): + """Evaluate a fixed TensorFlow graph repeatedly, safely, efficiently. + Extend this class to create a particular kind of op evaluator, like an + image encoder. In `initialize_graph`, create an appropriate TensorFlow + graph with placeholder inputs. In `run`, evaluate this graph and + return its result. This class will manage a singleton graph and + session to preserve memory usage, and will ensure that this graph and + session do not interfere with other concurrent sessions. + A subclass of this class offers a threadsafe, highly parallel Python + entry point for evaluating a particular TensorFlow graph. + Example usage: + class FluxCapacitanceEvaluator(PersistentOpEvaluator): + \"\"\"Compute the flux capacitance required for a system. + Arguments: + x: Available power input, as a `float`, in jigawatts. + Returns: + A `float`, in nanofarads. + \"\"\" + def initialize_graph(self): + self._placeholder = tf.placeholder(some_dtype) + self._op = some_op(self._placeholder) + def run(self, x): + return self._op.eval(feed_dict: {self._placeholder: x}) + evaluate_flux_capacitance = FluxCapacitanceEvaluator() + for x in xs: + evaluate_flux_capacitance(x) + """ + + def __init__(self): + super(PersistentOpEvaluator, self).__init__() + self._session = None + self._initialization_lock = threading.Lock() + + + def _lazily_initialize(self): + """Initialize the graph and session, if this has not yet been done.""" + with self._initialization_lock: + if self._session: + return + graph = tf.Graph() + with graph.as_default(): + self.initialize_graph() + + config = tf.ConfigProto(device_count={'GPU': 0}) + self._session = tf.Session(graph=graph, config=config) + + + def initialize_graph(self): + """Create the TensorFlow graph needed to compute this operation. + This should write ops to the default graph and return `None`. + """ + raise NotImplementedError('Subclasses must implement "initialize_graph".') + + + def run(self, *args, **kwargs): + """Evaluate the ops with the given input. + When this function is called, the default session will have the + graph defined by a previous call to `initialize_graph`. This + function should evaluate any ops necessary to compute the result of + the query for the given *args and **kwargs, likely returning the + result of a call to `some_op.eval(...)`. + """ + raise NotImplementedError('Subclasses must implement "run".') + + + def __call__(self, *args, **kwargs): + self._lazily_initialize() + with self._session.as_default(): + return self.run(*args, **kwargs) + + +class PNGDecoder(PersistentOpEvaluator): + + def __init__(self): + super(PNGDecoder, self).__init__() + self._image_placeholder = None + self._decode_op = None + + + def initialize_graph(self): + self._image_placeholder = tf.placeholder(dtype=tf.string) + self._decode_op = tf.image.decode_png(self._image_placeholder) + + + # pylint: disable=arguments-differ + def run(self, image): + return self._decode_op.eval(feed_dict={ + self._image_placeholder: image, + }) + + +class PNGEncoder(PersistentOpEvaluator): + + def __init__(self): + super(PNGEncoder, self).__init__() + self._image_placeholder = None + self._encode_op = None + + + def initialize_graph(self): + self._image_placeholder = tf.placeholder(dtype=tf.uint8) + self._encode_op = tf.image.encode_png(self._image_placeholder) + + + # pylint: disable=arguments-differ + def run(self, image): + if len(image.shape) == 2: + image = image.reshape([image.shape[0], image.shape[1], 1]) + + return self._encode_op.eval(feed_dict={ + self._image_placeholder: image, + }) + + +class Resizer(PersistentOpEvaluator): + + def __init__(self): + super(Resizer, self).__init__() + self._image_placeholder = None + self._size_placeholder = None + self._resize_op = None + + + def initialize_graph(self): + self._image_placeholder = tf.placeholder(dtype=tf.float32) + self._size_placeholder = tf.placeholder(dtype=tf.int32) + self._resize_op = tf.image.resize_nearest_neighbor(self._image_placeholder, + self._size_placeholder) + + # pylint: disable=arguments-differ + def run(self, image, height, width): + if len(image.shape) == 2: + image = image.reshape([image.shape[0], image.shape[1], 1]) + + resized = np.squeeze(self._resize_op.eval(feed_dict={ + self._image_placeholder: [image], + self._size_placeholder: [height, width] + })) + + return resized + + +decode_png = PNGDecoder() +encode_png = PNGEncoder() +resize = Resizer() + + +def read_image(filename): + with tf.gfile.Open(filename, 'rb') as image_file: + return np.array(decode_png(image_file.read())) + + +def write_image(array, filename): + with tf.gfile.Open(filename, 'w') as image_file: + image_file.write(encode_png(array)) + + +def get_image_relative_to_script(filename): + script_directory = os.path.dirname(__file__) + filename = os.path.join(script_directory, 'resources', filename) + + return read_image(filename) diff --git a/tensorboard/plugins/beholder/resources/arrays-missing.png b/tensorboard/plugins/beholder/resources/arrays-missing.png new file mode 100644 index 0000000000000000000000000000000000000000..9474efc0339b28a60d6ea0ebc1f09a577beb244e GIT binary patch literal 3903 zcma)9X*iVc+nx}zMEsI%%*d84O!j>pnKU%~$eu0xzHiz0HL~v^$(o%k6&iyGk(7NM zlOiO>^4>nYAKv%N|Nk7vJ@*{XGxt2#eVx~NUgsUJi`Jy2Vx@vWAhcR{)iGdx4+ff& z9DI6a;L^cFZgo#n9n9c5QhO5$rYr7ujl3WbYQ~Fk36fX93~o|*Yu(qNSfXHKqLKJu zB)bZ3v3P4d^j33oaj|vvhNyYkT6){s!2KM&?cthQ_jL`!uik(_u2yQPtLXdBZWjjW z>aVe3d*6EQ4K{~`jZg_}1(GtDU^2)^yhyx+FKLmB1hAlD^N^o~_7IKY^QyO>i*wP~ zIO^hb>#nQFpa(w3sqsd{!E@{fHNG=VAX}L7R#ZoN`a2m-pXwuKCX&7edghqux1vI1F_xU7G}Pgi4=MkvM>&_Uql5fO@07AP9bN>{)hucY%#n3(m4w*v#0Ra8`@1NUF5m}+Qf=vL{c4X^DS z9%9cctD+7&rysG zg5@$f`NVv85Wj>(W(*w+?b%TML|K~>FN1ACx>~q-NQN+j~Q}tbi8R| zVv-OS_bHKIQZfsymynQnimoi1sy1YV7`HmJ*xK5TPgLBy0_py{oXAU%cyi+3*w$7# zUZ$z%>4|WeX$S||zElzsLDEDJ;UVV_sI%xrgA{O6lGK{CrA$mr*v~dQkcClEQx613 zM@N%|1qTP)@2!7q24MdF{rjeqlM|*&A4xdrqf^p#b-jJ5q`h5w!m;J^%#8i`-v%3k zA!mU?P!^dA93;=t%}t7*41DqQ@?sRMo5a7Yt4jfZ+|Luot#)LISfEiA>Bh#!{mtp= z3?#~OzCS)OMs#;86v_u+e);R+F~iAmaqys^pw7Sp0lc?tr^gC;M|=AwEr*|<9|&<$ zFcg>0W8>zg)6&wy8@;%`&YPfBIr-rJeOeh=+4SDt-mf00cokI9{=PSfG62}BsI^t{ z_wH_KSQyCAnx~9}xOkicSiHKrYJ;H!d>So~QG?i@9v=kaL!huM=hNe)#kIBQ`}vYf z`}oP37N_iSdAqTq0sLg;Yy0t=?d|Pxcu?05yZpt_g|gAqH*Pa4NJ>at2ba6I_dk;# zlG!LAeR~LJ2ZLR-WreVK)Bn2JP$46poYpcIG zDT$`JwH0t?(&ig2nJN^LyobvLfBi43y zvDxN!QG4a~9YvB*dkSg?KG#~_svQ2Ju^)pYdn>PU$5)u+1 z5aU`VL@td;?@410-<>;*Qf~86fN+3m4IV4VlU*zyG^`V6F7f*{ ze;G0}JIl%*MEv6d3CqmP9B;gXK+vrEFG7U63lM2;N7c#tM?5a z)|tH_zj5P6e}6xmmzUhk%q%W775iuV+eJ-f_}oDr3Wv3|wXK4jE#mR~{Advo5pgLg zE&+j|)kQ+e_PmMEy#||LnwKo4`Q_!@No-PAW3NIH^thSV)=x7sB2!YXigmZNTt7NG z@;}B`F0HIsH;;U4b1SQ^zUP*8bVmFooHAA5+1J;%w7eWar}!5lDJdD)rU3#B$5i!$ zKnBDC?c@)?dqKhgWirG}dxBXC3k#Qj^`0G5Yo@Tj18i7YT!hP@`Ni%g12h;ue*8Fy z!;Q;f>wC`xioO87KRY|Sb?err>V=AX+54{eq;Z8w9}B!Nz}OYJS+`oG5Jbz4@1xs;4sSO$HF*%GYdOBn_4SbgzDfuG z75zRpCkEO>5{E+P*oyVrE=|!^B~H$FkB;<#NEsQiM90KTl&C~*R#g@~H)?u39?!@Z zC~s(D!p6hH^HjAIE+;3KC+WhPnU$4ZU0prjmRN7uk0W+C((3J_aGT5`(g-cbSZlGAY{)zk=S7b_OB zt%*exUt^BB82`uKoLO_eoZ6!JogHy5L5eHh_`Nm^m=xRACIwPZXWD@%nRPtanu?2$ z4_H0US>Tels(JXD1e*%O4rK~1ZEw?dm@1YmSJ&5Tnwdf4Hp7FZL`&?V!dG?fq;3B4 z?EqF`XlO_@^eot3oRyoK`|bSv6XmJeh6Z{*ouc4!^|xb`smwr);WuynL*<$j&Kos< z;t+NowA;1wp4#2VXP`fxF?m)AbuiYE-OwQV_xPB#JQZk$V~g>LONv{XerVDBNpzso z4KD(5J>Si0Bu5Mq=JKr-Xl@tr=bOhZk;b(j?u^Mt0=Y6Lu56B%QPR>z=jXFML-82Y z8b@bkWtkJ0*x8e`GI;TsJOgEe1vBL3^uRZy<>ymF;t~@h4XUG26%2cqXb}mENEa>07G|ocV%yH@n&O}Uj63saVL9}Sf7#8@(9qxCzoe>4OFLt^*qXol zh&*>?%sQnvUFRt10L9 zfCb)i$`BQ5+U5TsxTpPwpr9Z(FK^zzb^58#i~4=e9|qJ!xhu_`$vAzfljvNSnmPzC zI5b~?dz}Oz5QqlfZR7rdfu%yf`FFtHjst-Nos(ws7*%RAc>4O}s6jq~!-(_l?wD8% zXb2jHi9sQe1qB8Bbyf|5dr2eQ0kMaQ^E0&O*>X z3eAUs(+66x;ii*$9Z zleQ*N21=|iklDT}R51q2(i~UIzovaOc~|3A=;*1shQ`v{aMHMx6r{7W5FmcUDe;it z^GFI499e$6clSUAF?Bnnww`~)A$A@ZvnXWQJ2tnj@UKmFT(YvPP$=}Sj!tZ9Djk!s zF?Yw9@NNSl8vG=jl)@eO2;i`2Yu5Di&cv6NmR5iMbWQs*Je&xO&hcuo*Lkbcm)pQo zasq3P5y*U&nW=%Qm;f4|aKUd5ui!N8qd)rq^HrArJ<8=^N&iS#F&F)R gCH&6^J+C9!OhP4Ymq!l@&b1IN4YYcl>Z6GN0T}*e=l}o! literal 0 HcmV?d00001 diff --git a/tensorboard/plugins/beholder/resources/colormaps.npy b/tensorboard/plugins/beholder/resources/colormaps.npy new file mode 100644 index 0000000000000000000000000000000000000000..b0549818313f8c020a20c80101d346658ba554f0 GIT binary patch literal 24656 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 Zaxp=Tp=N)y3Zj=u=1D&56@O0*{tw7%2eAME literal 0 HcmV?d00001 diff --git a/tensorboard/plugins/beholder/resources/frame-missing.png b/tensorboard/plugins/beholder/resources/frame-missing.png new file mode 100644 index 0000000000000000000000000000000000000000..401ad75735bb4edf3bb230b7f44856425707e6fd GIT binary patch literal 3817 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 KqY4Apxc>uwVLCGa literal 0 HcmV?d00001 diff --git a/tensorboard/plugins/beholder/server_side/BUILD b/tensorboard/plugins/beholder/server_side/BUILD new file mode 100644 index 00000000000..7fa4d53983f --- /dev/null +++ b/tensorboard/plugins/beholder/server_side/BUILD @@ -0,0 +1,20 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "beholder_plugin", + srcs = [ + "beholder_plugin.py", + "//tensorboard/plugins/beholder:shared_config.py" + ], + srcs_version = "PY2AND3", + deps = [ + "//tensorboard/plugins/beholder", + "//tensorboard/plugins/beholder:im_util", + "//tensorboard/plugins/beholder:file_system_tools", + "@org_pocoo_werkzeug", + "@org_tensorflow_tensorboard//tensorboard/backend:http_util", + "@org_tensorflow_tensorboard//tensorboard/backend/event_processing:plugin_asset_util", + "@org_tensorflow_tensorboard//tensorboard/backend/event_processing:event_accumulator", + "@org_tensorflow_tensorboard//tensorboard/plugins:base_plugin", + ], +) diff --git a/tensorboard/plugins/beholder/server_side/beholder_plugin.py b/tensorboard/plugins/beholder/server_side/beholder_plugin.py new file mode 100644 index 00000000000..fbb4a48e5fc --- /dev/null +++ b/tensorboard/plugins/beholder/server_side/beholder_plugin.py @@ -0,0 +1,160 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import io +import time + +from google.protobuf import message +import numpy as np +import tensorboard +from tensorboard.backend import http_util +from tensorboard.backend.event_processing import plugin_asset_util as pau +from tensorboard.plugins import base_plugin +import tensorflow as tf +from werkzeug import wrappers + +from beholder.im_util import get_image_relative_to_script, encode_png +from beholder.shared_config import PLUGIN_NAME, SECTION_HEIGHT, IMAGE_WIDTH +from beholder.shared_config import SECTION_INFO_FILENAME, CONFIG_FILENAME,\ + TAG_NAME, SUMMARY_FILENAME, DEFAULT_CONFIG +from beholder.file_system_tools import read_tensor_summary, read_pickle,\ + write_pickle + +import sys +print(sys.version) + +class BeholderPlugin(base_plugin.TBPlugin): + + plugin_name = PLUGIN_NAME + + def __init__(self, context): + self._MULTIPLEXER = context.multiplexer + self.PLUGIN_LOGDIR = pau.PluginDirectory(context.logdir, PLUGIN_NAME) + self.FPS = 10 + self.most_recent_frame = get_image_relative_to_script('no-data.png') + self.most_recent_info = [{ + 'name': 'Waiting for data...', + }] + + if not tf.gfile.Exists(self.PLUGIN_LOGDIR): + tf.gfile.MakeDirs(self.PLUGIN_LOGDIR) + write_pickle(DEFAULT_CONFIG, '{}/{}'.format(self.PLUGIN_LOGDIR, + CONFIG_FILENAME)) + + + def get_plugin_apps(self): + return { + '/change-config': self._serve_change_config, + '/beholder-frame': self._serve_beholder_frame, + '/section-info': self._serve_section_info, + '/ping': self._serve_ping, + '/tags': self._serve_tags, + '/is-active': self._serve_is_active, + } + + + def is_active(self): + summary_filename = '{}/{}'.format(self.PLUGIN_LOGDIR, SUMMARY_FILENAME) + info_filename = '{}/{}'.format(self.PLUGIN_LOGDIR, SECTION_INFO_FILENAME) + return tf.gfile.Exists(summary_filename) and\ + tf.gfile.Exists(info_filename) + + + @wrappers.Request.application + def _serve_is_active(self, request): + return http_util.Respond(request, + {'is_active': self.is_active()}, + 'application/json') + + + def _fetch_current_frame(self): + path = '{}/{}'.format(self.PLUGIN_LOGDIR, SUMMARY_FILENAME) + + try: + frame = read_tensor_summary(path).astype(np.uint8) + self.most_recent_frame = frame + return frame + + except (message.DecodeError, IOError, tf.errors.NotFoundError): + return self.most_recent_frame + + + @wrappers.Request.application + def _serve_tags(self, request): + if self.is_active: + runs_and_tags = { + 'plugins/{}'.format(PLUGIN_NAME): {'tensors': [TAG_NAME]} + } + else: + runs_and_tags = {} + + return http_util.Respond(request, + runs_and_tags, + 'application/json') + + + @wrappers.Request.application + def _serve_change_config(self, request): + config = {} + + for key, value in request.form.items(): + try: + config[key] = int(value) + except ValueError: + if value == 'false': + config[key] = False + elif value == 'true': + config[key] = True + else: + config[key] = value + + self.FPS = config['FPS'] + + write_pickle(config, '{}/{}'.format(self.PLUGIN_LOGDIR, CONFIG_FILENAME)) + return http_util.Respond(request, {'config': config}, 'application/json') + + + @wrappers.Request.application + def _serve_section_info(self, request): + path = '{}/{}'.format(self.PLUGIN_LOGDIR, SECTION_INFO_FILENAME) + info = read_pickle(path, default=self.most_recent_info) + self.most_recent_info = info + return http_util.Respond(request, info, 'application/json') + + + def _frame_generator(self): + + while True: + last_duration = 0 + + if self.FPS == 0: + continue + else: + time.sleep(max(0, 1/(self.FPS) - last_duration)) + + start_time = time.time() + array = self._fetch_current_frame() + image_bytes = encode_png(array) + + frame_text = b'--frame\r\n' + content_type = b'Content-Type: image/png\r\n\r\n' + + response_content = frame_text + content_type + image_bytes + b'\r\n\r\n' + + last_duration = time.time() - start_time + yield response_content + + + @wrappers.Request.application + def _serve_beholder_frame(self, request): # pylint: disable=unused-argument + # Thanks to Miguel Grinberg for this technique: + # https://blog.miguelgrinberg.com/post/video-streaming-with-flask + mimetype = 'multipart/x-mixed-replace; boundary=frame' + return wrappers.Response(response=self._frame_generator(), + status=200, + mimetype=mimetype) + + @wrappers.Request.application + def _serve_ping(self, request): # pylint: disable=unused-argument + return http_util.Respond(request, {'status': 'alive'}, 'application/json') diff --git a/tensorboard/plugins/beholder/shared_config.py b/tensorboard/plugins/beholder/shared_config.py new file mode 100644 index 00000000000..6f4695a4549 --- /dev/null +++ b/tensorboard/plugins/beholder/shared_config.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +PLUGIN_NAME = 'beholder' +TAG_NAME = 'beholder-frame' +SUMMARY_FILENAME = 'frame.summary' +CONFIG_FILENAME = 'config.pkl' +SECTION_INFO_FILENAME = 'section-info.pkl' + +DEFAULT_CONFIG = { + 'values': 'trainable_variables', + 'mode': 'variance', + 'scaling': 'layer', + 'window_size': 15, + 'FPS': 10, + 'is_recording': False, + 'show_all': False, + 'colormap': 'magma' +} + +SECTION_HEIGHT = 128 +IMAGE_WIDTH = 512 + 256 + +TB_WHITE = 245 diff --git a/tensorboard/plugins/beholder/video_writing.py b/tensorboard/plugins/beholder/video_writing.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tensorboard/plugins/beholder/visualizer.py b/tensorboard/plugins/beholder/visualizer.py new file mode 100644 index 00000000000..22d9b5a7fd3 --- /dev/null +++ b/tensorboard/plugins/beholder/visualizer.py @@ -0,0 +1,293 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from collections import deque +from math import floor, sqrt + +import numpy as np +import tensorflow as tf + +from tensorboard.plugins.beholder import im_util +from tensorboard.plugins.beholder.shared_config import SECTION_HEIGHT,\ + IMAGE_WIDTH, DEFAULT_CONFIG, SECTION_INFO_FILENAME +from tensorboard.plugins.beholder.file_system_tools import write_pickle + +MIN_SQUARE_SIZE = 3 + +class Visualizer(object): + + def __init__(self, logdir): + self.logdir = logdir + self.sections_over_time = deque([], DEFAULT_CONFIG['window_size']) + self.config = dict(DEFAULT_CONFIG) + self.old_config = dict(DEFAULT_CONFIG) + + + def _reshape_conv_array(self, array, section_height, image_width): + '''Reshape a rank 4 array to be rank 2, where each column of block_width is + a filter, and each row of block height is an input channel. For example: + + [[[[ 11, 21, 31, 41], + [ 51, 61, 71, 81], + [ 91, 101, 111, 121]], + [[ 12, 22, 32, 42], + [ 52, 62, 72, 82], + [ 92, 102, 112, 122]], + [[ 13, 23, 33, 43], + [ 53, 63, 73, 83], + [ 93, 103, 113, 123]]], + [[[ 14, 24, 34, 44], + [ 54, 64, 74, 84], + [ 94, 104, 114, 124]], + [[ 15, 25, 35, 45], + [ 55, 65, 75, 85], + [ 95, 105, 115, 125]], + [[ 16, 26, 36, 46], + [ 56, 66, 76, 86], + [ 96, 106, 116, 126]]], + [[[ 17, 27, 37, 47], + [ 57, 67, 77, 87], + [ 97, 107, 117, 127]], + [[ 18, 28, 38, 48], + [ 58, 68, 78, 88], + [ 98, 108, 118, 128]], + [[ 19, 29, 39, 49], + [ 59, 69, 79, 89], + [ 99, 109, 119, 129]]]] + + should be reshaped to: + + [[ 11, 12, 13, 21, 22, 23, 31, 32, 33, 41, 42, 43], + [ 14, 15, 16, 24, 25, 26, 34, 35, 36, 44, 45, 46], + [ 17, 18, 19, 27, 28, 29, 37, 38, 39, 47, 48, 49], + [ 51, 52, 53, 61, 62, 63, 71, 72, 73, 81, 82, 83], + [ 54, 55, 56, 64, 65, 66, 74, 75, 76, 84, 85, 86], + [ 57, 58, 59, 67, 68, 69, 77, 78, 79, 87, 88, 89], + [ 91, 92, 93, 101, 102, 103, 111, 112, 113, 121, 122, 123], + [ 94, 95, 96, 104, 105, 106, 114, 115, 116, 124, 125, 126], + [ 97, 98, 99, 107, 108, 109, 117, 118, 119, 127, 128, 129]] + ''' + + # E.g. [100, 24, 24, 10]: this shouldn't be reshaped like normal. + if array.shape[1] == array.shape[2] and array.shape[0] != array.shape[1]: + array = np.rollaxis(np.rollaxis(array, 2), 2) + + block_height, block_width, in_channels = array.shape[:3] + rows = [] + + max_element_count = section_height * int(image_width / MIN_SQUARE_SIZE) + element_count = 0 + + for i in range(in_channels): + rows.append(array[:, :, i, :].reshape(block_height, -1, order='F')) + + # This line should be left in this position. Gives it one extra row. + if element_count >= max_element_count and not self.config['show_all']: + break + + element_count += block_height * in_channels * block_width + + return np.vstack(rows) + + + def _reshape_irregular_array(self, array, section_height, image_width): + '''Reshapes arrays of ranks not in {1, 2, 4} + ''' + section_area = section_height * image_width + flattened_array = np.ravel(array) + + if not self.config['show_all']: + flattened_array = flattened_array[:int(section_area/MIN_SQUARE_SIZE)] + + cell_count = np.prod(flattened_array.shape) + cell_area = section_area / cell_count + + cell_side_length = max(1, floor(sqrt(cell_area))) + row_count = max(1, int(section_height / cell_side_length)) + col_count = int(cell_count / row_count) + + # Reshape the truncated array so that it has the same aspect ratio as + # the section. + + # Truncate whatever remaining values there are that don't fit. Hopefully + # it doesn't matter that the last few (< section count) aren't there. + section = np.reshape(flattened_array[:row_count * col_count], + (row_count, col_count)) + + return section + + + def _determine_image_width(self, arrays, show_all): + final_width = IMAGE_WIDTH + + if show_all: + for array in arrays: + rank = len(array.shape) + + if rank == 1: + width = len(array) + elif rank == 2: + width = array.shape[1] + elif rank == 4: + width = array.shape[1] * array.shape[3] + else: + width = IMAGE_WIDTH + + if width > final_width: + final_width = width + + return final_width + + + def _determine_section_height(self, array, show_all): + rank = len(array.shape) + height = SECTION_HEIGHT + + if show_all: + if rank == 1: + height = SECTION_HEIGHT + if rank == 2: + height = max(SECTION_HEIGHT, array.shape[0]) + elif rank == 4: + height = max(SECTION_HEIGHT, array.shape[0] * array.shape[2]) + else: + height = max(SECTION_HEIGHT, np.prod(array.shape) // IMAGE_WIDTH) + + return height + + + def _arrays_to_sections(self, arrays): + ''' + input: unprocessed numpy arrays. + returns: columns of the size that they will appear in the image, not scaled + for display. That needs to wait until after variance is computed. + ''' + sections = [] + sections_to_resize_later = {} + show_all = self.config['show_all'] + image_width = self._determine_image_width(arrays, show_all) + + for array_number, array in enumerate(arrays): + rank = len(array.shape) + section_height = self._determine_section_height(array, show_all) + + if rank == 1: + section = np.atleast_2d(array) + elif rank == 2: + section = array + elif rank == 4: + section = self._reshape_conv_array(array, section_height, image_width) + else: + section = self._reshape_irregular_array(array, + section_height, + image_width) + # Only calculate variance for what we have to. In some cases (biases), + # the section is larger than the array, so we don't want to calculate + # variance for the same value over and over - better to resize later. + # About a 6-7x speedup for a big network with a big variance window. + section_size = section_height * image_width + array_size = np.prod(array.shape) + + if section_size > array_size: + sections.append(section) + sections_to_resize_later[array_number] = section_height + else: + sections.append(im_util.resize(section, section_height, image_width)) + + self.sections_over_time.append(sections) + + if self.config['mode'] == 'variance': + sections = self._sections_to_variance_sections(self.sections_over_time) + + for array_number, height in sections_to_resize_later.items(): + sections[array_number] = im_util.resize(sections[array_number], + height, + image_width) + return sections + + + def _sections_to_variance_sections(self, sections_over_time): + '''Computes the variance of corresponding sections over time. + + Returns: + a list of np arrays. + ''' + variance_sections = [] + + for i in range(len(sections_over_time[0])): + time_sections = [sections[i] for sections in sections_over_time] + variance = np.var(time_sections, axis=0) + variance_sections.append(variance) + + return variance_sections + + + def _sections_to_image(self, sections): + padding_size = 5 + + sections = im_util.scale_sections(sections, self.config['scaling']) + + final_stack = [sections[0]] + padding = np.zeros((padding_size, sections[0].shape[1])) + + for section in sections[1:]: + final_stack.append(padding) + final_stack.append(section) + + return np.vstack(final_stack).astype(np.uint8) + + + def _maybe_clear_deque(self): + '''Clears the deque if certain parts of the config have changed.''' + + for config_item in ['values', 'mode', 'show_all']: + if self.config[config_item] != self.old_config[config_item]: + self.sections_over_time.clear() + break + + self.old_config = self.config + + window_size = self.config['window_size'] + if window_size != self.sections_over_time.maxlen: + self.sections_over_time = deque(self.sections_over_time, window_size) + + + def _save_section_info(self, arrays, sections): + infos = [] + + if self.config['values'] == 'trainable_variables': + names = [x.name for x in tf.trainable_variables()] + else: + names = range(len(arrays)) + + for array, section, name in zip(arrays, sections, names): + info = {} + + info['name'] = name + info['shape'] = str(array.shape) + info['min'] = '{:.3e}'.format(section.min()) + info['mean'] = '{:.3e}'.format(section.mean()) + info['max'] = '{:.3e}'.format(section.max()) + info['range'] = '{:.3e}'.format(section.max() - section.min()) + info['height'] = section.shape[0] + + infos.append(info) + + write_pickle(infos, '{}/{}'.format(self.logdir, SECTION_INFO_FILENAME)) + + + def build_frame(self, arrays): + self._maybe_clear_deque() + + arrays = arrays if isinstance(arrays, list) else [arrays] + + sections = self._arrays_to_sections(arrays) + self._save_section_info(arrays, sections) + final_image = self._sections_to_image(sections) + final_image = im_util.apply_colormap(final_image, self.config['colormap']) + + return final_image + + def update(self, config): + self.config = config