From 45a51d90094e637d4290284bfafee5971bc7f982 Mon Sep 17 00:00:00 2001 From: Ethan Tang Date: Mon, 10 Jul 2017 17:10:45 -0700 Subject: [PATCH] Preparing Tensorflow branch for upstream merge --- .gitignore | 12 + .travis.yml | 8 +- README.md | 4 +- digits-lint | 4 +- digits/config/tensorflow.py | 2 +- .../view/rawData/header_template.html | 7 + digits/model/forms.py | 8 +- .../model/images/classification/test_views.py | 15 ++ digits/model/views.py | 10 +- .../standard-networks/tensorflow/alexnet.py | 7 +- .../tensorflow/alexnet_slim.py | 7 +- .../tensorflow/binary_segmentation.py | 23 -- .../standard-networks/tensorflow/googlenet.py | 201 +++++++++++++++ digits/standard-networks/tensorflow/lenet.py | 7 +- .../tensorflow/lenet_slim.py | 7 +- .../standard-networks/tensorflow/rnn_mnist.py | 53 ---- .../standard-networks/tensorflow/siamese.py | 38 --- .../tensorflow/siamese_simple.py | 38 --- digits/standard-networks/tensorflow/vgg16.py | 2 +- .../custom_network_explanation.html | 7 + .../models/images/classification/new.html | 26 ++ .../generic/custom_network_explanation.html | 7 + .../models/images/generic/large_graph.html | 39 +++ .../templates/models/images/generic/new.html | 26 ++ digits/tools/tensorflow/gan_grid.py | 28 +- digits/tools/tensorflow/gandisplay.py | 70 ++--- digits/tools/tensorflow/main.py | 4 +- digits/tools/tensorflow/model.py | 75 +++--- digits/tools/tensorflow/tf_data.py | 12 +- digits/tools/tensorflow/utils.py | 8 +- docs/BuildDigits.md | 4 + docs/BuildTensorflow.md | 51 +++- docs/BuildTorch.md | 2 +- docs/DevelopmentSetup.md | 36 +++ docs/GettingStartedTensorflow.md | 244 ++++++++++++++++++ docs/GettingStartedTorch.md | 3 - docs/ModelStore.md | 16 ++ docs/images/Select_TensorFlow.png | Bin 0 -> 36045 bytes docs/images/TensorBoard.png | Bin 0 -> 56199 bytes docs/images/job-dir.png | Bin 0 -> 29935 bytes docs/images/visualize-btn.png | Bin 0 -> 46115 bytes docs/images/visualize_button.png | Bin 0 -> 15731 bytes examples/autoencoder/README.md | 47 +++- .../autoencoder/autoencoder-TF.py | 0 examples/binary-segmentation/README.md | 12 +- .../binary_segmentation-TF.py | 22 ++ .../segmentation-model.lua | 2 +- examples/fine-tuning/README.md | 6 + examples/fine-tuning/lenet-fine-tune-tf.py | 84 ++++++ examples/fine-tuning/lenet-fine-tune.lua | 8 +- examples/gan/README.md | 4 +- examples/gan/gan_embeddings.py | 4 +- examples/gan/network-celebA-encoder.py | 45 ++-- examples/gan/network-celebA.py | 58 ++--- examples/gan/network-mnist-encoder.py | 33 ++- examples/gan/network-mnist.py | 44 ++-- examples/question-answering/memn2n.py | 14 +- examples/regression/README.md | 41 ++- examples/regression/regression_mnist-TF.py | 33 +++ examples/siamese/README.md | 4 + examples/siamese/siamese-TF.py | 40 +++ packaging/deb/build.sh | 2 + packaging/deb/templates/control | 4 +- plugins/view/gan/digitsViewPluginGan/forms.py | 7 +- plugins/view/gan/digitsViewPluginGan/view.py | 17 +- requirements.txt | 2 +- scripts/travis/bust-cache.sh | 1 - scripts/travis/install-caffe.sh | 1 - scripts/travis/install-openblas.sh | 28 -- scripts/travis/install-tensorflow.sh | 3 +- scripts/travis/install-torch.sh | 1 - scripts/travis/ppa-upload.sh | 2 + scripts/travis/pypi-upload.sh | 2 + 73 files changed, 1216 insertions(+), 466 deletions(-) create mode 100644 digits/extensions/view/rawData/header_template.html delete mode 100644 digits/standard-networks/tensorflow/binary_segmentation.py create mode 100644 digits/standard-networks/tensorflow/googlenet.py delete mode 100644 digits/standard-networks/tensorflow/rnn_mnist.py delete mode 100644 digits/standard-networks/tensorflow/siamese.py delete mode 100644 digits/standard-networks/tensorflow/siamese_simple.py create mode 100644 digits/templates/models/images/generic/large_graph.html create mode 100644 docs/DevelopmentSetup.md create mode 100644 docs/GettingStartedTensorflow.md create mode 100644 docs/images/Select_TensorFlow.png create mode 100644 docs/images/TensorBoard.png create mode 100644 docs/images/job-dir.png create mode 100644 docs/images/visualize-btn.png create mode 100644 docs/images/visualize_button.png rename digits/standard-networks/tensorflow/autoencoder.py => examples/autoencoder/autoencoder-TF.py (100%) create mode 100644 examples/binary-segmentation/binary_segmentation-TF.py create mode 100644 examples/fine-tuning/lenet-fine-tune-tf.py create mode 100644 examples/regression/regression_mnist-TF.py create mode 100644 examples/siamese/siamese-TF.py delete mode 100755 scripts/travis/install-openblas.sh diff --git a/.gitignore b/.gitignore index ed945921c..3aa64db37 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,16 @@ TAGS /build/ /dist/ *.egg-info/ + +#Intellij files +.idea/ + +#vscode +.vscode/ + +#.project +.project /.project + +#.tb +.tb/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 8b4a505ee..898c3c3e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ env: - OPENBLAS_ROOT=~/openblas - CAFFE_ROOT=~/caffe - TORCH_ROOT=~/torch + - OMP_NUM_THREADS=1 + - OPENBLAS_MAIN_FREE=1 - secure: "WSqrE+PQm76DdoRLRGKTK6fRWfXZjIb0BWCZm3IgHgFO7OE6fcK2tBnpDNNw4XQjmo27FFWlEhxN32g18P84n5PvErHaH65IuS9Nv6FkLlPXZlVqGNxbPmEA4oTkD/6Y6kZyZWZtLh2+/1ijuzQAPnIy/4BEuL8pdO+PsoJ9hYM=" matrix: - DIGITS_TEST_FRAMEWORK=caffe CAFFE_FORK=NVIDIA @@ -73,7 +75,6 @@ matrix: cache: apt: true directories: - - $OPENBLAS_ROOT - $CAFFE_ROOT - $TORCH_ROOT @@ -95,6 +96,7 @@ addons: - libhdf5-serial-dev - libleveldb-dev - liblmdb-dev + - libopenblas-dev - libopencv-dev - libprotobuf-dev - libsnappy-dev @@ -128,15 +130,13 @@ before_install: install: - mkdir -p ~/.config/matplotlib - echo "backend:agg" > ~/.config/matplotlib/matplotlibrc - - ./scripts/travis/install-openblas.sh $OPENBLAS_ROOT - ./scripts/travis/install-caffe.sh $CAFFE_ROOT - if [ "$DIGITS_TEST_FRAMEWORK" == "torch" ]; then travis_wait ./scripts/travis/install-torch.sh $TORCH_ROOT; else unset TORCH_ROOT; fi + - pip install -r ./requirements.txt --force-reinstall - if [ "$DIGITS_TEST_FRAMEWORK" == "tensorflow" ]; then travis_wait ./scripts/travis/install-tensorflow.sh; fi - - pip install -r ./requirements.txt - pip install -r ./requirements_test.txt - pip install -e . - if [ "$WITH_PLUGINS" != "false" ]; then find ./plugins/*/* -maxdepth 0 -type d | xargs -n1 pip install -e; fi script: - ./digits-test -v - diff --git a/README.md b/README.md index 434ccae73..7798f094e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ DIGITS (the **D**eep Learning **G**PU **T**raining **S**ystem) is a webapp for training deep learning models. +The currently supported frameworks are: Caffe 1, Torch, and Tensorflow + # Installation | Installation method | Supported platform[s] | Available versions | Instructions | @@ -18,6 +20,7 @@ Once you have installed DIGITS, visit [docs/GettingStarted.md](docs/GettingStart Then, take a look at some of the other documentation at [docs/](docs/) and [examples/](examples/): +* [Getting started with TensorFlow](docs/GettingStartedTensorflow.md) * [Getting started with Torch](docs/GettingStartedTorch.md) * [Fine-tune a pretrained model](examples/fine-tuning/README.md) * [Train an autoencoder network](examples/autoencoder/README.md) @@ -44,4 +47,3 @@ Then, take a look at some of the other documentation at [docs/](docs/) and [exam * Please let us know by [filing a new issue](https://github.com/NVIDIA/DIGITS/issues/new) * Bonus points if you want to contribute by opening a [pull request](https://help.github.com/articles/using-pull-requests/)! * You will need to send a signed copy of the [Contributor License Agreement](CLA) to digits@nvidia.com before your change can be accepted. - diff --git a/digits-lint b/digits-lint index b11b50021..fc5f3e892 100755 --- a/digits-lint +++ b/digits-lint @@ -5,9 +5,9 @@ set -e echo "=== Checking for Python lint ..." if which flake8 >/dev/null 2>&1; then - python2 `which flake8` . + python2 `which flake8` --exclude ./examples,./digits/standard-networks/tensorflow,./digits/jobs . else - python2 -m flake8 . + python2 -m flake8 --exclude ./examples,./digits/standard-networks/tensorflow,./digits/jobs . fi echo "=== Checking for JavaScript lint ..." diff --git a/digits/config/tensorflow.py b/digits/config/tensorflow.py index bbf6f46b7..10a4465fe 100644 --- a/digits/config/tensorflow.py +++ b/digits/config/tensorflow.py @@ -34,7 +34,7 @@ def test_tf_import(python_exe): if not tf_enabled: print('Tensorflow support disabled.') -# print('Failed importing Tensorflow with python executable "%s"\n%s' % (tf_python_exe, err)) +# print('Failed importing Tensorflow with python executable "%s"\n%s' % (tf_python_exe, err)) if tf_enabled: option_list['tensorflow'] = { diff --git a/digits/extensions/view/rawData/header_template.html b/digits/extensions/view/rawData/header_template.html new file mode 100644 index 000000000..fcd137d9d --- /dev/null +++ b/digits/extensions/view/rawData/header_template.html @@ -0,0 +1,7 @@ +{# Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. #} + +{% from "helper.html" import print_flashes %} +{% from "helper.html" import print_errors %} +{% from "helper.html" import mark_errors %} + +{{data}} diff --git a/digits/model/forms.py b/digits/model/forms.py index 34b3ed2ce..dcb3e1a11 100644 --- a/digits/model/forms.py +++ b/digits/model/forms.py @@ -313,10 +313,10 @@ def validate_lr_multistep_values(form, field): def validate_custom_network_snapshot(form, field): pass - #if form.method.data == 'custom': - # for filename in field.data.strip().split(os.path.pathsep): - # if filename and not os.path.exists(filename): - # raise validators.ValidationError('File "%s" does not exist' % filename) +# if form.method.data == 'custom': +# for filename in field.data.strip().split(os.path.pathsep): +# if filename and not os.path.exists(filename): +# raise validators.ValidationError('File "%s" does not exist' % filename) # Select one of several GPUs select_gpu = wtforms.RadioField( diff --git a/digits/model/images/classification/test_views.py b/digits/model/images/classification/test_views.py index 5f58ce73a..6673ef7cc 100644 --- a/digits/model/images/classification/test_views.py +++ b/digits/model/images/classification/test_views.py @@ -174,6 +174,7 @@ class BaseViewsTestWithDataset(BaseViewsTest, AUG_HSV_H = None AUG_HSV_S = None AUG_HSV_V = None + OPTIMIZER = None @classmethod def setUpClass(cls): @@ -242,6 +243,8 @@ def create_model(cls, network=None, **kwargs): data['aug_hsv_s'] = cls.AUG_HSV_S if cls.AUG_HSV_V is not None: data['aug_hsv_v'] = cls.AUG_HSV_V + if cls.OPTIMIZER is not None: + data['solver_type'] = cls.OPTIMIZER data.update(kwargs) @@ -1158,6 +1161,10 @@ class TestCaffeLeNet(BaseTestCreated, test_utils.CaffeMixin): ).read() +class TestCaffeLeNetADAMOptimizer(TestCaffeLeNet): + OPTIMIZER = 'ADAM' + + class TestTorchCreatedCropInForm(BaseTestCreatedCropInForm, test_utils.TorchMixin): pass @@ -1196,6 +1203,10 @@ def test_inference_while_training(self): raise unittest.SkipTest('Torch CPU inference on CuDNN-trained model not supported') +class TestTorchLeNetADAMOptimizer(TestTorchLeNet): + OPTIMIZER = 'ADAM' + + class TestTorchLeNetHdf5Shuffle(TestTorchLeNet): BACKEND = 'hdf5' SHUFFLE = True @@ -1366,6 +1377,10 @@ class TestTensorflowLeNet(BaseTestCreated, test_utils.TensorflowMixin): 'lenet.py')).read() +class TestTensorflowLeNetADAMOptimizer(TestTensorflowLeNet): + OPTIMIZER = 'ADAM' + + class TestTensorflowLeNetSlim(BaseTestCreated, test_utils.TensorflowMixin): IMAGE_WIDTH = 28 IMAGE_HEIGHT = 28 diff --git a/digits/model/views.py b/digits/model/views.py index fa8aedfc6..28df10231 100644 --- a/digits/model/views.py +++ b/digits/model/views.py @@ -295,13 +295,13 @@ def download(job_id, extension): mode = 'gz' elif extension in ['tar.bz2']: mode = 'bz2' - with tarfile.open(fileobj=b, mode='w:%s' % mode) as tf: + with tarfile.open(fileobj=b, mode='w:%s' % mode) as tar: for path, name in job.download_files(epoch): - tf.add(path, arcname=name) - tf_info = tarfile.TarInfo("info.json") - tf_info.size = len(info_io.getvalue()) + tar.add(path, arcname=name) + tar_info = tarfile.TarInfo("info.json") + tar_info.size = len(info_io.getvalue()) info_io.seek(0) - tf.addfile(tf_info, info_io) + tar.addfile(tar_info, info_io) elif extension in ['zip']: with zipfile.ZipFile(b, 'w') as zf: for path, name in job.download_files(epoch): diff --git a/digits/standard-networks/tensorflow/alexnet.py b/digits/standard-networks/tensorflow/alexnet.py index 93dc48f50..361ead2d2 100644 --- a/digits/standard-networks/tensorflow/alexnet.py +++ b/digits/standard-networks/tensorflow/alexnet.py @@ -87,7 +87,8 @@ def conv_net(x, weights, biases): @model_property def loss(self): - loss = digits.classification_loss(self.inference, self.y) - accuracy = digits.classification_accuracy(self.inference, self.y) - self.summaries.append(tf.scalar_summary(accuracy.op.name, accuracy)) + model = self.inference + loss = digits.classification_loss(model, self.y) + accuracy = digits.classification_accuracy(model, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) return loss diff --git a/digits/standard-networks/tensorflow/alexnet_slim.py b/digits/standard-networks/tensorflow/alexnet_slim.py index d51ec5e36..5655b4a1b 100644 --- a/digits/standard-networks/tensorflow/alexnet_slim.py +++ b/digits/standard-networks/tensorflow/alexnet_slim.py @@ -24,7 +24,8 @@ def inference(self): @model_property def loss(self): - loss = digits.classification_loss(self.inference, self.y) - accuracy = digits.classification_accuracy(self.inference, self.y) - self.summaries.append(tf.scalar_summary(accuracy.op.name, accuracy)) + model = self.inference + loss = digits.classification_loss(model, self.y) + accuracy = digits.classification_accuracy(model, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) return loss diff --git a/digits/standard-networks/tensorflow/binary_segmentation.py b/digits/standard-networks/tensorflow/binary_segmentation.py deleted file mode 100644 index 8b9ff6298..000000000 --- a/digits/standard-networks/tensorflow/binary_segmentation.py +++ /dev/null @@ -1,23 +0,0 @@ -# Tensorflow Triangle binary segmentation model using TensorFlow-Slim - -def build_model(params): - _x = tf.reshape(params['x'], shape=[-1, params['input_shape'][0], params['input_shape'][1], params['input_shape'][2]]) - with slim.arg_scope([slim.conv2d, slim.conv2d_transpose, slim.fully_connected], - weights_initializer=tf.contrib.layers.xavier_initializer(), - weights_regularizer=slim.l2_regularizer(0.0005) ): - - model = slim.conv2d(_x, 32, [3, 3], padding='SAME', scope='conv1') # 1*H*W -> 32*H*W - model = slim.conv2d(model, 1024, [16, 16], padding='VALID', scope='conv2', stride=16) # 32*H*W -> 1024*H/16*W/16 - model = slim.conv2d_transpose(model, params['input_shape'][2], [16, 16], stride=16, padding='VALID', activation_fn=None, scope='deconv_1') - - def loss(y): - y = tf.reshape(y, shape=[-1, params['input_shape'][0], params['input_shape'][1], params['input_shape'][2]]) - # For a fancy tensorboard summary: put the input, label and model side by side (sbs) for a fancy image summary: - # sbs = tf.concat(2, [_x, y, model]) - # tf.image_summary(sbs.op.name, sbs, max_images=3, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - return digits.mse_loss(model, y) - - return { - 'model' : model, - 'loss' : loss - } diff --git a/digits/standard-networks/tensorflow/googlenet.py b/digits/standard-networks/tensorflow/googlenet.py new file mode 100644 index 000000000..9c8997c50 --- /dev/null +++ b/digits/standard-networks/tensorflow/googlenet.py @@ -0,0 +1,201 @@ +# The auxillary branches as spcified in the original googlenet V1 model do exist in this implementation of +# googlenet but it is not used. To use it, be sure to check self.is_training to ensure that it is only used +# during training. + +class UserModel(Tower): + + all_inception_settings = { + '3a': [[64], [96, 128], [16, 32], [32]], + '3b': [[128], [128, 192], [32, 96], [64]], + '4a': [[192], [96, 208], [16, 48], [64]], + '4b': [[160], [112, 224], [24, 64], [64]], + '4c': [[128], [128, 256], [24, 64], [64]], + '4d': [[112], [144, 288], [32, 64], [64]], + '4e': [[256], [160, 320], [32, 128], [128]], + '5a': [[256], [160, 320], [32, 128], [128]], + '5b': [[384], [192, 384], [48, 128], [128]] + } + + @model_property + def inference(self): + # rescale to proper form, really we expect 224 x 224 x 1 in HWC form + model = tf.reshape(self.x, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + + conv_7x7_2s_weight, conv_7x7_2s_bias = self.create_conv_vars([7, 7, self.input_shape[2], 64], 'conv_7x7_2s') + model = self.conv_layer_with_relu(model, conv_7x7_2s_weight, conv_7x7_2s_bias, 2) + + model = self.max_pool(model, 3, 2) + + model = tf.nn.local_response_normalization(model) + + conv_1x1_vs_weight, conv_1x1_vs_bias = self.create_conv_vars([1, 1, 64, 64], 'conv_1x1_vs') + model = self.conv_layer_with_relu(model, conv_1x1_vs_weight, conv_1x1_vs_bias, 1, 'VALID') + + conv_3x3_1s_weight, conv_3x3_1s_bias = self.create_conv_vars([3, 3, 64, 192], 'conv_3x3_1s') + model = self.conv_layer_with_relu(model, conv_3x3_1s_weight, conv_3x3_1s_bias, 1) + + model = tf.nn.local_response_normalization(model) + + model = self.max_pool(model, 3, 2) + + inception_settings_3a = InceptionSettings(192, UserModel.all_inception_settings['3a']) + model = self.inception(model, inception_settings_3a, '3a') + + inception_settings_3b = InceptionSettings(256, UserModel.all_inception_settings['3b']) + model = self.inception(model, inception_settings_3b, '3b') + + model = self.max_pool(model, 3, 2) + + inception_settings_4a = InceptionSettings(480, UserModel.all_inception_settings['4a']) + model = self.inception(model, inception_settings_4a, '4a') + + # first auxiliary branch for making training faster + aux_branch_1 = self.auxiliary_classifier(model, 512, "aux_1") + + inception_settings_4b = InceptionSettings(512, UserModel.all_inception_settings['4b']) + model = self.inception(model, inception_settings_4b, '4b') + + inception_settings_4c = InceptionSettings(512, UserModel.all_inception_settings['4c']) + model = self.inception(model, inception_settings_4c, '4c') + + inception_settings_4d = InceptionSettings(512, UserModel.all_inception_settings['4d']) + model = self.inception(model, inception_settings_4d, '4d') + + # second auxiliary branch for making training faster + aux_branch_2 = self.auxiliary_classifier(model, 528, "aux_2") + + inception_settings_4e = InceptionSettings(528, UserModel.all_inception_settings['4e']) + model = self.inception(model, inception_settings_4e, '4e') + + model = self.max_pool(model, 3, 2) + + inception_settings_5a = InceptionSettings(832, UserModel.all_inception_settings['5a']) + model = self.inception(model, inception_settings_5a, '5a') + + inception_settings_5b = InceptionSettings(832, UserModel.all_inception_settings['5b']) + model = self.inception(model, inception_settings_5b, '5b') + + model = self.avg_pool(model, 7, 1, 'VALID') + + fc_weight, fc_bias = self.create_fc_vars([1024, self.nclasses], 'fc') + model = self.fully_connect(model, fc_weight, fc_bias) + + return model + + @model_property + def loss(self): + model = self.inference + loss = digits.classification_loss(model, self.y) + accuracy = digits.classification_accuracy(model, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) + return loss + + + def inception(self, model, inception_setting, layer_name): + weights, biases = self.create_inception_variables(inception_setting, layer_name) + conv_1x1 = self.conv_layer_with_relu(model, weights['conv_1x1_1'], biases['conv_1x1_1'], 1) + + conv_3x3 = self.conv_layer_with_relu(model, weights['conv_1x1_2'], biases['conv_1x1_2'], 1) + conv_3x3 = self.conv_layer_with_relu(conv_3x3, weights['conv_3x3'], biases['conv_3x3'], 1) + + conv_5x5 = self.conv_layer_with_relu(model, weights['conv_1x1_3'], biases['conv_1x1_3'], 1) + conv_5x5 = self.conv_layer_with_relu(conv_5x5, weights['conv_5x5'], biases['conv_5x5'], 1) + + conv_pool = self.max_pool(model, 3, 1) + conv_pool = self.conv_layer_with_relu(conv_pool, weights['conv_pool'], biases['conv_pool'], 1) + + final_model = tf.concat([conv_1x1, conv_3x3, conv_5x5, conv_pool], 3) + + return final_model + + def create_inception_variables(self, inception_setting, layer_name): + model_dim = inception_setting.model_dim + conv_1x1_1_weight, conv_1x1_1_bias = self.create_conv_vars([1, 1, model_dim, inception_setting.conv_1x1_1_layers], layer_name + '-conv_1x1_1') + conv_1x1_2_weight, conv_1x1_2_bias = self.create_conv_vars([1, 1, model_dim, inception_setting.conv_1x1_2_layers], layer_name + '-conv_1x1_2') + conv_1x1_3_weight, conv_1x1_3_bias = self.create_conv_vars([1, 1, model_dim, inception_setting.conv_1x1_3_layers], layer_name + '-conv_1x1_3') + conv_3x3_weight, conv_3x3_bias = self.create_conv_vars([3, 3, inception_setting.conv_1x1_2_layers, inception_setting.conv_3x3_layers], layer_name + '-conv_3x3') + conv_5x5_weight, conv_5x5_bias = self.create_conv_vars([5, 5, inception_setting.conv_1x1_3_layers, inception_setting.conv_5x5_layers], layer_name + '-conv_5x5') + conv_pool_weight, conv_pool_bias = self.create_conv_vars([1, 1, model_dim, inception_setting.conv_pool_layers], layer_name + '-conv_pool') + + weights = { + 'conv_1x1_1': conv_1x1_1_weight, + 'conv_1x1_2': conv_1x1_2_weight, + 'conv_1x1_3': conv_1x1_3_weight, + 'conv_3x3': conv_3x3_weight, + 'conv_5x5': conv_5x5_weight, + 'conv_pool': conv_pool_weight + } + + biases = { + 'conv_1x1_1': conv_1x1_1_bias, + 'conv_1x1_2': conv_1x1_2_bias, + 'conv_1x1_3': conv_1x1_3_bias, + 'conv_3x3': conv_3x3_bias, + 'conv_5x5': conv_5x5_bias, + 'conv_pool': conv_pool_bias + } + + return weights, biases + + def auxiliary_classifier(self, model, input_size, name): + aux_classifier = self.avg_pool(model, 5, 3, 'VALID') + + conv_weight, conv_bias = self.create_conv_vars([1, 1, input_size, input_size], name + '-conv_1x1') + aux_classifier = self.conv_layer_with_relu(aux_classifier, conv_weight, conv_bias, 1) + + fc_weight, fc_bias = self.create_fc_vars([4*4*input_size, self.nclasses], name + '-fc') + aux_classifier = self.fully_connect(aux_classifier, fc_weight, fc_bias) + + aux_classifier = tf.nn.dropout(aux_classifier, 0.7) + + return aux_classifier + + def conv_layer_with_relu(self, model, weights, biases, stride_size, padding='SAME'): + new_model = tf.nn.conv2d(model, weights, strides=[1, stride_size, stride_size, 1], padding=padding) + new_model = tf.nn.bias_add(new_model, biases) + new_model = tf.nn.relu(new_model) + return new_model + + def max_pool(self, model, kernal_size, stride_size, padding='SAME'): + new_model = tf.nn.max_pool(model, ksize=[1, kernal_size, kernal_size, 1], strides=[1, stride_size, stride_size, 1], padding=padding) + return new_model + + def avg_pool(self, model, kernal_size, stride_size, padding='SAME'): + new_model = tf.nn.avg_pool(model, ksize=[1, kernal_size, kernal_size, 1], strides=[1, stride_size, stride_size, 1], padding=padding) + return new_model + + def fully_connect(self, model, weights, biases): + fc_model = tf.reshape(model, [-1, weights.get_shape().as_list()[0]]) + fc_model = tf.matmul(fc_model, weights) + fc_model = tf.add(fc_model, biases) + fc_model = tf.nn.relu(fc_model) + return fc_model + + def create_conv_vars(self, size, name): + weight = self.create_weight(size, name + '_W') + bias = self.create_bias(size[3], name + '_b') + return weight, bias + + def create_fc_vars(self, size, name): + weight = self.create_weight(size, name + '_W') + bias = self.create_bias(size[1], name + '_b') + return weight, bias + + def create_weight(self, size, name): + weight = tf.get_variable(name, size, initializer=tf.contrib.layers.xavier_initializer()) + return weight + + def create_bias(self, size, name): + bias = tf.get_variable(name, [size], initializer=tf.constant_initializer(0.2)) + return bias + +class InceptionSettings(): + + def __init__(self, model_dim, inception_settings): + self.model_dim = model_dim + self.conv_1x1_1_layers = inception_settings[0][0] + self.conv_1x1_2_layers = inception_settings[1][0] + self.conv_1x1_3_layers = inception_settings[2][0] + self.conv_3x3_layers = inception_settings[1][1] + self.conv_5x5_layers = inception_settings[2][1] + self.conv_pool_layers = inception_settings[3][0] \ No newline at end of file diff --git a/digits/standard-networks/tensorflow/lenet.py b/digits/standard-networks/tensorflow/lenet.py index 677c905d6..f52a78205 100644 --- a/digits/standard-networks/tensorflow/lenet.py +++ b/digits/standard-networks/tensorflow/lenet.py @@ -67,7 +67,8 @@ def conv_net(x, weights, biases): @model_property def loss(self): - loss = digits.classification_loss(self.inference, self.y) - accuracy = digits.classification_accuracy(self.inference, self.y) - self.summaries.append(tf.scalar_summary(accuracy.op.name, accuracy)) + model = self.inference + loss = digits.classification_loss(model, self.y) + accuracy = digits.classification_accuracy(model, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) return loss \ No newline at end of file diff --git a/digits/standard-networks/tensorflow/lenet_slim.py b/digits/standard-networks/tensorflow/lenet_slim.py index 58f64f020..8d3a71b77 100644 --- a/digits/standard-networks/tensorflow/lenet_slim.py +++ b/digits/standard-networks/tensorflow/lenet_slim.py @@ -20,7 +20,8 @@ def inference(self): @model_property def loss(self): - loss = digits.classification_loss(self.inference, self.y) - accuracy = digits.classification_accuracy(self.inference, self.y) - self.summaries.append(tf.scalar_summary(accuracy.op.name, accuracy)) + model = self.inference + loss = digits.classification_loss(model, self.y) + accuracy = digits.classification_accuracy(model, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) return loss diff --git a/digits/standard-networks/tensorflow/rnn_mnist.py b/digits/standard-networks/tensorflow/rnn_mnist.py deleted file mode 100644 index 73aa0bdba..000000000 --- a/digits/standard-networks/tensorflow/rnn_mnist.py +++ /dev/null @@ -1,53 +0,0 @@ -from tensorflow.python.ops import rnn, rnn_cell - -def build_model(params): - n_hidden = 28 - n_classes = params['nclasses'] - n_steps = params['input_shape'][0] - n_input = params['input_shape'][1] - - x = tf.reshape(params['x'], shape=[-1, params['input_shape'][0], params['input_shape'][1], params['input_shape'][2]]) - - tf.image_summary(x.op.name, x, max_images=1, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - x = tf.squeeze(x) - - - - # Define weights - weights = { - 'w1': tf.get_variable('w1', [n_hidden, params['nclasses']]) - } - biases = { - 'b1': tf.get_variable('b1', [params['nclasses']]) - } - - # Prepare data shape to match `rnn` function requirements - # Current data input shape: (batch_size, n_steps, n_input) - # Required shape: 'n_steps' tensors list of shape (batch_size, n_input) - - # Permuting batch_size and n_steps - x = tf.transpose(x, [1, 0, 2]) - # Reshaping to (n_steps*batch_size, n_input) - x = tf.reshape(x, [-1, n_input]) - # Split to get a list of 'n_steps' tensors of shape (batch_size, n_input) - x = tf.split(0, n_steps, x) - - # Define a lstm cell with tensorflow - lstm_cell = rnn_cell.BasicLSTMCell(n_hidden, forget_bias=1.0) - - # Get lstm cell output - outputs, states = rnn.rnn(lstm_cell, x, dtype=tf.float32) - - # Linear activation, using rnn inner loop last output - model = tf.matmul(outputs[-1], weights['w1']) + biases['b1'] - - def loss(y): - loss = digits.classification_loss(model, y) - accuracy = digits.classification_accuracy(model, y) - tf.scalar_summary(accuracy.op.name, accuracy, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - return loss - - return { - 'model' : model, - 'loss' : loss - } diff --git a/digits/standard-networks/tensorflow/siamese.py b/digits/standard-networks/tensorflow/siamese.py deleted file mode 100644 index 2b0fe588e..000000000 --- a/digits/standard-networks/tensorflow/siamese.py +++ /dev/null @@ -1,38 +0,0 @@ -def build_model(params): - _x = tf.reshape(params['x'], shape=[-1, params['input_shape'][0], params['input_shape'][1], params['input_shape'][2]]) - #tf.image_summary(_x.op.name, _x, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - - # Split out the color channels - _, model_g, model_b = tf.split(3, 3, _x, name='split_channels') - #tf.image_summary(model_g.op.name, model_g, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - #tf.image_summary(model_b.op.name, model_b, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - - with slim.arg_scope([slim.conv2d, slim.fully_connected], - weights_initializer=tf.contrib.layers.xavier_initializer(), - weights_regularizer=slim.l2_regularizer(0.0005) ): - with tf.variable_scope("siamese") as scope: - def make_tower(net): - net = slim.conv2d(net, 20, [5, 5], padding='VALID', scope='conv1') - net = slim.max_pool2d(net, [2, 2], padding='VALID', scope='pool1') - net = slim.conv2d(net, 50, [5, 5], padding='VALID', scope='conv2') - net = slim.max_pool2d(net, [2, 2], padding='VALID', scope='pool2') - net = slim.flatten(net) - net = slim.fully_connected(net, 500, scope='fc1') - net = slim.fully_connected(net, 2, activation_fn=None, scope='fc2') - return net - - model_g = make_tower(model_g) - model_g = tf.reshape(model_g, shape=[-1, 2]) - scope.reuse_variables() - model_b = make_tower(model_b) - model_b = tf.reshape(model_b, shape=[-1, 2]) - - def loss(y): - y = tf.reshape(y, shape=[-1]) - y = tf.to_float(y) - return digits.constrastive_loss(model_g, model_b, y) - - return { - 'model' : model_g, - 'loss' : loss, - } diff --git a/digits/standard-networks/tensorflow/siamese_simple.py b/digits/standard-networks/tensorflow/siamese_simple.py deleted file mode 100644 index bd0cd8d15..000000000 --- a/digits/standard-networks/tensorflow/siamese_simple.py +++ /dev/null @@ -1,38 +0,0 @@ -def build_model(params): - _x = tf.reshape(params['x'], shape=[-1, params['input_shape'][0], params['input_shape'][1], params['input_shape'][2]]) - #tf.image_summary(_x.op.name, _x, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) - - # Split out the channel in two - lhs, rhs = tf.split(0, 2, _x, name='split_batch') - - with slim.arg_scope([slim.conv2d, slim.fully_connected], - weights_initializer=tf.contrib.layers.xavier_initializer(), - weights_regularizer=slim.l2_regularizer(0.0005) ): - with tf.variable_scope("siamese") as scope: - def make_tower(net): - net = slim.conv2d(net, 20, [5, 5], padding='VALID', scope='conv1') - net = slim.max_pool2d(net, [2, 2], padding='VALID', scope='pool1') - net = slim.conv2d(net, 50, [5, 5], padding='VALID', scope='conv2') - net = slim.max_pool2d(net, [2, 2], padding='VALID', scope='pool2') - net = slim.flatten(net) - net = slim.fully_connected(net, 500, scope='fc1') - net = slim.fully_connected(net, 2, activation_fn=None, scope='fc2') - return net - - lhs = make_tower(lhs) - lhs = tf.reshape(lhs, shape=[-1, 2]) - scope.reuse_variables() - rhs = make_tower(rhs) - rhs = tf.reshape(rhs, shape=[-1, 2]) - - def loss(y): - y = tf.reshape(y, shape=[-1]) - ylhs, yrhs = tf.split(0, 2, y, name='split_label') - y = tf.equal(ylhs, yrhs) - y = tf.to_float(y) - return digits.constrastive_loss(lhs, rhs, y) - - return { - 'model' : tf.concat(0, [lhs, rhs]), - 'loss' : loss, - } diff --git a/digits/standard-networks/tensorflow/vgg16.py b/digits/standard-networks/tensorflow/vgg16.py index 3ce89edcc..6efd55bde 100644 --- a/digits/standard-networks/tensorflow/vgg16.py +++ b/digits/standard-networks/tensorflow/vgg16.py @@ -28,5 +28,5 @@ def inference(self): def loss(self): loss = digits.classification_loss(self.inference, self.y) accuracy = digits.classification_accuracy(self.inference, self.y) - self.summaries.append(tf.scalar_summary(accuracy.op.name, accuracy)) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) return loss diff --git a/digits/templates/models/images/classification/custom_network_explanation.html b/digits/templates/models/images/classification/custom_network_explanation.html index c18686a89..90aaaf5bb 100644 --- a/digits/templates/models/images/classification/custom_network_explanation.html +++ b/digits/templates/models/images/classification/custom_network_explanation.html @@ -105,3 +105,10 @@

Specifying a custom Torch network

Use this field to enter a Torch network using Lua code. Refer to the documentation for more information.

+ +

Specifying a custom Tensorflow network

+ +

+ Use this field to enter a Tensorflow network using python. + Refer to the documentation for more information. +

\ No newline at end of file diff --git a/digits/templates/models/images/classification/new.html b/digits/templates/models/images/classification/new.html index e93e61f05..2def43d80 100644 --- a/digits/templates/models/images/classification/new.html +++ b/digits/templates/models/images/classification/new.html @@ -624,9 +624,35 @@

Data Augmentations

return the_data; } +//copied from https://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome/13348618#13348618 +function isChrome() { + var isChromium = window.chrome, + winNav = window.navigator, + vendorName = winNav.vendor, + isOpera = winNav.userAgent.indexOf("OPR") > -1, + isIEedge = winNav.userAgent.indexOf("Edge") > -1, + isIOSChrome = winNav.userAgent.match("CriOS"); + + if(isIOSChrome){ + return true; + } else if(isChromium !== null && isChromium !== undefined && vendorName === "Google Inc." && isOpera == false && isIEedge == false) { + return true; + } else { + return false; + } +} + function visualizeNetwork() { var framework = $('#framework').val(); var is_tf = framework.includes("ensorflow") // @TODO(tzaman) - dirty + + if (is_tf) { + if (!isChrome()) { + bootbox.alert({title: "Visualization Error", message: "Tensorflow network visualization is only available for Google Chrome"}); + return; + } + } + var num_sel_gpus = 0 var sel_gpus = $("#select_gpus").val() if (sel_gpus) { diff --git a/digits/templates/models/images/generic/custom_network_explanation.html b/digits/templates/models/images/generic/custom_network_explanation.html index 7100a1724..93e01f00d 100644 --- a/digits/templates/models/images/generic/custom_network_explanation.html +++ b/digits/templates/models/images/generic/custom_network_explanation.html @@ -89,3 +89,10 @@

Specifying a custom Torch network

Use this field to enter a Torch network using Lua code. Refer to the documentation for more information.

+ +

Specifying a custom Tensorflow network

+ +

+ Use this field to enter a Tensorflow network using python. + Refer to the documentation for more information. +

\ No newline at end of file diff --git a/digits/templates/models/images/generic/large_graph.html b/digits/templates/models/images/generic/large_graph.html new file mode 100644 index 000000000..495043caf --- /dev/null +++ b/digits/templates/models/images/generic/large_graph.html @@ -0,0 +1,39 @@ +{# Copyright (c) 2015-2017, NVIDIA CORPORATION. All rights reserved. #} + +{% extends "layout.html" %} + +{% block title %} +{{job.name()}} - Large graph +{% endblock %} + +{% block head %} + +{% endblock %} + +{% block nav %} +
  • {{job.job_type()}}
  • +{% endblock %} + +{% block content %} + +{% set task = job.train_task() %} + +
    +
    +
    + {% set combined_graph_data = job.train_task().combined_graph_data(cull=False) %} + {% if combined_graph_data %} +
    + + {% else %} + No data. + {% endif %} + +
    +
    +
    + +{% endblock %} + diff --git a/digits/templates/models/images/generic/new.html b/digits/templates/models/images/generic/new.html index ba9a41405..ea682ff6c 100644 --- a/digits/templates/models/images/generic/new.html +++ b/digits/templates/models/images/generic/new.html @@ -593,9 +593,35 @@

    Data Augmentations

    return the_data; } +//copied from https://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome/13348618#13348618 +function isChrome() { + var isChromium = window.chrome, + winNav = window.navigator, + vendorName = winNav.vendor, + isOpera = winNav.userAgent.indexOf("OPR") > -1, + isIEedge = winNav.userAgent.indexOf("Edge") > -1, + isIOSChrome = winNav.userAgent.match("CriOS"); + + if(isIOSChrome){ + return true; + } else if(isChromium !== null && isChromium !== undefined && vendorName === "Google Inc." && isOpera == false && isIEedge == false) { + return true; + } else { + return false; + } +} + function visualizeNetwork() { var framework = $('#framework').val(); var is_tf = framework.includes("ensorflow") // @TODO(tzaman) - dirty + + if (is_tf) { + if (!isChrome()) { + bootbox.alert({title: "Visualization Error", message: "Tensorflow network visualization is only available for Google Chrome"}); + return; + } + } + var num_sel_gpus = 0 var sel_gpus = $("#select_gpus").val() if (sel_gpus) { diff --git a/digits/tools/tensorflow/gan_grid.py b/digits/tools/tensorflow/gan_grid.py index ddbe37071..a93e6c35b 100644 --- a/digits/tools/tensorflow/gan_grid.py +++ b/digits/tools/tensorflow/gan_grid.py @@ -22,13 +22,11 @@ import datetime import inspect -import json import logging import math import numpy as np import os import pickle -import time from six.moves import xrange # noqa import tensorflow as tf @@ -48,14 +46,11 @@ import tf_data import gandisplay - - # Constants TF_INTRA_OP_THREADS = 0 TF_INTER_OP_THREADS = 0 MIN_LOGS_PER_TRAIN_EPOCH = 8 # torch default: 8 - CELEBA_ALL_ATTRIBUTES = """ 5_o_Clock_Shadow Arched_Eyebrows Attractive Bags_Under_Eyes Bald Bangs Big_Lips Big_Nose Black_Hair Blond_Hair Blurry Brown_Hair Bushy_Eyebrows @@ -67,7 +62,8 @@ """.split() CELEBA_EDITABLE_ATTRIBUTES = [ - 'Bald', 'Black_Hair', 'Blond_Hair', 'Eyeglasses', 'Male', 'Mustache', 'Smiling', 'Young', 'Attractive', 'Pale_Skin', 'Big_Nose' + 'Bald', 'Black_Hair', 'Blond_Hair', 'Eyeglasses', 'Male', 'Mustache', + 'Smiling', 'Young', 'Attractive', 'Pale_Skin', 'Big_Nose' ] CELEBA_EDITABLE_ATTRIBUTES_IDS = [CELEBA_ALL_ATTRIBUTES.index(attr) for attr in CELEBA_EDITABLE_ATTRIBUTES] @@ -377,29 +373,18 @@ def Inference(sess, model): with open(FLAGS.attributes_file, 'rb') as f: attribute_zs = pickle.load(f) - while not False: # model.queue_coord.should_stop(): - + while not False: + # model.queue_coord.should_stop(): attributes = app.GetAttributes() - z = np.zeros(100) for idx, attr_scale in enumerate(attributes): - z += (attr_scale / 25. ) * attribute_zs[CELEBA_EDITABLE_ATTRIBUTES_IDS[idx]] + z += (attr_scale / 25) * attribute_zs[CELEBA_EDITABLE_ATTRIBUTES_IDS[idx]] feed_dict = {model.time_placeholder: float(t), model.attribute_placeholder: z} preds = sess.run(fetches=inference_op, feed_dict=feed_dict) - if FLAGS.visualize_inf: - save_weight_visualization(weight_vars, activation_ops, w, a) - - # @TODO(tzaman): error on no output? - #for i in range(len(keys)): - # # for j in range(len(preds)): - # # We're allowing multiple predictions per image here. DIGITS doesnt support that iirc - # logging.info('Predictions for image ' + str(model.dataloader.get_key_index(keys[i])) + - # ': ' + json.dumps(preds[i].tolist())) - #logging.info('Predictions shape: %s' % str(preds.shape)) app.DisplayCell(preds) t += 1e-5 * app.GetSpeed() * FLAGS.batch_size @@ -445,7 +430,7 @@ def l2_norm(x): return euclidean_norm def dot_product(x, y): - return tf.reduce_sum(tf.mul(x,y)) + return tf.reduce_sum(tf.mul(x, y)) def slerp(initial, final, progress): omega = tf.acos(dot_product(initial / l2_norm(initial), final / l2_norm(final))) @@ -480,6 +465,7 @@ def slerp(initial, final, progress): return batch, time_placeholder, attribute_placeholder + def main(_): # Always keep the cpu as default diff --git a/digits/tools/tensorflow/gandisplay.py b/digits/tools/tensorflow/gandisplay.py index 9f8d6d8df..3479ffa19 100644 --- a/digits/tools/tensorflow/gandisplay.py +++ b/digits/tools/tensorflow/gandisplay.py @@ -1,21 +1,18 @@ -import wxversion - -import wx -import numpy as np -import random import time - +import numpy as np +import wx # This has been set up to optionally use the wx.BufferedDC if # USE_BUFFERED_DC is True, it will be used. Otherwise, it uses the raw # wx.Memory DC , etc. -#USE_BUFFERED_DC = False +# USE_BUFFERED_DC = False USE_BUFFERED_DC = True myEVT = wx.NewEventType() DISPLAY_GRID_EVT = wx.PyEventBinder(myEVT, 1) + class MyEvent(wx.PyCommandEvent): """Event to signal that a count value is ready""" def __init__(self, etype, eid, value=None): @@ -30,6 +27,7 @@ def GetValue(self): """ return self._value + class BufferedWindow(wx.Window): """ @@ -62,11 +60,10 @@ def __init__(self, *args, **kwargs): self.paint_count = 0 def Draw(self, dc): - ## just here as a place holder. - ## This method should be over-ridden when subclassed + # just here as a place holder. + # This method should be over-ridden when subclassed pass - def OnPaint(self, event): # All that is needed here is to draw the buffer to screen if USE_BUFFERED_DC: @@ -75,11 +72,11 @@ def OnPaint(self, event): dc = wx.PaintDC(self) dc.DrawBitmap(self._Buffer, 0, 0) - def OnSize(self,event): + def OnSize(self, event): # The Buffer init is done here, to make sure the buffer is always # the same size as the Window - #Size = self.GetClientSizeTuple() - Size = self.ClientSize + # Size = self.GetClientSizeTuple() + Size = self.ClientSize # Make new offscreen bitmap: this bitmap will always have the # current drawing in it, so it can be used to save the image to @@ -88,9 +85,9 @@ def OnSize(self,event): self.UpdateDrawing() def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG): - ## This will save the contents of the buffer - ## to the specified file. See the wxWindows docs for - ## wx.Bitmap::SaveFile for the details + # This will save the contents of the buffer + # to the specified file. See the wxWindows docs for + # wx.Bitmap::SaveFile for the details self._Buffer.SaveFile(FileName, FileType) def UpdateDrawing(self): @@ -106,21 +103,21 @@ def UpdateDrawing(self): dc = wx.MemoryDC() dc.SelectObject(self._Buffer) self.Draw(dc) - del dc # need to get rid of the MemoryDC before Update() is called. + del dc # need to get rid of the MemoryDC before Update() is called. self.Refresh() self.Update() + class DrawWindow(BufferedWindow): def __init__(self, *args, **kwargs): - ## Any data the Draw() function needs must be initialized before - ## calling BufferedWindow.__init__, as it will call the Draw - ## function. + # Any data the Draw() function needs must be initialized before + # calling BufferedWindow.__init__, as it will call the Draw function. self.DrawData = {} BufferedWindow.__init__(self, *args, **kwargs) def Draw(self, dc): - dc.SetBackground( wx.Brush("White") ) - dc.Clear() # make sure you clear the bitmap! + dc.SetBackground(wx.Brush("White")) + dc.Clear() # make sure you clear the bitmap! # Here's the actual drawing code. for key, data in self.DrawData.items(): @@ -131,22 +128,24 @@ def Draw(self, dc): img_count = data.shape[0] height = data.shape[1] width = data.shape[2] - channels = data.shape[3] grid_size = int(np.sqrt(img_count)) size = (grid_size * width, grid_size * height) - if True: # self.size != size: + + if True: # self.size != size: self.size = size self.SetSize(size) - image = wx.EmptyImage(width,height) + image = wx.EmptyImage(width, height) + for i in xrange(img_count): x = width * (i // grid_size) y = height * (i % grid_size) s = data[i].tostring() image.SetData(s) - wxBitmap = image.ConvertToBitmap() # + + wxBitmap = image.ConvertToBitmap() dc.DrawBitmap(wxBitmap, x=x, y=y) @@ -158,11 +157,11 @@ class TestFrame(wx.Frame): def __init__(self, parent=None, grid_size=640, attributes=[]): wx.Frame.__init__(self, parent, - size = (grid_size + self.SLIDER_WIDTH + self.SLIDER_BORDER, grid_size + self.STATUS_HEIGHT), + size=(grid_size + self.SLIDER_WIDTH + self.SLIDER_BORDER, grid_size + self.STATUS_HEIGHT), title="GAN Demo", style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) - ## Set up the MenuBar + # Set up the MenuBar MenuBar = wx.MenuBar() file_menu = wx.Menu() @@ -185,8 +184,10 @@ def __init__(self, parent=None, grid_size=640, attributes=[]): # Sliders vbox = wx.BoxSizer(wx.VERTICAL) - self.speed_slider = wx.Slider(panel, -1, value=5, minValue=0, maxValue=10, pos=wx.DefaultPosition, size=(self.SLIDER_WIDTH, -1), - style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL | wx.SL_LABELS) + self.speed_slider = wx.Slider(panel, -1, value=5, minValue=0, maxValue=10, pos=wx.DefaultPosition, + size=(self.SLIDER_WIDTH, -1), + style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL | wx.SL_LABELS) + slider_text = wx.StaticText(panel, label='Speed') vbox.Add(slider_text, 0, wx.ALIGN_CENTRE) vbox.Add(self.speed_slider, 0, wx.ALIGN_CENTRE) @@ -194,8 +195,10 @@ def __init__(self, parent=None, grid_size=640, attributes=[]): self.attribute_sliders = [] for attribute in attributes: slider_text = wx.StaticText(panel, label=attribute) - slider = wx.Slider(panel, -1, value=0, minValue=-100, maxValue=100, pos=wx.DefaultPosition, size=(self.SLIDER_WIDTH, -1), - style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL | wx.SL_LABELS) + slider = wx.Slider(panel, -1, value=0, minValue=-100, maxValue=100, pos=wx.DefaultPosition, + size=(self.SLIDER_WIDTH, -1), + style=wx.SL_AUTOTICKS | wx.SL_HORIZONTAL | wx.SL_LABELS) + vbox.Add(slider_text, 0, wx.ALIGN_CENTRE) vbox.Add(slider, 0, wx.ALIGN_CENTRE) self.attribute_sliders.append(slider) @@ -221,7 +224,7 @@ def __init__(self, parent=None, grid_size=640, attributes=[]): self.Bind(DISPLAY_GRID_EVT, self.OnDisplayCell) - def OnQuit(self,event): + def OnQuit(self, event): self.Close(True) def OnDisplayCell(self, evt): @@ -236,6 +239,7 @@ def OnDisplayCell(self, evt): self.last_fps_update = time.time() self.last_frame_timestamp = time.time() + class DemoApp(wx.App): def __init__(self, arg, grid_size, attributes): diff --git a/digits/tools/tensorflow/main.py b/digits/tools/tensorflow/main.py index 93f0a8e09..5175efec1 100644 --- a/digits/tools/tensorflow/main.py +++ b/digits/tools/tensorflow/main.py @@ -493,7 +493,7 @@ def main(_): if FLAGS.validation_db: with tf.name_scope(digits.STAGE_VAL) as stage_scope: - val_model = Model(digits.STAGE_VAL, FLAGS.croplen, nclasses) + val_model = Model(digits.STAGE_VAL, FLAGS.croplen, nclasses, reuse_variable=True) val_model.create_dataloader(FLAGS.validation_db) val_model.dataloader.setup(FLAGS.validation_labels, False, @@ -544,7 +544,7 @@ def main(_): load_snapshot(sess, FLAGS.weights, tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES)) # Tensorboard: Merge all the summaries and write them out - writer = tf.train.SummaryWriter(os.path.join(FLAGS.summaries_dir, 'tb'), sess.graph) + writer = tf.summary.FileWriter(os.path.join(FLAGS.summaries_dir, 'tb'), sess.graph) # If we are inferencing, only do that. if FLAGS.inference_db: diff --git a/digits/tools/tensorflow/model.py b/digits/tools/tensorflow/model.py index ed9ac3bb2..0dc0ba804 100644 --- a/digits/tools/tensorflow/model.py +++ b/digits/tools/tensorflow/model.py @@ -54,13 +54,14 @@ def average_gradients(tower_grads): # Append on a 'tower' dimension which we will average over below. grads.append(expanded_g) # Average over the 'tower' dimension. - grad = tf.concat(0, grads) - grad = tf.reduce_mean(grad, 0) + grads_transformed = tf.concat(grads, 0) + grads_transformed = tf.reduce_mean(grads_transformed, 0) + # Keep in mind that the Variables are redundant because they are shared # across towers. So .. we will just return the first tower's pointer to # the Variable. v = grad_and_vars[0][1] - grad_and_var = (grad, v) + grad_and_var = (grads_transformed, v) average_grads.append(grad_and_var) return average_grads @@ -69,7 +70,7 @@ class Model(object): """ @TODO(tzaman) """ - def __init__(self, stage, croplen, nclasses, optimization=None, momentum=None): + def __init__(self, stage, croplen, nclasses, optimization=None, momentum=None, reuse_variable=False): self.stage = stage self.croplen = croplen self.nclasses = nclasses @@ -82,12 +83,13 @@ def __init__(self, stage, croplen, nclasses, optimization=None, momentum=None): self.summaries = [] self.towers = [] self._train = None + self._reuse = reuse_variable # Touch to initialize - if optimization: - self.learning_rate - self.global_step - self.optimizer + # if optimization: + # self.learning_rate + # self.global_step + # self.optimizer def create_dataloader(self, db_path): self.dataloader = tf_data.LoaderFactory.set_source(db_path, is_inference=(self.stage == digits.STAGE_INF)) @@ -126,9 +128,9 @@ def create_model(self, obj_UserModel, stage_scope, batch_x=None): else: with tf.name_scope('parallelize'): # Split them up - batch_x_split = tf.split(0, len(available_devices), batch_x, name='split_batch') + batch_x_split = tf.split(batch_x, len(available_devices), 0, name='split_batch') if self.stage != digits.STAGE_INF: # Has no labels - batch_y_split = tf.split(0, len(available_devices), batch_y, name='split_batch') + batch_y_split = tf.split(batch_y, len(available_devices), 0, name='split_batch') # Run the user model through the build_model function that should be filled in grad_towers = [] @@ -146,35 +148,35 @@ def create_model(self, obj_UserModel, stage_scope, batch_x=None): x=batch_x_split[dev_i], y=None) - with tf.variable_scope(digits.GraphKeys.MODEL, reuse=dev_i > 0): + with tf.variable_scope(digits.GraphKeys.MODEL, reuse=dev_i > 0 or self._reuse): tower_model.inference # touch to initialize - if self.stage == digits.STAGE_INF: - # For inferencing we will only use the inference part of the graph - continue + # Reuse the variables in this scope for the next tower/device + tf.get_variable_scope().reuse_variables() - with tf.name_scope(digits.GraphKeys.LOSS): - for loss in self.get_tower_losses(tower_model): - tf.add_to_collection(digits.GraphKeys.LOSSES, loss['loss']) + if self.stage == digits.STAGE_INF: + # For inferencing we will only use the inference part of the graph + continue - # Assemble all made within this scope so far. The user can add custom - # losses to the digits.GraphKeys.LOSSES collection - losses = tf.get_collection(digits.GraphKeys.LOSSES, scope=scope_tower) - losses += ops.get_collection(ops.GraphKeys.REGULARIZATION_LOSSES, scope=None) - tower_loss = tf.add_n(losses, name='loss') + with tf.name_scope(digits.GraphKeys.LOSS): + for loss in self.get_tower_losses(tower_model): + tf.add_to_collection(digits.GraphKeys.LOSSES, loss['loss']) - self.summaries.append(tf.scalar_summary(tower_loss.op.name, tower_loss)) + # Assemble all made within this scope so far. The user can add custom + # losses to the digits.GraphKeys.LOSSES collection + losses = tf.get_collection(digits.GraphKeys.LOSSES, scope=scope_tower) + losses += ops.get_collection(ops.GraphKeys.REGULARIZATION_LOSSES, scope=None) + tower_loss = tf.add_n(losses, name='loss') - # Reuse the variables in this scope for the next tower/device - tf.get_variable_scope().reuse_variables() + self.summaries.append(tf.summary.scalar(tower_loss.op.name, tower_loss)) - if self.stage == digits.STAGE_TRAIN: - grad_tower_losses = [] - for loss in self.get_tower_losses(tower_model): - grad_tower_loss = self.optimizer.compute_gradients(loss['loss'], loss['vars']) - grad_tower_loss = tower_model.gradientUpdate(grad_tower_loss) - grad_tower_losses.append(grad_tower_loss) - grad_towers.append(grad_tower_losses) + if self.stage == digits.STAGE_TRAIN: + grad_tower_losses = [] + for loss in self.get_tower_losses(tower_model): + grad_tower_loss = self.optimizer.compute_gradients(loss['loss'], loss['vars']) + grad_tower_loss = tower_model.gradientUpdate(grad_tower_loss) + grad_tower_losses.append(grad_tower_loss) + grad_towers.append(grad_tower_losses) # Assemble and average the gradients from all towers if self.stage == digits.STAGE_TRAIN: @@ -235,7 +237,7 @@ def summary(self): if not len(self.summaries): logging.error("No summaries defined. Please define at least one summary.") exit(-1) - return tf.merge_summary(self.summaries) + return tf.summary.merge(self.summaries) @model_property def global_step(self): @@ -250,7 +252,7 @@ def learning_rate(self): # define it entirely in tf ops, instead of a placeholder and feeding. with tf.device('/cpu:0'): lr = tf.placeholder(tf.float32, shape=[], name='learning_rate') - self.summaries.append(tf.scalar_summary('lr', lr)) + self.summaries.append(tf.summary.scalar('lr', lr)) return lr @model_property @@ -283,9 +285,10 @@ def get_tower_losses(self, tower): """ Return list of losses - If user-defined model returns only one loss then this is encapsulated into the expected list of - dicts structure + If user-defined model returns only one loss then this is encapsulated into + the expected list of dicts structure """ + if isinstance(tower.loss, list): return tower.loss else: diff --git a/digits/tools/tensorflow/tf_data.py b/digits/tools/tensorflow/tf_data.py index a2071bd62..f0775ba58 100644 --- a/digits/tools/tensorflow/tf_data.py +++ b/digits/tools/tensorflow/tf_data.py @@ -320,9 +320,8 @@ def create_input_pipeline(self): with tf.name_scope('mean_subtraction'): single_data = self.mean_loader.subtract_mean_op(single_data) if LOG_MEAN_FILE: - self.summaries.append(tf.image_summary('mean_image', - tf.expand_dims(self.mean_loader.tf_mean_image, 0), - max_images=1)) + expanded_data = tf.expand_dims(self.mean_loader.tf_mean_image, 0) + self.summaries.append(tf.summary.image('mean_image', expanded_data, max_outputs=1)) # (Random) Cropping if self.croplen: @@ -397,8 +396,7 @@ def create_input_pipeline(self): shapes=[[0], self.get_shape(), []], # Only makes sense is dynamic_pad=False #@TODO(tzaman) - FIXME min_after_dequeue=5*self.batch_size, allow_smaller_final_batch=True, # Happens if total%batch_size!=0 - name='batcher' - ) + name='batcher') else: batch = tf.train.batch( single_batch, @@ -409,8 +407,7 @@ def create_input_pipeline(self): num_threads=NUM_THREADS_DATA_LOADER if not self.is_inference else 1, capacity=max_queue_capacity, # Max amount that will be loaded and queued allow_smaller_final_batch=True, # Happens if total%batch_size!=0 - name='batcher', - ) + name='batcher') self.batch_k = batch[0] # Key self.batch_x = batch[1] # Input @@ -849,6 +846,7 @@ def __del__(self): for db in self.h5dbs: db.close() + class GanGridLoader(LoaderFactory): """ The GanGridLoader generates data for a GAN. diff --git a/digits/tools/tensorflow/utils.py b/digits/tools/tensorflow/utils.py index 495a755d0..3e927e12f 100644 --- a/digits/tools/tensorflow/utils.py +++ b/digits/tools/tensorflow/utils.py @@ -43,7 +43,7 @@ def classification_loss(pred, y): """ Definition of the loss for regular classification """ - ssoftmax = tf.nn.sparse_softmax_cross_entropy_with_logits(pred, y, name='cross_entropy_single') + ssoftmax = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=pred, labels=y, name='cross_entropy_single') return tf.reduce_mean(ssoftmax, name='cross_entropy_batch') @@ -55,7 +55,7 @@ def constrastive_loss(lhs, rhs, y, margin=1.0): """ Contrastive loss confirming to the Caffe definition """ - d = tf.reduce_sum(tf.square(tf.sub(lhs, rhs)), 1) + d = tf.reduce_sum(tf.square(tf.subtract(lhs, rhs)), 1) d_sqrt = tf.sqrt(1e-6 + d) loss = (y * d) + ((1 - y) * tf.square(tf.maximum(margin - d_sqrt, 0))) return tf.reduce_mean(loss) # Note: constant component removed (/2) @@ -92,11 +92,11 @@ def chw_to_hwc(x): def bgr_to_rgb(x): - return tf.reverse(x, [False, False, True]) + return tf.reverse(x, [2]) def rgb_to_bgr(x): - return tf.reverse(x, [False, False, True]) + return tf.reverse(x, [2]) def get_available_gpus(): diff --git a/docs/BuildDigits.md b/docs/BuildDigits.md index 2bc6c4f8d..e1e12241a 100644 --- a/docs/BuildDigits.md +++ b/docs/BuildDigits.md @@ -72,6 +72,10 @@ optional arguments: Now that you're up and running, check out the [Getting Started Guide](GettingStarted.md). +# Development + +If you are interested in developing for DIGITS or work with its source code, check out the [Development Setup Guide](DevelopmentSetup.md) + ## Troubleshooting Most configuration options should have appropriate defaults. diff --git a/docs/BuildTensorflow.md b/docs/BuildTensorflow.md index 292a94279..33e22a2d2 100644 --- a/docs/BuildTensorflow.md +++ b/docs/BuildTensorflow.md @@ -1,19 +1,48 @@ -# Building Torch +# Installing TensorFlow -DIGITS now supports Tensorflow as an optional alternative backend to Caffe or Torch. +DIGITS now supports TensorFlow as an optional alternative backend to Caffe or Torch. -> NOTE: Tensorflow support is still experimental! +> NOTE: TensorFlow support is still experimental! -@TODO +We recommend installing TensorFlow in a fixed and separate environment. This is because TensorFlow support is still in development and stability is not ensured. -Table of Contents -================= -* @TODO +Installation for [Ubuntu](https://www.tensorflow.org/install/install_linux#installing_with_virtualenv) -We recommend installing Tensorflow in a fixed and separate environment. +Installation for [Mac](https://www.tensorflow.org/install/install_mac#installing_with_virtualenv) -Installation per: https://www.tensorflow.org/versions/r0.10/get_started/os_setup.html#virtualenv-installation +## Requirements -## Prerequisites +DIGITS is current targetting tensorflow-gpu V1.1. -@TODO +TensorFlow for DIGITS requires one or more NVIDIA GPUs with CUDA Compute Capbility of 3.0 or higher. See [the official GPU support list](https://developer.nvidia.com/cuda-gpus) to see if your GPU supports it. + +Along with that requirement, the following should be installed + +* One or more NVIDIA GPUs ([details](InstallCuda.md#gpu)) +* An NVIDIA driver ([details and installation instructions](InstallCuda.md#driver)) +* A CUDA toolkit ([details and installation instructions](InstallCuda.md#cuda-toolkit)) +* cuDNN 5.1 ([download page](https://developer.nvidia.com/cudnn)) + +### A Note About cuDNN and TensorFlow +Currently tensorflow v1.1 targets cuDNN 5.1. The latest cuDNN version is 6. **To have tensorflow running in digits, you must have cuDNN 5.1 installed. Currently, cuDNN 6 is incompatiable with tensorflow.** To install it, use the following command in a terminal + +``` +sudo apt-get install libcudnn5 +``` + + +## Installation + +These instructions are based on [the official TensorFlow instructions] +(https://www.tensorflow.org/versions/master/install/) + +TensorFlow comes with pip, to install it, just simply use the command +``` +pip install tensorflow-gpu=1.2.0 +``` + +TensorFlow should then install effortlessly and pull in all its required dependices. + +## Getting Started With TensorFlow In DIGITS + +Follow [these instructions](GettingStartedTensorflow.md) for information on getting started with TensorFlow in DIGITS diff --git a/docs/BuildTorch.md b/docs/BuildTorch.md index d59b8031a..b354b1eae 100644 --- a/docs/BuildTorch.md +++ b/docs/BuildTorch.md @@ -16,7 +16,7 @@ Install some dependencies with Deb packages: sudo apt-get install --no-install-recommends git software-properties-common ``` -## Basic install +## Basic Installation These instructions are based on [the official Torch instructions](http://torch.ch/docs/getting-started.html). ```sh diff --git a/docs/DevelopmentSetup.md b/docs/DevelopmentSetup.md new file mode 100644 index 000000000..a22b5a2b3 --- /dev/null +++ b/docs/DevelopmentSetup.md @@ -0,0 +1,36 @@ +# Development + +The source code for DIGITS is available on [github](https://github.com/NVIDIA/DIGITS). + +To have access to your local machine, you may clone from the github repository with +``` +git clone https://github.com/NVIDIA/DIGITS.git +``` +Or you may download the source code as a zip file from the github website. + +## Running DIGITS in Development + +DIGITS comes with the script to run for a development server. +To run the development server, use +``` +./digits-devserver +``` + +## Running unit tests for DIGITS + +To successfully run all the unit tests, the following plugins have to be installed +``` +sudo pip install -e ./plugins/data/imageGradients +sudo pip install -e ./plugins/view/imageGradients +sudo pip install -r ./requirements_test.txt +``` + +To run all the tests for DIGITS, use +``` +./digits-test +``` + +If you would like to have a verbose output with the name of the tests, use +``` +./digits-test -v +``` \ No newline at end of file diff --git a/docs/GettingStartedTensorflow.md b/docs/GettingStartedTensorflow.md new file mode 100644 index 000000000..3e161b03c --- /dev/null +++ b/docs/GettingStartedTensorflow.md @@ -0,0 +1,244 @@ +# Getting Started with TensorFlowâ„¢ in DIGITS + +Table of Contents +================= +* [Enabling Support For TensorFlow In DIGITS](#enabling-support-for-tensorflow-in-digits) +* [Selecting TensorFlow When Creating A Model In DIGITS](#selecting-tensorflow-when-creating-a-model-in-digits) +* [Defining A TensorFlow Model In DIGITS](#defining-a-tensorflow-model-in-digits) + * [Provided Properties](#provided-properties) + * [Internal Properties](#internal-properties) + * [Tensors](#tensors) +* [Other TensorFlow Tools in DIGITS](#other-tensorflow-tools-in-digits) + * [Provided Helpful Functions](#provided-helpful-functions) + * [Visualization With TensorBoard](#visualization-with-tensorboard) +* [Examples](#examples) + * [Simple Auto-Encoder Network](#simple-auto-encoder-network) + * [Freezing Variables in Pre-Trained Models by Renaming](#freezing-variables-in-pre-trained-models-by-renaming) + * [Multi-GPU Training](#multi-gpu-training) + +## Enabling Support For TensorFlow In DIGITS + +DIGITS will automatically enable support for TensorFlow if it detects that TensorFlow-gpu is installed in the system. This is done by a line of python code that attempts to ```import tensorflow``` to see if it actually imports. + +If DIGITS cannot enable tensorflow, a message will be printed in the console saying: ```TensorFlow support is disabled``` + +## Selecting TensorFlow When Creating A Model In DIGITS + +Click on the "TensorFlow" tab on the model creation page + +![Select TensorFlow](images/Select_TensorFlow.png) + +## Defining A TensorFlow Model In DIGITS + +To define a TensorFlow model in DIGITS, you need to write a python class that follows this basic template + +```python +class UserModel(Tower): + + @model_propertyOther TensorFlow Tools in DIGITS + def inference(self): + # Your code here + return model + + @model_property#with tf.variable_scope(digits.GraphKeys.MODEL, reuse=None): + def loss(self): + # Your code here + return loss +``` + +For example, this is what it looks like for [LeNet-5](http://yann.lecun.com/exdb/lenet/), a model that was created for the classification of hand written digits by Yann Lecun: + +```python +class UserModel(Tower): + + @model_property + def inference(self): + x = tf.reshape(self.x, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + # scale (divide by MNIST std) + x = x * 0.0125 + with slim.arg_scope([slim.conv2d, slim.fully_connected], + weights_initializer=tf.contrib.layers.xavier_initializer(), + weights_regularizer=slim.l2_regularizer(0.0005) ): + model = slim.conv2d(x, 20, [5, 5], padding='VALID', scope='conv1') + model = slim.max_pool2d(model, [2, 2], padding='VALID', scope='pool1') + model = slim.conv2d(model, 50, [5, 5], padding='VALID', scope='conv2') + model = slim.max_pool2d(model, [2, 2], padding='VALID', scope='pool2') + model = slim.flatten(model) + model = slim.fully_connected(model, 500, scope='fc1') + model = slim.dropout(model, 0.5, is_training=self.is_training, scope='do1') + model = slim.fully_connected(model, self.nclasses, activation_fn=None, scope='fc2') + return model + + @model_property + def loss(self): + loss = digits.classification_loss(self.inference, self.y) + accuracy = digits.classification_accuracy(self.inference, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) + return loss +``` + +The properties ```inference``` and ```loss``` must be defined and the class must be called ```UserModel``` and it must inherit ```Tower```. This is how DIGITS will interact with the python code. + +### Provided Properties + +Properties that are accessible through ```self``` + +Property name | Type | Description +--------------|-----------|------------ +`nclasses` | number | Number of classes (for classification datasets). For other type of datasets, this is undefined +`input_shape` | Tensor | Shape (1D Tensor) of the first input Tensor. For image data, this is set to height, width, and channels accessible by [0], [1], and [2] respectively. +`is_training` | boolean | Whether this is a training graph +`is_inference` | boolean | Whether this graph is created for inference/testing +`x` | Tensor | The input node, with the shape of [N, H, W, C] +`y` | Tensor | The label, [N] for scalar labels, [N, H, W, C] otherwise. Defined only if self.is_training is True + +### Internal Properties + +These properties are in the `UserModel` class written by the user + +Property name | Return Type | Description +--------------|-------------|------------ +`__init()__` | None | The constructor for the `UserModel` class +`inference()` | Tensor | Called during training and inference +`loss()` | Tensor | Called during training to determine the loss and variables to train + +### Tensors + +The network are fed with TensorFlow Tensor objects that are in [N, H, W, C] format. + +## Other TensorFlow Tools in DIGITS + +DIGITS provides a few useful tools to help with your development with TensorFlow. + +### Provided Helpful Functions + +DIGITS provides a few helpful functions to help you with creating the model. Here are the functions we provide inside the `digits` class + +Function Name | Parameters | Description +--------------------|---------------------|------------- +`classification_loss` | pred - the images to be classified
    y - the labels | Used for classification training to calculate the loss of image classification +`mse_loss` | lhs - left hand tensor
    rhs - right hand tensor | Used for calculating the mean square loss between 2 tensors +`constrastive_loss` | lhs - left hand tensor
    rhs - right hand tensor
    y - `labels` | Calculates the contrastive loss with respect to the Caffe definition +`classification_accuracy` | pred - the image to be classified
    y - the labels | Used to measure how accurate the classification task is +`nhwc_to_nchw` | x - the tensor to transpose | Transpose the tensor that was originally NHWC format to NCHW. The tensor must be a degree of 4 +`nchw_to_nhwc` | x - the tensor to transpose | Transpose the tensor that was originally NCHW format to NHWC. The tensor must be a degree of 4 +`hwc_to_chw` | x - the tensor to transpose | Transpose the tensor that was originally HWC format to CHW. The tensor must be a degree of 3 +`chw_to_hwc` | x - the tensor to transpose | Transpose the tensor that was originally CHW format to HWC. The tensor must be a degree of 3 +`bgr_to_rgb` | x - the tensor to transform | Transform the tensor that was originally in BGR channels to RGB. +`rgb_to_bgr` | x - the tensor to transform | Transform the tensor that was originally in RGB channels to BGR. + +### Visualization With TensorBoard + +![TensorBoard](images/TensorBoard.png) + +TensorBoard is a visualization tools provided by TensorFlow to see the graph of your neural network. DIGITS provides easy access to TensorBoard network visualization for your network while creating it. This can be accessed by clicking on the ```Visualize``` button under ```Custom Network``` as seen in the image below. + +![Visualize TensorBoard](images/visualize_button.png) + +If there is something wrong with the network model, DIGITS will automatically provide with you the stacktrace and the error message to help you understand where the problem is. + +You can also spin up the full Tensorboard server while your model is training with the command +``` +$ tensorboard --logdir /tb/ +``` +where `` is the directory where them model is being trained at, which can be found here: + +![Job Dir](images/job-dir.png) + +Afterwards, you can open up the Tensorboard page by going to +```http://localhost:6006``` + +Or you can click the ```Tensorboard``` link under Visualization + +![Visualize Button](images/visualize-btn.png) + +To know more about how TensorBoard works, its official documentation is availabile in the [official tensorflow documentaton](https://www.tensorflow.org/get_started/summaries_and_tensorboard) + +## Examples + +### Simple Auto-Encoder Network + +The following network is a simple auto encoder to demostate the structure of how to use tensorflow in DIGITS. An auto encoder is a 2 part network that basically acts as a compression mechanism. The first part will try to compress an image to a size smaller than original while the second part will try to decompress the compressed representation created by the compression network. + +```python +class UserModel(Tower): + + @model_property + def inference(self): + + # the order for input shape is [0] -> H, [1] -> W, [2] -> C + # this is because tensorflow's default order is NHWC + model = tf.reshape(self.x, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + image_dim = self.input_shape[0] * self.input_shape[1] + + with slim.arg_scope([slim.fully_connected], + weights_initializer=tf.contrib.layers.xavier_initializer(), + weights_regularizer=slim.l2_regularizer(0.0005)): + + # first we reshape the images to something + model = tf.reshape(_x, shape=[-1, image_dim]) + + # encode the image + model = slim.fully_connected(model, 300, scope='fc1') + model = slim.fully_connected(model, 50, scope='fc2') + + # decode the image + model = slim.fully_connected(model, 300, scope='fc3') + model = slim.fully_connected(model, image_dim, activation_fn=None, scope='fc4') + + # form it back to the original + model = tf.reshape(model, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + + return model + + @model_property + def loss(self): + + # In an autoencoder, we compare the encoded and then decoded image with the original + original = tf.reshape(self.x, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + + # self.inference is called to get the processed image + model = self.inference + loss = digits.mse_loss(original, model) + + return loss +``` + +### Freezing Variables in Pre-Trained Models by Renaming + +The following is a demonstration of how to specifying which weights we would like to use for training. This works best if we are using a pre-trained model. This is applicable for fine tuning a model. + +When you originally train a model, tensorflow will save the variables with their specified names. When you reload the model to retrain it, tensorflow will simutainously reload all those variables and mark them available to retrain if they are specified in the model definition. When you change the name of the variables in the model, tensorflow will then know to not train that variable and thus "freezes" it. + +```python +class UserModel(Tower): + + @model_property + def inference(self): + + model = construct_model() + """code to construct the network omitted""" + + # assuming the original model have weight2 and bias2 variables + # in here, we renamed them by adding the suffix _not_in_use + # this tells TensorFlow that these variables in the pre-trained model should + # not be retrained and it should be frozen + # If we would like to freeze a weight, all we have to do is just rename it + self.weights = { + 'weight1': tf.get_variable('weight1', [5, 5, self.input_shape[2], 20], initializer=tf.contrib.layers.xavier_initializer()), + 'weight2': tf.get_variable('weight2_not_in_use', [5, 5, 20, 50], initializer=tf.contrib.layers.xavier_initializer()) + } + + self.biases = { + 'bias1': tf.get_variable('bias1', [20], initializer=tf.constant_initializer(0.0)), + 'bias2': tf.get_variable('bias2_not_in_use', [50], initializer=tf.constant_initializer(0.0)) + } + + return model + + @model_property + def loss(self): + loss = calculate_loss() + """code to calculate loss omitted""" + return loss +``` diff --git a/docs/GettingStartedTorch.md b/docs/GettingStartedTorch.md index b1bb1703b..8eae62386 100644 --- a/docs/GettingStartedTorch.md +++ b/docs/GettingStartedTorch.md @@ -14,9 +14,6 @@ Table of Contents * [Supervised Regression Learning](#supervised-regression-learning) * [Command Line Inference](#command-line-inference) * [Multi-GPU training](#multi-gpu-training) -* [Tutorials](#tutorials) - * [Training an autoencoder](#training-an-autoencoder) - * [Training a regression model](#training-a-regression-model) ## Enabling support for Torch7 in DIGITS diff --git a/docs/ModelStore.md b/docs/ModelStore.md index 818e72dee..9ecffc023 100644 --- a/docs/ModelStore.md +++ b/docs/ModelStore.md @@ -1,6 +1,22 @@ # Model Store ## Introduction +Model Store lists models in user-specified servers. +Users can imports models from Model Store into DIGITS. + + +## Setting up environment variable +The configuration of Model Store requires one environment variable DIGITS_MODEL_STORE_URL to be set. +NVIDIA plans to publish one public Model Store at http://developer.download.nvidia.com/compute/machine-learning/modelstore/5.0. +You can set up the environment variable with that url before launching DIGITS. +For example, run the following command in your Bash shell. +``` shell +export DIGITS_MODEL_STORE_URL='http://developer.download.nvidia.com/compute/machine-learning/modelstore/5.0' +``` +If multiple model stores are available, specify their url's, separated by the comma (,). +``` shell +export DIGITS_MODEL_STORE_URL='http://localhost/mymodelstore,http://dlserver/teammodelstore' +``` DIGITS 5.0 introduces the concept of a "model store," which is a collection of trained models that can be used as pre-trained weights to accelerate training convergence. A DIGITS server can be configured to connect to one or more model stores to download these trained models from the store to the server. diff --git a/docs/images/Select_TensorFlow.png b/docs/images/Select_TensorFlow.png new file mode 100644 index 0000000000000000000000000000000000000000..cd2859a16b360ee26372d7ec383e99917b55fed1 GIT binary patch literal 36045 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⪚;#2y#CObaA$?az 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 WLf!8eJQq#;F?nfasX_^Z$NvYAKo$i6 literal 0 HcmV?d00001 diff --git a/docs/images/job-dir.png b/docs/images/job-dir.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b748770493f3ab756c6458eaff5a457c74c64a GIT binary patch literal 29935 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 literal 0 HcmV?d00001 diff --git a/docs/images/visualize-btn.png b/docs/images/visualize-btn.png new file mode 100644 index 0000000000000000000000000000000000000000..80a3326065790fa59313950f5b9442e69100aa77 GIT binary patch literal 46115 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

    s?m}r~u`huW=f;SL+8k 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 OHn~sA(lt`1A^!&`^hh26 literal 0 HcmV?d00001 diff --git a/docs/images/visualize_button.png b/docs/images/visualize_button.png new file mode 100644 index 0000000000000000000000000000000000000000..dae2c4c8e6abd81cd6e02ac5603c471f84cfe2e8 GIT binary patch literal 15731 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 literal 0 HcmV?d00001 diff --git a/examples/autoencoder/README.md b/examples/autoencoder/README.md index da46652d2..7a6e7963e 100644 --- a/examples/autoencoder/README.md +++ b/examples/autoencoder/README.md @@ -1,10 +1,11 @@ -# Training an image autoencoder with DIGITS and Torch7 +# Training an image autoencoder with DIGITS Table of Contents ================= * [Introduction](#introduction) * [Dataset creation](#dataset-creation) -* [Model definition](#model-creation) +* [Model definition (Torch)](#model-creation-torch) +* [Model definition (Tensorflow)](#model-creation-tensorflow) * [Verification](#verification) ## Introduction @@ -34,7 +35,7 @@ In the generic dataset creation form you need to provide the paths to the train ![Create generic dataset form](create-generic-dataset-form.png) -## Model creation +## Model creation (Torch) Now that you have a generic dataset to train on, you may create a generic model by clicking on `New Model\Images\Other` on the main page: @@ -44,7 +45,7 @@ Select the dataset you just created and under the `Custom network` tab, select ` ```lua local autoencoder = nn.Sequential() autoencoder:add(nn.MulConstant(0.00390625)) -autoencoder:add(nn.View(-1):setNumInputDims(3)) -- 1*28*8 -> 784 +autoencoder:add(nn.View(-1):setNumInputDims(3)) -- 1*28*28 -> 784 autoencoder:add(nn.Linear(784,300)) autoencoder:add(nn.ReLU()) autoencoder:add(nn.Linear(300,50)) @@ -90,6 +91,44 @@ After training for 30 epochs the loss function should look similar to this: ![Training loss](training-loss.png) +## Model creation (Tensorflow) + +The following example was made using TensorFlow-Slim. However you can also do this in vanilla Tensorflow and Keras + +Select the dataset you just created and under the `Custom network` tab, select `Tensorflow`. There you can paste the following network definition: +```python +# Tensorflow MNIST autoencoder model using TensorFlow-Slim +# The format of the data in this example is: batch_size * height * width * channel +class UserModel(Tower): + + @model_property + def inference(self): + + with slim.arg_scope([slim.fully_connected], + weights_initializer=tf.contrib.layers.xavier_initializer(), + weights_regularizer=slim.l2_regularizer(0.0005) ): + const = tf.constant(0.00390625) + model = tf.multiply(self.x, const) + model = tf.reshape(model, shape=[-1, 784]) # equivalent to `model = slim.flatten(_x)` + model = slim.fully_connected(model, 300, scope='fc1') + model = slim.fully_connected(model, 50, scope='fc2') + model = slim.fully_connected(model, 300, scope='fc3') + model = slim.fully_connected(model, 784, activation_fn=None, scope='fc4') + model = tf.reshape(model, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + + # The below image summary makes it very easy to review your result + tf.summary.image(self.x.op.name, self.x, max_outputs=5, collections=['summaries']) + tf.summary.image(model.op.name, model, max_outputs=5, collections=['summaries']) + + return model + + @model_property + def loss(self): + return digits.mse_loss(self.inference, self.x) +``` + +The result from running the Tensorflow example should produce results that are similar to Torch. + ## Verification Now we can assess the quality of the autoencoder. On the model page, select an image from the MNIST test set (one that the network has never seen during training) and diff --git a/digits/standard-networks/tensorflow/autoencoder.py b/examples/autoencoder/autoencoder-TF.py similarity index 100% rename from digits/standard-networks/tensorflow/autoencoder.py rename to examples/autoencoder/autoencoder-TF.py diff --git a/examples/binary-segmentation/README.md b/examples/binary-segmentation/README.md index a2c46aaea..322accba7 100644 --- a/examples/binary-segmentation/README.md +++ b/examples/binary-segmentation/README.md @@ -61,7 +61,7 @@ Set the number of training epochs to `150`. ### Using Caffe Select the `Custom network` tab then click `Caffe`. -In the text area copy/paste the contents of this [Caffe model](segmentation-model.prototxt). +In the text area, copy/paste the contents of this [Caffe model](segmentation-model.prototxt). Set the base learning rate to `1e-7`. You may click `Visualize` to review the network topology: @@ -71,9 +71,15 @@ You may click `Visualize` to review the network topology: ### Using Torch7 Select the `Custom network` tab then click `Torch`. -In the text area copy/paste the contents of this [Torch model](segmentation-model.lua). +In the text area, copy/paste the contents of this [Torch model](segmentation-model.lua). Set the base learning rate to `0.001`. +### Using Tensorflow + +Select the `Custom netowrk` tab then click `Tensorflow`, +In the text area, copy/paste the contents of this [Tensorflow model](binary_segmentation-TF.py). +Set the base learning rate to `1e-5` + ### Some words on the model The proposed network is a simple Fully Convolutional Network (FCN). @@ -99,6 +105,8 @@ The `create_images.py` script created many images we can use for testing. You may also run the command again to produce new images. To help visualize the output of the network, select the `Image` visualization method and set `Pixel Conversion` to `clip`: +For Tensorflow only, select `HWC` for data order + ![select visualization](select-visualization.png) You can test images individually with `Test One`: diff --git a/examples/binary-segmentation/binary_segmentation-TF.py b/examples/binary-segmentation/binary_segmentation-TF.py new file mode 100644 index 000000000..63e57fb3d --- /dev/null +++ b/examples/binary-segmentation/binary_segmentation-TF.py @@ -0,0 +1,22 @@ +# Tensorflow Triangle binary segmentation model using TensorFlow-Slim +class UserModel(Tower): + + @model_property + def inference(self): + _x = tf.reshape(self.x, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + with slim.arg_scope([slim.conv2d, slim.conv2d_transpose], + weights_initializer=tf.contrib.layers.xavier_initializer(), + weights_regularizer=slim.l2_regularizer(0.05) ): + + model = slim.conv2d(_x, 32, [3, 3], padding='SAME', scope='conv1') # 1*H*W -> 32*H*W + model = slim.conv2d(model, 1024, [16, 16], padding='VALID', scope='conv2', stride=16) # 32*H*W -> 1024*H/16*W/16 + model = slim.conv2d_transpose(model, self.input_shape[2], [16, 16], stride=16, padding='VALID', activation_fn=None, scope='deconv_1') + return model + + @model_property + def loss(self): + y = tf.reshape(self.y, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + + # For a fancy tensorboard summary: put the input, label and model side by side (sbs) for a fancy image summary: + # tf.summary.image(sbs.op.name, sbs, max_outputs=3, collections=["training summary"]) + return digits.mse_loss(self.inference, y) diff --git a/examples/binary-segmentation/segmentation-model.lua b/examples/binary-segmentation/segmentation-model.lua index 167795066..6f42b08d6 100644 --- a/examples/binary-segmentation/segmentation-model.lua +++ b/examples/binary-segmentation/segmentation-model.lua @@ -11,7 +11,7 @@ return function(params) end if pcall(function() require('cudnn') end) then - print('Using CuDNN backend') + --print('Using CuDNN backend') backend = cudnn convLayer = cudnn.SpatialConvolution convLayerName = 'cudnn.SpatialConvolution' diff --git a/examples/fine-tuning/README.md b/examples/fine-tuning/README.md index 6ae1aeca2..1864e82ae 100644 --- a/examples/fine-tuning/README.md +++ b/examples/fine-tuning/README.md @@ -70,6 +70,12 @@ The description is similar to the standard LeNet model except for the following - the `fineTuneHook` hook sets the `accGradParameters` fields of the original layers to `nil` in order to keep the existing features unchanged - the `fineTuneHook` adds a `nn.Linear(10, 2)` layer on top of the network +### Using Tensorflow + +In the `Custom network` text area, paste the contents of this [python file](lenet-fine-tune-tf.py). +The description is similar to the standard LeNet model except for the following differences: +- the weight and bias with the name `out` had their tensorflow variable renamed to `wout_not_in_use` and `bout_not_in_use`. This signifies that these weights will not be used and the original weights are frozen + ## Verification Now if you give your network a name and click `Create` the loss function should go down very sharply and the validation accuracy should exceed 90%: diff --git a/examples/fine-tuning/lenet-fine-tune-tf.py b/examples/fine-tuning/lenet-fine-tune-tf.py new file mode 100644 index 000000000..ef7b3f5e8 --- /dev/null +++ b/examples/fine-tuning/lenet-fine-tune-tf.py @@ -0,0 +1,84 @@ +class UserModel(Tower): + + @model_property + def inference(self): + + # Create some wrappers for simplicity + def conv2d(x, W, b, s, name, padding='SAME'): + # Conv2D wrapper, with bias and relu activation + x = tf.nn.conv2d(x, W, strides=[1, s, s, 1], padding=padding, name=name + '_conv2d') + x = tf.nn.bias_add(x, b, name=name + 'bias_add') + return tf.nn.relu(x) + + def maxpool2d(x, k, s, name, padding='VALID'): + # MaxPool2D wrapper + return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, s, s, 1], padding=padding, name=name + '_maxpool2d') + + # Create model + def conv_net(x, weights, biases): + # scale (divide by MNIST std) + x = x * 0.0125 + + # Convolution Layer + conv1 = conv2d(x, weights['wc1'], biases['bc1'], s=1, name='CONV1', padding='VALID') + # Max Pooling (down-sampling) + conv1 = maxpool2d(conv1, k=2, s=2, name='CONV1', padding='VALID') + + # Convolution Layer + conv2 = conv2d(conv1, weights['wc2'], biases['bc2'], s=1, name='CONV2', padding='VALID') + # Max Pooling (down-sampling) + conv2 = maxpool2d(conv2, k=2, s=2, name='CONV2', padding='VALID') + + # Fully connected layer + # Reshape conv2 output to fit fully connected layer input + fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]], name="FC1_reshape") + fc1 = tf.add(tf.matmul(fc1, weights['wd1'], name='FC1_multi'), biases['bd1'], name='FC1_add') + fc1 = tf.nn.relu(fc1, name='FC1_relu') + + # Apply Dropout + if self.is_training: + fc1 = tf.nn.dropout(fc1, 0.5, name='FC1_drop') + + # Output, class prediction + out = tf.add(tf.matmul(fc1, weights['out'], name='OUT_multi'), biases['out'], name='OUT_add') + + true_out = tf.add(tf.matmul(out, weights['true_out'], name='OUT_multi'), biases['true_out'], name='TRUE_OUT_add') + return true_out + + # Store layers weight & bias + weights = { + # 5x5 conv, 1 input, 20 outputs + 'wc1': tf.get_variable('wc1', [5, 5, self.input_shape[2], 20], initializer=tf.contrib.layers.xavier_initializer()), + # 5x5 conv, 20 inputs, 50 outputs + 'wc2': tf.get_variable('wc2', [5, 5, 20, 50], initializer=tf.contrib.layers.xavier_initializer()), + # fully connected, 4*4*16=800 inputs, 500 outputs + 'wd1': tf.get_variable('wd1', [4*4*50, 500], initializer=tf.contrib.layers.xavier_initializer()), + # 500 inputs, 10 outputs (class prediction) + 'out': tf.get_variable('wout_not_in_use', [500, 10], initializer=tf.contrib.layers.xavier_initializer()), + # adjust from 10 classes to 2 output + 'true_out': tf.get_variable('twout', [10, 2], initializer=tf.contrib.layers.xavier_initializer()) + } + + self.weights = weights + + # Leave the intial biases zero + biases = { + 'bc1': tf.get_variable('bc1', [20], initializer=tf.constant_initializer(0.0)), + 'bc2': tf.get_variable('bc2', [50], initializer=tf.constant_initializer(0.0)), + 'bd1': tf.get_variable('bd1', [500], initializer=tf.constant_initializer(0.0)), + 'out': tf.get_variable('bout_not_in_use', [10], initializer=tf.constant_initializer(0.0)), + 'true_out': tf.get_variable('tbout', [2], initializer=tf.constant_initializer(0.0)) + } + + self.biases = biases + + model = conv_net(self.x, weights, biases) + return model + + @model_property + def loss(self): + loss = digits.classification_loss(self.inference, self.y) + accuracy = digits.classification_accuracy(self.inference, self.y) + self.summaries.append(tf.summary.scalar(accuracy.op.name, accuracy)) + return loss + \ No newline at end of file diff --git a/examples/fine-tuning/lenet-fine-tune.lua b/examples/fine-tuning/lenet-fine-tune.lua index 56b8886bc..24c88c271 100644 --- a/examples/fine-tuning/lenet-fine-tune.lua +++ b/examples/fine-tuning/lenet-fine-tune.lua @@ -13,17 +13,17 @@ return function(params) end if pcall(function() require('cudnn') end) then - print('Using CuDNN backend') + --print('Using CuDNN backend') backend = cudnn convLayer = cudnn.SpatialConvolution convLayerName = 'cudnn.SpatialConvolution' else print('Failed to load cudnn backend (is libcudnn.so in your library path?)') if pcall(function() require('cunn') end) then - print('Falling back to legacy cunn backend') + --print('Falling back to legacy cunn backend') else - print('Failed to load cunn backend (is CUDA installed?)') - print('Falling back to legacy nn backend') + --print('Failed to load cunn backend (is CUDA installed?)') + --print('Falling back to legacy nn backend') end backend = nn -- works with cunn or nn convLayer = nn.SpatialConvolutionMM diff --git a/examples/gan/README.md b/examples/gan/README.md index e2f4bfdfa..ad7e029a6 100644 --- a/examples/gan/README.md +++ b/examples/gan/README.md @@ -219,8 +219,8 @@ Now click `Test` and you will see a class sweep using the particular style that ### Downloading the CelebA dataset -The Celebrity Faces (a.k.a. "CelebA") dataset may be downloaded from this [Dropbox account](https://www.dropbox.com/sh/8oqt9vytwxb3s4r/AAB06FXaQRUNtjW9ntaoPGvCa?dl=0). -Download `img/img_align_celeba.zip` and `Anno/list_attr_celeba.txt`. +The Celebrity Faces (a.k.a. "CelebA") dataset is available from this [location](http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html). +Download [img/img_align_celeba.zip](https://drive.google.com/drive/folders/0B7EVK8r0v71pTUZsaXdaSnZBZzg) and [Anno/list_attr_celeba.txt](https://drive.google.com/drive/folders/0B7EVK8r0v71pOC0wOVZlQnFfaGs). Extract the ZIP file into a local folder. ### Creating the CelebA dataset diff --git a/examples/gan/gan_embeddings.py b/examples/gan/gan_embeddings.py index 9d4d89f4b..127033912 100755 --- a/examples/gan/gan_embeddings.py +++ b/examples/gan/gan_embeddings.py @@ -14,6 +14,7 @@ TB_DIR = os.path.join(os.getcwd(), "gan-tb") SPRITE_IMAGE_FILENAME = os.path.join(TB_DIR, "sprite.png") + def save_tb_embeddings(embeddings_filename): f = open(embeddings_filename, 'rb') embeddings = pickle.load(f) @@ -43,11 +44,12 @@ def save_tb_embeddings(embeddings_filename): projector.visualize_embeddings(summary_writer, config) # save embeddings - sess=tf.Session() + sess = tf.Session() sess.run(embedding_var.initializer) saver = tf.train.Saver([embedding_var]) saver.save(sess, os.path.join(TB_DIR, 'model.ckpt')) + def save_sprite_image(images): n_embeddings = images.shape[0] grid_cols = int(np.sqrt(n_embeddings)) diff --git a/examples/gan/network-celebA-encoder.py b/examples/gan/network-celebA-encoder.py index 605b4fde3..5bfead789 100644 --- a/examples/gan/network-celebA-encoder.py +++ b/examples/gan/network-celebA-encoder.py @@ -21,12 +21,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import math -import numpy as np import tensorflow as tf -from tensorflow.python.framework import ops - image_summary = tf.summary.image scalar_summary = tf.summary.scalar histogram_summary = tf.summary.histogram @@ -117,7 +113,7 @@ def deconv2d(input_, output_shape, # filter : [height, width, output_channels, in_channels] w = tf.get_variable('w', [k_h, k_w, output_shape[-1], - input_.get_shape()[-1]], + input_.get_shape()[-1]], initializer=tf.random_normal_initializer(stddev=stddev)) deconv = tf.nn.conv2d_transpose(input_, w, output_shape=output_shape, @@ -158,7 +154,7 @@ def linear(input_, output_size, scope=None, stddev=0.02, bias_start=0.0, with_w= matrix = tf.get_variable("Matrix", [shape[1], output_size], tf.float32, tf.random_normal_initializer(stddev=stddev)) bias = tf.get_variable("bias", [output_size], - initializer=tf.constant_initializer(bias_start)) + initializer=tf.constant_initializer(bias_start)) if with_w: return tf.matmul(input_, matrix) + bias, matrix, bias else: @@ -202,8 +198,7 @@ def __init__(self, *args, **kwargs): self.dcgan_init(image_size=image_size, output_size=output_size, c_dim=c_dim, - z_dim=z_dim, - ) + z_dim=z_dim) @model_property def inference(self): @@ -214,7 +209,7 @@ def inference(self): images_flat = tf.reshape(images, [self.batch_size, self.image_size * self.image_size * self.c_dim]) # concatenate encoded z and generated image into a single flat structure zgen_flat = tf.reshape(self.DzGEN, [self.batch_size, self.z_dim]) - return tf.concat(1, [zgen_flat, images_flat]) + return tf.concat([zgen_flat, images_flat], 1) @model_property def loss(self): @@ -244,8 +239,7 @@ def dcgan_init(self, gf_dim=64, df_dim=64, gfc_dim=1024, - dfc_dim=1024, - ): + dfc_dim=1024): """ Args: @@ -289,15 +283,14 @@ def dcgan_init(self, def build_model(self): # reshape/rescale x - self.images = (tf.reshape(self.x, - shape=[self.batch_size, - self.image_size, - self.image_size, - self.c_dim], - name='x_reshaped') - 128)/ 127. + self.images = (tf.reshape(self.x, shape=[self.batch_size, + self.image_size, + self.image_size, + self.c_dim], + name='x_reshaped') - 128) / 127. # create discriminator/encoder - self.DzGEN, self.D_logits = self.discriminator(self.images, reuse=False) + self.DzGEN, self.D_logits = self.discriminator(self.images, reuse=False) # create generator self.G = self.generator(self.DzGEN) # loss is now L2 distance between input image and generator output @@ -389,19 +382,19 @@ def generator(self, z, y=None): self.h0 = tf.reshape(self.z_, [-1, s16, s16, self.gf_dim * 8]) h0 = tf.nn.relu(self.g_bn0(self.h0, train=False)) - self.h1, self.h1_w, self.h1_b = deconv2d(h0, - [self.batch_size, s8, s8, self.gf_dim*4], name='g_h1', with_w=True) + self.h1, self.h1_w, self.h1_b = deconv2d(h0, [self.batch_size, s8, s8, self.gf_dim*4], + name='g_h1', with_w=True) h1 = tf.nn.relu(self.g_bn1(self.h1, train=False)) - h2, self.h2_w, self.h2_b = deconv2d(h1, - [self.batch_size, s4, s4, self.gf_dim*2], name='g_h2', with_w=True) + h2, self.h2_w, self.h2_b = deconv2d(h1, [self.batch_size, s4, s4, self.gf_dim*2], + name='g_h2', with_w=True) h2 = tf.nn.relu(self.g_bn2(h2, train=False)) - h3, self.h3_w, self.h3_b = deconv2d(h2, - [self.batch_size, s2, s2, self.gf_dim*1], name='g_h3', with_w=True) + h3, self.h3_w, self.h3_b = deconv2d(h2, [self.batch_size, s2, s2, self.gf_dim*1], + name='g_h3', with_w=True) h3 = tf.nn.relu(self.g_bn3(h3, train=False)) - h4, self.h4_w, self.h4_b = deconv2d(h3, - [self.batch_size, s, s, self.c_dim], name='g_h4', with_w=True) + h4, self.h4_w, self.h4_b = deconv2d(h3, [self.batch_size, s, s, self.c_dim], + name='g_h4', with_w=True) return tf.nn.tanh(h4) diff --git a/examples/gan/network-celebA.py b/examples/gan/network-celebA.py index 8e454b5bb..0bcf847e6 100644 --- a/examples/gan/network-celebA.py +++ b/examples/gan/network-celebA.py @@ -21,12 +21,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import math -import numpy as np import tensorflow as tf -from tensorflow.python.framework import ops - image_summary = tf.summary.image scalar_summary = tf.summary.scalar histogram_summary = tf.summary.histogram @@ -117,7 +113,7 @@ def deconv2d(input_, output_shape, # filter : [height, width, output_channels, in_channels] w = tf.get_variable('w', [k_h, k_w, output_shape[-1], - input_.get_shape()[-1]], + input_.get_shape()[-1]], initializer=tf.random_normal_initializer(stddev=stddev)) deconv = tf.nn.conv2d_transpose(input_, w, output_shape=output_shape, @@ -158,7 +154,7 @@ def linear(input_, output_size, scope=None, stddev=0.02, bias_start=0.0, with_w= matrix = tf.get_variable("Matrix", [shape[1], output_size], tf.float32, tf.random_normal_initializer(stddev=stddev)) bias = tf.get_variable("bias", [output_size], - initializer=tf.constant_initializer(bias_start)) + initializer=tf.constant_initializer(bias_start)) if with_w: return tf.matmul(input_, matrix) + bias, matrix, bias else: @@ -202,8 +198,7 @@ def __init__(self, *args, **kwargs): self.dcgan_init(image_size=image_size, output_size=output_size, c_dim=c_dim, - z_dim=z_dim, - ) + z_dim=z_dim) @model_property def inference(self): @@ -241,8 +236,7 @@ def dcgan_init(self, gf_dim=64, df_dim=64, gfc_dim=1024, - dfc_dim=1024, - ): + dfc_dim=1024): """ Args: @@ -299,7 +293,7 @@ def build_model(self): self.image_size, self.image_size, self.c_dim], - name='x_reshaped') - 128)/ 127. + name='x_reshaped') - 128) / 127. # create generator self.G = self.generator(self.z) @@ -313,19 +307,23 @@ def build_model(self): # we are using the cross entropy loss for all these losses # note the use of the soft label smoothing here to prevent D from getting overly confident # on real samples - self.d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(self.D_logits, - tf.ones_like(self.D) - self.soft_label_margin, - name="loss_D_real")) - self.d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(self.D_logits_, - tf.zeros_like(self.D_), - name="loss_D_fake")) + d_real = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.D_logits, + labels=(tf.ones_like(self.D) - self.soft_label_margin), + name="loss_D_real") + self.d_loss_real = tf.reduce_mean(d_real) + d_fake = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.D_logits_, + labels=(tf.zeros_like(self.D_)), + name="loss_D_fake") + self.d_loss_fake = tf.reduce_mean(d_fake) self.d_loss = (self.d_loss_real + self.d_loss_fake) / 2. - # the typical GAN set-up is that of a minimax game where D is trying to minimize its own error and G is trying to maximize D's error - # however note how we are flipping G labels here: instead of maximizing D's error, we are minimizing D's error on the 'wrong' label + # the typical GAN set-up is that of a minimax game where D is trying to minimize + # its own error and G is trying to maximize D's error however note how we are flipping G labels here: + # instead of maximizing D's error, we are minimizing D's error on the 'wrong' label # this trick helps produce a stronger gradient - self.g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(self.D_logits_, - tf.ones_like(self.D_) + self.soft_label_margin, - name="loss_G")) + g_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.D_logits_, + labels=(tf.ones_like(self.D_) + self.soft_label_margin), + name="loss_G") + self.g_loss = tf.reduce_mean(g_loss) # debug self.summaries.append(image_summary("G", self.G, max_outputs=3)) @@ -429,19 +427,19 @@ def generator(self, z, y=None): self.h0 = tf.reshape(self.z_, [-1, s16, s16, self.gf_dim * 8]) h0 = tf.nn.relu(self.g_bn0(self.h0, train=self.is_training)) - self.h1, self.h1_w, self.h1_b = deconv2d(h0, - [self.batch_size, s8, s8, self.gf_dim * 4], name='g_h1', with_w=True) + self.h1, self.h1_w, self.h1_b = deconv2d(h0, [self.batch_size, s8, s8, self.gf_dim * 4], + name='g_h1', with_w=True) h1 = tf.nn.relu(self.g_bn1(self.h1, train=self.is_training)) - h2, self.h2_w, self.h2_b = deconv2d(h1, - [self.batch_size, s4, s4, self.gf_dim * 2], name='g_h2', with_w=True) + h2, self.h2_w, self.h2_b = deconv2d(h1, [self.batch_size, s4, s4, self.gf_dim * 2], + name='g_h2', with_w=True) h2 = tf.nn.relu(self.g_bn2(h2, train=self.is_training)) - h3, self.h3_w, self.h3_b = deconv2d(h2, - [self.batch_size, s2, s2, self.gf_dim * 1], name='g_h3', with_w=True) + h3, self.h3_w, self.h3_b = deconv2d(h2, [self.batch_size, s2, s2, self.gf_dim * 1], + name='g_h3', with_w=True) h3 = tf.nn.relu(self.g_bn3(h3, train=self.is_training)) - h4, self.h4_w, self.h4_b = deconv2d(h3, - [self.batch_size, s, s, self.c_dim], name='g_h4', with_w=True) + h4, self.h4_w, self.h4_b = deconv2d(h3, [self.batch_size, s, s, self.c_dim], + name='g_h4', with_w=True) return tf.nn.tanh(h4) diff --git a/examples/gan/network-mnist-encoder.py b/examples/gan/network-mnist-encoder.py index 2567aaa9f..48e856d0c 100644 --- a/examples/gan/network-mnist-encoder.py +++ b/examples/gan/network-mnist-encoder.py @@ -21,12 +21,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import math -import numpy as np import tensorflow as tf -from tensorflow.python.framework import ops - image_summary = tf.summary.image scalar_summary = tf.summary.scalar histogram_summary = tf.summary.histogram @@ -82,7 +78,7 @@ def conv_cond_concat(x, y): x_shapes = x.get_shape() y_shapes = y.get_shape() batch_size = tf.shape(x)[0] - return tf.concat(3, [x, y * tf.ones([batch_size, int(x_shapes[1]), int(x_shapes[2]), int(y_shapes[3])])]) + return tf.concat([x, y * tf.ones([batch_size, int(x_shapes[1]), int(x_shapes[2]), int(y_shapes[3])])], 3) def conv2d(input_, output_dim, @@ -138,7 +134,7 @@ def deconv2d(input_, output_shape, # filter : [height, width, output_channels, in_channels] w = tf.get_variable('w', [k_h, k_w, output_shape[-1], - input_.get_shape()[-1]], + input_.get_shape()[-1]], initializer=tf.random_normal_initializer(stddev=stddev)) deconv = tf.nn.conv2d_transpose(input_, w, output_shape=output_shape, @@ -179,7 +175,7 @@ def linear(input_, output_size, scope=None, stddev=0.02, bias_start=0.0, with_w= matrix = tf.get_variable("Matrix", [shape[1], output_size], tf.float32, tf.random_normal_initializer(stddev=stddev)) bias = tf.get_variable("bias", [output_size], - initializer=tf.constant_initializer(bias_start)) + initializer=tf.constant_initializer(bias_start)) if with_w: return tf.matmul(input_, matrix) + bias, matrix, bias else: @@ -219,8 +215,7 @@ def __init__(self, *args, **kwargs): self.dcgan_init(image_size=28, y_dim=10, output_size=28, - c_dim=1, - ) + c_dim=1) @model_property def inference(self): @@ -233,7 +228,7 @@ def inference(self): # during inference the visualization script will need to extract # both z and the generated image to display them separately zgen_flat = tf.reshape(self.DzGEN, [self.batch_size, self.z_dim]) - return tf.concat(1, [zgen_flat, images_flat]) + return tf.concat([zgen_flat, images_flat], 1) @model_property def loss(self): @@ -310,14 +305,16 @@ def build_model(self): # self.y is a vector of labels - shape: [N] # rescale to [0,1] range - x_reshaped = tf.reshape(self.x, shape=[self.batch_size, self.image_size, self.image_size, self.c_dim], name='x_reshaped') + x_reshaped = tf.reshape(self.x, shape=[self.batch_size, self.image_size, self.image_size, self.c_dim], + name='x_reshaped') + self.images = x_reshaped / 255. # one-hot encode y - shape: [N] -> [N, self.y_dim] self.y = tf.one_hot(self.y, self.y_dim, name='y_onehot') # create discriminator/encoder - self.DzGEN, self.D_logits = self.discriminator(self.images, self.y, reuse=False) + self.DzGEN, self.D_logits = self.discriminator(self.images, self.y, reuse=False) # create generator self.G = self.generator(self.DzGEN, self.y) @@ -381,15 +378,14 @@ def discriminator(self, image, y=None, reuse=False): h1 = lrelu(self.d_bn1(conv2d(h0, self.df_dim + self.y_dim, name='d_h1_conv'), train=self.is_training)) sz = h1.get_shape() h1 = tf.reshape(h1, [self.batch_size, int(sz[1] * sz[2] * sz[3])]) - h1 = tf.concat(1, [h1, y]) + h1 = tf.concat([h1, y], 1) h2 = lrelu(self.d_bn2(linear(h1, self.dfc_dim, 'd_h2_lin'), train=self.is_training)) - h2 = tf.concat(1, [h2, y]) + h2 = tf.concat([h2, y], 1) h3 = linear(h2, self.z_dim, 'd_h3_lin_retrain') return h3, h3 - def generator(self, z, y=None): """ Create the generator @@ -416,17 +412,18 @@ def generator(self, z, y=None): # yb = tf.expand_dims(tf.expand_dims(y, 1),2) yb = tf.reshape(y, [self.batch_size, 1, 1, self.y_dim]) - z = tf.concat(1, [z, y]) + z = tf.concat([z, y], 1) h0 = tf.nn.relu(self.g_bn0(linear(z, self.gfc_dim, 'g_h0_lin'), train=False)) - h0 = tf.concat(1, [h0, y]) + h0 = tf.concat([h0, y], 1) h1 = tf.nn.relu(self.g_bn1(linear(h0, self.gf_dim*2*s4*s4, 'g_h1_lin'), train=False)) h1 = tf.reshape(h1, [self.batch_size, s4, s4, self.gf_dim * 2]) h1 = conv_cond_concat(h1, yb) - h2 = tf.nn.relu(self.g_bn2(deconv2d(h1, [self.batch_size, s2, s2, self.gf_dim * 2], name='g_h2'), train=False)) + h2 = tf.nn.relu(self.g_bn2(deconv2d(h1, [self.batch_size, s2, s2, self.gf_dim * 2], + name='g_h2'), train=False)) h2 = conv_cond_concat(h2, yb) return tf.nn.sigmoid(deconv2d(h2, [self.batch_size, s, s, self.c_dim], name='g_h3')) diff --git a/examples/gan/network-mnist.py b/examples/gan/network-mnist.py index 458ae6acc..e2e79818f 100644 --- a/examples/gan/network-mnist.py +++ b/examples/gan/network-mnist.py @@ -21,12 +21,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import math -import numpy as np import tensorflow as tf -from tensorflow.python.framework import ops - image_summary = tf.summary.image scalar_summary = tf.summary.scalar histogram_summary = tf.summary.histogram @@ -82,7 +78,7 @@ def conv_cond_concat(x, y): x_shapes = x.get_shape() y_shapes = y.get_shape() batch_size = tf.shape(x)[0] - return tf.concat(3, [x, y * tf.ones([batch_size, int(x_shapes[1]), int(x_shapes[2]), int(y_shapes[3])])]) + return tf.concat([x, y * tf.ones([batch_size, int(x_shapes[1]), int(x_shapes[2]), int(y_shapes[3])])], 3) def conv2d(input_, output_dim, @@ -138,7 +134,7 @@ def deconv2d(input_, output_shape, # filter : [height, width, output_channels, in_channels] w = tf.get_variable('w', [k_h, k_w, output_shape[-1], - input_.get_shape()[-1]], + input_.get_shape()[-1]], initializer=tf.random_normal_initializer(stddev=stddev)) deconv = tf.nn.conv2d_transpose(input_, w, output_shape=output_shape, @@ -179,7 +175,7 @@ def linear(input_, output_size, scope=None, stddev=0.02, bias_start=0.0, with_w= matrix = tf.get_variable("Matrix", [shape[1], output_size], tf.float32, tf.random_normal_initializer(stddev=stddev)) bias = tf.get_variable("bias", [output_size], - initializer=tf.constant_initializer(bias_start)) + initializer=tf.constant_initializer(bias_start)) if with_w: return tf.matmul(input_, matrix) + bias, matrix, bias else: @@ -219,8 +215,7 @@ def __init__(self, *args, **kwargs): self.dcgan_init(image_size=28, y_dim=10, output_size=28, - c_dim=1, - ) + c_dim=1) @model_property def inference(self): @@ -306,7 +301,8 @@ def build_model(self): self.z = tf.random_normal(shape=[self.batch_size, self.z_dim], dtype=tf.float32, seed=None, name='z') # rescale x to [0, 1] - x_reshaped = tf.reshape(self.x, shape=[self.batch_size, self.image_size, self.image_size, self.c_dim], name='x_reshaped') + x_reshaped = tf.reshape(self.x, shape=[self.batch_size, self.image_size, self.image_size, self.c_dim], + name='x_reshaped') self.images = x_reshaped / 255. # one hot encode the label - shape: [N] -> [N, self.y_dim] @@ -317,7 +313,7 @@ def build_model(self): # create one instance of the discriminator for real images (the input is # images from the dataset) - self.D, self.D_logits = self.discriminator(self.images, self.y, reuse=False) + self.D, self.D_logits = self.discriminator(self.images, self.y, reuse=False) # create another instance of the discriminator for fake images (the input is # the discriminator). Note how we are reusing variables to share weights between @@ -327,13 +323,17 @@ def build_model(self): # aggregate losses across batch # we are using the cross entropy loss for all these losses - self.d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(self.D_logits, tf.ones_like(self.D), name="loss_D_real")) - self.d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(self.D_logits_, tf.zeros_like(self.D_), name="loss_D_fake")) + d_real = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.D_logits, labels=tf.ones_like(self.D), name="loss_D_real") + self.d_loss_real = tf.reduce_mean(d_real) + d_fake = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.D_logits_, labels=tf.zeros_like(self.D_), name="loss_D_fake") + self.d_loss_fake = tf.reduce_mean(d_fake) self.d_loss = (self.d_loss_real + self.d_loss_fake) / 2. - # the typical GAN set-up is that of a minimax game where D is trying to minimize its own error and G is trying to maximize D's error - # however note how we are flipping G labels here: instead of maximizing D's error, we are minimizing D's error on the 'wrong' label + # the typical GAN set-up is that of a minimax game where D is trying to minimize + # its own error and G is trying to maximize D's error however note how we are flipping G labels here: + # instead of maximizing D's error, we are minimizing D's error on the 'wrong' label # this trick helps produce a stronger gradient - self.g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(self.D_logits_, tf.ones_like(self.D_), name="loss_G")) + g_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.D_logits_, labels=tf.ones_like(self.D_), name="loss_G") + self.g_loss = tf.reduce_mean(g_loss) # create some summaries for debug and monitoring self.summaries.append(histogram_summary("z", self.z)) @@ -413,10 +413,10 @@ def discriminator(self, image, y=None, reuse=False): h1 = lrelu(self.d_bn1(conv2d(h0, self.df_dim + self.y_dim, name='d_h1_conv'), train=self.is_training)) sz = h1.get_shape() h1 = tf.reshape(h1, [self.batch_size, int(sz[1] * sz[2] * sz[3])]) - h1 = tf.concat(1, [h1, y]) + h1 = tf.concat([h1, y], 1) h2 = lrelu(self.d_bn2(linear(h1, self.dfc_dim, 'd_h2_lin'), train=self.is_training)) - h2 = tf.concat(1, [h2, y]) + h2 = tf.concat([h2, y], 1) h3 = linear(h2, 1, 'd_h3_lin') @@ -448,17 +448,17 @@ def generator(self, z, y=None): s2, s4 = int(s/2), int(s/4) yb = tf.reshape(y, [self.batch_size, 1, 1, self.y_dim]) - z = tf.concat(1, [z, y]) + z = tf.concat([z, y], 1) h0 = tf.nn.relu(self.g_bn0(linear(z, self.gfc_dim, 'g_h0_lin'), train=self.is_training)) - h0 = tf.concat(1, [h0, y]) + h0 = tf.concat([h0, y], 1) h1 = tf.nn.relu(self.g_bn1(linear(h0, self.gf_dim*2*s4*s4, 'g_h1_lin'), train=self.is_training)) h1 = tf.reshape(h1, [self.batch_size, s4, s4, self.gf_dim * 2]) h1 = conv_cond_concat(h1, yb) - - h2 = tf.nn.relu(self.g_bn2(deconv2d(h1, [self.batch_size, s2, s2, self.gf_dim * 2], name='g_h2'), train=self.is_training)) + h2 = tf.nn.relu(self.g_bn2(deconv2d(h1, [self.batch_size, s2, s2, self.gf_dim * 2], name='g_h2'), + train=self.is_training)) h2 = conv_cond_concat(h2, yb) return tf.nn.sigmoid(deconv2d(h2, [self.batch_size, s, s, self.c_dim], name='g_h3')) diff --git a/examples/question-answering/memn2n.py b/examples/question-answering/memn2n.py index 914afecfe..5a5d80991 100644 --- a/examples/question-answering/memn2n.py +++ b/examples/question-answering/memn2n.py @@ -1,3 +1,9 @@ +# License needed + +import numpy as np +import tensorflow as tf + + class UserModel(Tower): @model_property @@ -114,14 +120,16 @@ def add_gradient_noise(t, stddev=1e-3, name=None): t = tf.convert_to_tensor(t, name="t") gn = tf.random_normal(tf.shape(t), stddev=stddev) return tf.add(t, gn, name=name) + def zero_nil_slot(t, name=None): t = tf.convert_to_tensor(t, name="t") s = tf.shape(t)[1] z = tf.zeros(tf.pack([1, s])) return tf.concat(0, [z, tf.slice(t, [1, 0], [-1, -1])], name=name) - max_grad_norm=40.0 - grads_and_vars = [(tf.clip_by_norm(g, max_grad_norm), v) for g,v in grads_and_vars] - grads_and_vars = [(add_gradient_noise(g), v) for g,v in grads_and_vars] + + max_grad_norm = 40.0 + grads_and_vars = [(tf.clip_by_norm(g, max_grad_norm), v) for g, v in grads_and_vars] + grads_and_vars = [(add_gradient_noise(g), v) for g, v in nil_grads_and_vars = [] for g, v in grads_and_vars: if v.name in self._nil_vars: diff --git a/examples/regression/README.md b/examples/regression/README.md index b987bdde8..1a2c31235 100644 --- a/examples/regression/README.md +++ b/examples/regression/README.md @@ -154,11 +154,50 @@ return function(p) end ``` +### Using Tensorflow +Under the `Custom Network` tab, select `Tensorflow`. There you can paste the following network definition: +```python +class UserModel(Tower): + + @model_property + def inference(self): + n_hidden = 32 + + const = tf.constant(0.004) + normed = tf.multiply(self.x, const) + + # The reshaping have to be done for tensorflow to get the shape right + right_shape = tf.reshape(normed, shape=[-1, 32, 32]) + transposed = tf.transpose(right_shape, [0, 2, 1]) + squeezed = tf.reshape(transposed, shape=[-1, 1024]) + + # Define weights + weights = { + 'w1': tf.get_variable('w1', [1024, 2]) + } + biases = { + 'b1': tf.get_variable('b1', [2]) + } + + # Linear activation + model = tf.matmul(squeezed, weights['w1'] ) + biases['b1'] + tf.summary.image(model.op.name, model, max_outputs=1, collections=["Training Summary"]) + return model + + @model_property + def loss(self): + label = tf.reshape(self.y, shape=[-1, 2]) + model = self.inference + loss = digits.mse_loss(model, label) + return loss +``` +Set the learning rate to `0.01` to ensure a smooth training curve. + ## Verification After training for 15 epochs the loss function should look similar to this: -![Training loss](regression-loss.png)) +![Training loss](regression-loss.png) Now we can assess the quality of the model. To this avail, we can use the test image that was generated by `test_lmdb_creator.py`: diff --git a/examples/regression/regression_mnist-TF.py b/examples/regression/regression_mnist-TF.py new file mode 100644 index 000000000..e14b0f9a6 --- /dev/null +++ b/examples/regression/regression_mnist-TF.py @@ -0,0 +1,33 @@ +class UserModel(Tower): + + @model_property + def inference(self): + n_hidden = 32 + + const = tf.constant(0.004) + normed = tf.multiply(self.x, const) + + # The reshaping have to be done for tensorflow to get the shape right + right_shape = tf.reshape(normed, shape=[-1, 32, 32]) + transposed = tf.transpose(right_shape, [0, 2, 1]) + squeezed = tf.reshape(transposed, shape=[-1, 1024]) + + # Define weights + weights = { + 'w1': tf.get_variable('w1', [1024, 2]) + } + biases = { + 'b1': tf.get_variable('b1', [2]) + } + + # Linear activation + model = tf.matmul(squeezed, weights['w1'] ) + biases['b1'] + tf.summary.image(model.op.name, model, max_outputs=1, collections=["Training Summary"]) + return model + + @model_property + def loss(self): + label = tf.reshape(self.y, shape=[-1, 2]) + model = self.inference + loss = digits.mse_loss(model, label) + return loss diff --git a/examples/siamese/README.md b/examples/siamese/README.md index 2237b4f55..d7ee84f54 100644 --- a/examples/siamese/README.md +++ b/examples/siamese/README.md @@ -92,6 +92,10 @@ Finally, the `nn.CosineEmbeddingCriterion` criterion is used. Similar to Caffe's pull apart images from different classes. However, since the `Cosine` distance is used, the model will learn to minimize the *angle* between features that are extracted from images of the same class and conversely will maximize the angle between features extracted from images from different classes. See below for a visual illustration of the impact this difference has on extracted features. +## Using Tensorflow + +Under the `Custom Network` tab, select `Tensorflow`. There you can paste this [network definition](siamese-TF.py) then give your model a name and click `Create`. + ## Verification After training the Caffe model for 30 epochs the loss function should look similar to this: diff --git a/examples/siamese/siamese-TF.py b/examples/siamese/siamese-TF.py new file mode 100644 index 000000000..3c9e21314 --- /dev/null +++ b/examples/siamese/siamese-TF.py @@ -0,0 +1,40 @@ +class UserModel(Tower): + + @model_property + def inference(self): + _x = tf.reshape(self.x, shape=[-1, self.input_shape[0], self.input_shape[1], self.input_shape[2]]) + #tf.image_summary(_x.op.name, _x, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) + + # Split out the color channels + _, model_g, model_b = tf.split(_x, 3, 3, name='split_channels') + #tf.image_summary(model_g.op.name, model_g, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) + #tf.image_summary(model_b.op.name, model_b, max_images=10, collections=[digits.GraphKeys.SUMMARIES_TRAIN]) + + with slim.arg_scope([slim.conv2d, slim.fully_connected], + weights_initializer=tf.contrib.layers.xavier_initializer(), + weights_regularizer=slim.l2_regularizer(0.0005) ): + with tf.variable_scope("siamese") as scope: + def make_tower(net): + net = slim.conv2d(net, 20, [5, 5], padding='VALID', scope='conv1') + net = slim.max_pool2d(net, [2, 2], padding='VALID', scope='pool1') + net = slim.conv2d(net, 50, [5, 5], padding='VALID', scope='conv2') + net = slim.max_pool2d(net, [2, 2], padding='VALID', scope='pool2') + net = slim.flatten(net) + net = slim.fully_connected(net, 500, scope='fc1') + net = slim.fully_connected(net, 2, activation_fn=None, scope='fc2') + return net + + model_g = make_tower(model_g) + model_g = tf.reshape(model_g, shape=[-1, 2]) + scope.reuse_variables() + model_b = make_tower(model_b) + model_b = tf.reshape(model_b, shape=[-1, 2]) + + return [model_g, model_b] + + @model_property + def loss(self): + _y = tf.reshape(self.y, shape=[-1]) + _y = tf.to_float(_y) + model = self.inference + return digits.constrastive_loss(model[0], model[1], _y) \ No newline at end of file diff --git a/packaging/deb/build.sh b/packaging/deb/build.sh index 220740f5c..0c2c014c1 100755 --- a/packaging/deb/build.sh +++ b/packaging/deb/build.sh @@ -98,3 +98,5 @@ mkdir -p "$DIST_ROOT" docker cp "${DOCKER_BUILD_ID}:/dist" "$DIST_DIR" docker rm "$DOCKER_BUILD_ID" find "$DIST_DIR" -type f | sort + +echo "build.sh finished" diff --git a/packaging/deb/templates/control b/packaging/deb/templates/control index dc9897017..d3b4bcefb 100644 --- a/packaging/deb/templates/control +++ b/packaging/deb/templates/control @@ -19,7 +19,8 @@ Depends: ${misc:Depends}, ${python:Depends}, python-lmdb (>= 0.87), python-setuptools, python-wtforms (>= 2.0.1) -Recommends: torch7-nv +Recommends: torch7-nv, + tensorflow-gpu Conflicts: digits-devbox-config (<< 3.0.0), torch7 Description: NVIDIA DIGITS webserver This package installs the DIGITS webserver as a service. @@ -27,4 +28,3 @@ Description: NVIDIA DIGITS webserver DIGITS is the Deep Learning GPU Training System from NVIDIA. It provides an interactive environment for training, evaluating and experimenting with neural networks. - diff --git a/plugins/view/gan/digitsViewPluginGan/forms.py b/plugins/view/gan/digitsViewPluginGan/forms.py index 1e49a744d..fb3a19728 100644 --- a/plugins/view/gan/digitsViewPluginGan/forms.py +++ b/plugins/view/gan/digitsViewPluginGan/forms.py @@ -5,7 +5,8 @@ from digits import utils from digits.utils import subclass -from flask.ext.wtf import Form +from flask_wtf import Form +import wtforms.validators @subclass @@ -20,12 +21,10 @@ def validate_file_path(form, field): else: # make sure the filesystem path exists if not os.path.exists(field.data) and not os.path.isdir(field.data): - raise validators.ValidationError( - 'File does not exist or is not reachable') + raise wtforms.validators.ValidationError('File does not exist or is not reachable') else: return True - gan_view_task_id = utils.forms.SelectField( 'Task', choices=[ diff --git a/plugins/view/gan/digitsViewPluginGan/view.py b/plugins/view/gan/digitsViewPluginGan/view.py index a5a63ffcd..eb64c60c1 100644 --- a/plugins/view/gan/digitsViewPluginGan/view.py +++ b/plugins/view/gan/digitsViewPluginGan/view.py @@ -2,7 +2,6 @@ from __future__ import absolute_import import os -import tempfile # Find the best implementation available try: @@ -139,7 +138,7 @@ def get_image_html(self, image): else: raise ValueError("Unhandled number of channels: %d" % channels) - #image.save(fname) + # image.save(fname) image_html = digits.utils.image.embed_image_html(image) @@ -187,12 +186,7 @@ def process_data(self, input_id, input_data, output_data): self.animated_images = [None] * (4 * self.grid_size - 4) print("animated: %s" % repr(self.animated_images)) - if ( - col_id == 0 or - row_id == 0 or - col_id == (self.grid_size - 1) or - row_id == (self.grid_size - 1) - ): + if (col_id == 0 or row_id == 0 or col_id == (self.grid_size - 1) or row_id == (self.grid_size - 1)): if row_id == 0: idx = col_id elif col_id == (self.grid_size - 1): @@ -204,7 +198,7 @@ def process_data(self, input_id, input_data, output_data): self.animated_images[idx] = data.astype('uint8') print("set idx %d " % idx) else: - raise ValueEror("Unhandled image size: %d" % img_size) + raise ValueError("Unhandled image size: %d" % img_size) return {'image': image_html, 'col_id': col_id, @@ -251,11 +245,10 @@ def process_data(self, input_id, input_data, output_data): with open(self.attributes_file, 'rb') as f: attributes_z = pickle.load(f) - #inner_products = np.inner(z, attributes_z) + # inner_products = np.inner(z, attributes_z) inner_products = np.empty((40)) for i in range(40): - #if i in [ 1, 2, 18, 19, 20, 21, 25, 27, 31, 33, 36]: - if True: #i in [ 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 18, 19, 20, 21, 23, 24, 25, 27, 28, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39]: + if True: attr = attributes_z[i] inner_products[i] = np.inner(z, attr) / np.linalg.norm(attr) else: diff --git a/requirements.txt b/requirements.txt index e5920125b..381e726f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Pillow>=2.3.0,<=3.1.2 -numpy>=1.8.1,<=1.11.0 +numpy>1.8.1,<=1.11.0 scipy>=0.13.3,<=0.17.0 protobuf>=2.5.0,<=2.6.1 six>=1.5.2,<=1.10.0 diff --git a/scripts/travis/bust-cache.sh b/scripts/travis/bust-cache.sh index 9982f7ceb..091b25218 100755 --- a/scripts/travis/bust-cache.sh +++ b/scripts/travis/bust-cache.sh @@ -18,4 +18,3 @@ do fi fi done - diff --git a/scripts/travis/install-caffe.sh b/scripts/travis/install-caffe.sh index a17bb729d..75be24de7 100755 --- a/scripts/travis/install-caffe.sh +++ b/scripts/travis/install-caffe.sh @@ -39,4 +39,3 @@ make --jobs="$(nproc)" # mark cache WEEK=$(date +%Y-%W) echo "$WEEK" > "${INSTALL_DIR}/cache-version.txt" - diff --git a/scripts/travis/install-openblas.sh b/scripts/travis/install-openblas.sh deleted file mode 100755 index 9ce8b4f2f..000000000 --- a/scripts/travis/install-openblas.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# Copyright (c) 2016-2017, NVIDIA CORPORATION. All rights reserved. -set -e - -LOCAL_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - -if [ "$#" -ne 1 ]; -then - echo "Usage: $0 INSTALL_DIR" - exit 1 -fi - -INSTALL_DIR=$(readlink -f "$1") -if [ -d "$INSTALL_DIR" ] && ls "$INSTALL_DIR/"*.so >/dev/null 2>&1; then - echo "Using cached build at $INSTALL_DIR ..." -else - rm -rf "$INSTALL_DIR" - git clone https://github.com/xianyi/OpenBLAS.git "$INSTALL_DIR" -b v0.2.18 --depth 1 - cd "$INSTALL_DIR" - - # Redirect build output to a log and only show it if an error occurs - # Otherwise there is too much output for TravisCI to display properly - LOG_FILE="$LOCAL_DIR/openblas-build.log" - make NO_AFFINITY=1 USE_OPENMP=1 >"$LOG_FILE" 2>&1 || (cat "$LOG_FILE" && false) -fi - -cd "$INSTALL_DIR" -sudo make install PREFIX=/usr/local diff --git a/scripts/travis/install-tensorflow.sh b/scripts/travis/install-tensorflow.sh index db11a125f..f691f67fd 100755 --- a/scripts/travis/install-tensorflow.sh +++ b/scripts/travis/install-tensorflow.sh @@ -11,5 +11,4 @@ fi set -x -pip install --upgrade https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.11.0rc0-cp27-none-linux_x86_64.whl - +pip install https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-1.2.0rc0-cp27-none-linux_x86_64.whl --upgrade diff --git a/scripts/travis/install-torch.sh b/scripts/travis/install-torch.sh index 8b989ea34..690e92af7 100755 --- a/scripts/travis/install-torch.sh +++ b/scripts/travis/install-torch.sh @@ -42,4 +42,3 @@ LOG_FILE="$LOCAL_DIR/torch-install.log" # mark cache WEEK=$(date +%Y-%W) echo "$WEEK" >"${INSTALL_DIR}/cache-version.txt" - diff --git a/scripts/travis/ppa-upload.sh b/scripts/travis/ppa-upload.sh index 8d2d5d1e3..2cd1d2692 100755 --- a/scripts/travis/ppa-upload.sh +++ b/scripts/travis/ppa-upload.sh @@ -28,3 +28,5 @@ cd "$ROOT_DIR/packaging/deb/dist/" cd ./*xenial/ debsign -k 97A4B458 ./*source.changes dput -U "ppa:nvidia-digits/${PPA_NAME}/ubuntu/xenial" ./*source.changes + +echo "ppa-upload.sh finished" diff --git a/scripts/travis/pypi-upload.sh b/scripts/travis/pypi-upload.sh index a8f1cf173..d6ea968f0 100755 --- a/scripts/travis/pypi-upload.sh +++ b/scripts/travis/pypi-upload.sh @@ -15,3 +15,5 @@ username = luke.yeager password = ${PYPI_PASSWORD} EOF twine upload -r pypi dist/* + +echo "pypi-upload.sh finished" \ No newline at end of file