diff --git a/tensorflow/BUILD b/tensorflow/BUILD index 055c55a7170..ce1387ba43c 100644 --- a/tensorflow/BUILD +++ b/tensorflow/BUILD @@ -380,6 +380,7 @@ filegroup( "//tensorflow/tensorboard/java/org/tensorflow/tensorboard/vulcanize:all_files", "//tensorflow/tensorboard/plugins:all_files", "//tensorflow/tensorboard/plugins/audio:all_files", + "//tensorflow/tensorboard/plugins/distributions:all_files", "//tensorflow/tensorboard/plugins/histograms:all_files", "//tensorflow/tensorboard/plugins/images:all_files", "//tensorflow/tensorboard/plugins/projector:all_files", diff --git a/tensorflow/contrib/cmake/tf_python.cmake b/tensorflow/contrib/cmake/tf_python.cmake index 132d84d00bb..95dbefc37ab 100755 --- a/tensorflow/contrib/cmake/tf_python.cmake +++ b/tensorflow/contrib/cmake/tf_python.cmake @@ -230,6 +230,7 @@ add_python_module("tensorflow/tensorboard/backend") add_python_module("tensorflow/tensorboard/backend/event_processing") add_python_module("tensorflow/tensorboard/plugins") add_python_module("tensorflow/tensorboard/plugins/audio") +add_python_module("tensorflow/tensorboard/plugins/distributions") add_python_module("tensorflow/tensorboard/plugins/histograms") add_python_module("tensorflow/tensorboard/plugins/images") add_python_module("tensorflow/tensorboard/plugins/projector") diff --git a/tensorflow/tensorboard/BUILD b/tensorflow/tensorboard/BUILD index 0b9c254b514..a8a4fb16614 100644 --- a/tensorflow/tensorboard/BUILD +++ b/tensorflow/tensorboard/BUILD @@ -14,6 +14,7 @@ py_binary( "//tensorflow/tensorboard/backend:application", "//tensorflow/tensorboard/backend/event_processing:event_file_inspector", "//tensorflow/tensorboard/plugins/audio:audio_plugin", + "//tensorflow/tensorboard/plugins/distributions:distributions_plugin", "//tensorflow/tensorboard/plugins/histograms:histograms_plugin", "//tensorflow/tensorboard/plugins/images:images_plugin", "//tensorflow/tensorboard/plugins/projector:projector_plugin", diff --git a/tensorflow/tensorboard/backend/application.py b/tensorflow/tensorboard/backend/application.py index ef2d0c6d693..46f081a67c9 100644 --- a/tensorflow/tensorboard/backend/application.py +++ b/tensorflow/tensorboard/backend/application.py @@ -22,15 +22,12 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -import csv import os import re import threading import time import six -from six import StringIO -from six.moves import xrange # pylint: disable=redefined-builtin from six.moves.urllib import parse as urlparse import tensorflow as tf from werkzeug import wrappers @@ -59,6 +56,7 @@ DEFAULT_SIZE_GUIDANCE = { # /data/runs entirely. _MIGRATED_DATA_KEYS = frozenset(( 'audio', + 'distributions', 'histograms', 'images', 'scalars', @@ -69,7 +67,6 @@ LOGDIR_ROUTE = '/logdir' RUNS_ROUTE = '/runs' PLUGIN_PREFIX = '/plugin' PLUGINS_LISTING_ROUTE = '/plugins_listing' -COMPRESSED_HISTOGRAMS_ROUTE = '/' + event_accumulator.COMPRESSED_HISTOGRAMS GRAPH_ROUTE = '/' + event_accumulator.GRAPH RUN_METADATA_ROUTE = '/' + event_accumulator.RUN_METADATA TAB_ROUTES = ['', '/events', '/images', '/audio', '/graphs', '/histograms'] @@ -80,16 +77,6 @@ TAB_ROUTES = ['', '/events', '/images', '/audio', '/graphs', '/histograms'] _VALID_PLUGIN_RE = re.compile(r'^[A-Za-z0-9_.-]+$') -class _OutputFormat(object): - """An enum used to list the valid output formats for API calls. - - Not all API calls support all formats (for example, only scalars and - compressed histograms support CSV). - """ - JSON = 'json' - CSV = 'csv' - - def standard_tensorboard_wsgi( logdir, purge_orphaned_data, @@ -159,8 +146,6 @@ class TensorBoardWSGIApp(object): reload_multiplexer(self._multiplexer, path_to_run) self.data_applications = { - DATA_PREFIX + COMPRESSED_HISTOGRAMS_ROUTE: - self._serve_compressed_histograms, DATA_PREFIX + GRAPH_ROUTE: self._serve_graph, DATA_PREFIX + LOGDIR_ROUTE: @@ -278,35 +263,6 @@ class TensorBoardWSGIApp(object): return http_util.Respond( request, str(run_metadata), 'text/x-protobuf') # pbtxt - @wrappers.Request.application - def _serve_compressed_histograms(self, request): - """Given a tag and single run, return an array of compressed histograms.""" - tag = request.args.get('tag') - run = request.args.get('run') - compressed_histograms = self._multiplexer.CompressedHistograms(run, tag) - if request.args.get('format') == _OutputFormat.CSV: - string_io = StringIO() - writer = csv.writer(string_io) - - # Build the headers; we have two columns for timing and two columns for - # each compressed histogram bucket. - headers = ['Wall time', 'Step'] - if compressed_histograms: - bucket_count = len(compressed_histograms[0].compressed_histogram_values) - for i in xrange(bucket_count): - headers += ['Edge %d basis points' % i, 'Edge %d value' % i] - writer.writerow(headers) - - for compressed_histogram in compressed_histograms: - row = [compressed_histogram.wall_time, compressed_histogram.step] - for value in compressed_histogram.compressed_histogram_values: - row += [value.rank_in_bps, value.value] - writer.writerow(row) - return http_util.Respond(request, string_io.getvalue(), 'text/csv') - else: - return http_util.Respond( - request, compressed_histograms, 'application/json') - @wrappers.Request.application def _serve_plugins_listing(self, request): """Serves an object mapping plugin name to whether it is enabled. diff --git a/tensorflow/tensorboard/backend/application_test.py b/tensorflow/tensorboard/backend/application_test.py index 9729e6395a0..f05c9352466 100644 --- a/tensorflow/tensorboard/backend/application_test.py +++ b/tensorflow/tensorboard/backend/application_test.py @@ -167,7 +167,6 @@ class TensorboardServerTest(tf.test.TestCase): run_json, { 'run1': { - 'compressedHistograms': ['histogram'], # if only_use_meta_graph, the graph is from the metagraph 'graph': True, 'meta_graph': self._only_use_meta_graph, @@ -265,13 +264,8 @@ class TensorboardServerTest(tf.test.TestCase): """Generates the test data directory. The test data has a single run named run1 which contains: - - a histogram [1] - a graph definition - [1]: Histograms no longer appear in `/runs`, but compressed - histograms do, and they use the same test data. Thus, histograms are - still here for now. - Returns: temp_dir: The directory the test data is generated under. """ @@ -281,14 +275,6 @@ class TensorboardServerTest(tf.test.TestCase): os.makedirs(run1_path) writer = tf.summary.FileWriter(run1_path) - histogram_value = tf.HistogramProto( - min=0, - max=2, - num=3, - sum=6, - sum_squares=5, - bucket_limit=[0, 1, 2], - bucket=[1, 1, 1]) # Add a simple graph event. graph_def = tf.GraphDef() node1 = graph_def.node.add() @@ -309,13 +295,6 @@ class TensorboardServerTest(tf.test.TestCase): device_stats = run_metadata.step_stats.dev_stats.add() device_stats.device = 'test device' writer.add_run_metadata(run_metadata, 'test run') - writer.add_event( - tf.Event( - wall_time=0, - step=0, - summary=tf.Summary(value=[ - tf.Summary.Value(tag='histogram', histo=histogram_value), - ]))) writer.flush() writer.close() diff --git a/tensorflow/tensorboard/backend/event_processing/event_accumulator.py b/tensorflow/tensorboard/backend/event_processing/event_accumulator.py index 1669c060844..1562f0f8339 100644 --- a/tensorflow/tensorboard/backend/event_processing/event_accumulator.py +++ b/tensorflow/tensorboard/backend/event_processing/event_accumulator.py @@ -72,7 +72,7 @@ SUMMARY_TYPES = { ## The tagTypes below are just arbitrary strings chosen to pass the type ## information of the tag from the backend to the frontend -COMPRESSED_HISTOGRAMS = 'compressedHistograms' +COMPRESSED_HISTOGRAMS = 'distributions' HISTOGRAMS = 'histograms' IMAGES = 'images' AUDIO = 'audio' diff --git a/tensorflow/tensorboard/components/tf_backend/backend.ts b/tensorflow/tensorboard/components/tf_backend/backend.ts index a7a222beaaf..2db8ddc23d2 100644 --- a/tensorflow/tensorboard/components/tf_backend/backend.ts +++ b/tensorflow/tensorboard/components/tf_backend/backend.ts @@ -193,8 +193,9 @@ export class Backend { * Return a promise showing the Run-to-Tag mapping for compressedHistogram * data. */ - public compressedHistogramRuns(): Promise { - return this.runs().then((x) => _.mapValues(x, 'compressedHistograms')); + public compressedHistogramTags(): Promise { + return this.requestManager.request( + this.router.pluginRoute('distributions', '/tags')); } /** @@ -343,7 +344,8 @@ export class Backend { */ private compressedHistogram(tag: string, run: string): Promise> { - const url = this.router.compressedHistograms(tag, run); + const url = (this.router.pluginRunTagRoute( + 'distributions', '/distributions')(tag, run)); let p: Promise[]>; p = this.requestManager.request(url); return p.then(map(detupler((x) => x))); diff --git a/tensorflow/tensorboard/components/tf_backend/router.ts b/tensorflow/tensorboard/components/tf_backend/router.ts index b31f9b366ea..115634be125 100644 --- a/tensorflow/tensorboard/components/tf_backend/router.ts +++ b/tensorflow/tensorboard/components/tf_backend/router.ts @@ -21,7 +21,6 @@ export interface Router { logdir: () => string; runs: () => string; isDemoMode: () => boolean; - compressedHistograms: RunTagUrlFn; graph: (run: string, limit_attr_size?: number, large_attrs_key?: string) => string; @@ -88,7 +87,6 @@ export function router(dataDir = 'data', demoMode = false): Router { runs: () => dataDir + '/runs' + (demoMode ? '.json' : ''), isDemoMode: () => demoMode, graph: graphUrl, - compressedHistograms: standardRoute('compressedHistograms'), runMetadata: standardRoute('run_metadata', '.pbtxt'), healthPills: () => dataDir + '/plugin/debugger/health_pills', textRuns: () => dataDir + '/plugin/text/runs' + (demoMode ? '.json' : ''), diff --git a/tensorflow/tensorboard/http_api.md b/tensorflow/tensorboard/http_api.md index 541394cbe00..b015539cf52 100644 --- a/tensorflow/tensorboard/http_api.md +++ b/tensorflow/tensorboard/http_api.md @@ -55,13 +55,11 @@ all of the data available from the TensorBoard server. Here is an example: { "train_run": { - "compressedHistograms": ["foo_histogram", "bar_histogram"], "graph": true, "firstEventTimestamp": 123456.789 "run_metadata": ["forward prop", "inference"] }, "eval": { - "compressedHistograms": ["foo_histogram", "bar_histogram"], "graph": false, "run_metadata": [] } @@ -81,6 +79,7 @@ and will not appear in the output from this route: - `audio` - `images` - `scalars` + - `compressedHistograms`, moved to `distributions` - `histograms` ## `/data/plugin/scalars/tags` @@ -160,7 +159,21 @@ Annotated Example: (note - real data is higher precision) ] ] -## '/data/compressedHistograms?run=foo&tag=bar' +## `/data/plugin/distributions/tags` + +Returns a dictionary mapping from `run_name` (quoted string) to arrays of +`tag_name` (quoted string), where each array contains the names of all +distribution tags present in the corresponding run. Here is an example: + + { + "train_run": ["foo_histogram", "bar_histogram"], + "eval": ["foo_histogram", "bar_histogram"] + } + +Note that runs without any distribution tags are included as keys with +value the empty array. + +## `/data/plugin/distributions/distributions?run=foo&tag=bar` Returns an array of event_accumulator.CompressedHistogramEvents ([wall_time, step, CompressedHistogramValues]) for the given run and tag. @@ -180,8 +193,8 @@ Annotated Example: (note - real data is higher precision) [ 1441154832.580509, # wall_time 5, # step - [ [0, -3.67], # CompressedHistogramValue for 0th percentile - [2500, -4.19], # CompressedHistogramValue for 25th percentile + [ [0, -3.67], # CompressedHistogramValue for 0th percentile + [2500, -4.19], # CompressedHistogramValue for 25th percentile [5000, 6.29], [7500, 1.64], [10000, 3.67] diff --git a/tensorflow/tensorboard/plugins/distributions/BUILD b/tensorflow/tensorboard/plugins/distributions/BUILD new file mode 100644 index 00000000000..de1f73143c6 --- /dev/null +++ b/tensorflow/tensorboard/plugins/distributions/BUILD @@ -0,0 +1,50 @@ +# Description: +# TensorBoard plugin for distributions + +package(default_visibility = ["//tensorflow:internal"]) + +licenses(["notice"]) # Apache 2.0 + +exports_files(["LICENSE"]) + +load("//tensorflow:tensorflow.bzl", "py_test") + +## Distributions Plugin ## +py_library( + name = "distributions_plugin", + srcs = ["distributions_plugin.py"], + srcs_version = "PY2AND3", + visibility = [ + "//tensorflow:internal", + ], + deps = [ + "//tensorflow/tensorboard/backend:http_util", + "//tensorflow/tensorboard/backend/event_processing:event_accumulator", + "//tensorflow/tensorboard/plugins:base_plugin", + "@org_pocoo_werkzeug//:werkzeug", + "@six_archive//:six", + ], +) + +py_test( + name = "distributions_plugin_test", + size = "small", + srcs = ["distributions_plugin_test.py"], + main = "distributions_plugin_test.py", + srcs_version = "PY2AND3", + deps = [ + ":distributions_plugin", + "//tensorflow:tensorflow_py", + "//tensorflow/tensorboard/backend:application", + "//tensorflow/tensorboard/backend/event_processing:event_accumulator", + "//tensorflow/tensorboard/backend/event_processing:event_multiplexer", + "@org_pocoo_werkzeug//:werkzeug", + "@six_archive//:six", + ], +) + +filegroup( + name = "all_files", + srcs = glob(["**"]), + visibility = ["//tensorflow:__pkg__"], +) diff --git a/tensorflow/tensorboard/plugins/distributions/distributions_plugin.py b/tensorflow/tensorboard/plugins/distributions/distributions_plugin.py new file mode 100644 index 00000000000..4bb9dfaf545 --- /dev/null +++ b/tensorflow/tensorboard/plugins/distributions/distributions_plugin.py @@ -0,0 +1,69 @@ +# Copyright 2017 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. +# ============================================================================== +"""The TensorBoard Distributions (a.k.a. compressed histograms) plugin.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from werkzeug import wrappers + +from tensorflow.tensorboard.backend import http_util +from tensorflow.tensorboard.backend.event_processing import event_accumulator +from tensorflow.tensorboard.plugins import base_plugin + +_PLUGIN_PREFIX_ROUTE = event_accumulator.COMPRESSED_HISTOGRAMS + + +class DistributionsPlugin(base_plugin.TBPlugin): + """Distributions Plugin for TensorBoard.""" + + plugin_name = _PLUGIN_PREFIX_ROUTE + + def get_plugin_apps(self, multiplexer, unused_logdir): + self._multiplexer = multiplexer + return { + '/distributions': self.distributions_route, + '/tags': self.tags_route, + } + + def is_active(self): + """This plugin is active iff any run has at least one relevant tag.""" + return any(self.index_impl().values()) + + def index_impl(self): + return { + run_name: run_data[event_accumulator.COMPRESSED_HISTOGRAMS] + for (run_name, run_data) in self._multiplexer.Runs().items() + if event_accumulator.COMPRESSED_HISTOGRAMS in run_data + } + + def distributions_impl(self, tag, run): + """Result of the form `(body, mime_type)`.""" + values = self._multiplexer.CompressedHistograms(run, tag) + return (values, 'application/json') + + @wrappers.Request.application + def tags_route(self, request): + index = self.index_impl() + return http_util.Respond(request, index, 'application/json') + + @wrappers.Request.application + def distributions_route(self, request): + """Given a tag and single run, return array of compressed histograms.""" + tag = request.args.get('tag') + run = request.args.get('run') + (body, mime_type) = self.distributions_impl(tag, run) + return http_util.Respond(request, body, mime_type) diff --git a/tensorflow/tensorboard/plugins/distributions/distributions_plugin_test.py b/tensorflow/tensorboard/plugins/distributions/distributions_plugin_test.py new file mode 100644 index 00000000000..b5aae6dea79 --- /dev/null +++ b/tensorflow/tensorboard/plugins/distributions/distributions_plugin_test.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 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. +# ============================================================================== +"""Integration tests for the Distributions Plugin.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os.path + +from six.moves import xrange # pylint: disable=redefined-builtin +import tensorflow as tf + +from tensorflow.tensorboard.backend.event_processing import event_accumulator +from tensorflow.tensorboard.backend.event_processing import event_multiplexer +from tensorflow.tensorboard.plugins.distributions import distributions_plugin + + +class DistributionsPluginTest(tf.test.TestCase): + + _STEPS = 99 + + _DISTRIBUTION_TAG = 'my-favorite-distribution' + _SCALAR_TAG = 'my-boring-scalars' + + _RUN_WITH_DISTRIBUTION = '_RUN_WITH_DISTRIBUTION' + _RUN_WITH_SCALARS = '_RUN_WITH_SCALARS' + + def set_up_with_runs(self, run_names): + self.logdir = self.get_temp_dir() + for run_name in run_names: + self.generate_run(run_name) + multiplexer = event_multiplexer.EventMultiplexer(size_guidance={ + # don't truncate my test data, please + event_accumulator.COMPRESSED_HISTOGRAMS: + self._STEPS, + }) + multiplexer.AddRunsFromDirectory(self.logdir) + multiplexer.Reload() + self.plugin = distributions_plugin.DistributionsPlugin() + self.apps = self.plugin.get_plugin_apps(multiplexer, None) + + def generate_run(self, run_name): + if run_name == self._RUN_WITH_DISTRIBUTION: + (use_distributions, use_scalars) = (True, False) + elif run_name == self._RUN_WITH_SCALARS: + (use_distributions, use_scalars) = (False, True) + else: + assert False, 'Invalid run name: %r' % run_name + tf.reset_default_graph() + sess = tf.Session() + placeholder = tf.placeholder(tf.float32, shape=[3]) + if use_distributions: + tf.summary.histogram(self._DISTRIBUTION_TAG, placeholder) + if use_scalars: + tf.summary.scalar(self._SCALAR_TAG, tf.reduce_mean(placeholder)) + summ = tf.summary.merge_all() + + subdir = os.path.join(self.logdir, run_name) + writer = tf.summary.FileWriter(subdir) + writer.add_graph(sess.graph) + for step in xrange(self._STEPS): + feed_dict = {placeholder: [1 + step, 2 + step, 3 + step]} + s = sess.run(summ, feed_dict=feed_dict) + writer.add_summary(s, global_step=step) + writer.close() + + def test_index(self): + self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION, + self._RUN_WITH_SCALARS]) + self.assertEqual({ + self._RUN_WITH_DISTRIBUTION: [self._DISTRIBUTION_TAG], + self._RUN_WITH_SCALARS: [], + }, self.plugin.index_impl()) + + def _test_distributions_json(self, run_name, should_have_distributions): + self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION, + self._RUN_WITH_SCALARS]) + if should_have_distributions: + (data, mime_type) = self.plugin.distributions_impl( + self._DISTRIBUTION_TAG, run_name) + self.assertEqual('application/json', mime_type) + self.assertEqual(len(data), self._STEPS) + for i in xrange(self._STEPS): + self.assertEqual(i, data[i].step) + else: + with self.assertRaises(KeyError): + self.plugin.distributions_impl( + self._DISTRIBUTION_TAG, run_name) + + def test_distributions_json_with_scalars(self): + self._test_distributions_json(self._RUN_WITH_DISTRIBUTION, True) + + def test_distributions_json_with_histogram(self): + self._test_distributions_json(self._RUN_WITH_SCALARS, False) + + def test_active_with_distribution(self): + self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION]) + self.assertTrue(self.plugin.is_active()) + + def test_active_with_scalars(self): + self.set_up_with_runs([self._RUN_WITH_SCALARS]) + self.assertFalse(self.plugin.is_active()) + + def test_active_with_both(self): + self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION, + self._RUN_WITH_SCALARS]) + self.assertTrue(self.plugin.is_active()) + + +if __name__ == '__main__': + tf.test.main() diff --git a/tensorflow/tensorboard/tensorboard.py b/tensorflow/tensorboard/tensorboard.py index bce5dd259dd..70830b9a8c8 100644 --- a/tensorflow/tensorboard/tensorboard.py +++ b/tensorflow/tensorboard/tensorboard.py @@ -33,6 +33,7 @@ from werkzeug import serving from tensorflow.tensorboard.backend import application from tensorflow.tensorboard.backend.event_processing import event_file_inspector as efi from tensorflow.tensorboard.plugins.audio import audio_plugin +from tensorflow.tensorboard.plugins.distributions import distributions_plugin from tensorflow.tensorboard.plugins.histograms import histograms_plugin from tensorflow.tensorboard.plugins.images import images_plugin from tensorflow.tensorboard.plugins.projector import projector_plugin @@ -204,10 +205,11 @@ def main(unused_argv=None): return 0 else: plugins = [ - audio_plugin.AudioPlugin(), - histograms_plugin.HistogramsPlugin(), - images_plugin.ImagesPlugin(), scalars_plugin.ScalarsPlugin(), + images_plugin.ImagesPlugin(), + audio_plugin.AudioPlugin(), + distributions_plugin.DistributionsPlugin(), + histograms_plugin.HistogramsPlugin(), projector_plugin.ProjectorPlugin(), text_plugin.TextPlugin(), ]