diff options
Diffstat (limited to 'tensorflow/tensorboard/components/tf-categorizer')
5 files changed, 490 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/tf-categorizer/categorizer.ts b/tensorflow/tensorboard/components/tf-categorizer/categorizer.ts new file mode 100644 index 0000000000..e05078279e --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/categorizer.ts @@ -0,0 +1,133 @@ +/// <reference path="../../typings/tsd.d.ts" /> + +module Categorizer { + /** + * This module contains methods that allow sorting tags into "categories". + * A category contains a name and a list of tags. + * The sorting strategy is defined by a "CustomCategorization", which contains + * "categoryDefinitions" which are regex rules used to construct a category. + * E.g. the regex rule "xent" will create a category called "xent" that + * contains values whose tags match the regex. + * + * After custom categories are evaluated, the tags are sorted by a hardcoded + * fallback categorizer, which may, for example, group tags into categories + * based on their top namespace. + */ + + export interface Category { + // Categories that data is sorted into + name: string; + tags: string[]; + } + + export interface CustomCategorization { + // Defines a categorization strategy + categoryDefinitions: string[]; + fallbackCategorizer: string; + /* {"TopLevelNamespaceCategorizer", + "LegacyUnderscoreCategorizer"} */ + } + + export interface Categorizer { + // Function that generates categories + (tags: string[]): Category[]; + } + + /* Canonical TensorFlow ops are namespaced using forward slashes. + * This fallback categorizer categorizes by the top-level namespace. + */ + export var topLevelNamespaceCategorizer: Categorizer = splitCategorizer(/\//); + + // Try to produce good categorizations on legacy graphs, which often + // are namespaced like l1_foo/bar or l2_baz/bam. + // If there is no leading underscore before the first forward slash, + // then it behaves the same as topLevelNamespaceCategorizer + export var legacyUnderscoreCategorizer: Categorizer = splitCategorizer(/[\/_]/); + + export function fallbackCategorizer(s: string): Categorizer { + switch (s) { + case "TopLevelNamespaceCategorizer": + return topLevelNamespaceCategorizer; + case "LegacyUnderscoreCategorizer": + return legacyUnderscoreCategorizer; + default: + throw new Error("Unrecognized categorization strategy: " + s); + } + } + + /* An "extractor" is a function that takes a tag name, and "extracts" a category name. + * This function takes an extractor, and produces a categorizer. + * Currently, it is just used for the fallbackCategorizer, but we may want to + * refactor the general categorization logic to use the concept of extractors. + */ + function extractorToCategorizer(extractor: (s: string) => string): Categorizer { + return (tags: string[]): Category[] => { + if (tags.length === 0) { + return []; + } + var sortedTags = tags.slice().sort(); + var categories: Category[] = []; + var currentCategory = { + name: extractor(sortedTags[0]), + tags: [], + }; + sortedTags.forEach((t: string) => { + var topLevel = extractor(t); + if (currentCategory.name !== topLevel) { + categories.push(currentCategory); + currentCategory = { + name: topLevel, + tags: [], + }; + } + currentCategory.tags.push(t); + }); + categories.push(currentCategory); + return categories; + }; + } + + function splitCategorizer(r: RegExp): Categorizer { + var extractor = (t: string) => { + return t.split(r)[0]; + }; + return extractorToCategorizer(extractor); + } + + export interface CategoryDefinition { + name: string; + matches: (t: string) => boolean; + } + + export function defineCategory(ruledef: string): CategoryDefinition { + var r = new RegExp(ruledef); + var f = function(tag: string): boolean { + return r.test(tag); + }; + return { name: ruledef, matches: f }; + } + + export function _categorizer(rules: CategoryDefinition[], fallback: Categorizer) { + return function(tags: string[]): Category[] { + var remaining: d3.Set = d3.set(tags); + var userSpecified = rules.map((def: CategoryDefinition) => { + var tags: string[] = []; + remaining.forEach((t: string) => { + if (def.matches(t)) { + tags.push(t); + } + }); + var cat = { name: def.name, tags: tags.sort() }; + return cat; + }); + var defaultCategories = fallback(remaining.values()); + return userSpecified.concat(defaultCategories); + }; + } + + export function categorizer(s: CustomCategorization): Categorizer { + var rules = s.categoryDefinitions.map(defineCategory); + var fallback = fallbackCategorizer(s.fallbackCategorizer); + return _categorizer(rules, fallback); + }; +} diff --git a/tensorflow/tensorboard/components/tf-categorizer/demo/index.html b/tensorflow/tensorboard/components/tf-categorizer/demo/index.html new file mode 100644 index 0000000000..ea3f162aa5 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/demo/index.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <script src="../../../bower_components/d3/d3.js"></script> + <link rel="import" href="../tf-categorizer.html"> + <link rel="import" href="../../../bower_components/iron-flex-layout/classes/iron-flex-layout.html"> + + </head> + <body> + <style> + </style> + <dom-module id="x-demo"> + <style> + .container { + width: 255px; + padding: 10px; + border: 1px solid var(--paper-indigo-900); + border-radius: 5px; + position: fixed; + } + :host { + margin: 0px; + } + + .categories { + font-family: "RobotoDraft",Helvetica; + margin-left: 300px; + width: 500px; + border: 1px solid var(--paper-indigo-500); + border-radius: 5px; + } + + .category { + background-color: var(--paper-indigo-50); + margin: 20px; + padding: 20px; + border-radius: 5px; + } + + .cat-name { + font-size: 20px; + } + + .tag { + border-radius: 5px; + padding: 5px; + margin: 5px; + background-color: var(--paper-indigo-900); + color: white; + } + </style> + <template> + <div class="container"> + <tf-categorizer categories="{{categories}}" tags="[[tags]]" id="demo"></tf-categorizer> + </div> + <div class="categories"> + <template is="dom-repeat" items="[[categories]]"> + <div class="category"> + <p class="cat-name">Category: <span>[[item.name]]</span></p> + <div class="tags-container layout horizontal wrap"> + <template is="dom-repeat" items="[[item.tags]]"> + <span class="tag layout vertical center-center">[[item]]</span> + </template> + </div> + </template> + </div> + </template> + <script> + + function tagsGenerator() { + var tags = ["special1", "special2", "special3", "special4", "special5"]; + ["l1", "l2", "l3", "l4", "l5"].forEach(function(l) { + ["foo", "bar", "baz", "boink", "zod", "specialx"].forEach(function(x) { + tags.push(l + "/" + x); + }); + }); + return tags; + } + + Polymer({ + is: "x-demo", + properties: { + tags: { type: Array, value: tagsGenerator }, + }, + }); + </script> + </dom-module> + + <x-demo id="demo"></x-demo> + </body> + <script> + HTMLImports.whenReady(function() { + window.demo = document.getElementById("demo"); + }) + </script> +</html> diff --git a/tensorflow/tensorboard/components/tf-categorizer/index.html b/tensorflow/tensorboard/components/tf-categorizer/index.html new file mode 100644 index 0000000000..f08a125f7c --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/index.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> + + <title>tf-categorizer</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../../bower_components/iron-component-page/iron-component-page.html"> + +</head> +<body> + + <iron-component-page></iron-component-page> + +</body> +</html> diff --git a/tensorflow/tensorboard/components/tf-categorizer/test/categorizerTest.ts b/tensorflow/tensorboard/components/tf-categorizer/test/categorizerTest.ts new file mode 100644 index 0000000000..be09c56c41 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/test/categorizerTest.ts @@ -0,0 +1,139 @@ +/// <reference path="../../../typings/tsd.d.ts" /> +/// <reference path="../categorizer.ts" /> +var assert = chai.assert; + +module Categorizer { + describe("categorizer", () => { + describe("topLevelNamespaceCategorizer", () => { + it("returns empty array on empty tags", () => { + assert.lengthOf(topLevelNamespaceCategorizer([]), 0); + }); + + it("handles a simple case", () => { + var simple = ["foo1/bar", "foo1/zod", "foo2/bar", "foo2/zod", + "gosh/lod/mar", "gosh/lod/ned"]; + var expected = [ + { name: "foo1", tags: ["foo1/bar", "foo1/zod"] }, + { name: "foo2", tags: ["foo2/bar", "foo2/zod"] }, + { name: "gosh", tags: ["gosh/lod/mar", "gosh/lod/ned"] }, + ]; + assert.deepEqual(topLevelNamespaceCategorizer(simple), expected); + }); + + it("orders the categories", () => { + var test = ["e", "f", "g", "a", "b", "c"]; + var expected = [ + { name: "a", tags: ["a"] }, + { name: "b", tags: ["b"] }, + { name: "c", tags: ["c"] }, + { name: "e", tags: ["e"] }, + { name: "f", tags: ["f"] }, + { name: "g", tags: ["g"] }, + ]; + assert.deepEqual(topLevelNamespaceCategorizer(test), expected); + }); + + it("handles cases where category names overlap node names", () => { + var test = ["a", "a/a", "a/b", "a/c", "b", "b/a"]; + var actual = topLevelNamespaceCategorizer(test); + var expected = [ + { name: "a", tags: ["a", "a/a", "a/b", "a/c"] }, + { name: "b", tags: ["b", "b/a"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("handles singleton case", () => { + assert.deepEqual(topLevelNamespaceCategorizer(["a"]), [{ name: "a", tags: ["a"] }]); + }); + }); + + describe("legacyUnderscoreCategorizer", () => { + it("splits by shorter of first _ or /", () => { + var tags = ["l0_bar/foo", "l0_bar/baz", "l0_foo/wob", "l1_zoink/bla", + "l1_wibble/woz", "l1/foo_woink", "l2/wozzle_wizzle"]; + var actual = legacyUnderscoreCategorizer(tags); + var expected = [ + { name: "l0", tags: ["l0_bar/baz", "l0_bar/foo", "l0_foo/wob"] }, + { name: "l1", tags: ["l1/foo_woink", "l1_wibble/woz", "l1_zoink/bla"] }, + { name: "l2", tags: ["l2/wozzle_wizzle"] }, + ]; + assert.deepEqual(actual, expected); + }); + }); + + describe("customCategorizer", () => { + function noFallbackCategorizer(tags: string[]): Category[] { + return []; + } + + function testCategorizer(defs: string[], + fallback: Categorizer, tags: string[]): Category[] { + var catDefs = defs.map(defineCategory); + return _categorizer(catDefs, fallback)(tags); + } + + it("categorizes by regular expression", () => { + var defs = ["foo..", "bar.."]; + var tags = ["fooab", "fooxa", "barts", "barms"]; + var actual = testCategorizer(defs, noFallbackCategorizer, tags); + var expected = [ + { name: "foo..", tags: ["fooab", "fooxa"] }, + { name: "bar..", tags: ["barms", "barts"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("matches non-exclusively", () => { + var tags = ["abc", "bar", "zod"]; + var actual = testCategorizer(["...", "bar"], noFallbackCategorizer, tags); + var expected = [ + { name: "...", tags: ["abc", "bar", "zod"] }, + { name: "bar", tags: ["bar"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("creates categories for unmatched rules", () => { + var actual = testCategorizer(["a", "b", "c"], noFallbackCategorizer, []); + var expected = [ + { name: "a", tags: [] }, + { name: "b", tags: [] }, + { name: "c", tags: [] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("category regexs work with special characters", () => { + var defs = ["^\\w+$", "^\\d+$", "^\\/..$"]; + var tags = ["foo", "3243", "/xa"]; + var actual = testCategorizer(defs, noFallbackCategorizer, tags); + var expected = [ + { name: "^\\w+$", tags: ["3243", "foo"] }, + { name: "^\\d+$", tags: ["3243"] }, + { name: "^\\/..$", tags: ["/xa"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("category tags are sorted", () => { + var tags = ["a", "z", "c", "d", "e", "x", "f", "y", "g"]; + var sorted = tags.slice().sort(); + var expected = [{ name: ".*", tags: sorted}]; + var actual = testCategorizer([".*"], noFallbackCategorizer, tags); + assert.deepEqual(actual, expected); + }); + + it("if nonexclusive: all tags passed to fallback", () => { + var passedToDefault = null; + function defaultCategorizer(tags: string[]): Category[] { + passedToDefault = tags; + return []; + } + var tags = ["foo", "bar", "foo123"]; + testCategorizer(["foo"], defaultCategorizer, tags); + assert.deepEqual(passedToDefault, tags); + }); + }); + }); +} diff --git a/tensorflow/tensorboard/components/tf-categorizer/tf-categorizer.html b/tensorflow/tensorboard/components/tf-categorizer/tf-categorizer.html new file mode 100644 index 0000000000..3672db38a2 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/tf-categorizer.html @@ -0,0 +1,103 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-toggle-button/paper-toggle-button.html"> + +<link rel="import" href="../tf-regex-group/tf-regex-group.html"> +<link rel="import" href="../tf-dashboard-common/tensorboard-color.html"> + +<!-- +`tf-categorizer` turns an array of tags into an array of categories + +The transformation from tags to categories is controlled by the user, through +interacting with the categorizer widget. + +(See type signatures in categorizer.ts) + +Example: + <tf-categorizer tags="[[tags]]" categories="{{categories}}"></tf-categorizer> + +Public Properties: +`tags` - Array of strings that are the tags to categorize. Should be one-way bound downward. +`categories` - Array of Categorizer.Category objects that are generated by the Categorizer. + Are readOnly and notify: True. Expected to be one-way bound upward. + +The categorizer provides inputs for adding regular expression rules and toggling whether +categories are exclusive. +--> +<dom-module id="tf-categorizer"> + <template> + <div class="inputs"> + <tf-regex-group id="regex-group" regexes="{{regexes}}"></tf-regex-group> + </div> + <div id="underscore-categorization"> + <span>Split On Underscores:</span> + <paper-toggle-button checked="{{splitOnUnderscore}}"></paper-toggle-button> + </div> + <style> + :host { + display: block; + padding-bottom: 5px; + padding-top: 5px; + } + + .inputs { + padding-left: 5px; + } + + paper-toggle-button { + --paper-toggle-button-checked-button-color: var(--tb-orange-strong); + --paper-toggle-button-checked-bar-color: var(--tb-orange-weak); + } + #underscore-categorization { + padding-left: 94px; + color: var(--paper-grey-700); + font-size: 14px; + } + </style> + </template> + <script src="categorizer.js"></script> + <script> + Polymer({ + is: "tf-categorizer", + properties: { + regexes: {type: Array}, + tags: {type: Array}, + categoriesAreExclusive: {type: Boolean, value: true}, + fallbackCategorizer: { + type: String, + computed: "chooseFallbackCategorizer(splitOnUnderscore)" + }, + splitOnUnderscore: { + type: Boolean, + value: false, + }, + categorizer: { + type: Object, + computed: "computeCategorization(regexes.*, categoriesAreExclusive, fallbackCategorizer)", + }, + categories: {type: Array, value: function() {return [];}, notify: true, readOnly: true}, + }, + observers: ['recategorize(tags.*, categorizer)'], + computeCategorization: function(regexes, categoriesAreExclusive, fallbackCategorizer) { + var categorizationStrategy = { + categoryDefinitions: regexes.base, + categoriesAreExclusive: categoriesAreExclusive, + fallbackCategorizer: fallbackCategorizer, + }; + return Categorizer.categorizer(categorizationStrategy); + }, + recategorize: function() { + this.debounce("tf-categorizer-recategorize", function (){ + var categories = this.categorizer(this.tags); + this._setCategories(categories); + }) + }, + chooseFallbackCategorizer: function(splitOnUnderscore) { + if (splitOnUnderscore) { + return "LegacyUnderscoreCategorizer"; + } else { + return "TopLevelNamespaceCategorizer"; + } + }, + }); + </script> +</dom-module> |