Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable WIT usage as a jupyter extension #1662

Merged
merged 51 commits into from
Jan 4, 2019
Merged

Conversation

jameswex
Copy link
Contributor

@jameswex jameswex commented Dec 5, 2018

  • Motivation for features / changes

This PR enables the What-If Tool to work as a Jupyter extension, while not affecting its use in TensorBoard as well, which many customers have asked for.

  • Technical description of changes

The code for the jupyter extension sits in the witwidget directory under the top-level plugin directory. The jupyter extension can be installed as its own pip package named witwidget and is built using a bazel command and shell script just like the main tensorboard pip package. There are added instructions for this mode in the WIT top-level readme and the WIT_from_scratch tutorial jupyter notebook.

wit.py and wit.js contain the special code for using WIT in a jupyter extension, using the ipywidget framework with traitlets. Most of the rest of the new files are necessary boilerplate from https://github.com/jupyter-widgets/widget-cookiecutter

  • Screenshots of UI changes

Screenshot showing invocation and display of WIT inside jupyter notebook.

wit-jupyter

  • Detailed steps to verify changes work correctly (as executed by you)

Ran tensorboard with WIT on existing models to ensure no functionality loss in TensorBoard.

Built pip package and installed it, ran WIT inside of notebook as per the updated tutorial notebook and tested all of its features.

  • Alternate designs / implementations considered

Colab implementation of extension will be in separate PR. Colab doesn't allow for ipywidget use so the implementation must be different unfortunately. This follows the same patterns as the TensorFlow Model Analysis jupyter/colab extensions.

@jameswex jameswex requested a review from nfelt December 5, 2018 19:24
Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

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

Hi @jameswex, thanks a lot for opening the PR.
Just curious, since setup.py runs/generates the static file, you don't need to check those files in, right?
It is just required at the pip package generation time for the data_files?

@jameswex
Copy link
Contributor Author

jameswex commented Dec 6, 2018

I think you are right that static/extension.js, static/index.js and static/index.js.map don't need to be checked in as they are created by the pip setup. This is my first pip package so I'm not very familiar with it, so its possible there is some trimming down that can be done. I believe the wit_jupyter.html needs to stay as part of the submission, unless the setup.py can be altered to build it from the appopriate bazel run and copy it to the right place for packaging. Is that also doable in the setup.py?

@stephanwlee
Copy link
Contributor

James, would it be possible to run the bazel build tensorboard/plugins/interactive_inference/tf_interactive_inference_dashboard:wit_jupyter in the setup.py? You may need to find the file in the bazel-out directory but it seems cleaner than checking in transpiled code.

@nfelt
Copy link
Contributor

nfelt commented Dec 6, 2018

Another option re: the generated code might be checking in a script that first generates those intermediate files in a temporary location, and then invokes setup.py to build the pip package using those files.

This is basically what we do for the main pip package in build_pip_package.sh - we build the python code as data dependencies to a sh_binary script, and then the script pulls the python files out of the bazel runfiles directory into a new directory and generates the pip package from there.

# rm -rf /tmp/tensorboard
# bazel run //tensorboard/pip_package:build_pip_package
# pip install -U /tmp/tensorboard/*py2*.pip
sh_binary(
name = "build_pip_package",
srcs = ["build_pip_package.sh"],
data = [
"LICENSE",
"MANIFEST.in",
"README.rst",
"setup.cfg",
"setup.py",
"//tensorboard", # Main tensorboard binary and everything it uses
"//tensorboard:lib", # User-facing overall TensorBoard API
"//tensorboard:version", # Version module (read by setup.py)
"//tensorboard/plugins/beholder", # User-facing beholder API
"//tensorboard/plugins/projector", # User-facing projector API
],
tags = [
"local",
"manual",
],
)

cp -LR "${RUNFILES}/org_tensorflow_tensorboard/tensorboard" .
mv -f "tensorboard/pip_package/LICENSE" .
mv -f "tensorboard/pip_package/MANIFEST.in" .
mv -f "tensorboard/pip_package/README.rst" .
mv -f "tensorboard/pip_package/setup.cfg" .
mv -f "tensorboard/pip_package/setup.py" .
rm -rf tensorboard/pip_package
rm -f tensorboard/tensorboard # bazel py_binary sh wrapper
chmod -x LICENSE # bazel symlinks confuse cp
find . -name __init__.py | xargs chmod -x # which goes for all genfiles
mkdir -p tensorboard/_vendor
touch tensorboard/_vendor/__init__.py
cp -LR "${RUNFILES}/org_html5lib/html5lib" tensorboard/_vendor
cp -LR "${RUNFILES}/org_mozilla_bleach/bleach" tensorboard/_vendor
# Vendor tensorflow-serving-api because it depends directly on TensorFlow.
# TODO(nickfelt): de-vendor if they're able to relax that dependency.
cp -LR "${RUNFILES}/org_tensorflow_serving_api/tensorflow_serving" tensorboard/_vendor
chmod -R u+w,go+r .
find tensorboard -name \*.py |
xargs $sedi -e '
s/^import html5lib$/from tensorboard._vendor import html5lib/
s/^from html5lib/from tensorboard._vendor.html5lib/
s/^import bleach$/from tensorboard._vendor import bleach/
s/^from bleach/from tensorboard._vendor.bleach/
s/from tensorflow_serving/from tensorboard._vendor.tensorflow_serving/
'
virtualenv venv
export VIRTUAL_ENV=venv
export PATH="$PWD/venv/bin:${PATH}"
unset PYTHON_HOME
# Require wheel for bdist_wheel command, and setuptools 36.2.0+ so that
# env markers are handled (https://github.com/pypa/setuptools/pull/1081)
pip install -qU wheel 'setuptools>=36.2.0'
python setup.py bdist_wheel --python-tag py2 >/dev/null
python setup.py bdist_wheel --python-tag py3 >/dev/null

@jameswex
Copy link
Contributor Author

jameswex commented Dec 6, 2018

I added a new command to setup.py that builds wit_jupyter and moves it to the static dir, so the static dir has been removed from the repo.

@jameswex jameswex changed the title Proof-of-concept WIT as a jupyter extension Enable WIT usage as a jupyter extension Dec 28, 2018
@jameswex
Copy link
Contributor Author

@stephanwlee @tolga-b This PR now only contains the necessary code to use WIT as a jupyter extension (including building the pip package for the extension). The colab extension code will be reviewed as a separate PR after this is submitted.

Can be tested locally if desired by building the pip package whl files as per the new bazel rule in pip_package directory then using 'pip install [path to built whl file] and running the jupyter nbextension commands as per the updated plugin documentation, then running through the WIT_from_scratch.ipynb notebook which has been updated to show this usage.


## How do I use it in a Jupyter notebook?
First, install and enable WIT for Jupyter through the following commands:
> pip install witwidget
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems to read better like this

\```sh
pip install witwidget
jupyter nbextension install --py --symlink --sys-prefix witwidget
jupyter nbextension enable --py --sys-prefix witwidget
\```

(omit the backslash before backticks in code)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

Choose a reason for hiding this comment

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

Drive-by markdown tip: @stephanwlee, you can write

Seems to read better like this

``````markdown
```sh
pip install witwidget
jupyter nbextension install --py --symlink --sys-prefix witwidget
jupyter nbextension enable --py --sys-prefix witwidget
```
``````

which will render like this:

Seems to read better like this

```sh
pip install witwidget
jupyter nbextension install --py --symlink --sys-prefix witwidget
jupyter nbextension enable --py --sys-prefix witwidget
```

In general, you can start code fences or inline code spans with any
number of backticks, and close them with the matching number of
backticks.

@@ -0,0 +1,13 @@
# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

omit this file for now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

// python backend.
var WITView = widgets.DOMWidgetView.extend({
render: function() {
loadVulcanizedTemplate();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a synchronous operation? I forgot whether adding a script tag to the head is synchronous. Anyways, depending on the answer, it may impact L42

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i'm seeing this code function correctly as-is when i test locally. should i be instead deferring to some point in the future to be more defensive? The setTimeouts (now requestAnimationFrames) in my code may be accomplishing this for me already.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have tried this. Not sure of <link> differs too much from <script> but it sounds like it is worth doing some investigation to invoke L26: http://jsfiddle.net/b726ykhc/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for bringing this up. Fixed this all with change to wait for onload of the import before creating the WIT polymer element. Now there are no more setTimeouts OR requestAnimationFrame calls. Much simpler logic.


// Add listeners for changes from WIT Polymer element. Passes changes
// along to python.
this.view_.addEventListener('infer-examples', e => {
Copy link
Contributor

Choose a reason for hiding this comment

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

would you be factor out view creation to its own method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

this.mutantFeature = e.detail.feature_name;
this.touch();
});
setTimeout(()=> {
Copy link
Contributor

Choose a reason for hiding this comment

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

prefer to use requestAnimationFrame unless it has to happen in nextTick.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

// Callback functions for when changes made on python side.
examples_changed: function() {
const examples = this.model.get('examples');
if (!this.view_.updateExampleContents_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It should not read the private property of the view. Can this be refactored?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

made this method public as it is needed to be called here.

i added this because i noticed that in practice when the element is created, it doesn't initially have its methods defined (must have to do with how polymer elements are created?)

examples_changed: function() {
const examples = this.model.get('examples');
if (!this.view_.updateExampleContents_) {
setTimeout(() => this.examples_changed(), 100);
Copy link
Contributor

Choose a reason for hiding this comment

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

If above if statement is truthy, we can get stuck in infinite loop. Also, since examples_changed can get triggered multiple times, consider clearing the timeout before doing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

switched to requestAnimationFrame. i added this because i noticed that in practice when the element is created, it doesn't initially have its methods defined (must have to do with how polymer elements are created?), so we need to wait until the method exists before calling it.

},
eligible_features_changed: function() {
const features = this.model.get('eligible_features');
this.view_.partialDepPlotEligibleFeatures = features
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: missing trailing semi-colon (for consistency)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

},
config_changed: function() {
const config = this.model.get('config');
if ('inference_address' in config) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious: is config guaranteed to be non-null or non-undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added protection against null

},
sprite_changed: function() {
if (!this.view_.updateSprite_) {
setTimeout(() => this.sprite_changed(), 100);
Copy link
Contributor

Choose a reason for hiding this comment

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

consider clearing timeout before setting a new one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

switched to requestAnimationFrame

limitations under the License.
==============================================================================*/

__webpack_public_path__ =
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this currently being exported as a global variable? If so, can we be a little bit more explicit with (global || window).__webpack_public_path__? Or, pull it out to own module and exporting the value that wit.js requires would be more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

used window.

output: {
filename: 'index.js',
path: path.resolve(__dirname, 'static'),
libraryTarget: 'amd'
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

doesn't matter to me where it is set, we don't expect it to be updated in future PRs.

"""WIT widget for Jupyter."""
_view_name = Unicode('WITView').tag(sync=True)
_view_module = Unicode('wit-widget').tag(sync=True)
_view_module_version = Unicode('^0.1.5').tag(sync=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like the version number is encoded in three different places (version.py for pip package gen, package.json, and here). Can we do anything to make it more maintainable? If not, we might need to update the RELEASE.md

Copy link
Contributor Author

Choose a reason for hiding this comment

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

looking back at TFMA's use, only the version.py is important for the package. The package.json doesn't need a version as it isn't published and the view module versions can be kept static at 0.1.0 - they do not need to match the pip version and don't need to ever be updated as this is the only wit widget to be pip installed for a jupyter kernel. made appropriate changes.


def __init__(self, config_builder, height=1000):
super(WitWidget, self).__init__(layout=Layout(height='%ipx' % height))
tf.logging.set_verbosity(tf.logging.WARN)
Copy link
Contributor

Choose a reason for hiding this comment

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

please note that this is going away in tf 2.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this plugin and widget version of it will need tf 2.0 updates, but i imagine outside of this PR

this.view_.hasSprite = true;
this.view_.localAtlasUrl = spriteUrl;
this.view_.updateSprite();
},
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: indentation is off

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor

@tolga-b tolga-b left a comment

Choose a reason for hiding this comment

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

Thank you James, just some high-level comments for now:

  • Run Inference button in jupyter notebook always stays on.
  • I get the following deprecation warning under the widget in jupyter:
    WARNING:tensorflow: .../python2.7/site-packages/tensorflow_estimator/python/estimator/util.py:104: make_initializable_iterator (from tensorflow.python.data.ops.dataset_ops) is deprecated and will be removed in a future version. Instructions for updating: Use for ... in dataset:to iterate over a dataset. If usingtf.estimator, return the Datasetobject directly from your input function. As a last resort, you can usetf.compat.v1.data.make_initializable_iterator(dataset).

fi

set -x
command -v virtualenv >/dev/null
Copy link
Contributor

Choose a reason for hiding this comment

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

this line fails silently when virtualenv is not installed (does not warn that it is not installed, just says build completed successfully).

@tolga-b
Copy link
Contributor

tolga-b commented Jan 2, 2019

In jupyter mode PDP and Global PDP panels do not seem to always open the top plot as it was in tensorboard mode. For example in WIT_From_Scratch notebook, if you open PDP panel then close and open it again, all PDPs are closed and one needs to click on "Age" again for the plot to be computed.

@jameswex
Copy link
Contributor Author

jameswex commented Jan 3, 2019

Thanks Tolga. Fixed the warning with a change in the example ipython notebook and fixed the inference button issue with a fix in the WitConfigBuilder object handling when a label vocab isn't set.

Copy link
Contributor

@nfelt nfelt left a comment

Choose a reason for hiding this comment

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

Pip package scripting LGTM.

"//tensorboard/plugins/interactive_inference/tf_interactive_inference_dashboard:wit_jupyter", # For HTML payload
"//tensorboard/plugins/interactive_inference/witwidget/notebook/jupyter/js:js_files",
"//tensorboard/plugins/interactive_inference/utils:inference_utils", # Utility method that visualization code uses.
"//tensorboard/plugins/interactive_inference/witwidget", # Version module (read by setup.py)
Copy link
Contributor

Choose a reason for hiding this comment

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

Stale comment? The line below is the version module

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

REQUIRED_PACKAGES = [
'ipywidgets>=7.0.0',
'jupyter>=1.0,<2',
'tensorflow>=1.12.0',
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW - TensorBoard doesn't express a direct dep on TF because if this dep won't be satisfied by e.g. the tensorflow-gpu or tf-nightly packages, as pip isn't smart enough to realize that they're all providing the tensorflow package. If the user already has one of those packages installed, then installing witwidget will cause tensorflow to be installed and clobber the existing package, which can result in a bit of a mess.

I guess tensorflow-serving-api already depends on TF directly, though (it partially gets around this by shipping a -gpu version that depends on tensorflow-gpu instead, apparently), so you're kind of stuck. Just something to be aware of at least.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm not ideal. thanks for explaining this.

'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
Copy link
Contributor

Choose a reason for hiding this comment

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

Also include 3.6? Not that it makes much functional difference AFAIK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@jameswex jameswex merged commit 7713137 into tensorflow:master Jan 4, 2019
@jameswex jameswex deleted the wit-jup branch January 4, 2019 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants