Skip to content

Commit 6d47ca3

Browse files
authored
Added categorization for data_selector.Selection (#1324)
Added categorization for data_selector.Selection The utility divides the data, based on selection, into tag prefix and tags. After those division, it does not make assumption about how the data is used and returns list of experiment/run pair that matches the criteria. The change also populates tags per run per experiment as well as tag's plugin information. A tag is created for a specific plugin and, now, categorization utils take the pluginName to filter out unrelated tags based on information we pipe from the backend.
1 parent bc4e7a6 commit 6d47ca3

File tree

9 files changed

+326
-21
lines changed

9 files changed

+326
-21
lines changed

tensorboard/components/tf_backend/type.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@ namespace tf_backend {
1616

1717
export type Experiment = {id: number, name: string, startTime: number};
1818

19-
// `id` is null when data source is logdir.
20-
export type Run = {id: number | null, name: string, startTime: number};
19+
export type Run = {
20+
id: number | null,
21+
name: string,
22+
startTime: number,
23+
tags: Tag[],
24+
};
2125

22-
export type Tag = {id: number, name: string, displayName: string};
26+
export type Tag = {
27+
id: number,
28+
name: string,
29+
displayName: string,
30+
pluginName: string,
31+
};
2332

2433
} // namespace tf_backend

tensorboard/components/tf_categorization_utils/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ tf_web_library(
1818
deps = [
1919
"//tensorboard/components/tf_backend",
2020
"//tensorboard/components/tf_dashboard_common",
21+
"//tensorboard/components/tf_data_selector:type",
2122
"//tensorboard/components/tf_imports:lodash",
2223
"//tensorboard/components/tf_imports:polymer",
2324
"//tensorboard/components/tf_storage",

tensorboard/components/tf_categorization_utils/categorizationUtils.ts

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export enum CategoryType {
2828
PREFIX_GROUP,
2929
}
3030
export interface PrefixGroupMetadata {
31-
type: CategoryType.PREFIX_GROUP;
31+
type: CategoryType;
3232
}
3333
export interface SearchResultsMetadata {
34-
type: CategoryType.SEARCH_RESULTS;
34+
type: CategoryType;
3535
validRegex: boolean;
3636
universalRegex: boolean; // is the search query ".*"? ("(?:)" doesn't count)
3737
}
@@ -45,6 +45,18 @@ export interface Category<T> {
4545
export type TagCategory = Category<{tag: string, runs: string[]}>;
4646
export type RunTagCategory = Category<{tag: string, run: string}>;
4747

48+
/**
49+
* Organize data by tagPrefix, tag, then list of series which is comprised of
50+
* an experiment and a run.
51+
*/
52+
export type SeriesCategory = Category<{
53+
tag: string,
54+
series: Array<{
55+
experiment: string,
56+
run: string,
57+
}>,
58+
}>;
59+
4860
export type RawCategory = Category<string>; // Intermediate structure.
4961

5062
/**
@@ -110,25 +122,103 @@ export function categorizeTags(
110122
query?: string): TagCategory[] {
111123
const tags = tf_backend.getTags(runToTag);
112124
const categories = categorize(tags, query);
113-
const tagToRuns: {[tag: string]: string[]} = {};
114-
tags.forEach(tag => {
115-
tagToRuns[tag] = [];
116-
});
117-
selectedRuns.forEach(run => {
118-
(runToTag[run] || []).forEach(tag => {
119-
tagToRuns[tag].push(run);
120-
});
121-
});
125+
const tagToRuns = createTagToRuns(_.pick(runToTag, selectedRuns));
126+
122127
return categories.map(({name, metadata, items}) => ({
123128
name,
124129
metadata,
125130
items: items.map(tag => ({
126131
tag,
127-
runs: tagToRuns[tag].slice(),
132+
runs: tagToRuns.get(tag).slice(),
128133
})),
129134
}));
130135
}
131136

137+
/**
138+
* Creates grouping of the data based on selection from tf-data-selector. It
139+
* groups data by prefixes of tag names and by tag names. Each group contains
140+
* series, a tuple of experiment name and run name.
141+
*/
142+
export function categorizeSelection(
143+
selection: tf_data_selector.Selection[], pluginName: string):
144+
SeriesCategory[] {
145+
const tagToSeries = new Map();
146+
const searchTags = new Set();
147+
148+
selection.forEach(({experiment, runs, tagRegex}) => {
149+
const runNames = runs.map(({name}) => name);
150+
const selectedRunToTag = createRunToTagForPlugin(runs, pluginName);
151+
const tagToSelectedRuns = createTagToRuns(selectedRunToTag);
152+
const tags = tf_backend.getTags(selectedRunToTag);
153+
154+
const searchCategory = categorizeBySearchQuery(tags, tagRegex);
155+
// list of matching tags.
156+
searchCategory.items.forEach(tag => searchTags.add(tag));
157+
158+
// list of all tags that has selected runs.
159+
tags.forEach(tag => {
160+
const series = tagToSeries.get(tag) || [];
161+
series.push(...tagToSelectedRuns.get(tag)
162+
.map(run => ({experiment: experiment.name, run})));
163+
tagToSeries.set(tag, series);
164+
});
165+
});
166+
167+
const searchCategory = {
168+
name: selection.length == 1 ? selection[0].tagRegex : 'multi',
169+
metadata: {
170+
type: CategoryType.SEARCH_RESULTS,
171+
validRegex: false,
172+
universalRegex: false,
173+
},
174+
items: Array.from(searchTags)
175+
.sort(vz_sorting.compareTagNames)
176+
.map(tag => ({
177+
tag,
178+
series: tagToSeries.get(tag),
179+
})),
180+
};
181+
182+
// Organize the tag to items by prefix.
183+
const prefixCategories = categorizeByPrefix(Array.from(tagToSeries.keys()))
184+
.map(({name, metadata, items}) => ({
185+
name,
186+
metadata,
187+
items: items.map(tag => ({
188+
tag,
189+
series: tagToSeries.get(tag),
190+
})),
191+
}));
192+
193+
return [
194+
searchCategory,
195+
...prefixCategories,
196+
];
197+
}
198+
199+
function createTagToRuns(runToTag: RunToTag): Map<string, string[]> {
200+
const tagToRun = new Map();
201+
Object.keys(runToTag).forEach(run => {
202+
runToTag[run].forEach(tag => {
203+
const runs = tagToRun.get(tag) || [];
204+
runs.push(run);
205+
tagToRun.set(tag, runs);
206+
});
207+
});
208+
return tagToRun;
209+
}
210+
211+
function createRunToTagForPlugin(runs: tf_backend.Run[], pluginName: string):
212+
RunToTag {
213+
const runToTag = {};
214+
runs.forEach((run) => {
215+
runToTag[run.name] = run.tags
216+
.filter(tag => tag.pluginName == pluginName)
217+
.map(({name}) => name);
218+
})
219+
return runToTag;
220+
}
221+
132222
function compareTagRun(a, b: {tag: string, run: string}): number {
133223
const c = vz_sorting.compareTagNames(a.tag, b.tag);
134224
if (c != 0) {

tensorboard/components/tf_categorization_utils/test/categorizationUtilsTests.ts

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ limitations under the License.
1414
==============================================================================*/
1515
namespace tf_categorization_utils {
1616

17-
const assert = chai.assert;
17+
const {assert, expect} = chai;
1818

1919
describe('categorizationUtils', () => {
2020
const {CategoryType} = tf_categorization_utils;
@@ -195,6 +195,201 @@ describe('categorizationUtils', () => {
195195
});
196196
});
197197

198+
describe('categorizeSelection', () => {
199+
const {categorizeSelection} = tf_categorization_utils;
200+
201+
beforeEach(function() {
202+
const tag1 = {
203+
id: 1, pluginName: 'scalar',
204+
name: 'tag1', displayName: 'tag1',
205+
206+
};
207+
const tag2_1 = {
208+
id: 2, pluginName: 'scalar',
209+
name: 'tag2/subtag1', displayName: 'tag2/subtag1',
210+
211+
};
212+
const tag2_2 = {
213+
id: 3, pluginName: 'scalar',
214+
name: 'tag2/subtag2', displayName: 'tag2/subtag2',
215+
216+
};
217+
const tag3 = {
218+
id: 4, pluginName: 'scalar',
219+
name: 'tag3', displayName: 'tag3',
220+
};
221+
const tag4 = {
222+
id: 5, pluginName: 'custom_scalar',
223+
name: 'tag4', displayName: 'tag4',
224+
};
225+
226+
this.run1 = {id: 1, name: 'run1', startTime: 10, tags: [tag1, tag4]};
227+
this.run2 = {id: 2, name: 'run2', startTime: 5, tags: [tag2_1, tag2_2]};
228+
this.run3 = {id: 3, name: 'run3', startTime: 0, tags: [tag2_1, tag3]};
229+
230+
this.experiment1 = {
231+
experiment: {id: 1, name: 'exp1', startTime: 0},
232+
runs: [this.run1, this.run2],
233+
tagRegex: '',
234+
};
235+
this.experiment2 = {
236+
experiment: {id: 2, name: 'exp2', startTime: 0},
237+
runs: [this.run2, this.run3],
238+
tagRegex: '(subtag1|tag3)',
239+
};
240+
this.experiment3 = {
241+
experiment: {id: 3, name: 'exp3', startTime: 0},
242+
runs: [this.run1, this.run2, this.run3],
243+
tagRegex: 'junk',
244+
};
245+
});
246+
247+
it('merges the results of the query and the prefix groups', function() {
248+
const result = categorizeSelection(
249+
[this.experiment1], 'scalar');
250+
251+
expect(result).to.have.lengthOf(3);
252+
expect(result[0]).to.have.property('metadata')
253+
.that.has.property('type', CategoryType.SEARCH_RESULTS);
254+
255+
expect(result[1]).to.have.property('metadata')
256+
.that.has.property('type', CategoryType.PREFIX_GROUP);
257+
expect(result[2]).to.have.property('metadata')
258+
.that.has.property('type', CategoryType.PREFIX_GROUP);
259+
});
260+
261+
describe('search group', () => {
262+
it('filters groups by tag with a tagRegex', function() {
263+
const [searchResult] = categorizeSelection(
264+
[this.experiment2], 'scalar');
265+
266+
// should match 'tag2/subtag1' and 'tag3'.
267+
expect(searchResult).to.have.property('items')
268+
.that.has.lengthOf(2);
269+
expect(searchResult.items[0]).to.have.property('tag', 'tag2/subtag1');
270+
expect(searchResult.items[1]).to.have.property('tag', 'tag3');
271+
});
272+
273+
it('combines selection without tagRegex with one', function() {
274+
const [searchResult] = categorizeSelection(
275+
[this.experiment1, this.experiment2], 'scalar');
276+
277+
// should match 'tag1', 'tag2/subtag1', 'tag2/subtag2', and 'tag3'.
278+
expect(searchResult).to.have.property('items')
279+
.that.has.lengthOf(4);
280+
expect(searchResult.items[0]).to.have.property('tag', 'tag1');
281+
expect(searchResult.items[1]).to.have.property('tag', 'tag2/subtag1');
282+
expect(searchResult.items[2]).to.have.property('tag', 'tag2/subtag2');
283+
expect(searchResult.items[3]).to.have.property('tag', 'tag3');
284+
285+
expect(searchResult.items[1]).to.have.property('series')
286+
.that.has.lengthOf(3)
287+
.and.that.deep.equal([
288+
{experiment: 'exp1', run: 'run2'},
289+
{experiment: 'exp2', run: 'run2'},
290+
{experiment: 'exp2', run: 'run3'},
291+
]);
292+
});
293+
294+
it('sorts the tag by name', function() {
295+
const [searchResult] = categorizeSelection(
296+
[this.experiment2, this.experiment1], 'scalar');
297+
298+
// should match 'tag1', 'tag2/subtag1', 'tag2/subtag2', and 'tag3'.
299+
expect(searchResult).to.have.property('items')
300+
.that.has.lengthOf(4);
301+
expect(searchResult.items[0]).to.have.property('tag', 'tag1');
302+
expect(searchResult.items[1]).to.have.property('tag', 'tag2/subtag1');
303+
expect(searchResult.items[2]).to.have.property('tag', 'tag2/subtag2');
304+
expect(searchResult.items[3]).to.have.property('tag', 'tag3');
305+
});
306+
307+
it('returns name `multi` when there are multiple selections', function() {
308+
const [searchResult2] = categorizeSelection(
309+
[this.experiment2], 'scalar');
310+
expect(searchResult2).to.have.property('name', '(subtag1|tag3)');
311+
312+
const [searchResult1] = categorizeSelection(
313+
[this.experiment1, this.experiment2], 'scalar');
314+
expect(searchResult1).to.have.property('name', 'multi');
315+
});
316+
317+
it('returns an empty array when tagRegex does not match any', function() {
318+
const result = categorizeSelection(
319+
[this.experiment3], 'custom_scalar');
320+
321+
expect(result).to.have.lengthOf(2);
322+
expect(result[0]).to.have.property('items')
323+
.that.has.lengthOf(0);
324+
});
325+
});
326+
327+
describe('prefix group', () => {
328+
it('creates a group when a tag misses separator', function() {
329+
const result = categorizeSelection(
330+
[this.experiment1], 'scalar');
331+
332+
expect(result[1]).to.have.property('items')
333+
.that.has.lengthOf(1);
334+
335+
expect(result[1]).to.have.property('name', 'tag1');
336+
expect(result[1].items[0]).to.have.property('tag', 'tag1');
337+
expect(result[1].items[0]).to.have.property('series')
338+
.that.has.lengthOf(1);
339+
});
340+
341+
it('creates a grouping when tag has a separator', function() {
342+
const result = categorizeSelection(
343+
[this.experiment1], 'scalar');
344+
345+
expect(result[2]).to.have.property('items')
346+
.that.has.lengthOf(2);
347+
348+
expect(result[2]).to.have.property('name', 'tag2');
349+
expect(result[2].items[0]).to.have.property('tag', 'tag2/subtag1');
350+
expect(result[2].items[1]).to.have.property('tag', 'tag2/subtag2');
351+
expect(result[2].items[0]).to.have.property('series')
352+
.that.has.lengthOf(1);
353+
});
354+
355+
it('creates a group with items with experiment and run', function() {
356+
const result = categorizeSelection(
357+
[this.experiment1], 'scalar');
358+
359+
expect(result[1].items[0]).to.have.property('series')
360+
.that.has.lengthOf(1)
361+
.and.that.deep.equal([{experiment: 'exp1', run: 'run1'}]);
362+
});
363+
364+
it('creates distinct subitems when tags exactly match', function() {
365+
const result = categorizeSelection(
366+
[this.experiment2], 'scalar');
367+
368+
expect(result[1].items[0]).to.have.property('series')
369+
.that.has.lengthOf(2)
370+
.and.that.deep.equal([
371+
{experiment: 'exp2', run: 'run2'},
372+
{experiment: 'exp2', run: 'run3'},
373+
]);
374+
});
375+
376+
it('filters out tags of a different pluguin', function() {
377+
const result = categorizeSelection(
378+
[this.experiment3], 'custom_scalar');
379+
380+
expect(result).to.have.lengthOf(2);
381+
expect(result[1]).to.have.property('name', 'tag4');
382+
expect(result[1]).to.have.property('items')
383+
.that.has.lengthOf(1);
384+
expect(result[1].items[0]).to.have.property('series')
385+
.that.has.lengthOf(1)
386+
.and.that.deep.equal([
387+
{experiment: 'exp3', run: 'run1'},
388+
]);
389+
});
390+
});
391+
});
392+
198393
});
199394

200395
} // namespace tf_categorization_utils

0 commit comments

Comments
 (0)