From 7441f07df1a33e2b97ebe97fba92567fce2e26b9 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 25 Jan 2019 17:34:53 +0100 Subject: [PATCH 01/21] * removed TODO and return valid http response in case of an error --- tensorboard/plugins/scalar/scalars_plugin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tensorboard/plugins/scalar/scalars_plugin.py b/tensorboard/plugins/scalar/scalars_plugin.py index 33e99bc819..7cf73211ff 100644 --- a/tensorboard/plugins/scalar/scalars_plugin.py +++ b/tensorboard/plugins/scalar/scalars_plugin.py @@ -192,10 +192,14 @@ def tags_route(self, request): @wrappers.Request.application def scalars_route(self, request): """Given a tag and single run, return array of ScalarEvents.""" - # TODO: return HTTP status code for malformed requests tag = request.args.get('tag') run = request.args.get('run') experiment = request.args.get('experiment') output_format = request.args.get('format') - (body, mime_type) = self.scalars_impl(tag, run, experiment, output_format) - return http_util.Respond(request, body, mime_type) + try: + (body, mime_type) = self.scalars_impl(tag, run, experiment, output_format) + code = 200 + except ValueError as e: + (body, mime_type) = (str(e), 'text/plain') + code = 400 + return http_util.Respond(request, body, mime_type, code=code) From 7677de4bc379f3b97e3484f3da44bf736508e17e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 25 Jan 2019 17:37:31 +0100 Subject: [PATCH 02/21] * adding ability to download the histogram raw data from tensorboard --- .../plugins/histogram/histograms_plugin.py | 123 +++++++++++------- .../tf-histogram-dashboard.html | 19 +++ .../tf-histogram-loader.html | 99 +++++++++++--- 3 files changed, 182 insertions(+), 59 deletions(-) diff --git a/tensorboard/plugins/histogram/histograms_plugin.py b/tensorboard/plugins/histogram/histograms_plugin.py index 95df054939..1981b58ae7 100644 --- a/tensorboard/plugins/histogram/histograms_plugin.py +++ b/tensorboard/plugins/histogram/histograms_plugin.py @@ -24,9 +24,11 @@ import collections import random +import csv import numpy as np import six +from six import StringIO from werkzeug import wrappers from tensorboard import plugin_util @@ -34,6 +36,7 @@ from tensorboard.compat import tf from tensorboard.plugins import base_plugin from tensorboard.plugins.histogram import metadata +from tensorboard.plugins.scalar.scalars_plugin import OutputFormat from tensorboard.util import tensor_util @@ -63,8 +66,8 @@ def __init__(self, context): def get_plugin_apps(self): return { - '/histograms': self.histograms_route, - '/tags': self.tags_route, + '/histograms': self.histograms_route, + '/tags': self.tags_route, } def is_active(self): @@ -103,10 +106,10 @@ def index_impl(self): for row in cursor: tag_name, display_name, run_name = row result[run_name][tag_name] = { - 'displayName': display_name, - # TODO(chihuahua): Populate the description. Currently, the tags - # table does not link with the description table. - 'description': '', + 'displayName': display_name, + # TODO(chihuahua): Populate the description. Currently, the tags + # table does not link with the description table. + 'description': '', } return result @@ -120,11 +123,12 @@ def index_impl(self): summary_metadata = self._multiplexer.SummaryMetadata(run, tag) result[run][tag] = {'displayName': summary_metadata.display_name, 'description': plugin_util.markdown_to_safe_html( - summary_metadata.summary_description)} + summary_metadata.summary_description)} return result - def histograms_impl(self, tag, run, downsample_to=None): + def histograms_impl(self, tag, run, experiment, + output_format, downsample_to=None): """Result of the form `(body, mime_type)`, or `ValueError`. At most `downsample_to` events will be returned. If this value is @@ -136,17 +140,17 @@ def histograms_impl(self, tag, run, downsample_to=None): cursor = db.cursor() # Prefetch the tag ID matching this run and tag. cursor.execute( - ''' - SELECT - tag_id - FROM Tags - JOIN Runs USING (run_id) - WHERE - Runs.run_name = :run - AND Tags.tag_name = :tag - AND Tags.plugin_name = :plugin - ''', - {'run': run, 'tag': tag, 'plugin': metadata.PLUGIN_NAME}) + ''' + SELECT + tag_id + FROM Tags + JOIN Runs USING (run_id) + WHERE + Runs.run_name = :run + AND Tags.tag_name = :tag + AND Tags.plugin_name = :plugin + ''', + {'run': run, 'tag': tag, 'plugin': metadata.PLUGIN_NAME}) row = cursor.fetchone() if not row: raise ValueError('No histogram tag %r for run %r' % (tag, run)) @@ -160,32 +164,41 @@ def histograms_impl(self, tag, run, downsample_to=None): # can be formally expressed as the following: # [s_min + math.ceil(i / k * (s_max - s_min)) for i in range(0, k + 1)] cursor.execute( - ''' + ''' + SELECT + MIN(step) AS step, + computed_time, + data, + dtype, + shape + FROM Tensors + INNER JOIN ( SELECT - MIN(step) AS step, - computed_time, - data, - dtype, - shape + MIN(step) AS min_step, + MAX(step) AS max_step FROM Tensors - INNER JOIN ( - SELECT - MIN(step) AS min_step, - MAX(step) AS max_step - FROM Tensors - /* Filter out NULL so we can use TensorSeriesStepIndex. */ - WHERE series = :tag_id AND step IS NOT NULL - ) - /* Ensure we omit reserved rows, which have NULL step values. */ + /* Filter out NULL so we can use TensorSeriesStepIndex. */ WHERE series = :tag_id AND step IS NOT NULL - /* Bucket rows into sample_size linearly spaced buckets, or do - no sampling if sample_size is NULL. */ - GROUP BY - IFNULL(:sample_size - 1, max_step - min_step) - * (step - min_step) / (max_step - min_step) - ORDER BY step - ''', - {'tag_id': tag_id, 'sample_size': downsample_to}) + ) + JOIN Tags + ON Tensors.series = Tags.tag_id + JOIN Runs + ON Tags.run_id = Runs.run_id + /* Ensure we omit reserved rows, which have NULL step values. */ + WHERE + /* For backwards compatibility, ignore the experiment id + for matching purposes if it is empty. */ + (:exp == '' OR Runs.experiment_id == CAST(:exp AS INT)) + AND series = :tag_id + AND step IS NOT NULL + /* Bucket rows into sample_size linearly spaced buckets, or do + no sampling if sample_size is NULL. */ + GROUP BY + IFNULL(:sample_size - 1, max_step - min_step) + * (step - min_step) / (max_step - min_step) + ORDER BY step + ''', + {'tag_id': tag_id, 'sample_size': downsample_to}) events = [(computed_time, step, self._get_values(data, dtype, shape)) for step, computed_time, data, dtype, shape in cursor] else: @@ -196,11 +209,30 @@ def histograms_impl(self, tag, run, downsample_to=None): raise ValueError('No histogram tag %r for run %r' % (tag, run)) if downsample_to is not None and len(tensor_events) > downsample_to: rand_indices = random.Random(0).sample( - six.moves.xrange(len(tensor_events)), downsample_to) + six.moves.xrange(len(tensor_events)), downsample_to) indices = sorted(rand_indices) tensor_events = [tensor_events[i] for i in indices] events = [[e.wall_time, e.step, tensor_util.make_ndarray(e.tensor_proto).tolist()] for e in tensor_events] + if output_format == OutputFormat.CSV: + csv_events = [] + # Convert the events in a way that we can sensibly export + # them as csv. Therefore, we split the start, end, value of + # each bin by semicolon. + for e in events: + csv_events.append( + [e[0], e[1], + ';'.join(['{:.32f}'.format(el[0]) for el in e[2]]), + ';'.join(['{:.32f}'.format(el[1]) for el in e[2]]), + ';'.join(['{}'.format(el[2]) for el in e[2]]), + ] + ) + string_io = StringIO() + writer = csv.writer(string_io) + writer.writerow(['Wall time', 'Step', 'BinStart', 'BinEnd', 'BinValue']) + writer.writerows(csv_events) + return (string_io.getvalue(), 'text/csv') + return (events, 'application/json') def _get_values(self, data_blob, dtype_enum, shape_string): @@ -225,9 +257,12 @@ def histograms_route(self, request): """Given a tag and single run, return array of histogram values.""" tag = request.args.get('tag') run = request.args.get('run') + experiment = request.args.get('experiment') + output_format = request.args.get('format') try: (body, mime_type) = self.histograms_impl( - tag, run, downsample_to=self.SAMPLE_SIZE) + tag, run, experiment, output_format, + downsample_to=self.SAMPLE_SIZE) code = 200 except ValueError as e: (body, mime_type) = (str(e), 'text/plain') diff --git a/tensorboard/plugins/histogram/tf_histogram_dashboard/tf-histogram-dashboard.html b/tensorboard/plugins/histogram/tf_histogram_dashboard/tf-histogram-dashboard.html index 7de5d414c9..3a51faceed 100644 --- a/tensorboard/plugins/histogram/tf_histogram_dashboard/tf-histogram-dashboard.html +++ b/tensorboard/plugins/histogram/tf_histogram_dashboard/tf-histogram-dashboard.html @@ -41,6 +41,14 @@