Skip to content

Conversation

chihuahua
Copy link
Member

This frontend for the PR curves plugin makes use of the vz-line-chart component that had been generalized in #388. Each tf-pr-curve-card contains 1 PR curve. The tf-pr-curve-steps-selector creates 1 slider per run and lets the user select steps.

This PR is a part of dividing #334 (which contains resourceful input from @wchargin) into smaller PRs.

@chihuahua chihuahua force-pushed the chihuahua-pr-curves-frontend branch 5 times, most recently from fab71f6 to 6316efd Compare August 26, 2017 08:46
@chihuahua chihuahua requested review from jart and wchargin August 26, 2017 08:46
@chihuahua chihuahua force-pushed the chihuahua-pr-curves-frontend branch from 6316efd to c922b34 Compare August 26, 2017 08:50
This frontend for the PR curves plugin makes use of the `vz-line-chart` component that had been generalized in #388. Each `tf-pr-curve-card` contains 1 PR curve. The `tf-pr-curve-steps-selector` creates 1 slider per run and lets the user select steps.

This PR is a part of dividing #334 (which contains resourceful input from @wchargin) into smaller PRs.
@chihuahua chihuahua force-pushed the chihuahua-pr-curves-frontend branch from c922b34 to 9ae592f Compare August 26, 2017 09:10
@chihuahua
Copy link
Member Author

The UI WAI. With many runs, changing step is still a bit slow, but acceptable, which is in contrast to the UI being unusable when we redraw PR curves for all runs when the user changes step.

mfazx6eefkn

@chihuahua
Copy link
Member Author

A good next step would be to visualize the display name in the header.

@chihuahua chihuahua force-pushed the chihuahua-pr-curves-frontend branch from f231fa3 to 7dd8afe Compare August 27, 2017 10:42
Copy link
Contributor

@wchargin wchargin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High-level design looks good.

} else if (timeDisplayType === 'wall_time') {
return new Date(value * 1000).toString();
}
console.error(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just throw an error here? Returning undefined doesn't seem like the right thing to do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

let url = router.pluginRoute('pr_curves', '/pr_curves');
url += url.indexOf('?') > -1 ? '&' : '?';
const urlRunsPortion = this.runs.map(r => `&run=${r}`).join('');
url += `tag=${this.tag}${urlRunsPortion}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ordering of these two lines is confusing to me. What do you think of

let url = router.pluginRoute('pr_curves', '/pr_curves');
url += url.indexOf('?') > -1 ? '&' : '?';
url += `tag=${this.tag}`;
url += this.runs.map(r => `&run=${r}`).join('');

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, that is clearer.

_computeSetOfRelevantRuns(runsWithStepAvailable) {
const setOfRelevantRuns = {};
_.forEach(runsWithStepAvailable, run => {
setOfRelevantRuns[run] = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Common practice is to use true here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (!entry) {
return null;
}
return new Date(runToPrCurveEntry[run].wall_time * 1000).toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well use entry.wall_time * 1000 here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

max-width: 540px;
margin: 80px auto 0 auto;
}
/** Not not let the steps selector occlude the run selector. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Do not"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// do not make this handler run when _runToStepIndex is updated because
// asynchronously changing _runToStepIndex could create a rendering loop
// and jittery sliders.
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be replaced with this.async(() => { … })?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Done.

},
_runToAvailableTimeEntriesChanged(runToAvailableTimeEntries) {
// The step for each run just refreshed. Change every slider to point to
// the last available step.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, why? This behavior seems really strange. Suppose that an experiment is actively writing data to our logdir. Then, every thirty seconds, all the sliders will be shoved over to the end.

Would it suffice to make only newly added runs point to the last available step?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, that previous behavior seems jarring, ie after a reload, the steps unexpectedly change. Now, we only set sliders for newly added runs to the last available step - we initialize sliders to point to the last step, but never override the step again.

One small thing to note is that sampling logic samples a constant N values, so the step will slightly differ across reloads, but I think that's much less jarring than overriding step to the last value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

},
_updateSliders(runToStepIndex) {
const sliders = this.querySelectorAll("paper-slider");
if (sliders) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sliders should always be a NodeList, so will always be truthy:

var x = document.querySelectorAll('.not-very-likely'); [!!x, x.length]
// [true, 0]

You can dissolve this if-statement, and just use:

this.querySelectorAll('paper-slider').forEach(slider => {
  slider[value] = runToStepIndex[slider.dataset.run];
});

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Done.

this._updateSliders(newRunToStepIndex);
},
_updateSliders(runToStepIndex) {
const sliders = this.querySelectorAll("paper-slider");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Use single-quotes consistently?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

_.forOwn(runToStepIndex, (index, run) => {
const entries = runToAvailableTimeEntries[run];
if (!entries) {
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

I'm surprised that this doesn't raise a warning with the closure compiler?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Hmm, indeed, I see no Closure warnings. Maybe the transpilation from TS to uncompiled JS adds the semicolon back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this isn't TypeScript?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just double-checked that no warning appears with no semicolon. Indeed, a warning should appear since suspiciousCode warnings are on by default: https://github.com/google/closure-compiler/wiki/Warnings

Maybe the TS is transpiled into JS before being compiled into a small JS binary, and Closure applies in the 2nd step?

Copy link
Member Author

@chihuahua chihuahua left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dashboard WAI. Sliders no longer jump to the last step after each reload.

odcecaluehe

let url = router.pluginRoute('pr_curves', '/pr_curves');
url += url.indexOf('?') > -1 ? '&' : '?';
const urlRunsPortion = this.runs.map(r => `&run=${r}`).join('');
url += `tag=${this.tag}${urlRunsPortion}`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, that is clearer.

_computeSetOfRelevantRuns(runsWithStepAvailable) {
const setOfRelevantRuns = {};
_.forEach(runsWithStepAvailable, run => {
setOfRelevantRuns[run] = 1;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (!entry) {
return null;
}
return new Date(runToPrCurveEntry[run].wall_time * 1000).toString();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

max-width: 540px;
margin: 80px auto 0 auto;
}
/** Not not let the steps selector occlude the run selector. */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// The actual step value that each run should use. If a run + tag lacks a
// PR curve at this exact step value, the greatest step value less than
// this value will be used.
_runToStep: {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Done.

} else if (timeDisplayType === 'wall_time') {
return new Date(value * 1000).toString();
}
console.error(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

},
_runToAvailableTimeEntriesChanged(runToAvailableTimeEntries) {
// The step for each run just refreshed. Change every slider to point to
// the last available step.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, that previous behavior seems jarring, ie after a reload, the steps unexpectedly change. Now, we only set sliders for newly added runs to the last available step - we initialize sliders to point to the last step, but never override the step again.

One small thing to note is that sampling logic samples a constant N values, so the step will slightly differ across reloads, but I think that's much less jarring than overriding step to the last value.

this._updateSliders(newRunToStepIndex);
},
_updateSliders(runToStepIndex) {
const sliders = this.querySelectorAll("paper-slider");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

},
_updateSliders(runToStepIndex) {
const sliders = this.querySelectorAll("paper-slider");
if (sliders) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Done.

_.forOwn(runToStepIndex, (index, run) => {
const entries = runToAvailableTimeEntries[run];
if (!entries) {
return
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Hmm, indeed, I see no Closure warnings. Maybe the transpilation from TS to uncompiled JS adds the semicolon back.

Copy link
Contributor

@wchargin wchargin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for providing screenshots.

I note that the artifacts at the vertical axis—where the curve jumps down from 1.0 to 0.0—still exist. If I understand the previous discussion correctly, this is a problem with the summary op, not the frontend. Is this correct? (Also, is there a plan to fix it?)

Non-actionable: I wonder whether all the “[run] is at step [step]” will take up prohibitively much vertical space when many runs are used in practice. I suspect that we’ll hear feedback if that’s the case.

// plot. The entry is based on which step the user currently selects.
_runToPrCurveEntry: {
type: Object,
value: {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value: () => ({}) (likewise on _previousRunToPrCurveEntry)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Done.

(x) => isNaN(x) ? 'NaN' : valueFormatter(x);
return [
{
title: 'Name',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to call this "Name" or "Run"? (This isn't really a request to change; I have no preference.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, I like "Run" much better. That parallels what most TF users would use. Done.

this._updateSeriesDataForRun(run, entry);
});
},
_updateSeriesDataForRun(run, entryFor1Step) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/1/One, please. But perhaps prCurveEntry is clearer anyway—your choice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Yeah, maybe entryForOneStep for now.

Copy link
Contributor

@jart jart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a high level pass. Nothing stands out to me as problematic. I support merging this as soon as @wchargin gives his nod. It will be nice to be able to include this plugin in the talk I'm giving on Thursday.

@@ -0,0 +1,411 @@
<!--
@license
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please format these license headers the same way as the others, with the blank lines and all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

'histograms': 'tf-histogram-dashboard',
'projector': 'vz-projector-dashboard',
'text': 'tf-text-dashboard',
'pr_curves': 'tf-pr-curve-dashboard',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might look better, æsthetically speaking on the tab label, if this was "pr-curves".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man ... I think this is linked with the plugin name in the backend, which is pr_curves. What do you think about in a separate change, we replace _ with a space in the navigation bar?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep: right now we unfortunately have the restriction that the tab in the plugin name must equal the plugin name on the backend. That's probably not good to have long-term. If you look at my proposed dashboards.ts from #202, you can see the approach I had in mind for resolving this. By giving each plugin a little configuration object, we can easily add an optional (or required) tabName field that takes the string "PR Curves" instead of "pr_curves". I believe at the time we chose not to do this just for simplicity and ease of merging (as #205 was created around the same time and it wasn't worth reconciling), but we might want to do something like that now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @wchargin's idea of adding a tabName config property.

align-items: center;
bottom: 0;
display: flex;
display: flex;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated line here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member Author

@chihuahua chihuahua left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the drop is caused by a tiny precision value at a tiny (but non-zero) recall value.
csq8ynwugjk

That is a good point. Those runs listed under each PR curve sure add up, and there are definitely people out there with many runs. I wonder if just using the color paired with step value suffices.

// plot. The entry is based on which step the user currently selects.
_runToPrCurveEntry: {
type: Object,
value: {},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Done.

(x) => isNaN(x) ? 'NaN' : valueFormatter(x);
return [
{
title: 'Name',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, I like "Run" much better. That parallels what most TF users would use. Done.

this._updateSeriesDataForRun(run, entry);
});
},
_updateSeriesDataForRun(run, entryFor1Step) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Yeah, maybe entryForOneStep for now.

@chihuahua
Copy link
Member Author

For the strange, steep drop, I think we can consider solving the problem by removing the first point in the backend route, maybe here, we remove the point at recall = 0.
https://github.com/tensorflow/tensorboard/blob/master/tensorboard/plugins/pr_curve/pr_curves_plugin.py#L92

I want to also see if this could be solved at the summary op level though.

Copy link
Contributor

@wchargin wchargin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay—LGTM! 🎉

I want to also see if this could be solved at the summary op level though.

Great. I certainly prefer this over removing the point at recall=0.

'histograms': 'tf-histogram-dashboard',
'projector': 'vz-projector-dashboard',
'text': 'tf-text-dashboard',
'pr_curves': 'tf-pr-curve-dashboard',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep: right now we unfortunately have the restriction that the tab in the plugin name must equal the plugin name on the backend. That's probably not good to have long-term. If you look at my proposed dashboards.ts from #202, you can see the approach I had in mind for resolving this. By giving each plugin a little configuration object, we can easily add an optional (or required) tabName field that takes the string "PR Curves" instead of "pr_curves". I believe at the time we chose not to do this just for simplicity and ease of merging (as #205 was created around the same time and it wasn't worth reconciling), but we might want to do something like that now.

@chihuahua chihuahua merged commit 0c23b61 into master Aug 30, 2017
@chihuahua chihuahua deleted the chihuahua-pr-curves-frontend branch August 30, 2017 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants