diff --git a/docs/install/osx_setup.md b/docs/install/osx_setup.md
index 53039252888d..4e9293efce93 100644
--- a/docs/install/osx_setup.md
+++ b/docs/install/osx_setup.md
@@ -65,6 +65,10 @@ Install the dependencies, required for MXNet, with the following commands:
brew install openblas
brew tap homebrew/core
brew install opencv
+
+ # If building with MKLDNN
+ brew install llvm
+
# Get pip
easy_install pip
# For visualization of network graphs
@@ -89,12 +93,18 @@ The file called ```osx.mk``` has the configuration required for building MXNet o
make -j$(sysctl -n hw.ncpu)
```
+To build with MKLDNN
+
+```bash
+LIBRARY_PATH=$(brew --prefix llvm)/lib/ make -j $(sysctl -n hw.ncpu) CC=$(brew --prefix llvm)/bin/clang++ CXX=$(brew --prefix llvm)/bin/clang++ USE_OPENCV=1 USE_OPENMP=1 USE_MKLDNN=1 USE_BLAS=apple USE_PROFILER=1
+```
+
If building with ```GPU``` support, add the following configuration to config.mk and build:
```bash
echo "USE_CUDA = 1" >> ./config.mk
echo "USE_CUDA_PATH = /usr/local/cuda" >> ./config.mk
echo "USE_CUDNN = 1" >> ./config.mk
- make
+ make -j$(sysctl -n hw.ncpu)
```
**Note:** To change build parameters, edit ```config.mk```.
@@ -124,6 +134,18 @@ You have 2 options:
2. Building MXNet from Source Code
### Building MXNet with the Prebuilt Binary Package
+Install OpenCV and OpenBLAS.
+
+```bash
+brew install opencv
+brew install openblas@0.3.1
+```
+
+Add a soft link to the OpenBLAS installation. This example links the 0.3.1 version:
+
+```bash
+ln -sf /usr/local/opt/openblas/lib/libopenblasp-r0.3.* /usr/local/opt/openblas/lib/libopenblasp-r0.3.1.dylib
+```
Install the latest version (3.5.1+) of R from [CRAN](https://cran.r-project.org/bin/macosx/).
For OS X (Mac) users, MXNet provides a prebuilt binary package for CPUs. The prebuilt package is updated weekly. You can install the package directly in the R console using the following commands:
diff --git a/docs/install/requirements.txt b/docs/install/requirements.txt
index 3d8020cc6ecb..dfc3f70c96fb 100644
--- a/docs/install/requirements.txt
+++ b/docs/install/requirements.txt
@@ -2,7 +2,7 @@ cpplint==1.3.0
h5py==2.8.0rc1
nose
nose-timer
-numpy<1.15.0,>=1.8.2
+numpy<=1.15.2,>=1.8.2
pylint==1.8.3
requests<2.19.0,>=2.18.4
scipy==1.0.1
diff --git a/docs/install/ubuntu_setup.md b/docs/install/ubuntu_setup.md
index 804887aee863..8aac1432f8e0 100644
--- a/docs/install/ubuntu_setup.md
+++ b/docs/install/ubuntu_setup.md
@@ -162,7 +162,7 @@ If building on CPU and using OpenBLAS:
```bash
git clone --recursive https://github.com/apache/incubator-mxnet.git
- cd mxnet
+ cd incubator-mxnet
make -j $(nproc) USE_OPENCV=1 USE_BLAS=openblas
```
@@ -170,7 +170,7 @@ If building on CPU and using MKL and MKL-DNN (make sure MKL is installed accordi
```bash
git clone --recursive https://github.com/apache/incubator-mxnet.git
- cd mxnet
+ cd incubator-mxnet
make -j $(nproc) USE_OPENCV=1 USE_BLAS=mkl USE_MKLDNN=1
```
@@ -178,7 +178,7 @@ If building on GPU and you want OpenCV and OpenBLAS (make sure you have installe
```bash
git clone --recursive https://github.com/apache/incubator-mxnet.git
- cd mxnet
+ cd incubator-mxnet
make -j $(nproc) USE_OPENCV=1 USE_BLAS=openblas USE_CUDA=1 USE_CUDA_PATH=/usr/local/cuda USE_CUDNN=1
```
@@ -189,7 +189,7 @@ Building from source creates a library called ```libmxnet.so``` in the `lib` fol
You may also want to add the MXNet shared library to your `LD_LIBRARY_PATH`:
```bash
-export LD_LIBRARY_PATH=~/incubator-mxnet/lib
+export LD_LIBRARY_PATH=$PWD/lib
```
After building the MXNet library, you may install language bindings.
diff --git a/docs/install/validate_mxnet.md b/docs/install/validate_mxnet.md
index a4cf5446f606..dfe8d063f602 100644
--- a/docs/install/validate_mxnet.md
+++ b/docs/install/validate_mxnet.md
@@ -137,8 +137,25 @@ Please contribute an example!
### Perl
-Please contribute an example!
+Start the pdl2 terminal.
+
+```bash
+$ pdl2
+```
+Run a short *MXNet* Perl program to create a 2X3 matrix of ones, multiply each element in the matrix by 2 followed by adding 1. We expect the output to be a 2X3 matrix with all elements being 3.
+
+```perl
+pdl> use AI::MXNet qw(mx)
+pdl> $a = mx->nd->ones([2, 3])
+pdl> $b = $a * 2 + 1
+pdl> print $b->aspdl
+
+[
+ [3 3 3]
+ [3 3 3]
+]
+```
### R
diff --git a/docs/install/windows_setup.md b/docs/install/windows_setup.md
index 87fd1cc07d8f..8e612454a7e1 100755
--- a/docs/install/windows_setup.md
+++ b/docs/install/windows_setup.md
@@ -11,7 +11,7 @@ The following describes how to install with pip for computers with CPUs, Intel C
- [Build from Source](#build-from-source)
- Install MXNet with a Programming Language API
- [Python](#install-the-mxnet-package-for-python)
- - [R](#install-mxnet-package-for-r)
+ - [R](#install-the-mxnet-package-for-r)
- [Julia](#install-the-mxnet-package-for-julia)
@@ -119,7 +119,7 @@ We provide two primary options to build and install MXNet yourself using [Micros
**NOTE:** Visual Studio 2017's compiler is `vc15`. This is not to be confused with Visual Studio 2015's compiler, `vc14`.
-You also have the option to install MXNet with MKL or MKLDNN. In this case it is recommended that you refer to the [MKLDNN_README](https://github.com/apache/incubator-mxnet/blob/master/MKLDNN_README.md).
+You also have the option to install MXNet with MKL or MKL-DNN. In this case it is recommended that you refer to the [MKLDNN_README](https://github.com/apache/incubator-mxnet/blob/master/MKLDNN_README.md).
**Option 1: Build with Microsoft Visual Studio 2017 (VS2017)**
@@ -137,14 +137,14 @@ To build and install MXNet yourself using [VS2017](https://www.visualstudio.com/
```
1. Download and install [CMake](https://cmake.org/download) if it is not already installed. [CMake v3.12.2](https://cmake.org/files/v3.12/cmake-3.12.2-win64-x64.msi) has been tested with MXNet.
1. Download and run the [OpenCV](https://sourceforge.net/projects/opencvlibrary/files/opencv-win/3.4.1/opencv-3.4.1-vc14_vc15.exe/download) package. There are more recent versions of OpenCV, so please create an issue/PR to update this info if you validate one of these later versions.
-1. This will unzip several files. You can place them in another directory if you wish.
-1. Set the environment variable `OpenCV_DIR` to point to the OpenCV build directory that you just unzipped (e.g., `OpenCV_DIR = C:\utils\opencv\build`).
+1. This will unzip several files. You can place them in another directory if you wish. We will use `C:\utils`(```mkdir C:\utils```) as our default path.
+1. Set the environment variable `OpenCV_DIR` to point to the OpenCV build directory that you just unzipped. Start ```cmd``` and type `set OpenCV_DIR=C:\utils\opencv\build`.
1. If you don’t have the Intel Math Kernel Library (MKL) installed, you can install it and follow the [MKLDNN_README](https://github.com/apache/incubator-mxnet/blob/master/MKLDNN_README.md) from here, or you can use OpenBLAS. These instructions will assume you're using OpenBLAS.
1. Download the [OpenBlas](https://sourceforge.net/projects/openblas/files/v0.2.19/OpenBLAS-v0.2.19-Win64-int32.zip/download) package. Later versions of OpenBLAS are available, but you would need to build from source. v0.2.19 is the most recent version that ships with binaries. Contributions of more recent binaries would be appreciated.
-1. Unzip the file. You can place the unzipped files and folders in another directory if you wish.
-1. Set the environment variable `OpenBLAS_HOME` to point to the OpenBLAS directory that contains the `include` and `lib` directories (e.g., `OpenBLAS_HOME = C:\utils\OpenBLAS`).
-1. Download and install [CUDA](https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=10&target_type=exelocal). If you already had CUDA, then installed VS2017, you should reinstall CUDA now so that you get the CUDA toolkit components for VS2017 integration.
-1. Download and install cuDNN. To get access to the download link, register as an NVIDIA community user. Then Follow the [link](http://docs.nvidia.com/deeplearning/sdk/cudnn-install/index.html#install-windows) to install the cuDNN.
+1. Unzip the file, rename it to ```OpenBLAS``` and put it under `C:\utils`. You can place the unzipped files and folders in another directory if you wish.
+1. Set the environment variable `OpenBLAS_HOME` to point to the OpenBLAS directory that contains the `include` and `lib` directories and type `set OpenBLAS_HOME=C:\utils\OpenBLAS` on the command prompt(```cmd```).
+1. Download and install [CUDA](https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=10&target_type=exelocal). If you already had CUDA, then installed VS2017, you should reinstall CUDA now so that you get the CUDA toolkit components for VS2017 integration. Note that the latest CUDA version supported by MXNet is [9.2](https://developer.nvidia.com/cuda-92-download-archive). You might also want to find other CUDA verion on the [Legacy Releases](https://developer.nvidia.com/cuda-toolkit-archive).
+1. Download and install cuDNN. To get access to the download link, register as an NVIDIA community user. Then follow the [link](http://docs.nvidia.com/deeplearning/sdk/cudnn-install/index.html#install-windows) to install the cuDNN and put those libraries into ```C:\cuda```.
1. Download and install [git](https://git-for-windows.github.io/) if you haven't already.
After you have installed all of the required dependencies, build the MXNet source code:
@@ -158,13 +158,14 @@ git clone https://github.com/apache/incubator-mxnet.git --recursive
3. Verify that the `DCUDNN_INCLUDE` and `DCUDNN_LIBRARY` environment variables are pointing to the `include` folder and `cudnn.lib` file of your CUDA installed location, and `C:\incubator-mxnet` is the location of the source code you just cloned in the previous step.
4. Create a build dir using the following command and go to the directory, for example:
```
-mkdir C:\build
-cd C:\build
+mkdir C:\incubator-mxnet\build
+cd C:\incubator-mxnet\build
```
5. Compile the MXNet source code with `cmake` by using following command:
```
cmake -G "Visual Studio 15 2017 Win64" -T cuda=9.2,host=x64 -DUSE_CUDA=1 -DUSE_CUDNN=1 -DUSE_NVRTC=1 -DUSE_OPENCV=1 -DUSE_OPENMP=1 -DUSE_BLAS=open -DUSE_LAPACK=1 -DUSE_DIST_KVSTORE=0 -DCUDA_ARCH_LIST=Common -DCUDA_TOOLSET=9.2 -DCUDNN_INCLUDE=C:\cuda\include -DCUDNN_LIBRARY=C:\cuda\lib\x64\cudnn.lib "C:\incubator-mxnet"
```
+* Make sure you set the environment variables correctly (OpenBLAS_HOME, OpenCV_DIR) and change the version of the Visual studio 2017 to v14.11 before enter above command.
6. After the CMake successfully completed, compile the the MXNet source code by using following command:
```
msbuild mxnet.sln /p:Configuration=Release;Platform=x64 /maxcpucount
@@ -206,14 +207,28 @@ We have installed MXNet core library. Next, we will install MXNet interface pack
- [Julia](#install-the-mxnet-package-for-julia)
- **Scala** is not yet available for Windows
-## Install MXNet for Python
+## Install the MXNet Package for Python
These steps are required after building from source. If you already installed MXNet by using pip, you do not need to do these steps to use MXNet with Python.
1. Install ```Python``` using windows installer available [here](https://www.python.org/downloads/release/python-2712/).
2. Install ```Numpy``` using windows installer available [here](https://scipy.org/index.html).
-3. Next, we install Python package interface for MXNet. You can find the Python interface package for [MXNet on GitHub](https://github.com/dmlc/mxnet/tree/master/python/mxnet).
-
+3. Start ```cmd``` and create a folder named ```common```(```mkdir C:\common```)
+4. Download the [mingw64_dll.zip](https://sourceforge.net/projects/openblas/files/v0.2.12/mingw64_dll.zip/download), unzip and copy three libraries (.dll files) that openblas.dll depends on to ```C:\common```.
+5. Copy the required .dll file to ```C:\common``` and make sure following libraries (.dll files) in the folder.
+```
+libgcc_s_seh-1.dll (in mingw64_dll)
+libgfortran-3.dll (in mingw64_dll)
+libquadmath-0.dll (in mingw64_dll)
+libopenblas.dll (in OpenBlas folder you download)
+opencv_world341.dll (in OpenCV folder you download)
+```
+6. Add ```C:\common``` to Environment Variables.
+ * Type ```control sysdm.cpl``` on ```cmp```
+ * Select the **Advanced tab** and click **Environment Variables**
+ * Double click the **Path** and click **New**
+ * Add ```C:\common``` and click OK
+7. Use setup.py to install the package.
```bash
# Assuming you are in root mxnet source code folder
cd python
diff --git a/docs/mxdoc.py b/docs/mxdoc.py
index 7092b9ee9eaa..5e86c1c6dd48 100644
--- a/docs/mxdoc.py
+++ b/docs/mxdoc.py
@@ -40,11 +40,12 @@
for section in [ _DOC_SET ]:
print("Document sets to generate:")
- for candidate in [ 'scala_docs', 'clojure_docs', 'doxygen_docs', 'r_docs' ]:
+ for candidate in [ 'scala_docs', 'java_docs', 'clojure_docs', 'doxygen_docs', 'r_docs' ]:
print('%-12s : %s' % (candidate, parser.get(section, candidate)))
_MXNET_DOCS_BUILD_MXNET = parser.getboolean('mxnet', 'build_mxnet')
_SCALA_DOCS = parser.getboolean(_DOC_SET, 'scala_docs')
+_JAVA_DOCS = parser.getboolean(_DOC_SET, 'java_docs')
_CLOJURE_DOCS = parser.getboolean(_DOC_SET, 'clojure_docs')
_DOXYGEN_DOCS = parser.getboolean(_DOC_SET, 'doxygen_docs')
_R_DOCS = parser.getboolean(_DOC_SET, 'r_docs')
@@ -58,7 +59,8 @@
# language names and the according file extensions and comment symbol
_LANGS = {'python' : ('py', '#'),
'r' : ('R','#'),
- 'scala' : ('scala', '#'),
+ 'scala' : ('scala', '//'),
+ 'java' : ('java', '//'),
'julia' : ('jl', '#'),
'perl' : ('pl', '#'),
'cpp' : ('cc', '//'),
@@ -101,21 +103,39 @@ def build_r_docs(app):
_run_cmd('mkdir -p ' + dest_path + '; mv ' + pdf_path + ' ' + dest_path)
def build_scala(app):
- """build scala for scala docs and clojure docs to use"""
+ """build scala for scala docs, java docs, and clojure docs to use"""
_run_cmd("cd %s/.. && make scalapkg" % app.builder.srcdir)
_run_cmd("cd %s/.. && make scalainstall" % app.builder.srcdir)
def build_scala_docs(app):
"""build scala doc and then move the outdir"""
scala_path = app.builder.srcdir + '/../scala-package'
- # scaldoc fails on some apis, so exit 0 to pass the check
- _run_cmd('cd ' + scala_path + '; scaladoc `find . -type f -name "*.scala" | egrep \"\/core|\/infer\" | egrep -v \"Suite\"`; exit 0')
+ scala_doc_sources = 'find . -type f -name "*.scala" | egrep \"\.\/core|\.\/infer\" | egrep -v \"Suite\"'
+ scala_doc_classpath = ':'.join([
+ '`find native -name "*.jar" | grep "target/lib/" | tr "\\n" ":" `',
+ '`find macros -name "*-SNAPSHOT.jar" | tr "\\n" ":" `'
+ ])
+ _run_cmd('cd {}; scaladoc `{}` -classpath {} -feature -deprecation; exit 0'
+ .format(scala_path, scala_doc_sources, scala_doc_classpath))
dest_path = app.builder.outdir + '/api/scala/docs'
_run_cmd('rm -rf ' + dest_path)
_run_cmd('mkdir -p ' + dest_path)
+ # 'index' and 'package.html' do not exist in later versions of scala; delete these after upgrading scala>2.12.x
scaladocs = ['index', 'index.html', 'org', 'lib', 'index.js', 'package.html']
for doc_file in scaladocs:
- _run_cmd('cd ' + scala_path + ' && mv -f ' + doc_file + ' ' + dest_path)
+ _run_cmd('cd ' + scala_path + ' && mv -f ' + doc_file + ' ' + dest_path + '; exit 0')
+
+def build_java_docs(app):
+ """build java docs and then move the outdir"""
+ java_path = app.builder.srcdir + '/../scala-package/core/src/main/scala/org/apache/mxnet/'
+ # scaldoc fails on some apis, so exit 0 to pass the check
+ _run_cmd('cd ' + java_path + '; scaladoc `find . -type f -name "*.scala" | egrep \"\/javaapi\" | egrep -v \"Suite\"`; exit 0')
+ dest_path = app.builder.outdir + '/api/java/docs'
+ _run_cmd('rm -rf ' + dest_path)
+ _run_cmd('mkdir -p ' + dest_path)
+ javadocs = ['index', 'index.html', 'org', 'lib', 'index.js', 'package.html']
+ for doc_file in javadocs:
+ _run_cmd('cd ' + java_path + ' && mv -f ' + doc_file + ' ' + dest_path + '; exit 0')
def build_clojure_docs(app):
"""build clojure doc and then move the outdir"""
@@ -125,7 +145,7 @@ def build_clojure_docs(app):
_run_cmd('rm -rf ' + dest_path)
_run_cmd('mkdir -p ' + dest_path)
clojure_doc_path = app.builder.srcdir + '/../contrib/clojure-package/target/doc'
- _run_cmd('cd ' + clojure_doc_path + ' && cp -r * ' + dest_path)
+ _run_cmd('cd ' + clojure_doc_path + ' && cp -r * ' + dest_path + '; exit 0')
def _convert_md_table_to_rst(table):
"""Convert a markdown table to rst format"""
@@ -404,7 +424,6 @@ def add_buttons(app, docname, source):
# source[i] = '\n'.join(lines)
def setup(app):
-
# If MXNET_DOCS_BUILD_MXNET is set something different than 1
# Skip the build step
if os.getenv('MXNET_DOCS_BUILD_MXNET', '1') == '1' or _MXNET_DOCS_BUILD_MXNET:
@@ -419,6 +438,9 @@ def setup(app):
if _SCALA_DOCS:
print("Building Scala Docs!")
app.connect("builder-inited", build_scala_docs)
+ if _JAVA_DOCS:
+ print("Building Java Docs!")
+ app.connect("builder-inited", build_java_docs)
if _CLOJURE_DOCS:
print("Building Clojure Docs!")
app.connect("builder-inited", build_clojure_docs)
diff --git a/docs/settings.ini b/docs/settings.ini
index b8e486e58e87..1f2097125ff6 100644
--- a/docs/settings.ini
+++ b/docs/settings.ini
@@ -1,68 +1,85 @@
[mxnet]
build_mxnet = 0
+[document_sets_tutorial]
+clojure_docs = 0
+doxygen_docs = 1
+r_docs = 0
+scala_docs = 0
+
[document_sets_default]
clojure_docs = 1
doxygen_docs = 1
+java_docs = 1
r_docs = 0
scala_docs = 1
[document_sets_1.2.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 1
[document_sets_v1.2.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 1
[document_sets_1.1.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_v1.1.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_1.0.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_v1.0.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_0.12.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_v0.12.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_0.11.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
[document_sets_v0.11.0]
clojure_docs = 0
doxygen_docs = 1
+java_docs = 0
r_docs = 0
scala_docs = 0
diff --git a/example/cnn_visualization/gradcam.py b/docs/tutorial_utils/vision/cnn_visualization/gradcam.py
similarity index 98%
rename from example/cnn_visualization/gradcam.py
rename to docs/tutorial_utils/vision/cnn_visualization/gradcam.py
index a8708f787584..54cb65eef11b 100644
--- a/example/cnn_visualization/gradcam.py
+++ b/docs/tutorial_utils/vision/cnn_visualization/gradcam.py
@@ -249,8 +249,8 @@ def visualize(net, preprocessed_img, orig_img, conv_layer_name):
imggrad = get_image_grad(net, preprocessed_img)
conv_out, conv_out_grad = get_conv_out_grad(net, preprocessed_img, conv_layer_name=conv_layer_name)
- cam = get_cam(imggrad, conv_out)
-
+ cam = get_cam(conv_out_grad, conv_out)
+ cam = cv2.resize(cam, (imggrad.shape[1], imggrad.shape[2]))
ggcam = get_guided_grad_cam(cam, imggrad)
img_ggcam = grad_to_image(ggcam)
diff --git a/docs/tutorials/basic/module.md b/docs/tutorials/basic/module.md
index 191e3baaaffc..f7a4d6e25de7 100644
--- a/docs/tutorials/basic/module.md
+++ b/docs/tutorials/basic/module.md
@@ -39,11 +39,16 @@ training examples each time. A separate iterator is also created for test data.
```python
import logging
+import random
logging.getLogger().setLevel(logging.INFO)
+
import mxnet as mx
import numpy as np
mx.random.seed(1234)
+np.random.seed(1234)
+random.seed(1234)
+
fname = mx.test_utils.download('https://s3.us-east-2.amazonaws.com/mxnet-public/letter_recognition/letter-recognition.data')
data = np.genfromtxt(fname, delimiter=',')[:,1:]
label = np.array([ord(l.split(',')[0])-ord('A') for l in open(fname, 'r')])
@@ -64,7 +69,7 @@ net = mx.sym.FullyConnected(net, name='fc1', num_hidden=64)
net = mx.sym.Activation(net, name='relu1', act_type="relu")
net = mx.sym.FullyConnected(net, name='fc2', num_hidden=26)
net = mx.sym.SoftmaxOutput(net, name='softmax')
-mx.viz.plot_network(net)
+mx.viz.plot_network(net, node_attrs={"shape":"oval","fixedsize":"false"})
```
@@ -135,11 +140,17 @@ for epoch in range(5):
print('Epoch %d, Training %s' % (epoch, metric.get()))
```
- Epoch 0, Training ('accuracy', 0.4554375)
- Epoch 1, Training ('accuracy', 0.6485625)
- Epoch 2, Training ('accuracy', 0.7055625)
- Epoch 3, Training ('accuracy', 0.7396875)
- Epoch 4, Training ('accuracy', 0.764375)
+
+Expected output:
+
+
+```
+Epoch 0, Training ('accuracy', 0.434625)
+Epoch 1, Training ('accuracy', 0.6516875)
+Epoch 2, Training ('accuracy', 0.6968125)
+Epoch 3, Training ('accuracy', 0.7273125)
+Epoch 4, Training ('accuracy', 0.7575625)
+```
To learn more about these APIs, visit [Module API](http://mxnet.io/api/python/module/module.html).
@@ -172,34 +183,36 @@ mod.fit(train_iter,
optimizer='sgd',
optimizer_params={'learning_rate':0.1},
eval_metric='acc',
- num_epoch=8)
+ num_epoch=7)
```
- INFO:root:Epoch[0] Train-accuracy=0.364625
- INFO:root:Epoch[0] Time cost=0.388
- INFO:root:Epoch[0] Validation-accuracy=0.557250
- INFO:root:Epoch[1] Train-accuracy=0.633625
- INFO:root:Epoch[1] Time cost=0.470
- INFO:root:Epoch[1] Validation-accuracy=0.634750
- INFO:root:Epoch[2] Train-accuracy=0.697187
- INFO:root:Epoch[2] Time cost=0.402
- INFO:root:Epoch[2] Validation-accuracy=0.665500
- INFO:root:Epoch[3] Train-accuracy=0.735062
- INFO:root:Epoch[3] Time cost=0.402
- INFO:root:Epoch[3] Validation-accuracy=0.713000
- INFO:root:Epoch[4] Train-accuracy=0.762563
- INFO:root:Epoch[4] Time cost=0.408
- INFO:root:Epoch[4] Validation-accuracy=0.742000
- INFO:root:Epoch[5] Train-accuracy=0.782312
- INFO:root:Epoch[5] Time cost=0.400
- INFO:root:Epoch[5] Validation-accuracy=0.778500
- INFO:root:Epoch[6] Train-accuracy=0.797188
- INFO:root:Epoch[6] Time cost=0.392
- INFO:root:Epoch[6] Validation-accuracy=0.798250
- INFO:root:Epoch[7] Train-accuracy=0.807750
- INFO:root:Epoch[7] Time cost=0.401
- INFO:root:Epoch[7] Validation-accuracy=0.789250
+Expected output:
+
+
+```
+INFO:root:Epoch[0] Train-accuracy=0.325437
+INFO:root:Epoch[0] Time cost=0.550
+INFO:root:Epoch[0] Validation-accuracy=0.568500
+INFO:root:Epoch[1] Train-accuracy=0.622188
+INFO:root:Epoch[1] Time cost=0.552
+INFO:root:Epoch[1] Validation-accuracy=0.656500
+INFO:root:Epoch[2] Train-accuracy=0.694375
+INFO:root:Epoch[2] Time cost=0.566
+INFO:root:Epoch[2] Validation-accuracy=0.703500
+INFO:root:Epoch[3] Train-accuracy=0.732187
+INFO:root:Epoch[3] Time cost=0.562
+INFO:root:Epoch[3] Validation-accuracy=0.748750
+INFO:root:Epoch[4] Train-accuracy=0.755375
+INFO:root:Epoch[4] Time cost=0.484
+INFO:root:Epoch[4] Validation-accuracy=0.761500
+INFO:root:Epoch[5] Train-accuracy=0.773188
+INFO:root:Epoch[5] Time cost=0.383
+INFO:root:Epoch[5] Validation-accuracy=0.715000
+INFO:root:Epoch[6] Train-accuracy=0.794687
+INFO:root:Epoch[6] Time cost=0.378
+INFO:root:Epoch[6] Validation-accuracy=0.802250
+```
By default, `fit` function has `eval_metric` set to `accuracy`, `optimizer` to `sgd`
and optimizer_params to `(('learning_rate', 0.01),)`.
@@ -225,12 +238,17 @@ It can be used as follows:
```python
score = mod.score(val_iter, ['acc'])
print("Accuracy score is %f" % (score[0][1]))
-assert score[0][1] > 0.77, "Achieved accuracy (%f) is less than expected (0.77)" % score[0][1]
+assert score[0][1] > 0.76, "Achieved accuracy (%f) is less than expected (0.76)" % score[0][1]
```
- Accuracy score is 0.789250
+
+Expected output:
+```
+Accuracy score is 0.802250
+```
+
Some of the other metrics which can be used are `top_k_acc`(top-k-accuracy),
`F1`, `RMSE`, `MSE`, `MAE`, `ce`(CrossEntropy). To learn more about the metrics,
visit [Evaluation metric](http://mxnet.io/api/python/metric/metric.html).
@@ -252,22 +270,27 @@ mod = mx.mod.Module(symbol=net)
mod.fit(train_iter, num_epoch=5, epoch_end_callback=checkpoint)
```
- INFO:root:Epoch[0] Train-accuracy=0.101062
- INFO:root:Epoch[0] Time cost=0.422
- INFO:root:Saved checkpoint to "mx_mlp-0001.params"
- INFO:root:Epoch[1] Train-accuracy=0.263313
- INFO:root:Epoch[1] Time cost=0.785
- INFO:root:Saved checkpoint to "mx_mlp-0002.params"
- INFO:root:Epoch[2] Train-accuracy=0.452188
- INFO:root:Epoch[2] Time cost=0.624
- INFO:root:Saved checkpoint to "mx_mlp-0003.params"
- INFO:root:Epoch[3] Train-accuracy=0.544125
- INFO:root:Epoch[3] Time cost=0.427
- INFO:root:Saved checkpoint to "mx_mlp-0004.params"
- INFO:root:Epoch[4] Train-accuracy=0.605250
- INFO:root:Epoch[4] Time cost=0.399
- INFO:root:Saved checkpoint to "mx_mlp-0005.params"
+Expected output:
+
+
+```
+INFO:root:Epoch[0] Train-accuracy=0.098437
+INFO:root:Epoch[0] Time cost=0.421
+INFO:root:Saved checkpoint to "mx_mlp-0001.params"
+INFO:root:Epoch[1] Train-accuracy=0.257437
+INFO:root:Epoch[1] Time cost=0.520
+INFO:root:Saved checkpoint to "mx_mlp-0002.params"
+INFO:root:Epoch[2] Train-accuracy=0.457250
+INFO:root:Epoch[2] Time cost=0.562
+INFO:root:Saved checkpoint to "mx_mlp-0003.params"
+INFO:root:Epoch[3] Train-accuracy=0.558187
+INFO:root:Epoch[3] Time cost=0.434
+INFO:root:Saved checkpoint to "mx_mlp-0004.params"
+INFO:root:Epoch[4] Train-accuracy=0.617750
+INFO:root:Epoch[4] Time cost=0.414
+INFO:root:Saved checkpoint to "mx_mlp-0005.params"
+```
To load the saved module parameters, call the `load_checkpoint` function. It
loads the Symbol and the associated parameters. We can then set the loaded
@@ -299,16 +322,25 @@ mod.fit(train_iter,
assert score[0][1] > 0.77, "Achieved accuracy (%f) is less than expected (0.77)" % score[0][1]
```
- INFO:root:Epoch[3] Train-accuracy=0.544125
- INFO:root:Epoch[3] Time cost=0.398
- INFO:root:Epoch[4] Train-accuracy=0.605250
- INFO:root:Epoch[4] Time cost=0.545
- INFO:root:Epoch[5] Train-accuracy=0.644312
- INFO:root:Epoch[5] Time cost=0.592
- INFO:root:Epoch[6] Train-accuracy=0.675000
- INFO:root:Epoch[6] Time cost=0.491
- INFO:root:Epoch[7] Train-accuracy=0.695812
- INFO:root:Epoch[7] Time cost=0.363
+
+Expected output:
+
+
+```
+INFO:root:Epoch[3] Train-accuracy=0.555438
+INFO:root:Epoch[3] Time cost=0.377
+INFO:root:Epoch[4] Train-accuracy=0.616625
+INFO:root:Epoch[4] Time cost=0.457
+INFO:root:Epoch[5] Train-accuracy=0.658438
+INFO:root:Epoch[5] Time cost=0.518
+...........................................
+INFO:root:Epoch[18] Train-accuracy=0.788687
+INFO:root:Epoch[18] Time cost=0.532
+INFO:root:Epoch[19] Train-accuracy=0.789562
+INFO:root:Epoch[19] Time cost=0.531
+INFO:root:Epoch[20] Train-accuracy=0.796250
+INFO:root:Epoch[20] Time cost=0.531
+```
diff --git a/docs/tutorials/basic/symbol.md b/docs/tutorials/basic/symbol.md
index 7ebcadfc16f3..5e1e3cd8c62f 100644
--- a/docs/tutorials/basic/symbol.md
+++ b/docs/tutorials/basic/symbol.md
@@ -89,7 +89,7 @@ f = mx.sym.reshape(d+e, shape=(1,4))
# broadcast
g = mx.sym.broadcast_to(f, shape=(2,4))
# plot
-mx.viz.plot_network(symbol=g)
+mx.viz.plot_network(symbol=g, node_attrs={"shape":"oval","fixedsize":"false"})
```
The computations declared in the above examples can be bound to the input data
@@ -108,7 +108,7 @@ net = mx.sym.FullyConnected(data=net, name='fc1', num_hidden=128)
net = mx.sym.Activation(data=net, name='relu1', act_type="relu")
net = mx.sym.FullyConnected(data=net, name='fc2', num_hidden=10)
net = mx.sym.SoftmaxOutput(data=net, name='out')
-mx.viz.plot_network(net, shape={'data':(100,200)})
+mx.viz.plot_network(net, shape={'data':(100,200)}, node_attrs={"shape":"oval","fixedsize":"false"})
```
Each symbol takes a (unique) string name. NDArray and Symbol both represent
@@ -211,7 +211,7 @@ def ConvFactory(data, num_filter, kernel, stride=(1,1), pad=(0, 0),name=None, su
prev = mx.sym.Variable(name="Previous Output")
conv_comp = ConvFactory(data=prev, num_filter=64, kernel=(7,7), stride=(2, 2))
shape = {"Previous Output" : (128, 3, 28, 28)}
-mx.viz.plot_network(symbol=conv_comp, shape=shape)
+mx.viz.plot_network(symbol=conv_comp, shape=shape, node_attrs={"shape":"oval","fixedsize":"false"})
```
Then we can define a function that constructs an inception module based on
@@ -237,7 +237,7 @@ def InceptionFactoryA(data, num_1x1, num_3x3red, num_3x3, num_d3x3red, num_d3x3,
return concat
prev = mx.sym.Variable(name="Previous Output")
in3a = InceptionFactoryA(prev, 64, 64, 64, 64, 96, "avg", 32, name="in3a")
-mx.viz.plot_network(symbol=in3a, shape=shape)
+mx.viz.plot_network(symbol=in3a, shape=shape, node_attrs={"shape":"oval","fixedsize":"false"})
```
Finally, we can obtain the whole network by chaining multiple inception
diff --git a/docs/tutorials/c++/subgraphAPI.md b/docs/tutorials/c++/subgraphAPI.md
new file mode 100644
index 000000000000..0ae4341b287c
--- /dev/null
+++ b/docs/tutorials/c++/subgraphAPI.md
@@ -0,0 +1,104 @@
+## Subgraph API
+
+The subgraph API has been proposed and implemented as the default mechanism for integrating backend libraries to MXNet. The subgraph API is a very flexible interface. Although it was proposed as an integration mechanism, it has been used as a tool for manipulating NNVM graphs for graph-level optimizations, such as operator fusion.
+
+The subgraph API works as the following steps:
+
+* Search for particular patterns in a graph.
+* Group the operators/nodes with particular patterns into a subgraph and shrink the subgraph into a single node.
+* Replace the subgraph in the original graph with the subgraph node.
+
+The figure below illustrates the subgraph mechanism.
+
+![](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/tutorials/subgraph/subgraph.png)
+
+The subgraph API allows the backend developers to customize the subgraph mechanism in two places:
+
+* Subgraph searching: define a subgraph selector to search for particular patterns in a computation graph.
+* Subgraph node creation: attach an operator to run the computation in the subgraph. We can potentially manipulate the subgraph here.
+
+
+The following is a demonstration of how the subgraph API can be applied to a simple task. Refer to the previous figure for an overview of the process. That is, replacing `Convolution` and `BatchNorm` with the conv_bn.
+
+The first step is to define a subgraph selector to find the required pattern. To find a pattern that has `Convolution` and `BatchNorm`, we can start the search on the node with `Convolution`. Then from the `Convolution` node, we search for `BatchNorm` along the outgoing edge.
+
+```C++
+class SgSelector : public SubgraphSelector {
+ public:
+ SgSelector() {
+ find_bn = false;
+ }
+ bool Select(const nnvm::Node &n) override {
+ // Here we start on the Convolution node to search for a subgraph.
+ return n.op() && n.op()->name == "Convolution";
+ }
+ bool SelectInput(const nnvm::Node &n, const nnvm::Node &new_node) override {
+ // We don't need to search on the incoming edge.
+ return false;
+ }
+ bool SelectOutput(const nnvm::Node &n, const nnvm::Node &new_node) override {
+ // We search on the outgoing edge. Once we find a BatchNorm node, we won't
+ // accept any more BatchNorm nodes.
+ if (new_node.op() && new_node.op()->name == "BatchNorm" && !find_bn) {
+ find_bn = true;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ std::vector
Filter(const std::vector &candidates) override {
+ // We might have found a Convolution node, but we might have failed to find a BatchNorm
+ // node that uses the output of the Convolution node. If we failed, we should skip
+ // the Convolution node as well.
+ if (find_bn)
+ return candidates;
+ else
+ return std::vector();
+ }
+ private:
+ bool find_bn;
+};
+```
+
+The second step is to define a subgraph property to use the subgraph selector above to customize the subgraph searching. By defining this class, we can also customize subgraph node creation. When customizing node creation, we can specify what operator to run the subgraph on the node. In this example, we use `CachedOp`, which itself is a graph executor, to run the subgraph with `Convolution` and `BatchNorm`. In practice, it's most likely that we use a single operator from a backend library to replace the two operators for execution.
+
+```C++
+class SgProperty : public SubgraphProperty {
+ public:
+ static SubgraphPropertyPtr Create() {
+ return std::make_shared();
+ }
+ nnvm::NodePtr CreateSubgraphNode(
+ const nnvm::Symbol &sym, const int subgraph_id = 0) const override {
+ // We can use CachedOp to execute the subgraph.
+ nnvm::NodePtr n = nnvm::Node::Create();
+ n->attrs.op = Op::Get("_CachedOp");
+ n->attrs.name = "ConvBN" + std::to_string(subgraph_id);
+ n->attrs.subgraphs.push_back(std::make_shared(sym));
+ std::vector > flags{{"static_alloc", "true"}};
+ n->attrs.parsed = CachedOpPtr(new CachedOp(sym, flags));
+ return n;
+ }
+ SubgraphSelectorPtr CreateSubgraphSelector() const override {
+ return std::make_shared();
+ }
+};
+```
+
+After defining the subgraph property, we need to register it.
+
+```C++
+MXNET_REGISTER_SUBGRAPH_PROPERTY(SgTest, SgProperty);
+```
+
+After compiling this subgraph mechanism into MXNet, we can use the environment variable `MXNET_SUBGRAPH_BACKEND` to activate it.
+
+```bash
+export MXNET_SUBGRAPH_BACKEND=SgTest
+```
+
+This tutorial shows a simple example of how to use the subgraph API to search for patterns in an NNVM graph.
+Intested users can try different pattern matching rules (i.e., define their own `SubgraphSelector`) and
+attach different operators to execute the subgraphs.
+
+
diff --git a/docs/tutorials/control_flow/ControlFlowTutorial.md b/docs/tutorials/control_flow/ControlFlowTutorial.md
index 9e4c66f8521d..4b6a23136b5d 100644
--- a/docs/tutorials/control_flow/ControlFlowTutorial.md
+++ b/docs/tutorials/control_flow/ControlFlowTutorial.md
@@ -15,13 +15,13 @@ from mxnet.gluon import HybridBlock
## foreach
`foreach` is a for loop that iterates over the first dimension of the input data (it can be an array or a list of arrays). It is defined with the following signature:
-```python
+```
foreach(body, data, init_states, name) => (outputs, states)
```
It runs the Python function defined in `body` for every slice from the input arrays. The signature of the `body` function is defined as follows:
-```python
+```
body(data, states) => (outputs, states)
```
@@ -243,13 +243,13 @@ res, states = lstm(rnn_data, [x for x in init_states], valid_length)
## while_loop
`while_loop` defines a while loop. It has the following signature:
-```python
+```
while_loop(cond, body, loop_vars, max_iterations, name) => (outputs, states)
```
Instead of running over the first dimension of an array, `while_loop` checks a condition function in every iteration and runs a `body` function for computation. The signature of the `body` function is defined as follows:
-```python
+```
body(state1, state2, ...) => (outputs, states)
```
@@ -297,13 +297,13 @@ print(state)
## cond
`cond` defines an if condition. It has the following signature:
-```python
+```
cond(pred, then_func, else_func, name)
```
`cond` checks `pred`, which is a symbol or an NDArray with one element. If its value is true, it calls `then_func`. Otherwise, it calls `else_func`. The signature of `then_func` and `else_func` are as follows:
-```python
+```
func() => [outputs]
```
diff --git a/docs/tutorials/gluon/hybrid.md b/docs/tutorials/gluon/hybrid.md
index f9f2c112f532..6d64acdce275 100644
--- a/docs/tutorials/gluon/hybrid.md
+++ b/docs/tutorials/gluon/hybrid.md
@@ -125,6 +125,7 @@ with other language front-ends like C, C++ and Scala. To this end, we simply
use `export` and `SymbolBlock.imports`:
```python
+net(x)
net.export('model', epoch=1)
```
diff --git a/docs/tutorials/gluon/info_gan.md b/docs/tutorials/gluon/info_gan.md
new file mode 100644
index 000000000000..c8f07c6fda35
--- /dev/null
+++ b/docs/tutorials/gluon/info_gan.md
@@ -0,0 +1,437 @@
+
+# Image similarity search with InfoGAN
+
+This notebook shows how to implement an InfoGAN based on Gluon. InfoGAN is an extension of GANs, where the generator input is split in 2 parts: random noise and a latent code (see [InfoGAN Paper](https://arxiv.org/pdf/1606.03657.pdf)).
+The codes are made meaningful by maximizing the mutual information between code and generator output. InfoGAN learns a disentangled representation in a completely unsupervised manner. It can be used for many applications such as image similarity search. This notebook uses the DCGAN example from the [Straight Dope Book](https://gluon.mxnet.io/chapter14_generative-adversarial-networks/dcgan.html) and extends it to create an InfoGAN.
+
+
+```python
+from __future__ import print_function
+from datetime import datetime
+import logging
+import multiprocessing
+import os
+import sys
+import tarfile
+import time
+
+import numpy as np
+from matplotlib import pyplot as plt
+from mxboard import SummaryWriter
+import mxnet as mx
+from mxnet import gluon
+from mxnet import ndarray as nd
+from mxnet.gluon import nn, utils
+from mxnet import autograd
+
+```
+
+The latent code vector can contain several variables, which can be categorical and/or continuous. We set `n_continuous` to 2 and `n_categories` to 10.
+
+
+```python
+batch_size = 64
+z_dim = 100
+n_continuous = 2
+n_categories = 10
+ctx = mx.gpu() if mx.test_utils.list_gpus() else mx.cpu()
+```
+
+Some functions to load and normalize images.
+
+
+```python
+lfw_url = 'http://vis-www.cs.umass.edu/lfw/lfw-deepfunneled.tgz'
+data_path = 'lfw_dataset'
+if not os.path.exists(data_path):
+ os.makedirs(data_path)
+ data_file = utils.download(lfw_url)
+ with tarfile.open(data_file) as tar:
+ tar.extractall(path=data_path)
+
+```
+
+
+```python
+def transform(data, width=64, height=64):
+ data = mx.image.imresize(data, width, height)
+ data = nd.transpose(data, (2,0,1))
+ data = data.astype(np.float32)/127.5 - 1
+ if data.shape[0] == 1:
+ data = nd.tile(data, (3, 1, 1))
+ return data.reshape((1,) + data.shape)
+```
+
+
+```python
+def get_files(data_dir):
+ images = []
+ filenames = []
+ for path, _, fnames in os.walk(data_dir):
+ for fname in fnames:
+ if not fname.endswith('.jpg'):
+ continue
+ img = os.path.join(path, fname)
+ img_arr = mx.image.imread(img)
+ img_arr = transform(img_arr)
+ images.append(img_arr)
+ filenames.append(path + "/" + fname)
+ return images, filenames
+```
+
+Load the dataset `lfw_dataset` which contains images of celebrities.
+
+
+```python
+data_dir = 'lfw_dataset'
+images, filenames = get_files(data_dir)
+split = int(len(images)*0.8)
+test_images = images[split:]
+test_filenames = filenames[split:]
+train_images = images[:split]
+train_filenames = filenames[:split]
+
+train_data = gluon.data.ArrayDataset(nd.concatenate(train_images))
+train_dataloader = gluon.data.DataLoader(train_data, batch_size=batch_size, shuffle=True, last_batch='rollover', num_workers=multiprocessing.cpu_count()-1)
+```
+
+## Generator
+Define the Generator model. Architecture is taken from the DCGAN implementation in [Straight Dope Book](https://gluon.mxnet.io/chapter14_generative-adversarial-networks/dcgan.html). The Generator consist of 4 layers where each layer involves a strided convolution, batch normalization, and rectified nonlinearity. It takes as input random noise and the latent code and produces an `(64,64,3)` output image.
+
+
+```python
+class Generator(gluon.HybridBlock):
+ def __init__(self, **kwargs):
+ super(Generator, self).__init__(**kwargs)
+ with self.name_scope():
+ self.prev = nn.HybridSequential()
+ self.prev.add(nn.Dense(1024, use_bias=False), nn.BatchNorm(), nn.Activation(activation='relu'))
+ self.G = nn.HybridSequential()
+
+ self.G.add(nn.Conv2DTranspose(64 * 8, 4, 1, 0, use_bias=False))
+ self.G.add(nn.BatchNorm())
+ self.G.add(nn.Activation('relu'))
+ self.G.add(nn.Conv2DTranspose(64 * 4, 4, 2, 1, use_bias=False))
+ self.G.add(nn.BatchNorm())
+ self.G.add(nn.Activation('relu'))
+ self.G.add(nn.Conv2DTranspose(64 * 2, 4, 2, 1, use_bias=False))
+ self.G.add(nn.BatchNorm())
+ self.G.add(nn.Activation('relu'))
+ self.G.add(nn.Conv2DTranspose(64, 4, 2, 1, use_bias=False))
+ self.G.add(nn.BatchNorm())
+ self.G.add(nn.Activation('relu'))
+ self.G.add(nn.Conv2DTranspose(3, 4, 2, 1, use_bias=False))
+ self.G.add(nn.Activation('tanh'))
+
+ def hybrid_forward(self, F, x):
+ x = self.prev(x)
+ x = F.reshape(x, (0, -1, 1, 1))
+ return self.G(x)
+```
+
+## Discriminator
+Define the Discriminator and Q model. The Q model shares many layers with the Discriminator. Its task is to estimate the code `c` for a given fake image. It is used to maximize the lower bound to the mutual information.
+
+
+```python
+class Discriminator(gluon.HybridBlock):
+ def __init__(self, **kwargs):
+ super(Discriminator, self).__init__(**kwargs)
+ with self.name_scope():
+ self.D = nn.HybridSequential()
+ self.D.add(nn.Conv2D(64, 4, 2, 1, use_bias=False))
+ self.D.add(nn.LeakyReLU(0.2))
+ self.D.add(nn.Conv2D(64 * 2, 4, 2, 1, use_bias=False))
+ self.D.add(nn.BatchNorm())
+ self.D.add(nn.LeakyReLU(0.2))
+ self.D.add(nn.Conv2D(64 * 4, 4, 2, 1, use_bias=False))
+ self.D.add(nn.BatchNorm())
+ self.D.add(nn.LeakyReLU(0.2))
+ self.D.add(nn.Conv2D(64 * 8, 4, 2, 1, use_bias=False))
+ self.D.add(nn.BatchNorm())
+ self.D.add(nn.LeakyReLU(0.2))
+
+ self.D.add(nn.Dense(1024, use_bias=False), nn.BatchNorm(), nn.Activation(activation='relu'))
+
+ self.prob = nn.Dense(1)
+ self.feat = nn.HybridSequential()
+ self.feat.add(nn.Dense(128, use_bias=False), nn.BatchNorm(), nn.Activation(activation='relu'))
+ self.category_prob = nn.Dense(n_categories)
+ self.continuous_mean = nn.Dense(n_continuous)
+ self.Q = nn.HybridSequential()
+ self.Q.add(self.feat, self.category_prob, self.continuous_mean)
+
+ def hybrid_forward(self, F, x):
+ x = self.D(x)
+ prob = self.prob(x)
+ feat = self.feat(x)
+ category_prob = self.category_prob(feat)
+ continuous_mean = self.continuous_mean(feat)
+
+ return prob, category_prob, continuous_mean
+```
+
+The InfoGAN has the following layout.
+
+
+Discriminator and Generator are the same as in the DCGAN example. On top of the Disciminator is the Q model, which is estimating the code `c` for given fake images. The Generator's input is random noise and the latent code `c`.
+
+## Training Loop
+Initialize Generator and Discriminator and define correspoing trainer function.
+
+
+```python
+generator = Generator()
+generator.hybridize()
+generator.initialize(mx.init.Normal(0.002), ctx=ctx)
+
+discriminator = Discriminator()
+discriminator.hybridize()
+discriminator.initialize(mx.init.Normal(0.002), ctx=ctx)
+
+lr = 0.0001
+beta = 0.5
+
+g_trainer = gluon.Trainer(generator.collect_params(), 'adam', {'learning_rate': lr, 'beta1': beta})
+d_trainer = gluon.Trainer(discriminator.collect_params(), 'adam', {'learning_rate': lr, 'beta1': beta})
+q_trainer = gluon.Trainer(discriminator.Q.collect_params(), 'adam', {'learning_rate': lr, 'beta1': beta})
+```
+
+Create vectors with real (=1) and fake labels (=0).
+
+
+```python
+real_label = nd.ones((batch_size,), ctx=ctx)
+fake_label = nd.zeros((batch_size,),ctx=ctx)
+```
+
+Load a pretrained model.
+
+
+```python
+if os.path.isfile('infogan_d_latest.params') and os.path.isfile('infogan_g_latest.params'):
+ discriminator.load_parameters('infogan_d_latest.params', ctx=ctx, allow_missing=True, ignore_extra=True)
+ generator.load_parameters('infogan_g_latest.params', ctx=ctx, allow_missing=True, ignore_extra=True)
+```
+There are 2 differences between InfoGAN and DCGAN: the extra latent code and the Q network to estimate the code.
+The latent code is part of the Generator input and it contains mutliple variables (continuous, categorical) that can represent different distributions. In order to make sure that the Generator uses the latent code, mutual information is introduced into the GAN loss term. Mutual information measures how much X is known given Y or vice versa. It is defined as:
+
+![gif](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/doc/tutorials/info_gan/entropy.gif)
+
+The InfoGAN loss is:
+
+![gif](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/doc/tutorials/info_gan/loss.gif)
+
+where `V(D,G)` is the GAN loss and the mutual information `I(c, G(z, c))` goes in as regularization. The goal is to reach high mutual information, in order to learn meaningful codes for the data.
+
+
+Define the loss functions. `SoftmaxCrossEntropyLoss` for the categorical code, `L2Loss` for the continious code and `SigmoidBinaryCrossEntropyLoss` for the normal GAN loss.
+
+
+```python
+loss1 = gluon.loss.SigmoidBinaryCrossEntropyLoss()
+loss2 = gluon.loss.L2Loss()
+loss3 = gluon.loss.SoftmaxCrossEntropyLoss()
+```
+
+This function samples `c`, `z`, and concatenates them to create the generator input.
+
+
+```python
+def create_generator_input():
+
+ #create random noise
+ z = nd.random_normal(0, 1, shape=(batch_size, z_dim), ctx=ctx)
+ label = nd.array(np.random.randint(n_categories, size=batch_size)).as_in_context(ctx)
+ c1 = nd.one_hot(label, depth=n_categories).as_in_context(ctx)
+ c2 = nd.random.uniform(-1, 1, shape=(batch_size, n_continuous)).as_in_context(ctx)
+
+ # concatenate random noise with c which will be the input of the generator
+ return nd.concat(z, c1, c2, dim=1), label, c2
+```
+
+Define the training loop.
+1. The discriminator receives `real_data` and `loss1` measures how many real images have been identified as real
+2. The discriminator receives `fake_image` from the Generator and `loss1` measures how many fake images have been identified as fake
+3. Update Discriminator. Currently, it is updated every second iteration in order to avoid that the Discriminator becomes too strong. You may want to change that.
+4. The updated discriminator receives `fake_image` and `loss1` measures how many fake images have been been identified as real, `loss2` measures the difference between the sampled continuous latent code `c` and the output of the Q model and `loss3` measures the difference between the sampled categorical latent code `c` and the output of the Q model.
+4. Update Generator and Q
+
+
+```python
+with SummaryWriter(logdir='./logs/') as sw:
+
+ epochs = 1
+ counter = 0
+ for epoch in range(epochs):
+ print("Epoch", epoch)
+ starttime = time.time()
+
+ d_error_epoch = nd.zeros((1,), ctx=ctx)
+ g_error_epoch = nd.zeros((1,), ctx=ctx)
+
+ for idx, data in enumerate(train_dataloader):
+
+ #get real data and generator input
+ real_data = data.as_in_context(ctx)
+ g_input, label, c2 = create_generator_input()
+
+
+ #Update discriminator: Input real data and fake data
+ with autograd.record():
+ output_real,_,_ = discriminator(real_data)
+ d_error_real = loss1(output_real, real_label)
+
+ # create fake image and input it to discriminator
+ fake_image = generator(g_input)
+ output_fake,_,_ = discriminator(fake_image.detach())
+ d_error_fake = loss1(output_fake, fake_label)
+
+ # total discriminator error
+ d_error = d_error_real + d_error_fake
+
+ d_error_epoch += d_error.mean()
+
+ #Update D every second iteration
+ if (counter+1) % 2 == 0:
+ d_error.backward()
+ d_trainer.step(batch_size)
+
+ #Update generator: Input random noise and latent code vector
+ with autograd.record():
+ fake_image = generator(g_input)
+ output_fake, category_prob, continuous_mean = discriminator(fake_image)
+ g_error = loss1(output_fake, real_label) + loss3(category_prob, label) + loss2(c2, continuous_mean)
+
+ g_error.backward()
+ g_error_epoch += g_error.mean()
+
+ g_trainer.step(batch_size)
+ q_trainer.step(batch_size)
+
+ # logging
+ if idx % 10 == 0:
+
+ logging.info('speed: {} samples/s'.format(batch_size / (time.time() - starttime)))
+ logging.info('discriminator loss = %f, generator loss = %f at iter %d epoch %d'
+ %(d_error_epoch.asscalar()/idx,g_error_epoch.asscalar()/idx, idx, epoch))
+
+ g_input,_,_ = create_generator_input()
+
+ # create some fake image for logging in MXBoard
+ fake_image = generator(g_input)
+
+ sw.add_scalar(tag='Loss_D', value={'test':d_error_epoch.asscalar()/idx}, global_step=counter)
+ sw.add_scalar(tag='Loss_G', value={'test':d_error_epoch.asscalar()/idx}, global_step=counter)
+ sw.add_image(tag='data_image', image=((fake_image[0]+ 1.0) * 127.5).astype(np.uint8) , global_step=counter)
+ sw.flush()
+
+ discriminator.save_parameters("infogan_d_latest.params")
+ generator.save_parameters("infogan_g_latest.params")
+```
+
+## Image similarity
+Once the InfoGAN is trained, we can use the Discriminator to do an image similarity search. The idea is that the network learned meaningful features from the images based on the mutual information e.g. pose of people in an image.
+
+Load the trained discriminator and retrieve one of its last layers.
+
+
+```python
+discriminator = Discriminator()
+discriminator.load_parameters("infogan_d_latest.params", ctx=ctx, ignore_extra=True)
+
+discriminator = discriminator.D[:11]
+print (discriminator)
+
+discriminator.hybridize()
+```
+
+Nearest neighbor function, which takes a matrix of features and an input feature vector. It returns the 3 closest features.
+
+
+```python
+def get_knn(features, input_vector, k=3):
+ dist = (nd.square(features - input_vector).sum(axis=1))/features.shape[0]
+ indices = dist.asnumpy().argsort()[:k]
+ return [(index, dist[index].asscalar()) for index in indices]
+```
+
+A helper function to visualize image data.
+
+
+```python
+def visualize(img_array):
+ plt.imshow(((img_array.asnumpy().transpose(1, 2, 0) + 1.0) * 127.5).astype(np.uint8))
+ plt.axis('off')
+```
+
+Take some images from the test data, obtain its feature vector from `discriminator.D[:11]` and plot images of the corresponding closest vectors in the feature space.
+
+
+```python
+feature_size = 8192
+
+features = nd.zeros((len(test_images), feature_size), ctx=ctx)
+
+for idx, image in enumerate(test_images):
+
+ feature = discriminator(nd.array(image, ctx=ctx))
+ feature = feature.reshape(feature_size,)
+ features[idx,:] = feature.copyto(ctx)
+
+
+for image in test_images[:100]:
+
+ feature = discriminator(mx.nd.array(image, ctx=ctx))
+ feature = feature.reshape((feature_size,))
+ image = image.reshape((3,64,64))
+
+
+ indices = get_knn(features, feature, k=10)
+ fig = plt.figure(figsize=(15,12))
+ plt.subplot(1,10,1)
+
+ visualize(image)
+ for i in range(2,9):
+ if indices[i-1][1] < 1.5:
+ plt.subplot(1,10,i)
+ sim = test_images[indices[i-1][0]].reshape(3,64,64)
+ visualize(sim)
+ plt.show()
+ plt.clf()
+```
+![png](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/doc/tutorials/info_gan/output.png)
+
+## How the Generator learns
+We trained the Generator for a couple of epochs and stored a couple of fake images per epoch. Check the video.
+ ![alt text](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/doc/tutorials/info_gan/infogan.gif)
+
+
+The following function computes the TSNE on the feature matrix and stores the result in a json-file. This file can be loaded with [TSNEViewer](https://ml4a.github.io/guides/ImageTSNEViewer/)
+
+
+```python
+import json
+
+from sklearn.manifold import TSNE
+from scipy.spatial import distance
+
+tsne = TSNE(n_components=2, learning_rate=150, perplexity=30, verbose=2).fit_transform(features.asnumpy())
+
+# save data to json
+data = []
+counter = 0
+for i,f in enumerate(test_filenames):
+
+ point = [float((tsne[i,k] - np.min(tsne[:,k]))/(np.max(tsne[:,k]) - np.min(tsne[:,k]))) for k in range(2) ]
+ data.append({"path": os.path.abspath(os.path.join(os.getcwd(),f)), "point": point})
+
+with open("imagetsne.json", 'w') as outfile:
+ json.dump(data, outfile)
+```
+
+Load the file with TSNEViewer. You can now inspect whether similiar looking images are grouped nearby or not.
+
+
+
+
diff --git a/docs/tutorials/gluon/learning_rate_finder.md b/docs/tutorials/gluon/learning_rate_finder.md
index 661a017099e6..b571a53f674c 100644
--- a/docs/tutorials/gluon/learning_rate_finder.md
+++ b/docs/tutorials/gluon/learning_rate_finder.md
@@ -80,7 +80,6 @@ We also adjust our `DataLoader` so that it continuously provides batches of data
```python
-from multiprocessing import cpu_count
from mxnet.gluon.data.vision import transforms
transform = transforms.Compose([
@@ -109,7 +108,7 @@ class ContinuousBatchSampler():
sampler = mx.gluon.data.RandomSampler(len(dataset))
batch_sampler = ContinuousBatchSampler(sampler, batch_size=128)
-data_loader = mx.gluon.data.DataLoader(dataset, batch_sampler=batch_sampler, num_workers=cpu_count())
+data_loader = mx.gluon.data.DataLoader(dataset, batch_sampler=batch_sampler)
```
## Implementation
@@ -143,7 +142,7 @@ class LRFinder():
self.learner.trainer._init_kvstore()
# Store params and optimizer state for restore after lr_finder procedure
# Useful for applying the method partway through training, not just for initialization of lr.
- self.learner.net.save_params("lr_finder.params")
+ self.learner.net.save_parameters("lr_finder.params")
self.learner.trainer.save_states("lr_finder.state")
lr = lr_start
self.results = [] # List of (lr, loss) tuples
@@ -156,7 +155,7 @@ class LRFinder():
break
lr = lr * lr_multiplier
# Restore params (as finder changed them)
- self.learner.net.load_params("lr_finder.params", ctx=self.learner.ctx)
+ self.learner.net.load_parameters("lr_finder.params", ctx=self.learner.ctx)
self.learner.trainer.load_states("lr_finder.state")
return self.results
@@ -231,10 +230,10 @@ As discussed before, we should select a learning rate where the loss is falling
```python
-learner.net.save_params("net.params")
+learner.net.save_parameters("net.params")
lr = 0.05
-for iter_idx in range(500):
+for iter_idx in range(300):
learner.iteration(lr=lr)
if ((iter_idx % 100) == 0):
print("Iteration: {}, Loss: {:.5g}".format(iter_idx, learner.iteration_loss))
@@ -247,9 +246,6 @@ Iteration: 100, Loss: 1.6653
Iteration: 200, Loss: 1.4891
-Iteration: 300, Loss: 1.0846
-
-Iteration: 400, Loss: 1.0633
Final Loss: 1.1812
@@ -262,10 +258,10 @@ And now we have a baseline, let's see what happens when we train with a learning
```python
net = mx.gluon.model_zoo.vision.resnet18_v2(classes=10)
learner = Learner(net=net, data_loader=data_loader, ctx=ctx)
-learner.net.load_params("net.params", ctx=ctx)
+learner.net.load_parameters("net.params", ctx=ctx)
lr = 0.5
-for iter_idx in range(500):
+for iter_idx in range(300):
learner.iteration(lr=lr)
if ((iter_idx % 100) == 0):
print("Iteration: {}, Loss: {:.5g}".format(iter_idx, learner.iteration_loss))
@@ -278,9 +274,6 @@ Iteration: 100, Loss: 1.9666
Iteration: 200, Loss: 1.6919
-Iteration: 300, Loss: 1.3643
-
-Iteration: 400, Loss: 1.4743
Final Loss: 1.366
@@ -293,10 +286,10 @@ And lastly, we see how the model trains with a more conservative learning rate o
```python
net = mx.gluon.model_zoo.vision.resnet18_v2(classes=10)
learner = Learner(net=net, data_loader=data_loader, ctx=ctx)
-learner.net.load_params("net.params", ctx=ctx)
+learner.net.load_parameters("net.params", ctx=ctx)
lr = 0.005
-for iter_idx in range(500):
+for iter_idx in range(300):
learner.iteration(lr=lr)
if ((iter_idx % 100) == 0):
print("Iteration: {}, Loss: {:.5g}".format(iter_idx, learner.iteration_loss))
@@ -309,9 +302,6 @@ Iteration: 100, Loss: 1.8621
Iteration: 200, Loss: 1.6316
-Iteration: 300, Loss: 1.6295
-
-Iteration: 400, Loss: 1.4019
Final Loss: 1.2919
diff --git a/docs/tutorials/gluon/learning_rate_schedules.md b/docs/tutorials/gluon/learning_rate_schedules.md
index dc340b799b79..88b109e7f33e 100644
--- a/docs/tutorials/gluon/learning_rate_schedules.md
+++ b/docs/tutorials/gluon/learning_rate_schedules.md
@@ -12,7 +12,6 @@ In this tutorial, we visualize the schedules defined in `mx.lr_scheduler`, show
```python
-%matplotlib inline
from __future__ import print_function
import math
import matplotlib.pyplot as plt
@@ -20,6 +19,7 @@ import mxnet as mx
from mxnet.gluon import nn
from mxnet.gluon.data.vision import transforms
import numpy as np
+%matplotlib inline
```
```python
@@ -134,7 +134,7 @@ batch_size = 64
# Load the training data
train_dataset = mx.gluon.data.vision.MNIST(train=True).transform_first(transforms.ToTensor())
-train_dataloader = mx.gluon.data.DataLoader(train_dataset, batch_size, shuffle=True)
+train_dataloader = mx.gluon.data.DataLoader(train_dataset, batch_size, shuffle=True, num_workers=5)
# Build a simple convolutional network
def build_cnn():
diff --git a/docs/tutorials/gluon/save_load_params.md b/docs/tutorials/gluon/save_load_params.md
index d8eac88d8f59..ebc8103e7b45 100644
--- a/docs/tutorials/gluon/save_load_params.md
+++ b/docs/tutorials/gluon/save_load_params.md
@@ -243,7 +243,7 @@ One of the main reasons to serialize model architecture into a JSON file is to l
Serialized Hybrid networks (saved as .JSON and .params file) can be loaded and used inside Python frontend using `gluon.nn.SymbolBlock`. To demonstrate that, let's load the network we serialized above.
```python
-deserialized_net = gluon.nn.SymbolBlock.imports("lenet-symbol.json", ['data'], "lenet-0001.params")
+deserialized_net = gluon.nn.SymbolBlock.imports("lenet-symbol.json", ['data'], "lenet-0001.params", ctx=ctx)
```
`deserialized_net` now contains the network we deserialized from files. Let's test the deserialized network to make sure it works.
diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md
index df1d892247b7..23cf67529c19 100644
--- a/docs/tutorials/index.md
+++ b/docs/tutorials/index.md
@@ -3,6 +3,7 @@
```eval_rst
.. toctree::
:hidden:
+
basic/index.md
c++/index.md
control_flow/index.md
@@ -57,6 +58,7 @@ Select API:
* [Logistic Regression](/tutorials/gluon/logistic_regression_explained.html)
* [Word-level text generation with RNN, LSTM and GRU](http://gluon.mxnet.io/chapter05_recurrent-neural-networks/rnns-gluon.html)
* [Visual Question Answering](http://gluon.mxnet.io/chapter08_computer-vision/visual-question-answer.html)
+ * [Image similiarity search with InfoGAN](/tutorials/gluon/info_gan.html)
* Practitioner Guides
* [Gotchas using NumPy](/tutorials/gluon/gotchas_numpy_in_mxnet.html)
* [Multi-GPU training](http://gluon.mxnet.io/chapter07_distributed-learning/multiple-gpus-gluon.html)
@@ -69,6 +71,7 @@ Select API:
* [Learning Rate Schedules](/tutorials/gluon/learning_rate_schedules.html)
* [Advanced Learning Rate Schedules](/tutorials/gluon/learning_rate_schedules_advanced.html)
* [Profiling MXNet Models](/tutorials/python/profiler.html)
+ * [Hybridize Gluon models with control flows](/tutorials/control_flow/ControlFlowTutorial.html)
* API Guides
* Core APIs
* NDArray
@@ -118,7 +121,6 @@ Select API:
* [Fine-Tuning a pre-trained ImageNet model with a new dataset](/faq/finetune.html)
* [Large-Scale Multi-Host Multi-GPU Image Classification](/tutorials/vision/large_scale_classification.html)
* [Importing an ONNX model into MXNet](/tutorials/onnx/super_resolution.html)
- * [Hybridize Gluon models with control flows](/tutorials/control_flow/ControlFlowTutorial.html)
* [Optimizing Deep Learning Computation Graphs with TensorRT](/tutorials/tensorrt/inference_with_trt.html)
* API Guides
* Core APIs
@@ -154,11 +156,21 @@ Select API:
* [MXNet-Scala Examples](https://github.com/apache/incubator-mxnet/tree/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples)
+## Java Tutorials
+* Getting Started
+ * [Developer Environment Setup on IntelliJ IDE](/tutorials/java/mxnet_java_on_intellij.html)
+* [Multi Object Detection using pre-trained Single Shot Detector (SSD) Model](/tutorials/java/ssd_inference.html)
+* [MXNet-Java Examples](https://github.com/apache/incubator-mxnet/tree/master/scala-package/examples/src/main/java/org/apache/mxnetexamples)
+
+
## C++ Tutorials
* Models
* [MNIST Handwritten Digit Recognition with Fully Connected Network](/tutorials/c%2B%2B/basics.html)
+* Backends
+ * [Subgraph API](/tutorials/c%2B%2B/subgraphAPI.html)
+
## R Tutorials
diff --git a/docs/tutorials/java/mxnet_java_on_intellij.md b/docs/tutorials/java/mxnet_java_on_intellij.md
new file mode 100644
index 000000000000..d9a215998005
--- /dev/null
+++ b/docs/tutorials/java/mxnet_java_on_intellij.md
@@ -0,0 +1,171 @@
+# Run MXNet Java Examples Using the IntelliJ IDE (macOS)
+
+This tutorial guides you through setting up a simple Java project in IntelliJ IDE on macOS and demonstrates usage of the MXNet Java APIs.
+
+## Prerequisites:
+To use this tutorial you need the following pre-requisites:
+
+- [Java 8 JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html)
+- [Maven](https://maven.apache.org/install.html)
+- [OpenCV](https://opencv.org/)
+- [IntelliJ IDEA](https://www.jetbrains.com/idea/) (One can download the community edition from [here](https://www.jetbrains.com/idea/download))
+
+### MacOS Prerequisites
+
+You can run the following commands to install the prerequisites.
+```
+/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
+brew update
+brew tap caskroom/versions
+brew cask install java8
+brew install maven
+brew install opencv
+```
+
+You can also run this tutorial on an Ubuntu machine after installing the following prerequisites.
+### Ubuntu Prerequisites
+
+Run the following commands to install the prerequisites.
+
+```bash
+wget https://github.com/apache/incubator-mxnet/blob/master/ci/docker/install/ubuntu_core.sh
+sudo ./ubuntu_core.sh
+wget https://github.com/apache/incubator-mxnet/blob/master/ci/docker/install/ubuntu_scala.sh
+sudo ./ubuntu_scala.sh
+```
+
+Note : You might need to run `chmod u+x ubuntu_core.sh` and `chmod u+x ubuntu_scala` before running the scripts.
+
+The `ubuntu_scala.sh` installs the common dependencies required for both MXNet Scala and MXNet Java packages.
+
+## Set Up Your Project
+
+**Step 1.** Install and setup [IntelliJ IDEA](https://www.jetbrains.com/idea/)
+
+**Step 2.** Create a new Project:
+
+![intellij welcome](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/scala/intellij-welcome.png)
+
+From the IntelliJ welcome screen, select "Create New Project".
+
+Choose the Maven project type.
+
+Select the checkbox for `Create from archetype`, then choose `org.apache.maven.archetypes:maven-archetype-quickstart` from the list below. More on this can be found on a Maven tutorial : [Maven in 5 Minutes](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html).
+
+![maven project type - archetype](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/java/project-archetype.png)
+
+click `Next`.
+
+![project metadata](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/java/intellij-project-metadata.png)
+
+Set the project's metadata. For this tutorial, use the following:
+
+**GroupId**
+```
+mxnet
+```
+**ArtifactId**
+```
+ArtifactId: javaMXNet
+```
+**Version**
+```
+1.0-SNAPSHOT
+```
+
+![project properties](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/java/intellij-project-properties.png)
+
+Review the project's properties. The settings can be left as their default.
+
+![project location](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/java/intellij-project-location.png)
+
+Set the project's location. The rest of the settings can be left as their default.
+
+![project 1](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/java/intellij-project-pom.png)
+
+After clicking Finish, you will be presented with the project's first view.
+The project's `pom.xml` will be open for editing.
+
+**Step 3.** Add the following Maven dependency to your `pom.xml` file under the `dependencies` tag:
+
+```html
+
+ org.apache.mxnet
+ mxnet-full_2.11-osx-x86_64-cpu
+ 1.4.0
+
+```
+
+To view the latest MXNet Maven packages, you can check [MXNet Maven package repository](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.mxnet%22)
+
+Note :
+- Change the osx-x86_64 to linux-x86_64 if your platform is linux.
+- Change cpu into gpu if you have a gpu backed machine and want to use gpu.
+
+
+**Step 4.** Import dependencies with Maven:
+
+ - Note the prompt in the lower right corner that states "Maven projects need to be imported". If this is not visible, click on the little greed balloon that appears in the lower right corner.
+
+![import_dependencies](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/java/project-import-changes.png)
+
+Click "Import Changes" in this prompt.
+
+**Step 5.** Build the project:
+- To build the project, from the menu choose Build, and then choose Build Project.
+
+**Step 6.** Navigate to the App.java class in the project and paste the code from HelloWorld.java from [Java Demo project](https://github.com/apache/incubator-mxnet/blob/java-api/scala-package/mxnet-demo/java-demo/src/main/java/sample/HelloWorld.java) on MXNet repository, overwriting the original hello world code.
+You can also grab the entire [Java Demo project](https://github.com/apache/incubator-mxnet/tree/java-api/scala-package/mxnet-demo/java-demo) and run it by following the instructions on the [README](https://github.com/apache/incubator-mxnet/blob/java-api/scala-package/mxnet-demo/java-demo/README.md)
+
+**Step 7.** Now run the App.java.
+
+The result should be something similar to this:
+
+```
+Hello World!
+(1,2)
+Process finished with exit code 0
+```
+
+### Troubleshooting
+
+If you get an error, check the dependencies at the beginning of this tutorial. For example, you might see the following in the middle of the error messages, where `x.x` would the version it's looking for.
+
+```
+...
+Library not loaded: /usr/local/opt/opencv/lib/libopencv_calib3d.x.x.dylib
+...
+```
+
+This can be resolved be installing OpenCV.
+
+### Command Line Build Option
+
+- You can also compile the project by using the following command at the command line. Change directories to this project's root folder then run the following:
+
+```bash
+mvn clean install dependency:copy-dependencies
+```
+If the command succeeds, you should see a lot of info and some warning messages, followed by:
+
+```bash
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 3.475 s
+[INFO] Finished at: 2018-11-08T05:06:31-08:00
+[INFO] ------------------------------------------------------------------------
+```
+The build generates a new jar file in the `target` folder called `javaMXNet-1.0-SNAPSHOT.jar`.
+
+To run the App.java use the following command from the project's root folder and you should see the same output as we got when the project was run from IntelliJ.
+```bash
+java -cp target/javaMXNet-1.0-SNAPSHOT.jar:target/dependency/* mxnet.App
+```
+
+## Next Steps
+For more information about MXNet Java resources, see the following:
+
+* [Java Inference API](https://mxnet.incubator.apache.org/api/java/infer.html)
+* [Java Inference Examples](https://github.com/apache/incubator-mxnet/tree/java-api/scala-package/examples/src/main/java/org/apache/mxnetexamples/infer/)
+* [MXNet Tutorials Index](http://mxnet.io/tutorials/index.html)
diff --git a/docs/tutorials/java/ssd_inference.md b/docs/tutorials/java/ssd_inference.md
new file mode 100644
index 000000000000..6bcaaa2504a4
--- /dev/null
+++ b/docs/tutorials/java/ssd_inference.md
@@ -0,0 +1,186 @@
+# Multi Object Detection using pre-trained SSD Model via Java Inference APIs
+
+This tutorial shows how to use MXNet Java Inference APIs to run inference on a pre-trained Single Shot Detector (SSD) Model.
+
+The SSD model is trained on the Pascal VOC 2012 dataset. The network is a SSD model built on Resnet50 as the base network to extract image features. The model is trained to detect the following entities (classes): ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor']. For more details about the model, you can refer to the [MXNet SSD example](https://github.com/apache/incubator-mxnet/tree/master/example/ssd).
+
+## Prerequisites
+
+To complete this tutorial, you need the following:
+* [MXNet Java Setup on IntelliJ IDEA](/java/mxnet_java_on_intellij.html) (Optional)
+* [wget](https://www.gnu.org/software/wget/) To download model artifacts
+* SSD Model artifacts
+ * Use the following script to get the SSD Model files :
+```bash
+data_path=/tmp/resnet50_ssd
+mkdir -p "$data_path"
+wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-symbol.json -P $data_path
+wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-0000.params -P $data_path
+wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/synset.txt -P $data_path
+```
+* Test images : A few sample images to run inference on.
+ * Use the following script to download sample images :
+```bash
+image_path=/tmp/resnet50_ssd/images
+mkdir -p "$image_path"
+cd $image_path
+wget https://cloud.githubusercontent.com/assets/3307514/20012567/cbb60336-a27d-11e6-93ff-cbc3f09f5c9e.jpg -O dog.jpg
+wget https://cloud.githubusercontent.com/assets/3307514/20012563/cbb41382-a27d-11e6-92a9-18dab4fd1ad3.jpg -O person.jpg
+```
+
+Alternately, you can get the entire SSD Model artifacts + images in one single script from the MXNet Repository by running [get_ssd_data.sh script](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/scripts/infer/objectdetector/get_ssd_data.sh)
+
+## Time to code!
+1\. Following the [MXNet Java Setup on IntelliJ IDEA](/java/mxnet_java_on_intellij.html) tutorial, in the same project `JavaMXNet`, create a new empty class called : `ObjectDetectionTutorial.java`.
+
+2\. In the `main` function of `ObjectDetectionTutorial.java` define the downloaded model path and the image data paths. This is the same path where we downloaded the model artifacts and images in a previous step.
+
+```java
+String modelPathPrefix = "/tmp/resnet50_ssd/resnet50_ssd_model";
+String inputImagePath = "/tmp/resnet50_ssd/images/dog.jpg";
+```
+
+3\. We can run the inference code in this example on either CPU or GPU (if you have a GPU backed machine) by choosing the appropriate context.
+
+```java
+
+List context = getContext();
+...
+
+private static List getContext() {
+List ctx = new ArrayList<>();
+ctx.add(Context.cpu()); // Choosing CPU Context here
+
+return ctx;
+}
+```
+
+4\. To provide an input to the model, define the input shape to the model and the Input Data Descriptor (DataDesc) as shown below :
+
+```java
+Shape inputShape = new Shape(new int[] {1, 3, 512, 512});
+List inputDescriptors = new ArrayList();
+inputDescriptors.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+```
+
+The input shape can be interpreted as follows : The input has a batch size of 1, with 3 RGB channels in the image, and the height and width of the image is 512 each.
+
+5\. To run an actual inference on the given image, add the following lines to the `ObjectDetectionTutorial.java` class :
+
+```java
+BufferedImage img = ObjectDetector.loadImageFromFile(inputImagePath);
+ObjectDetector objDet = new ObjectDetector(modelPathPrefix, inputDescriptors, context, 0);
+List> output = objDet.imageObjectDetect(img, 3); // Top 3 objects detected will be returned
+```
+
+6\. Let's piece all of the above steps together by showing the final contents of the `ObjectDetectionTutorial.java`.
+
+```java
+package mxnet;
+
+import org.apache.mxnet.infer.javaapi.ObjectDetector;
+import org.apache.mxnet.infer.javaapi.ObjectDetectorOutput;
+import org.apache.mxnet.javaapi.Context;
+import org.apache.mxnet.javaapi.DType;
+import org.apache.mxnet.javaapi.DataDesc;
+import org.apache.mxnet.javaapi.Shape;
+
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class ObjectDetectionTutorial {
+
+ public static void main(String[] args) {
+
+ String modelPathPrefix = "/tmp/resnet50_ssd/resnet50_ssd_model";
+
+ String inputImagePath = "/tmp/resnet50_ssd/images/dog.jpg";
+
+ List context = getContext();
+
+ Shape inputShape = new Shape(new int[] {1, 3, 512, 512});
+
+ List inputDescriptors = new ArrayList();
+ inputDescriptors.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+
+ BufferedImage img = ObjectDetector.loadImageFromFile(inputImagePath);
+ ObjectDetector objDet = new ObjectDetector(modelPathPrefix, inputDescriptors, context, 0);
+ List> output = objDet.imageObjectDetect(img, 3);
+
+ printOutput(output, inputShape);
+ }
+
+
+ private static List getContext() {
+ List ctx = new ArrayList<>();
+ ctx.add(Context.cpu());
+
+ return ctx;
+ }
+
+ private static void printOutput(List> output, Shape inputShape) {
+
+ StringBuilder outputStr = new StringBuilder();
+
+ int width = inputShape.get(3);
+ int height = inputShape.get(2);
+
+ for (List ele : output) {
+ for (ObjectDetectorOutput i : ele) {
+ outputStr.append("Class: " + i.getClassName() + "\n");
+ outputStr.append("Probabilties: " + i.getProbability() + "\n");
+
+ List coord = Arrays.asList(i.getXMin() * width,
+ i.getXMax() * height, i.getYMin() * width, i.getYMax() * height);
+ StringBuilder sb = new StringBuilder();
+ for (float c: coord) {
+ sb.append(", ").append(c);
+ }
+ outputStr.append("Coord:" + sb.substring(2)+ "\n");
+ }
+ }
+ System.out.println(outputStr);
+
+ }
+}
+```
+
+7\. To compile and run this code, change directories to this project's root folder, then run the following:
+```bash
+mvn clean install dependency:copy-dependencies
+```
+
+The build generates a new jar file in the `target` folder called `javaMXNet-1.0-SNAPSHOT.jar`.
+
+To run the ObjectDetectionTutorial.java use the following command from the project's root folder.
+```bash
+java -cp target/javaMXNet-1.0-SNAPSHOT.jar:target/dependency/* mxnet.ObjectDetectionTutorial
+```
+
+You should see a similar output being generated for the dog image that we used:
+```bash
+Class: car
+Probabilties: 0.99847263
+Coord:312.21335, 72.02908, 456.01443, 150.66176
+Class: bicycle
+Probabilties: 0.9047381
+Coord:155.9581, 149.96365, 383.83694, 418.94516
+Class: dog
+Probabilties: 0.82268167
+Coord:83.82356, 179.14001, 206.63783, 476.78754
+```
+
+![dog_1](https://cloud.githubusercontent.com/assets/3307514/20012567/cbb60336-a27d-11e6-93ff-cbc3f09f5c9e.jpg)
+
+The results returned by the inference call translate into the regions in the image where the model detected objects.
+
+![dog_2](https://cloud.githubusercontent.com/assets/3307514/19171063/91ec2792-8be0-11e6-983c-773bd6868fa8.png)
+
+## Next Steps
+For more information about MXNet Java resources, see the following:
+
+* [Java Inference API](/api/java/infer.html)
+* [Java Inference Examples](https://github.com/apache/incubator-mxnet/tree/java-api/scala-package/examples/src/main/java/org/apache/mxnetexamples/infer/)
+* [MXNet Tutorials Index](/tutorials/index.html)
diff --git a/docs/tutorials/onnx/export_mxnet_to_onnx.md b/docs/tutorials/onnx/export_mxnet_to_onnx.md
index dc34bd520b43..3f925c7b5b84 100644
--- a/docs/tutorials/onnx/export_mxnet_to_onnx.md
+++ b/docs/tutorials/onnx/export_mxnet_to_onnx.md
@@ -49,7 +49,7 @@ Let us describe the MXNet's `export_model` API.
help(onnx_mxnet.export_model)
```
-```python
+```
Help on function export_model in module mxnet.contrib.onnx.mx2onnx.export_model:
export_model(sym, params, input_shape, input_type=, onnx_file_path=u'model.onnx', verbose=False)
@@ -121,7 +121,7 @@ from onnx import checker
import onnx
# Load onnx model
-model_proto = onnx.load(converted_model_path)
+model_proto = onnx.load_model(converted_model_path)
# Check if converted ONNX protobuf is valid
checker.check_graph(model_proto.graph)
diff --git a/docs/tutorials/onnx/fine_tuning_gluon.md b/docs/tutorials/onnx/fine_tuning_gluon.md
index 07d8bdf0aa45..750a6757272f 100644
--- a/docs/tutorials/onnx/fine_tuning_gluon.md
+++ b/docs/tutorials/onnx/fine_tuning_gluon.md
@@ -23,19 +23,23 @@ We recommend that you have first followed this tutorial:
```python
-import numpy as np
+import json
+import logging
+import multiprocessing
+import os
+import tarfile
+
+logging.basicConfig(level=logging.INFO)
+
+import matplotlib.pyplot as plt
import mxnet as mx
from mxnet import gluon, nd, autograd
from mxnet.gluon.data.vision.datasets import ImageFolderDataset
from mxnet.gluon.data import DataLoader
import mxnet.contrib.onnx as onnx_mxnet
+import numpy as np
+
%matplotlib inline
-import matplotlib.pyplot as plt
-import tarfile, os
-import json
-import multiprocessing
-import logging
-logging.basicConfig(level=logging.INFO)
```
@@ -122,7 +126,7 @@ We need to transform the images to a format accepted by the network
EDGE = 224
SIZE = (EDGE, EDGE)
BATCH_SIZE = 32
-NUM_WORKERS = multiprocessing.cpu_count()
+NUM_WORKERS = 6
```
We transform the dataset images using the following operations:
@@ -152,18 +156,18 @@ ____image4
```python
-dataset_train = ImageFolderDataset(root=training_path, transform=transform)
-dataset_test = ImageFolderDataset(root=testing_path, transform=transform)
+dataset_train = ImageFolderDataset(root=training_path)
+dataset_test = ImageFolderDataset(root=testing_path)
```
-We use num_workers=Number of CPU cores, which means the dataloading and pre-processing is going to be distributed across multiple processes. This will help preventing our GPU from starving and waiting for the data to be copied across
+We use several worker processes, which means the dataloading and pre-processing is going to be distributed across multiple processes. This will help preventing our GPU from starving and waiting for the data to be copied across
```python
-dataloader_train = DataLoader(dataset_train, batch_size=BATCH_SIZE, last_batch='discard',
+dataloader_train = DataLoader(dataset_train.transform(transform, lazy=False), batch_size=BATCH_SIZE, last_batch='rollover',
shuffle=True, num_workers=NUM_WORKERS)
-dataloader_test = DataLoader(dataset_test, batch_size=BATCH_SIZE, last_batch='discard',
- shuffle=True, num_workers=NUM_WORKERS)
+dataloader_test = DataLoader(dataset_test.transform(transform, lazy=False), batch_size=BATCH_SIZE, last_batch='rollover',
+ shuffle=False, num_workers=NUM_WORKERS)
print("Train dataset: {} images, Test dataset: {} images".format(len(dataset_train), len(dataset_test)))
```
@@ -183,7 +187,7 @@ Let's plot the 1000th image to test the dataset
```python
N = 1000
-plt.imshow(np.transpose(dataset_train[N][0].asnumpy(),(1,2,0)))
+plt.imshow((transform(dataset_train[N][0], 0)[0].asnumpy().transpose((1,2,0))))
plt.axis('off')
print(categories[dataset_train[N][1]])
```
@@ -251,7 +255,7 @@ We pick a context, fine-tuning on CPU will be **WAY** slower.
```python
-ctx = mx.gpu() if mx.test_utils.list_gpus() else mx.cpu()
+ctx = mx.gpu() if mx.context.num_gpus() > 0 else mx.cpu()
```
We create a symbol block that is going to hold all our pre-trained layers, and assign the weights of the different pre-trained layers to the newly created SymbolBlock
@@ -282,8 +286,9 @@ We add the SymbolBlock and the new dense layer to a HybridSequential network
```python
net = gluon.nn.HybridSequential()
-net.add(pre_trained)
-net.add(dense_layer)
+with net.name_scope():
+ net.add(pre_trained)
+ net.add(dense_layer)
```
### Loss
@@ -321,7 +326,7 @@ We measure the accuracy in a non-blocking way, using `nd.array` to take care of
```python
def evaluate_accuracy_gluon(data_iterator, net):
- num_instance = nd.zeros(1, ctx=ctx)
+ num_instance = 0
sum_metric = nd.zeros(1,ctx=ctx, dtype=np.int32)
for i, (data, label) in enumerate(data_iterator):
data = data.astype(np.float32).as_in_context(ctx)
@@ -330,7 +335,7 @@ We measure the accuracy in a non-blocking way, using `nd.array` to take care of
prediction = nd.argmax(output, axis=1).astype(np.int32)
num_instance += len(prediction)
sum_metric += (prediction==label).sum()
- accuracy = (sum_metric.astype(np.float32)/num_instance.astype(np.float32))
+ accuracy = (sum_metric.astype(np.float32)/num_instance)
return accuracy.asscalar()
```
diff --git a/docs/tutorials/python/linear-regression.md b/docs/tutorials/python/linear-regression.md
index f9656844052d..fd336ad2aed5 100644
--- a/docs/tutorials/python/linear-regression.md
+++ b/docs/tutorials/python/linear-regression.md
@@ -147,7 +147,7 @@ model = mx.mod.Module(
We can visualize the network we created by plotting it:
```python
-mx.viz.plot_network(symbol=lro)
+mx.viz.plot_network(symbol=lro, node_attrs={"shape":"oval","fixedsize":"false"})
```
## Training the model
diff --git a/docs/tutorials/python/predict_image.md b/docs/tutorials/python/predict_image.md
index a9a0d29010c1..8be98d991366 100644
--- a/docs/tutorials/python/predict_image.md
+++ b/docs/tutorials/python/predict_image.md
@@ -69,6 +69,7 @@ def get_image(url, show=False):
img = mx.image.imresize(img, 224, 224) # resize
img = img.transpose((2, 0, 1)) # Channel first
img = img.expand_dims(axis=0) # batchify
+ img = img.astype('float32') # for gpu context
return img
def predict(url):
diff --git a/docs/tutorials/r/MultidimLstm.md b/docs/tutorials/r/MultidimLstm.md
new file mode 100644
index 000000000000..8692086d180b
--- /dev/null
+++ b/docs/tutorials/r/MultidimLstm.md
@@ -0,0 +1,302 @@
+LSTM time series example
+=============================================
+
+This tutorial shows how to use an LSTM model with multivariate data, and generate predictions from it. For demonstration purposes, we used an open source [pollution data](https://archive.ics.uci.edu/ml/datasets/Beijing+PM2.5+Data).
+The tutorial is an illustration of how to use LSTM models with MXNet-R. We are forecasting the air pollution with data recorded at the US embassy in Beijing, China for five years.
+
+Dataset Attribution:
+"PM2.5 data of US Embassy in Beijing"
+We want to predict pollution levels(PM2.5 concentration) in the city given the above dataset.
+
+```r
+Dataset description:
+No: row number
+year: year of data in this row
+month: month of data in this row
+day: day of data in this row
+hour: hour of data in this row
+pm2.5: PM2.5 concentration
+DEWP: Dew Point
+TEMP: Temperature
+PRES: Pressure
+cbwd: Combined wind direction
+Iws: Cumulated wind speed
+Is: Cumulated hours of snow
+Ir: Cumulated hours of rain
+```
+
+We use past PM2.5 concentration, dew point, temperature, pressure, wind speed, snow and rain to predict
+PM2.5 concentration levels.
+
+Load and pre-process the data
+---------
+The first step is to load in the data and preprocess it. It is assumed that the data has been downloaded in a .csv file: data.csv from the [pollution dataset](https://archive.ics.uci.edu/ml/datasets/Beijing+PM2.5+Data)
+
+ ```r
+## Loading required packages
+library("readr")
+library("dplyr")
+library("mxnet")
+library("abind")
+ ```
+
+
+
+ ```r
+## Preprocessing steps
+Data <- read.csv(file = "/Users/khedia/Downloads/data.csv",
+ header = TRUE,
+ sep = ",")
+
+## Extracting specific features from the dataset as variables for time series We extract
+## pollution, temperature, pressue, windspeed, snowfall and rainfall information from dataset
+df <- data.frame(Data$pm2.5,
+ Data$DEWP,
+ Data$TEMP,
+ Data$PRES,
+ Data$Iws,
+ Data$Is,
+ Data$Ir)
+df[is.na(df)] <- 0
+
+## Now we normalise each of the feature set to a range(0,1)
+df <- matrix(as.matrix(df),
+ ncol = ncol(df),
+ dimnames = NULL)
+
+rangenorm <- function(x) {
+ (x - min(x))/(max(x) - min(x))
+}
+df <- apply(df, 2, rangenorm)
+df <- t(df)
+ ```
+For using multidimesional data with MXNet-R, we need to convert training data to the form
+(n_dim x seq_len x num_samples). For one-to-one RNN flavours labels should be of the form (seq_len x num_samples) while for many-to-one flavour, the labels should be of the form (1 x num_samples). Please note that MXNet-R currently supports only these two flavours of RNN.
+We have used n_dim = 7, seq_len = 100, and num_samples = 430 because the dataset has 430 samples, each the length of 100 timestamps, we have seven time series as input features so each input has dimesnion of seven at each time step.
+
+
+```r
+n_dim <- 7
+seq_len <- 100
+num_samples <- 430
+
+## extract only required data from dataset
+trX <- df[1:n_dim, 25:(24 + (seq_len * num_samples))]
+
+## the label data(next PM2.5 concentration) should be one time step
+## ahead of the current PM2.5 concentration
+trY <- df[1, 26:(25 + (seq_len * num_samples))]
+
+## reshape the matrices in the format acceptable by MXNetR RNNs
+trainX <- trX
+dim(trainX) <- c(n_dim, seq_len, num_samples)
+trainY <- trY
+dim(trainY) <- c(seq_len, num_samples)
+```
+
+
+
+Defining and training the network
+---------
+
+```r
+batch.size <- 32
+
+# take first 300 samples for training - remaining 100 for evaluation
+train_ids <- 1:300
+eval_ids <- 301:400
+
+## The number of samples used for training and evaluation is arbitrary. I have kept aside few
+## samples for testing purposes create dataiterators
+train.data <- mx.io.arrayiter(data = trainX[, , train_ids, drop = F],
+ label = trainY[, train_ids],
+ batch.size = batch.size, shuffle = TRUE)
+
+eval.data <- mx.io.arrayiter(data = trainX[, , eval_ids, drop = F],
+ label = trainY[, eval_ids],
+ batch.size = batch.size, shuffle = FALSE)
+
+## Create the symbol for RNN
+symbol <- rnn.graph(num_rnn_layer = 1,
+ num_hidden = 5,
+ input_size = NULL,
+ num_embed = NULL,
+ num_decode = 1,
+ masking = F,
+ loss_output = "linear",
+ dropout = 0.2,
+ ignore_label = -1,
+ cell_type = "lstm",
+ output_last_state = T,
+ config = "one-to-one")
+
+
+
+mx.metric.mse.seq <- mx.metric.custom("MSE", function(label, pred) {
+ label = mx.nd.reshape(label, shape = -1)
+ pred = mx.nd.reshape(pred, shape = -1)
+ res <- mx.nd.mean(mx.nd.square(label - pred))
+ return(as.array(res))
+})
+
+
+
+ctx <- mx.cpu()
+
+initializer <- mx.init.Xavier(rnd_type = "gaussian",
+ factor_type = "avg",
+ magnitude = 3)
+
+optimizer <- mx.opt.create("adadelta",
+ rho = 0.9,
+ eps = 1e-05,
+ wd = 1e-06,
+ clip_gradient = 1,
+ rescale.grad = 1/batch.size)
+
+logger <- mx.metric.logger()
+epoch.end.callback <- mx.callback.log.train.metric(period = 10,
+ logger = logger)
+
+## train the network
+system.time(model <- mx.model.buckets(symbol = symbol,
+ train.data = train.data,
+ eval.data = eval.data,
+ num.round = 100,
+ ctx = ctx,
+ verbose = TRUE,
+ metric = mx.metric.mse.seq,
+ initializer = initializer,
+ optimizer = optimizer,
+ batch.end.callback = NULL,
+ epoch.end.callback = epoch.end.callback))
+```
+Output:
+```
+Start training with 1 devices
+[1] Train-MSE=0.197570244409144
+[1] Validation-MSE=0.0153861071448773
+[2] Train-MSE=0.0152517843060195
+[2] Validation-MSE=0.0128299412317574
+[3] Train-MSE=0.0124418652616441
+[3] Validation-MSE=0.010827143676579
+[4] Train-MSE=0.0105128229130059
+[4] Validation-MSE=0.00940261723008007
+[5] Train-MSE=0.00914482437074184
+[5] Validation-MSE=0.00830172537826002
+[6] Train-MSE=0.00813581114634871
+[6] Validation-MSE=0.00747016374953091
+[7] Train-MSE=0.00735094994306564
+[7] Validation-MSE=0.00679832429159433
+[8] Train-MSE=0.00672049634158611
+[8] Validation-MSE=0.00623159145470709
+[9] Train-MSE=0.00620287149213254
+[9] Validation-MSE=0.00577476259786636
+[10] Train-MSE=0.00577280316501856
+[10] Validation-MSE=0.00539038667920977
+..........
+..........
+[91] Train-MSE=0.00177705133100972
+[91] Validation-MSE=0.00154715491225943
+[92] Train-MSE=0.00177639147732407
+[92] Validation-MSE=0.00154592350008897
+[93] Train-MSE=0.00177577760769054
+[93] Validation-MSE=0.00154474508599378
+[94] Train-MSE=0.0017752077546902
+[94] Validation-MSE=0.0015436161775142
+[95] Train-MSE=0.00177468206966296
+[95] Validation-MSE=0.00154253660002723
+[96] Train-MSE=0.00177419915562496
+[96] Validation-MSE=0.00154150440357625
+[97] Train-MSE=0.0017737578949891
+[97] Validation-MSE=0.00154051734716631
+[98] Train-MSE=0.00177335749613121
+[98] Validation-MSE=0.00153957353904843
+[99] Train-MSE=0.00177299699280411
+[99] Validation-MSE=0.00153867155313492
+[100] Train-MSE=0.00177267640829086
+[100] Validation-MSE=0.00153781197150238
+
+ user system elapsed
+ 21.937 1.914 13.402
+```
+We can see how mean squared error varies with epochs below.
+
+![png](https://github.com/dmlc/web-data/blob/master/mxnet/doc/tutorials/r/images/loss.png?raw=true)
+
+Inference on the network
+---------
+Now we have trained the network. Let's use it for inference.
+
+```r
+## We extract the state symbols for RNN
+internals <- model$symbol$get.internals()
+sym_state <- internals$get.output(which(internals$outputs %in% "RNN_state"))
+sym_state_cell <- internals$get.output(which(internals$outputs %in% "RNN_state_cell"))
+sym_output <- internals$get.output(which(internals$outputs %in% "loss_output"))
+symbol <- mx.symbol.Group(sym_output, sym_state, sym_state_cell)
+
+## We will predict 100 timestamps for 401st sample (first sample from the test samples)
+pred_length <- 100
+predicted <- numeric()
+
+## We pass the 400th sample through the network to get the weights and use it for predicting next
+## 100 time stamps.
+data <- mx.nd.array(trainX[, , 400, drop = F])
+label <- mx.nd.array(trainY[, 400, drop = F])
+
+
+## We create dataiterators for the input, please note that the label is required to create
+## iterator and will not be used in the inference. You can use dummy values too in the label.
+infer.data <- mx.io.arrayiter(data = data,
+ label = label,
+ batch.size = 1,
+ shuffle = FALSE)
+
+infer <- mx.infer.rnn.one(infer.data = infer.data,
+ symbol = symbol,
+ arg.params = model$arg.params,
+ aux.params = model$aux.params,
+ input.params = NULL,
+ ctx = ctx)
+## Once we get the weights for the above time series, we try to predict the next 100 steps for
+## this time series, which is technically our 401st time series.
+
+actual <- trainY[, 401]
+
+## Now we iterate one by one to generate each of the next timestamp pollution values
+
+for (i in 1:pred_length) {
+
+ data <- mx.nd.array(trainX[, i, 401, drop = F])
+ label <- mx.nd.array(trainY[i, 401, drop = F])
+ infer.data <- mx.io.arrayiter(data = data,
+ label = label,
+ batch.size = 1,
+ shuffle = FALSE)
+ ## note that we use rnn state values from previous iterations here
+ infer <- mx.infer.rnn.one(infer.data = infer.data,
+ symbol = symbol,
+ ctx = ctx,
+ arg.params = model$arg.params,
+ aux.params = model$aux.params,
+ input.params = list(rnn.state = infer[[2]],
+ rnn.state.cell = infer[[3]]))
+
+ pred <- infer[[1]]
+ predicted <- c(predicted, as.numeric(as.array(pred)))
+
+}
+
+```
+Now predicted contains the predicted 100 values. We use ggplot to plot the actual and predicted values as shown below.
+
+![png](https://github.com/dmlc/web-data/blob/master/mxnet/doc/tutorials/r/images/sample_401.png?raw=true)
+
+We also repeated the above experiments to generate the next 100 samples to 301st time series and we got the following results.
+
+![png](https://github.com/dmlc/web-data/blob/master/mxnet/doc/tutorials/r/images/sample_301.png?raw=true)
+
+The above tutorial is just for demonstration purposes and has not been tuned extensively for accuracy.
+
+For more tutorials on MXNet-R, head on to [MXNet-R tutorials](https://mxnet.incubator.apache.org/tutorials/r/index.html)
\ No newline at end of file
diff --git a/docs/tutorials/r/charRnnModel.md b/docs/tutorials/r/charRnnModel.md
index 82e10a11f8d5..cb21e77559b5 100644
--- a/docs/tutorials/r/charRnnModel.md
+++ b/docs/tutorials/r/charRnnModel.md
@@ -1,314 +1,293 @@
-Char RNN Example
-=============================================
-
-This tutorial shows how to use an LSTM model to build a char-level language model, and generate text from it. For demonstration purposes, we use a Shakespearean text. You can find the data on [GitHub](https://github.com/dmlc/web-data/tree/master/mxnet/tinyshakespeare).
-
-Load the Data
----------
-Load in the data and preprocess it:
-
- ```r
- require(mxnet)
- ```
-
- ```
- ## Loading required package: mxnet
- ```
-
- ```
- ## Loading required package: methods
- ```
-Set the basic network parameters:
-
- ```r
- batch.size = 32
- seq.len = 32
- num.hidden = 16
- num.embed = 16
- num.lstm.layer = 1
- num.round = 1
- learning.rate= 0.1
- wd=0.00001
- clip_gradient=1
- update.period = 1
- ```
-Download the data:
- ```r
- download.data <- function(data_dir) {
- dir.create(data_dir, showWarnings = FALSE)
- if (!file.exists(paste0(data_dir,'input.txt'))) {
- download.file(url='https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/tinyshakespeare/input.txt',
- destfile=paste0(data_dir,'input.txt'), method='wget')
- }
- }
- ```
-Make a dictionary from the text:
-
- ```r
- make.dict <- function(text, max.vocab=10000) {
- text <- strsplit(text, '')
- dic <- list()
- idx <- 1
- for (c in text[[1]]) {
- if (!(c %in% names(dic))) {
- dic[[c]] <- idx
- idx <- idx + 1
- }
- }
- if (length(dic) == max.vocab - 1)
- dic[["UNKNOWN"]] <- idx
- cat(paste0("Total unique char: ", length(dic), "\n"))
- return (dic)
- }
- ```
-Transfer the text into a data feature:
-
- ```r
- make.data <- function(file.path, seq.len=32, max.vocab=10000, dic=NULL) {
- fi <- file(file.path, "r")
- text <- paste(readLines(fi), collapse="\n")
- close(fi)
-
- if (is.null(dic))
- dic <- make.dict(text, max.vocab)
- lookup.table <- list()
- for (c in names(dic)) {
- idx <- dic[[c]]
- lookup.table[[idx]] <- c
- }
-
- char.lst <- strsplit(text, '')[[1]]
- num.seq <- as.integer(length(char.lst) / seq.len)
- char.lst <- char.lst[1:(num.seq * seq.len)]
- data <- array(0, dim=c(seq.len, num.seq))
- idx <- 1
- for (i in 1:num.seq) {
- for (j in 1:seq.len) {
- if (char.lst[idx] %in% names(dic))
- data[j, i] <- dic[[ char.lst[idx] ]]-1
- else {
- data[j, i] <- dic[["UNKNOWN"]]-1
- }
- idx <- idx + 1
- }
- }
- return (list(data=data, dic=dic, lookup.table=lookup.table))
- }
- ```
-Move the tail text:
-
- ```r
- drop.tail <- function(X, batch.size) {
- shape <- dim(X)
- nstep <- as.integer(shape[2] / batch.size)
- return (X[, 1:(nstep * batch.size)])
- }
- ```
-Get the label of X:
-
- ```r
- get.label <- function(X) {
- label <- array(0, dim=dim(X))
- d <- dim(X)[1]
- w <- dim(X)[2]
- for (i in 0:(w-1)) {
- for (j in 1:d) {
- label[i*d+j] <- X[(i*d+j)%%(w*d)+1]
- }
- }
- return (label)
- }
- ```
-Get the training data and evaluation data:
-
- ```r
- download.data("./data/")
- ret <- make.data("./data/input.txt", seq.len=seq.len)
- ```
-
- ```
- ## Total unique char: 65
- ```
-
- ```r
- X <- ret$data
- dic <- ret$dic
- lookup.table <- ret$lookup.table
-
- vocab <- length(dic)
-
- shape <- dim(X)
- train.val.fraction <- 0.9
- size <- shape[2]
-
- X.train.data <- X[, 1:as.integer(size * train.val.fraction)]
- X.val.data <- X[, -(1:as.integer(size * train.val.fraction))]
- X.train.data <- drop.tail(X.train.data, batch.size)
- X.val.data <- drop.tail(X.val.data, batch.size)
-
- X.train.label <- get.label(X.train.data)
- X.val.label <- get.label(X.val.data)
-
- X.train <- list(data=X.train.data, label=X.train.label)
- X.val <- list(data=X.val.data, label=X.val.label)
- ```
-
-Train the Model
---------------
-In `mxnet`, we have a function called `mx.lstm` so that users can build a general LSTM model:
-
-
- ```r
- model <- mx.lstm(X.train, X.val,
- ctx=mx.cpu(),
- num.round=num.round,
- update.period=update.period,
- num.lstm.layer=num.lstm.layer,
- seq.len=seq.len,
- num.hidden=num.hidden,
- num.embed=num.embed,
- num.label=vocab,
- batch.size=batch.size,
- input.size=vocab,
- initializer=mx.init.uniform(0.1),
- learning.rate=learning.rate,
- wd=wd,
- clip_gradient=clip_gradient)
- ```
-
- ```
- ## Epoch [31] Train: NLL=3.53787130224343, Perp=34.3936275728271
- ## Epoch [62] Train: NLL=3.43087958036949, Perp=30.903813186055
- ## Epoch [93] Train: NLL=3.39771238228587, Perp=29.8956319855751
- ## Epoch [124] Train: NLL=3.37581711716687, Perp=29.2481732041015
- ## Epoch [155] Train: NLL=3.34523331338447, Perp=28.3671933405139
- ## Epoch [186] Train: NLL=3.30756356274787, Perp=27.31848454823
- ## Epoch [217] Train: NLL=3.25642968403829, Perp=25.9566978956055
- ## Epoch [248] Train: NLL=3.19825967486207, Perp=24.4898727477925
- ## Epoch [279] Train: NLL=3.14013971549828, Perp=23.1070950525017
- ## Epoch [310] Train: NLL=3.08747601837462, Perp=21.9216781782189
- ## Epoch [341] Train: NLL=3.04015595674863, Perp=20.9085038031042
- ## Epoch [372] Train: NLL=2.99839339255659, Perp=20.0532932584534
- ## Epoch [403] Train: NLL=2.95940091012609, Perp=19.2864139984503
- ## Epoch [434] Train: NLL=2.92603311380224, Perp=18.6534872738302
- ## Epoch [465] Train: NLL=2.89482756896395, Perp=18.0803835531869
- ## Epoch [496] Train: NLL=2.86668230478397, Perp=17.5786009078994
- ## Epoch [527] Train: NLL=2.84089368534943, Perp=17.1310684830416
- ## Epoch [558] Train: NLL=2.81725862932279, Perp=16.7309220880514
- ## Epoch [589] Train: NLL=2.79518870141492, Perp=16.3657166956952
- ## Epoch [620] Train: NLL=2.77445683225304, Perp=16.0299176962855
- ## Epoch [651] Train: NLL=2.75490970113174, Perp=15.719621374694
- ## Epoch [682] Train: NLL=2.73697900634351, Perp=15.4402696117257
- ## Epoch [713] Train: NLL=2.72059739336781, Perp=15.1893935780915
- ## Epoch [744] Train: NLL=2.70462837571585, Perp=14.948760335793
- ## Epoch [775] Train: NLL=2.68909904683828, Perp=14.7184093476224
- ## Epoch [806] Train: NLL=2.67460054451836, Perp=14.5065539595711
- ## Epoch [837] Train: NLL=2.66078997776751, Perp=14.3075873113043
- ## Epoch [868] Train: NLL=2.6476781639279, Perp=14.1212134100373
- ## Epoch [899] Train: NLL=2.63529039846876, Perp=13.9473621677371
- ## Epoch [930] Train: NLL=2.62367693518974, Perp=13.7863219168709
- ## Epoch [961] Train: NLL=2.61238282674384, Perp=13.6314936713501
- ## Iter [1] Train: Time: 10301.6818172932 sec, NLL=2.60536539345356, Perp=13.5361704272949
- ## Iter [1] Val: NLL=2.26093848746227, Perp=9.59208699731232
- ```
-
-Build Inference from the Model
---------------------
-Use the helper function for random sample:
-
- ```r
- cdf <- function(weights) {
- total <- sum(weights)
- result <- c()
- cumsum <- 0
- for (w in weights) {
- cumsum <- cumsum+w
- result <- c(result, cumsum / total)
- }
- return (result)
- }
+# Character-level Language Model using RNN
- search.val <- function(cdf, x) {
- l <- 1
- r <- length(cdf)
- while (l <= r) {
- m <- as.integer((l+r)/2)
- if (cdf[m] < x) {
- l <- m+1
- } else {
- r <- m-1
- }
- }
- return (l)
- }
- choice <- function(weights) {
- cdf.vals <- cdf(as.array(weights))
- x <- runif(1)
- idx <- search.val(cdf.vals, x)
- return (idx)
- }
- ```
-Use random output or fixed output by choosing the greatest probability:
-
- ```r
- make.output <- function(prob, sample=FALSE) {
- if (!sample) {
- idx <- which.max(as.array(prob))
- }
- else {
- idx <- choice(prob)
- }
- return (idx)
+This tutorial will demonstrate creating a language model using a character level RNN model using MXNet-R package. You will need the following R packages to run this tutorial -
+ - readr
+ - stringr
+ - stringi
+ - mxnet
- }
- ```
-
-In `mxnet`, we have a function called `mx.lstm.inference` so that users can build an inference from an LSTM model, and then use the `mx.lstm.forward` function to get forward output from the inference.
-
-Build an inference from the model:
-
- ```r
- infer.model <- mx.lstm.inference(num.lstm.layer=num.lstm.layer,
- input.size=vocab,
- num.hidden=num.hidden,
- num.embed=num.embed,
- num.label=vocab,
- arg.params=model$arg.params,
- ctx=mx.cpu())
- ```
-Generate a sequence of 75 characters using the `mx.lstm.forward` function:
-
- ```r
- start <- 'a'
- seq.len <- 75
- random.sample <- TRUE
-
- last.id <- dic[[start]]
- out <- "a"
- for (i in (1:(seq.len-1))) {
- input <- c(last.id-1)
- ret <- mx.lstm.forward(infer.model, input, FALSE)
- infer.model <- ret$model
- prob <- ret$prob
- last.id <- make.output(prob, random.sample)
- out <- paste0(out, lookup.table[[last.id]])
- }
- cat (paste0(out, "\n"))
- ```
-The result:
+We will use the [tinyshakespeare](https://github.com/dmlc/web-data/tree/master/mxnet/tinyshakespeare) dataset to build this model.
- ```
- ah not a drobl greens
- Settled asing lately sistering sounted to their hight
- ```
-Create Other RNN Models
-----------------
-In `mxnet`, other RNN models, like custom RNN and GRU, are also provided:
+```R
+library("readr")
+library("stringr")
+library("stringi")
+library("mxnet")
+```
-- For a custom RNN model, you can replace `mx.lstm` with `mx.rnn` to train an RNN model. You can replace `mx.lstm.inference` and `mx.lstm.forward` with `mx.rnn.inference` and `mx.rnn.forward` to build inference from an RNN model and get the forward result from the inference model.
-- For a GRU model, you can replace `mx.lstm` with `mx.gru` to train a GRU model. You can replace `mx.lstm.inference` and `mx.lstm.forward` with `mx.gru.inference` and `mx.gru.forward` to build inference from a GRU model and get the forward result from the inference model.
+## Preprocess and prepare the data
-## Next Steps
-* [MXNet tutorials index](http://mxnet.io/tutorials/index.html)
+Download the data:
+
+
+```R
+download.data <- function(data_dir) {
+ dir.create(data_dir, showWarnings = FALSE)
+ if (!file.exists(paste0(data_dir,'input.txt'))) {
+ download.file(url='https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/tinyshakespeare/input.txt',
+ destfile=paste0(data_dir,'input.txt'), method='wget')
+ }
+}
+```
+
+Next we transform the test into feature vectors that is fed into the RNN model. The `make_data` function reads the dataset, cleans it of any non-alphanumeric characters, splits it into individual characters and groups it into sequences of length `seq.len`.
+
+
+```R
+make_data <- function(path, seq.len = 32, dic=NULL) {
+
+ text_vec <- read_file(file = path)
+ text_vec <- stri_enc_toascii(str = text_vec)
+ text_vec <- str_replace_all(string = text_vec, pattern = "[^[:print:]]", replacement = "")
+ text_vec <- strsplit(text_vec, '') %>% unlist
+
+ if (is.null(dic)) {
+ char_keep <- sort(unique(text_vec))
+ } else char_keep <- names(dic)[!dic == 0]
+
+ # Remove terms not part of dictionary
+ text_vec <- text_vec[text_vec %in% char_keep]
+
+ # Build dictionary
+ dic <- 1:length(char_keep)
+ names(dic) <- char_keep
+
+ # reverse dictionary
+ rev_dic <- names(dic)
+ names(rev_dic) <- dic
+
+ # Adjust by -1 to have a 1-lag for labels
+ num.seq <- (length(text_vec) - 1) %/% seq.len
+
+ features <- dic[text_vec[1:(seq.len * num.seq)]]
+ labels <- dic[text_vec[1:(seq.len*num.seq) + 1]]
+
+ features_array <- array(features, dim = c(seq.len, num.seq))
+ labels_array <- array(labels, dim = c(seq.len, num.seq))
+
+ return (list(features_array = features_array, labels_array = labels_array, dic = dic, rev_dic = rev_dic))
+}
+
+
+seq.len <- 100
+data_prep <- make_data(path = "input.txt", seq.len = seq.len, dic=NULL)
+```
+
+Fetch the features and labels for training the model, and split the data into training and evaluation in 9:1 ratio.
+
+
+```R
+X <- data_prep$features_array
+Y <- data_prep$labels_array
+dic <- data_prep$dic
+rev_dic <- data_prep$rev_dic
+vocab <- length(dic)
+
+samples <- tail(dim(X), 1)
+train.val.fraction <- 0.9
+
+X.train.data <- X[, 1:as.integer(samples * train.val.fraction)]
+X.val.data <- X[, -(1:as.integer(samples * train.val.fraction))]
+
+X.train.label <- Y[, 1:as.integer(samples * train.val.fraction)]
+X.val.label <- Y[, -(1:as.integer(samples * train.val.fraction))]
+
+train_buckets <- list("100" = list(data = X.train.data, label = X.train.label))
+eval_buckets <- list("100" = list(data = X.val.data, label = X.val.label))
+
+train_buckets <- list(buckets = train_buckets, dic = dic, rev_dic = rev_dic)
+eval_buckets <- list(buckets = eval_buckets, dic = dic, rev_dic = rev_dic)
+```
+
+Create iterators for training and evaluation datasets.
+
+
+```R
+vocab <- length(eval_buckets$dic)
+
+batch.size <- 32
+
+train.data <- mx.io.bucket.iter(buckets = train_buckets$buckets, batch.size = batch.size,
+ data.mask.element = 0, shuffle = TRUE)
+
+eval.data <- mx.io.bucket.iter(buckets = eval_buckets$buckets, batch.size = batch.size,
+ data.mask.element = 0, shuffle = FALSE)
+```
+
+## Train the Model
+
+
+This model is a multi-layer RNN for sampling from character-level language models. It has a one-to-one model configuration since for each character, we want to predict the next one. For a sequence of length 100, there are also 100 labels, corresponding the same sequence of characters but offset by a position of +1. The parameters output_last_state is set to TRUE in order to access the state of the RNN cells when performing inference.
+
+
+```R
+rnn_graph_one_one <- rnn.graph(num_rnn_layer = 3,
+ num_hidden = 96,
+ input_size = vocab,
+ num_embed = 64,
+ num_decode = vocab,
+ dropout = 0.2,
+ ignore_label = 0,
+ cell_type = "lstm",
+ masking = F,
+ output_last_state = T,
+ loss_output = "softmax",
+ config = "one-to-one")
+
+graph.viz(rnn_graph_one_one, type = "graph", direction = "LR",
+ graph.height.px = 180, shape=c(100, 64))
+
+devices <- mx.cpu()
+
+initializer <- mx.init.Xavier(rnd_type = "gaussian", factor_type = "avg", magnitude = 3)
+
+optimizer <- mx.opt.create("adadelta", rho = 0.9, eps = 1e-5, wd = 1e-8,
+ clip_gradient = 5, rescale.grad = 1/batch.size)
+
+logger <- mx.metric.logger()
+epoch.end.callback <- mx.callback.log.train.metric(period = 1, logger = logger)
+batch.end.callback <- mx.callback.log.train.metric(period = 50)
+
+mx.metric.custom_nd <- function(name, feval) {
+ init <- function() {
+ c(0, 0)
+ }
+ update <- function(label, pred, state) {
+ m <- feval(label, pred)
+ state <- c(state[[1]] + 1, state[[2]] + m)
+ return(state)
+ }
+ get <- function(state) {
+ list(name=name, value = (state[[2]] / state[[1]]))
+ }
+ ret <- (list(init = init, update = update, get = get))
+ class(ret) <- "mx.metric"
+ return(ret)
+}
+
+mx.metric.Perplexity <- mx.metric.custom_nd("Perplexity", function(label, pred) {
+ label <- mx.nd.reshape(label, shape = -1)
+ label_probs <- as.array(mx.nd.choose.element.0index(pred, label))
+ batch <- length(label_probs)
+ NLL <- -sum(log(pmax(1e-15, as.array(label_probs)))) / batch
+ Perplexity <- exp(NLL)
+ return(Perplexity)
+})
+
+model <- mx.model.buckets(symbol = rnn_graph_one_one,
+ train.data = train.data, eval.data = eval.data,
+ num.round = 20, ctx = devices, verbose = TRUE,
+ metric = mx.metric.Perplexity,
+ initializer = initializer, optimizer = optimizer,
+ batch.end.callback = NULL,
+ epoch.end.callback = epoch.end.callback)
+
+mx.model.save(model, prefix = "one_to_one_seq_model", iteration = 20)
+```
+
+ Start training with 1 devices
+ [1] Train-Perplexity=13.7040474322178
+ [1] Validation-Perplexity=7.94617194460922
+ [2] Train-Perplexity=6.57039815554525
+ [2] Validation-Perplexity=6.60806110658011
+ [3] Train-Perplexity=5.65360504501481
+ [3] Validation-Perplexity=6.18932770630876
+ [4] Train-Perplexity=5.32547285727298
+ [4] Validation-Perplexity=6.02198756798859
+ [5] Train-Perplexity=5.14373631472579
+ [5] Validation-Perplexity=5.8095658243407
+ [6] Train-Perplexity=5.03077673487379
+ [6] Validation-Perplexity=5.72582993567431
+ [7] Train-Perplexity=4.94453383291536
+ [7] Validation-Perplexity=5.6445258528126
+ [8] Train-Perplexity=4.88635290100261
+ [8] Validation-Perplexity=5.6730024536433
+ [9] Train-Perplexity=4.84205646230548
+ [9] Validation-Perplexity=5.50960780230982
+ [10] Train-Perplexity=4.80441673535513
+ [10] Validation-Perplexity=5.57002263750006
+ [11] Train-Perplexity=4.77763413242626
+ [11] Validation-Perplexity=5.55152143269169
+ [12] Train-Perplexity=4.74937775290777
+ [12] Validation-Perplexity=5.44968305351486
+ [13] Train-Perplexity=4.72824849541467
+ [13] Validation-Perplexity=5.50889348298234
+ [14] Train-Perplexity=4.70980846981694
+ [14] Validation-Perplexity=5.51473225859859
+ [15] Train-Perplexity=4.69685776886122
+ [15] Validation-Perplexity=5.45391985233811
+ [16] Train-Perplexity=4.67837107034824
+ [16] Validation-Perplexity=5.46636764997829
+ [17] Train-Perplexity=4.66866961934873
+ [17] Validation-Perplexity=5.44267086113492
+ [18] Train-Perplexity=4.65611469144194
+ [18] Validation-Perplexity=5.4290169469462
+ [19] Train-Perplexity=4.64614689879405
+ [19] Validation-Perplexity=5.44221549833917
+ [20] Train-Perplexity=4.63764001963654
+ [20] Validation-Perplexity=5.42114250842862
+
+
+## Inference on the Model
+
+We now use the saved model to do inference and sample text character by character that will look like the original training data.
+
+
+```R
+set.seed(0)
+model <- mx.model.load(prefix = "one_to_one_seq_model", iteration = 20)
+
+internals <- model$symbol$get.internals()
+sym_state <- internals$get.output(which(internals$outputs %in% "RNN_state"))
+sym_state_cell <- internals$get.output(which(internals$outputs %in% "RNN_state_cell"))
+sym_output <- internals$get.output(which(internals$outputs %in% "loss_output"))
+symbol <- mx.symbol.Group(sym_output, sym_state, sym_state_cell)
+
+infer_raw <- c("Thou ")
+infer_split <- dic[strsplit(infer_raw, '') %>% unlist]
+infer_length <- length(infer_split)
+
+infer.data <- mx.io.arrayiter(data = matrix(infer_split), label = matrix(infer_split),
+ batch.size = 1, shuffle = FALSE)
+
+infer <- mx.infer.rnn.one(infer.data = infer.data,
+ symbol = symbol,
+ arg.params = model$arg.params,
+ aux.params = model$aux.params,
+ input.params = NULL,
+ ctx = devices)
+
+pred_prob <- as.numeric(as.array(mx.nd.slice.axis(
+ infer$loss_output, axis = 0, begin = infer_length-1, end = infer_length)))
+pred <- sample(length(pred_prob), prob = pred_prob, size = 1) - 1
+predict <- c(predict, pred)
+
+for (i in 1:200) {
+
+ infer.data <- mx.io.arrayiter(data = as.matrix(pred), label = as.matrix(pred),
+ batch.size = 1, shuffle = FALSE)
+
+ infer <- mx.infer.rnn.one(infer.data = infer.data,
+ symbol = symbol,
+ arg.params = model$arg.params,
+ aux.params = model$aux.params,
+ input.params = list(rnn.state = infer[[2]],
+ rnn.state.cell = infer[[3]]),
+ ctx = devices)
+
+ pred_prob <- as.numeric(as.array(infer$loss_output))
+ pred <- sample(length(pred_prob), prob = pred_prob, size = 1, replace = T) - 1
+ predict <- c(predict, pred)
+}
+
+predict_txt <- paste0(rev_dic[as.character(predict)], collapse = "")
+predict_txt_tot <- paste0(infer_raw, predict_txt, collapse = "")
+print(predict_txt_tot)
+```
+
+ [1] "Thou NAknowledge thee my Comfort and his late she.FRIAR LAURENCE:Nothing a groats waterd forth. The lend he thank that;When she I am brother draw London: and not hear that know.BENVOLIO:How along, makes your "
+
+
+
diff --git a/docs/tutorials/scala/char_lstm.md b/docs/tutorials/scala/char_lstm.md
index e5f071b19c94..4d6a5aee921e 100644
--- a/docs/tutorials/scala/char_lstm.md
+++ b/docs/tutorials/scala/char_lstm.md
@@ -129,7 +129,7 @@ To prepare the data:
```scala
scala> // Build a vocabulary of what char we have in the content
scala> def buildVocab(path: String): Map[String, Int] = {
- val content = readContent(dataPath).split("\n")
+ val content = readContent(path).split("\n")
var idx = 1 // 0 is left for zero padding
var theVocab = Map[String, Int]()
for (line <- content) {
diff --git a/docs/tutorials/scala/mnist.md b/docs/tutorials/scala/mnist.md
index c5c953503dda..79f2129ef0ef 100644
--- a/docs/tutorials/scala/mnist.md
+++ b/docs/tutorials/scala/mnist.md
@@ -20,12 +20,12 @@ import org.apache.mxnet.optimizer.SGD
// model definition
val data = Symbol.Variable("data")
-val fc1 = Symbol.FullyConnected(name = "fc1")()(Map("data" -> data, "num_hidden" -> 128))
-val act1 = Symbol.Activation(name = "relu1")()(Map("data" -> fc1, "act_type" -> "relu"))
-val fc2 = Symbol.FullyConnected(name = "fc2")()(Map("data" -> act1, "num_hidden" -> 64))
-val act2 = Symbol.Activation(name = "relu2")()(Map("data" -> fc2, "act_type" -> "relu"))
-val fc3 = Symbol.FullyConnected(name = "fc3")()(Map("data" -> act2, "num_hidden" -> 10))
-val mlp = Symbol.SoftmaxOutput(name = "sm")()(Map("data" -> fc3))
+val fc1 = Symbol.api.FullyConnected(Some(data), num_hidden = 128, name = "fc1")
+val act1 = Symbol.api.Activation(Some(fc1), "relu", "relu1")
+val fc2 = Symbol.api.FullyConnected(Some(act1), num_hidden = 64, name = "fc2")
+val act2 = Symbol.api.Activation(Some(fc2), "relu", "relu2")
+val fc3 = Symbol.api.FullyConnected(Some(act2), num_hidden = 10, name = "fc3")
+val mlp = Symbol.api.SoftmaxOutput(Some(fc3), name = "sm")
```
## Load the Data
diff --git a/docs/tutorials/scala/mxnet_scala_on_intellij.md b/docs/tutorials/scala/mxnet_scala_on_intellij.md
index 615846bb9ee1..e28359b26960 100644
--- a/docs/tutorials/scala/mxnet_scala_on_intellij.md
+++ b/docs/tutorials/scala/mxnet_scala_on_intellij.md
@@ -38,7 +38,7 @@ brew install maven
**Step 4.** Install OpenCV:
```
-brew install opencv@2
+brew install opencv
```
@@ -73,7 +73,6 @@ The configuration you should update is in the pom file's dependency for MXNet:
org.apache.mxnet
mxnet-full_2.11-osx-x86_64-cpu
- 1.2.0
```
@@ -158,7 +157,7 @@ The project's `pom.xml` will be open for editing.
**Step 3.** Replace the pom file's content with the following code. Changes include:
- Project properties: `scala.version`, upgrading from `2.11.5` to `2.11.8`
- - Project dependencies: adding the MXNet package from Maven and updating the dependency for JUnitRunner (specs2-junit_)
+ - Project dependencies: adding the MXNet package from Maven and updating the dependency for JUnitRunner (specs2-junit_) and logging
- Build options: removing '-make:transitive'
@@ -204,19 +203,25 @@ The project's `pom.xml` will be open for editing.
UTF-8
2.11.8
2.11
+ 1.7.7
+ osx-x86_64-cpu
org.apache.mxnet
mxnet-full_2.11-osx-x86_64-cpu
- 1.2.0
org.scala-lang
scala-library
${scala.version}
+
+ args4j
+ args4j
+ 2.0.29
+
@@ -237,6 +242,18 @@ The project's `pom.xml` will be open for editing.
2.2.4
test
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4jVersion}
+
+
+ org.slf4j
+ slf4j-log4j12
+ ${slf4jVersion}
+
@@ -292,11 +309,24 @@ The project's `pom.xml` will be open for editing.
Click "Import Changes" in this prompt.
-**Step 5.** Build the project:
+**Step 5.** Setup log4j configuration
+
+Create a folder `src/main/resources` and a new file in it `src/main/resources/log4j.properties` with the contents:
+
+```
+log4j.rootLogger = info, stdout
+
+log4j.appender.stdout = org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target = System.out
+log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c] [%p] - %m%n
+```
+
+**Step 6.** Build the project:
- To build the project, from the menu choose Build, and then choose Build Project.
-**Step 6.** Run the Hello World App:
+**Step 7.** Run the Hello World App:
![hello world app](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/scala/intellij-project-hello-world-app.png)
@@ -306,7 +336,7 @@ Navigate to the App included with the project.
Run the App by clicking the green arrow, and verify the Hello World output
-**Step 7.** Run Sample MXNet Code in the App:
+**Step 8.** Run Sample MXNet Code in the App:
![run hello mxnet](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/scala/intellij-project-hello-mxnet.png)
@@ -336,29 +366,38 @@ Process finished with exit code 0
### Troubleshooting
-If you get an error, check the dependencies at the beginning of this tutorial. For example, you might see the following in the middle of the error messages.
+If you get an error, check the dependencies at the beginning of this tutorial. For example, you might see the following in the middle of the error messages, where `x.x` would the version it's looking for.
```
...
-Library not loaded: /usr/local/opt/opencv@2/lib/libopencv_calib3d.2.4.dylib
+Library not loaded: /usr/local/opt/opencv/lib/libopencv_calib3d.x.x.dylib
...
```
-This can be resolved be installing OpenCV2.
-
+This can be resolved be installing OpenCV.
-### Troubleshooting
+### Using MXNet from source
-If you get an error, check if it is like this one regarding OpenCV. For example, you might see the following in the middle of the error messages.
+If you chose to "Build from Source" when following the [install instructions](https://mxnet.incubator.apache.org/install/index.html) (or the detailed [build from source instructions](https://mxnet.incubator.apache.org/install/build_from_source.html#installing-mxnet-language-bindings)), you can use your custom build instead of the build from maven. Use your build by editing the `pom.xml` file and replacing the `org.apache.mxnet` dependency with the following:
```
-...
-Library not loaded: /usr/local/opt/opencv@2/lib/libopencv_calib3d.2.4.dylib
-...
+ org.apache.mxnet
+ mxnet-core_${scala.version}-${platform}-sources
+ system
+ /PathToMXNetSource/incubator-mxnet/scala-package/assembly/osx-x86_64-cpu/target/mxnet-full_${scala.version}-osx-x86_64-cpu-1.3.1-SNAPSHOT-sources.jar
+
+
+
+ org.apache.mxnet
+ mxnet-full_${scala.version}-${platform}
+ system
+ /PathToMXNetSource/incubator-mxnet/scala-package/assembly/osx-x86_64-cpu/target/mxnet-full_${scala.version}-osx-x86_64-cpu-1.3.1-SNAPSHOT.jar
+
```
-This can be resolved be installing OpenCV2.
+Note that you have to edit both of the `systemPath` properties to point to your generated jar files.
+
### Command Line Build Option
diff --git a/docs/tutorials/unsupervised_learning/gan.md b/docs/tutorials/unsupervised_learning/gan.md
index 1556bf609aa2..f436a15bac5d 100644
--- a/docs/tutorials/unsupervised_learning/gan.md
+++ b/docs/tutorials/unsupervised_learning/gan.md
@@ -394,7 +394,7 @@ As a result, we have created two neural nets: a Generator, which is able to crea
Along the way, we have learned how to do the image manipulation and visualization that is associated with the training of deep neural nets. We have also learned how to use MXNet's Module APIs to perform advanced model training functionality to fit the model.
## Acknowledgements
-This tutorial is based on [MXNet DCGAN codebase](https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dcgan.py),
+This tutorial is based on [MXNet DCGAN codebase](https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dc_gan/dcgan.py),
[The original paper on GANs](https://arxiv.org/abs/1406.2661), as well as [this paper on deep convolutional GANs](https://arxiv.org/abs/1511.06434).
\ No newline at end of file
diff --git a/docs/tutorials/vision/cnn_visualization.md b/docs/tutorials/vision/cnn_visualization.md
index ea027dff09ac..63d2b13271ba 100644
--- a/docs/tutorials/vision/cnn_visualization.md
+++ b/docs/tutorials/vision/cnn_visualization.md
@@ -22,7 +22,7 @@ from matplotlib import pyplot as plt
import numpy as np
gradcam_file = "gradcam.py"
-base_url = "https://raw.githubusercontent.com/indhub/mxnet/cnnviz/example/cnn_visualization/{}?raw=true"
+base_url = "https://raw.githubusercontent.com/apache/incubator-mxnet/master/docs/tutorial_utils/vision/cnn_visualization/{}?raw=true"
mx.test_utils.download(base_url.format(gradcam_file), fname=gradcam_file)
import gradcam
```
@@ -99,12 +99,18 @@ def get_vgg(num_layers, ctx=mx.cpu(), root=os.path.join('~', '.mxnet', 'models')
# Get the number of convolution layers and filters
layers, filters = vgg_spec[num_layers]
- # Build the VGG network
+ # Build the modified VGG network
net = VGG(layers, filters, **kwargs)
-
- # Load pretrained weights from model zoo
- from mxnet.gluon.model_zoo.model_store import get_model_file
- net.load_params(get_model_file('vgg%d' % num_layers, root=root), ctx=ctx)
+ net.initialize(ctx=ctx)
+
+ # Get the pretrained model
+ vgg = mx.gluon.model_zoo.vision.get_vgg(num_layers, pretrained=True, ctx=ctx)
+
+ # Set the parameters in the new network
+ params = vgg.collect_params()
+ for key in params:
+ param = params[key]
+ net.collect_params()[net.prefix+key.replace(vgg.prefix, '')].set_data(param.data())
return net
@@ -145,7 +151,8 @@ def show_images(pred_str, images):
for i in range(num_images):
fig.add_subplot(rows, cols, i+1)
plt.xlabel(titles[i])
- plt.imshow(images[i], cmap='gray' if i==num_images-1 else None)
+ img = images[i].astype(np.uint8)
+ plt.imshow(img, cmap='gray' if i==num_images-1 else None)
plt.show()
```
@@ -175,6 +182,7 @@ Next, we'll write a method to get an image, preprocess it, predict category and
2. **Guided Grad-CAM:** Guided Grad-CAM shows which exact pixels contributed the most to the CNN's decision.
3. **Saliency map:** Saliency map is a monochrome image showing which pixels contributed the most to the CNN's decision. Sometimes, it is easier to see the areas in the image that most influence the output in a monochrome image than in a color image.
+
```python
def visualize(net, img_path, conv_layer_name):
orig_img = mx.img.imread(img_path)
diff --git a/example/adversary/README.md b/example/adversary/README.md
index 51d295d30adb..5d5b44fb91ba 100644
--- a/example/adversary/README.md
+++ b/example/adversary/README.md
@@ -1,7 +1,7 @@
# Adversarial examples
This demonstrates the concept of "adversarial examples" from [1] showing how to fool a well-trained CNN.
-The surprising idea is that one can easily generate examples which the CNN will consistently
-make the wrong prediction for that a human can easily tell are correct.
+Adversarial examples are samples where the input has been manipulated to confuse a model (i.e. confident in an incorrect prediction) but where the correct answer still appears obvious to a human.
+This method for generating adversarial examples uses the gradient of the loss with respect to the input to craft the adversarial examples.
[1] Goodfellow, Ian J., Jonathon Shlens, and Christian Szegedy. "Explaining and harnessing adversarial examples." [arXiv preprint arXiv:1412.6572 (2014)](https://arxiv.org/abs/1412.6572)
diff --git a/example/adversary/adversary_generation.ipynb b/example/adversary/adversary_generation.ipynb
index b8804bd8137b..0b45366504e3 100644
--- a/example/adversary/adversary_generation.ipynb
+++ b/example/adversary/adversary_generation.ipynb
@@ -6,8 +6,7 @@
"source": [
"# Fast Sign Adversary Generation Example\n",
"\n",
- "This notebook demos find adversary example by using symbolic API and integration with Numpy\n",
- "Reference: \n",
+ "This notebook demos finds adversary examples using MXNet Gluon and taking advantage of the gradient information\n",
"\n",
"[1] Goodfellow, Ian J., Jonathon Shlens, and Christian Szegedy. \"Explaining and harnessing adversarial examples.\" arXiv preprint arXiv:1412.6572 (2014).\n",
"https://arxiv.org/abs/1412.6572"
@@ -15,7 +14,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"metadata": {
"collapsed": false
},
@@ -28,290 +27,312 @@
"import matplotlib.pyplot as plt\n",
"import matplotlib.cm as cm\n",
"\n",
- "from mxnet.test_utils import get_mnist_iterator"
+ "from mxnet import gluon"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Build Network\n",
- "\n",
- "note: in this network, we will calculate softmax, gradient in numpy"
+ "Build simple CNN network for solving the MNIST dataset digit recognition task"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 17,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
- "dev = mx.cpu()\n",
- "batch_size = 100\n",
- "train_iter, val_iter = get_mnist_iterator(batch_size=batch_size, input_shape = (1,28,28))"
+ "ctx = mx.gpu() if len(mx.test_utils.list_gpus()) else mx.cpu()\n",
+ "batch_size = 128"
]
},
{
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "cell_type": "markdown",
+ "metadata": {},
"source": [
- "# input\n",
- "data = mx.symbol.Variable('data')\n",
- "# first conv\n",
- "conv1 = mx.symbol.Convolution(data=data, kernel=(5,5), num_filter=20)\n",
- "tanh1 = mx.symbol.Activation(data=conv1, act_type=\"tanh\")\n",
- "pool1 = mx.symbol.Pooling(data=tanh1, pool_type=\"max\",\n",
- " kernel=(2,2), stride=(2,2))\n",
- "# second conv\n",
- "conv2 = mx.symbol.Convolution(data=pool1, kernel=(5,5), num_filter=50)\n",
- "tanh2 = mx.symbol.Activation(data=conv2, act_type=\"tanh\")\n",
- "pool2 = mx.symbol.Pooling(data=tanh2, pool_type=\"max\",\n",
- " kernel=(2,2), stride=(2,2))\n",
- "# first fullc\n",
- "flatten = mx.symbol.Flatten(data=pool2)\n",
- "fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=500)\n",
- "tanh3 = mx.symbol.Activation(data=fc1, act_type=\"tanh\")\n",
- "# second fullc\n",
- "fc2 = mx.symbol.FullyConnected(data=tanh3, num_hidden=10)"
+ "## Data Loading"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 3,
+ "metadata": {},
"outputs": [],
"source": [
- "def Softmax(theta):\n",
- " max_val = np.max(theta, axis=1, keepdims=True)\n",
- " tmp = theta - max_val\n",
- " exp = np.exp(tmp)\n",
- " norm = np.sum(exp, axis=1, keepdims=True)\n",
- " return exp / norm"
+ "transform = lambda x,y: (x.transpose((2,0,1)).astype('float32')/255., y)\n",
+ "\n",
+ "train_dataset = gluon.data.vision.MNIST(train=True).transform(transform)\n",
+ "test_dataset = gluon.data.vision.MNIST(train=False).transform(transform)\n",
+ "\n",
+ "train_data = gluon.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=5)\n",
+ "test_data = gluon.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Create the network"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
- "def LogLossGrad(alpha, label):\n",
- " grad = np.copy(alpha)\n",
- " for i in range(alpha.shape[0]):\n",
- " grad[i, int(label[i])] -= 1.\n",
- " return grad"
+ "net = gluon.nn.HybridSequential()\n",
+ "with net.name_scope():\n",
+ " net.add(\n",
+ " gluon.nn.Conv2D(kernel_size=5, channels=20, activation='tanh'),\n",
+ " gluon.nn.MaxPool2D(pool_size=2, strides=2),\n",
+ " gluon.nn.Conv2D(kernel_size=5, channels=50, activation='tanh'),\n",
+ " gluon.nn.MaxPool2D(pool_size=2, strides=2),\n",
+ " gluon.nn.Flatten(),\n",
+ " gluon.nn.Dense(500, activation='tanh'),\n",
+ " gluon.nn.Dense(10)\n",
+ " )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Prepare useful data for the network"
+ "## Initialize training"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 5,
"metadata": {
- "collapsed": false
+ "collapsed": true
},
"outputs": [],
"source": [
- "data_shape = (batch_size, 1, 28, 28)\n",
- "arg_names = fc2.list_arguments() # 'data' \n",
- "arg_shapes, output_shapes, aux_shapes = fc2.infer_shape(data=data_shape)\n",
- "\n",
- "arg_arrays = [mx.nd.zeros(shape, ctx=dev) for shape in arg_shapes]\n",
- "grad_arrays = [mx.nd.zeros(shape, ctx=dev) for shape in arg_shapes]\n",
- "reqs = [\"write\" for name in arg_names]\n",
- "\n",
- "model = fc2.bind(ctx=dev, args=arg_arrays, args_grad = grad_arrays, grad_req=reqs)\n",
- "arg_map = dict(zip(arg_names, arg_arrays))\n",
- "grad_map = dict(zip(arg_names, grad_arrays))\n",
- "data_grad = grad_map[\"data\"]\n",
- "out_grad = mx.nd.zeros(model.outputs[0].shape, ctx=dev)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Init weight "
+ "net.initialize(mx.initializer.Uniform(), ctx=ctx)\n",
+ "net.hybridize()"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 6,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
- "for name in arg_names:\n",
- " if \"weight\" in name:\n",
- " arr = arg_map[name]\n",
- " arr[:] = mx.rnd.uniform(-0.07, 0.07, arr.shape)"
+ "loss = gluon.loss.SoftmaxCELoss()"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 7,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
- "def SGD(weight, grad, lr=0.1, grad_norm=batch_size):\n",
- " weight[:] -= lr * grad / batch_size\n",
- "\n",
- "def CalAcc(pred_prob, label):\n",
- " pred = np.argmax(pred_prob, axis=1)\n",
- " return np.sum(pred == label) * 1.0\n",
- "\n",
- "def CalLoss(pred_prob, label):\n",
- " loss = 0.\n",
- " for i in range(pred_prob.shape[0]):\n",
- " loss += -np.log(max(pred_prob[i, int(label[i])], 1e-10))\n",
- " return loss"
+ "trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1, 'momentum':0.95})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Train a network"
+ "## Training loop"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 8,
"metadata": {
"collapsed": false
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Train Accuracy: 0.92\t Train Loss: 0.32142\n",
+ "Train Accuracy: 0.97\t Train Loss: 0.16773\n",
+ "Train Accuracy: 0.97\t Train Loss: 0.14660\n"
+ ]
+ }
+ ],
"source": [
- "num_round = 4\n",
- "train_acc = 0.\n",
- "nbatch = 0\n",
- "for i in range(num_round):\n",
+ "epoch = 3\n",
+ "for e in range(epoch):\n",
" train_loss = 0.\n",
- " train_acc = 0.\n",
- " nbatch = 0\n",
- " train_iter.reset()\n",
- " for batch in train_iter:\n",
- " arg_map[\"data\"][:] = batch.data[0]\n",
- " model.forward(is_train=True)\n",
- " theta = model.outputs[0].asnumpy()\n",
- " alpha = Softmax(theta)\n",
- " label = batch.label[0].asnumpy()\n",
- " train_acc += CalAcc(alpha, label) / batch_size\n",
- " train_loss += CalLoss(alpha, label) / batch_size\n",
- " losGrad_theta = LogLossGrad(alpha, label)\n",
- " out_grad[:] = losGrad_theta\n",
- " model.backward([out_grad])\n",
- " # data_grad[:] = grad_map[\"data\"]\n",
- " for name in arg_names:\n",
- " if name != \"data\":\n",
- " SGD(arg_map[name], grad_map[name])\n",
+ " acc = mx.metric.Accuracy()\n",
+ " for i, (data, label) in enumerate(train_data):\n",
+ " data = data.as_in_context(ctx)\n",
+ " label = label.as_in_context(ctx)\n",
+ " \n",
+ " with mx.autograd.record():\n",
+ " output = net(data)\n",
+ " l = loss(output, label)\n",
+ " \n",
+ " l.backward()\n",
+ " trainer.update(data.shape[0])\n",
" \n",
- " nbatch += 1\n",
- " #print(np.linalg.norm(data_grad.asnumpy(), 2))\n",
- " train_acc /= nbatch\n",
- " train_loss /= nbatch\n",
- " print(\"Train Accuracy: %.2f\\t Train Loss: %.5f\" % (train_acc, train_loss))"
+ " train_loss += l.mean().asscalar()\n",
+ " acc.update(label, output)\n",
+ " \n",
+ " print(\"Train Accuracy: %.2f\\t Train Loss: %.5f\" % (acc.get()[1], train_loss/(i+1)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Get pertubation by using fast sign method, check validation change.\n",
- "See that the validation set was almost entirely correct before the perturbations, but after the perturbations, it is much worse than random guessing."
+ "## Perturbation\n",
+ "\n",
+ "We first run a validation batch and measure the resulting accuracy.\n",
+ "We then perturbate this batch by modifying the input in the opposite direction of the gradient."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Validation batch accuracy 0.96875\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Get a batch from the testing set\n",
+ "for data, label in test_data:\n",
+ " data = data.as_in_context(ctx)\n",
+ " label = label.as_in_context(ctx)\n",
+ " break\n",
+ "\n",
+ "# Attach gradient to it to get the gradient of the loss with respect to the input\n",
+ "data.attach_grad()\n",
+ "with mx.autograd.record():\n",
+ " output = net(data) \n",
+ " l = loss(output, label)\n",
+ "l.backward()\n",
+ "\n",
+ "acc = mx.metric.Accuracy()\n",
+ "acc.update(label, output)\n",
+ "\n",
+ "print(\"Validation batch accuracy {}\".format(acc.get()[1]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now we perturb the input"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
"metadata": {
"collapsed": false
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Validation batch accuracy after perturbation 0.40625\n"
+ ]
+ }
+ ],
"source": [
- "val_iter.reset()\n",
- "batch = val_iter.next()\n",
- "data = batch.data[0]\n",
- "label = batch.label[0]\n",
- "arg_map[\"data\"][:] = data\n",
- "model.forward(is_train=True)\n",
- "theta = model.outputs[0].asnumpy()\n",
- "alpha = Softmax(theta)\n",
- "print(\"Val Batch Accuracy: \", CalAcc(alpha, label.asnumpy()) / batch_size)\n",
- "#########\n",
- "grad = LogLossGrad(alpha, label.asnumpy())\n",
- "out_grad[:] = grad\n",
- "model.backward([out_grad])\n",
- "noise = np.sign(data_grad.asnumpy())\n",
- "arg_map[\"data\"][:] = data.asnumpy() + 0.15 * noise\n",
- "model.forward(is_train=True)\n",
- "raw_output = model.outputs[0].asnumpy()\n",
- "pred = Softmax(raw_output)\n",
- "print(\"Val Batch Accuracy after pertubation: \", CalAcc(pred, label.asnumpy()) / batch_size)"
+ "data_perturbated = data + 0.15 * mx.nd.sign(data.grad)\n",
+ "\n",
+ "output = net(data_perturbated) \n",
+ "\n",
+ "acc = mx.metric.Accuracy()\n",
+ "acc.update(label, output)\n",
+ "\n",
+ "print(\"Validation batch accuracy after perturbation {}\".format(acc.get()[1]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Visualize an example after pertubation.\n",
- "Note that the prediction is consistently incorrect."
+ "## Visualization"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's visualize an example after pertubation.\n",
+ "\n",
+ "We can see that the prediction is often incorrect."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 16,
"metadata": {
"collapsed": false
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "true label: 1\n",
+ "predicted: 3\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "import random as rnd\n",
- "idx = rnd.randint(0, 99)\n",
- "images = data.asnumpy() + 0.15 * noise\n",
- "plt.imshow(images[idx, :].reshape(28,28), cmap=cm.Greys_r)\n",
- "print(\"true: %d\" % label.asnumpy()[idx])\n",
- "print(\"pred: %d\" % np.argmax(pred, axis=1)[idx])"
+ "from random import randint\n",
+ "idx = randint(0, batch_size-1)\n",
+ "\n",
+ "plt.imshow(data_perturbated[idx, :].asnumpy().reshape(28,28), cmap=cm.Greys_r)\n",
+ "print(\"true label: %d\" % label.asnumpy()[idx])\n",
+ "print(\"predicted: %d\" % np.argmax(output.asnumpy(), axis=1)[idx])"
]
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 2",
+ "display_name": "Python 3",
"language": "python",
- "name": "python2"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.13"
+ "pygments_lexer": "ipython3",
+ "version": "3.6.4"
}
},
"nbformat": 4,
- "nbformat_minor": 0
+ "nbformat_minor": 2
}
diff --git a/example/vae/README.md b/example/autoencoder/variational_autoencoder/README.md
similarity index 100%
rename from example/vae/README.md
rename to example/autoencoder/variational_autoencoder/README.md
diff --git a/example/vae/VAE.py b/example/autoencoder/variational_autoencoder/VAE.py
similarity index 100%
rename from example/vae/VAE.py
rename to example/autoencoder/variational_autoencoder/VAE.py
diff --git a/example/autoencoder/variational_autoencoder/VAE_example.ipynb b/example/autoencoder/variational_autoencoder/VAE_example.ipynb
new file mode 100755
index 000000000000..964e13725c69
--- /dev/null
+++ b/example/autoencoder/variational_autoencoder/VAE_example.ipynb
@@ -0,0 +1,1204 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import mxnet as mx\n",
+ "import numpy as np\n",
+ "import os\n",
+ "import logging\n",
+ "import matplotlib.pyplot as plt\n",
+ "import matplotlib.cm as cm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Building a Variational Autoencoder in MXNet\n",
+ "\n",
+ "#### Xiaoyu Lu, July 5th, 2017\n",
+ "\n",
+ "This tutorial guides you through the process of building a variational encoder in MXNet. In this notebook we'll focus on an example using the MNIST handwritten digit recognition dataset. Refer to [Auto-Encoding Variational Bayes](https://arxiv.org/abs/1312.6114/) for more details on the model description.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Prerequisites\n",
+ "\n",
+ "To complete this tutorial, we need following python packages:\n",
+ "\n",
+ "- numpy, matplotlib "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 1. Loading the Data\n",
+ "\n",
+ "We first load the MNIST dataset, which contains 60000 training and 10000 test examples. The following code imports required modules and loads the data. These images are stored in a 4-D matrix with shape (`batch_size, num_channels, width, height`). For the MNIST dataset, there is only one color channel, and both width and height are 28, so we reshape each image as a 28x28 array. See below for a visualization:\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "60000 784\n"
+ ]
+ }
+ ],
+ "source": [
+ "mnist = mx.test_utils.get_mnist()\n",
+ "image = np.reshape(mnist['train_data'],(60000,28*28))\n",
+ "label = image\n",
+ "image_test = np.reshape(mnist['test_data'],(10000,28*28))\n",
+ "label_test = image_test\n",
+ "[N,features] = np.shape(image) #number of examples and features\n",
+ "print(N,features)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "nsamples = 5\n",
+ "idx = np.random.choice(len(mnist['train_data']), nsamples)\n",
+ "_, axarr = plt.subplots(1, nsamples, sharex='col', sharey='row',figsize=(12,3))\n",
+ "\n",
+ "for i,j in enumerate(idx):\n",
+ " axarr[i].imshow(np.reshape(image[j,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can optionally save the parameters in the directory variable 'model_prefix'. We first create data iterators for MXNet, with each batch of data containing 100 images."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model_prefix = None\n",
+ "\n",
+ "batch_size = 100\n",
+ "latent_dim = 5\n",
+ "nd_iter = mx.io.NDArrayIter(data={'data':image},label={'loss_label':label},\n",
+ " batch_size = batch_size)\n",
+ "nd_iter_test = mx.io.NDArrayIter(data={'data':image_test},label={'loss_label':label_test},\n",
+ " batch_size = batch_size)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 2. Building the Network Architecture\n",
+ "\n",
+ "### 2.1 Gaussian MLP as encoder\n",
+ "Next we constuct the neural network, as in the [paper](https://arxiv.org/abs/1312.6114/), we use *Multilayer Perceptron (MLP)* for both the encoder and decoder. For encoder, a Gaussian MLP is used as follows:\n",
+ "\n",
+ "\\begin{align}\n",
+ "\\log q_{\\phi}(z|x) &= \\log \\mathcal{N}(z:\\mu,\\sigma^2I) \\\\\n",
+ "\\textit{ where } \\mu &= W_2h+b_2, \\log \\sigma^2 = W_3h+b_3\\\\\n",
+ "h &= \\tanh(W_1x+b_1)\n",
+ "\\end{align}\n",
+ "\n",
+ "where $\\{W_1,W_2,W_3,b_1,b_2,b_3\\}$ are the weights and biases of the MLP.\n",
+ "Note below that `encoder_mu`(`mu`) and `encoder_logvar`(`logvar`) are symbols. So, we can use `get_internals()` to get the values of them, after which we can sample the latent variable $z$.\n",
+ "\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "## define data and loss labels as symbols \n",
+ "data = mx.sym.var('data')\n",
+ "loss_label = mx.sym.var('loss_label')\n",
+ "\n",
+ "## define fully connected and activation layers for the encoder, where we used tanh activation function.\n",
+ "encoder_h = mx.sym.FullyConnected(data=data, name=\"encoder_h\",num_hidden=400)\n",
+ "act_h = mx.sym.Activation(data=encoder_h, act_type=\"tanh\",name=\"activation_h\")\n",
+ "\n",
+ "## define mu and log variance which are the fully connected layers of the previous activation layer\n",
+ "mu = mx.sym.FullyConnected(data=act_h, name=\"mu\",num_hidden = latent_dim)\n",
+ "logvar = mx.sym.FullyConnected(data=act_h, name=\"logvar\",num_hidden = latent_dim)\n",
+ "\n",
+ "## sample the latent variables z according to Normal(mu,var)\n",
+ "z = mu + mx.symbol.broadcast_mul(mx.symbol.exp(0.5 * logvar), \n",
+ " mx.symbol.random_normal(loc=0, scale=1, shape=(batch_size, latent_dim)),\n",
+ " name=\"z\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.2 Bernoulli MLP as decoder\n",
+ "\n",
+ "In this case let $p_\\theta(x|z)$ be a multivariate Bernoulli whose probabilities are computed from $z$ with a feed forward neural network with a single hidden layer:\n",
+ "\n",
+ "\\begin{align}\n",
+ "\\log p(x|z) &= \\sum_{i=1}^D x_i\\log y_i + (1-x_i)\\log (1-y_i) \\\\\n",
+ "\\textit{ where } y &= f_\\sigma(W_5\\tanh (W_4z+b_4)+b_5)\n",
+ "\\end{align}\n",
+ "\n",
+ "where $f_\\sigma(\\dot)$ is the elementwise sigmoid activation function, $\\{W_4,W_5,b_4,b_5\\}$ are the weights and biases of the decoder MLP. A Bernouilli likelihood is suitable for this type of data but you can easily extend it to other likelihood types by parsing into the argument `likelihood` in the `VAE` class, see section 4 for details."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# define fully connected and tanh activation layers for the decoder\n",
+ "decoder_z = mx.sym.FullyConnected(data=z, name=\"decoder_z\",num_hidden=400)\n",
+ "act_z = mx.sym.Activation(data=decoder_z, act_type=\"tanh\",name=\"activation_z\")\n",
+ "\n",
+ "# define the output layer with sigmoid activation function, where the dimension is equal to the input dimension\n",
+ "decoder_x = mx.sym.FullyConnected(data=act_z, name=\"decoder_x\",num_hidden=features)\n",
+ "y = mx.sym.Activation(data=decoder_x, act_type=\"sigmoid\",name='activation_x')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.3 Joint Loss Function for the Encoder and the Decoder\n",
+ "\n",
+ "The variational lower bound also called evidence lower bound (ELBO) can be estimated as:\n",
+ "\n",
+ "\\begin{align}\n",
+ "\\mathcal{L}(\\theta,\\phi;x_{(i)}) \\approx \\frac{1}{2}\\left(1+\\log ((\\sigma_j^{(i)})^2)-(\\mu_j^{(i)})^2-(\\sigma_j^{(i)})^2\\right) + \\log p_\\theta(x^{(i)}|z^{(i)})\n",
+ "\\end{align}\n",
+ "\n",
+ "where the first term is the KL divergence of the approximate posterior from the prior, and the second term is an expected negative reconstruction error. We would like to maximize this lower bound, so we can define the loss to be $-\\mathcal{L}$(minus ELBO) for MXNet to minimize."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# define the objective loss function that needs to be minimized\n",
+ "KL = 0.5*mx.symbol.sum(1+logvar-pow( mu,2)-mx.symbol.exp(logvar),axis=1)\n",
+ "loss = -mx.symbol.sum(mx.symbol.broadcast_mul(loss_label,mx.symbol.log(y)) \n",
+ " + mx.symbol.broadcast_mul(1-loss_label,mx.symbol.log(1-y)),axis=1)-KL\n",
+ "output = mx.symbol.MakeLoss(sum(loss),name='loss')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 3. Training the model\n",
+ "\n",
+ "Now, we can define the model and train it. First we will initilize the weights and the biases to be Gaussian(0,0.01), and then use stochastic gradient descent for optimization. To warm start the training, one may also initilize with pre-trainined parameters `arg_params` using `init=mx.initializer.Load(arg_params)`. \n",
+ "\n",
+ "To save intermediate results, we can optionally use `epoch_end_callback = mx.callback.do_checkpoint(model_prefix, 1)` which saves the parameters to the path given by model_prefix, and with period every $1$ epoch. To assess the performance, we output $-\\mathcal{L}$(minus ELBO) after each epoch, with the command `eval_metric = 'Loss'` which is defined above. We will also plot the training loss for mini batches by accessing the log and saving it to a list, and then parsing it to the argument `batch_end_callback`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# set up the log\n",
+ "nd_iter.reset()\n",
+ "logging.getLogger().setLevel(logging.DEBUG) \n",
+ "\n",
+ "# define function to trave back training loss\n",
+ "def log_to_list(period, lst):\n",
+ " def _callback(param):\n",
+ " \"\"\"The checkpoint function.\"\"\"\n",
+ " if param.nbatch % period == 0:\n",
+ " name, value = param.eval_metric.get()\n",
+ " lst.append(value)\n",
+ " return _callback\n",
+ "\n",
+ "# define the model\n",
+ "model = mx.mod.Module(\n",
+ " symbol = output ,\n",
+ " data_names=['data'],\n",
+ " label_names = ['loss_label'])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "INFO:root:Epoch[0] Train-loss=373.547317\n",
+ "INFO:root:Epoch[0] Time cost=5.020\n",
+ "INFO:root:Epoch[1] Train-loss=212.232684\n",
+ "INFO:root:Epoch[1] Time cost=4.651\n",
+ "INFO:root:Epoch[2] Train-loss=207.448528\n",
+ "INFO:root:Epoch[2] Time cost=4.665\n",
+ "INFO:root:Epoch[3] Train-loss=205.369479\n",
+ "INFO:root:Epoch[3] Time cost=4.758\n",
+ "INFO:root:Epoch[4] Train-loss=203.651983\n",
+ "INFO:root:Epoch[4] Time cost=4.672\n",
+ "INFO:root:Epoch[5] Train-loss=202.061007\n",
+ "INFO:root:Epoch[5] Time cost=5.087\n",
+ "INFO:root:Epoch[6] Train-loss=199.348143\n",
+ "INFO:root:Epoch[6] Time cost=5.056\n",
+ "INFO:root:Epoch[7] Train-loss=196.266242\n",
+ "INFO:root:Epoch[7] Time cost=4.813\n",
+ "INFO:root:Epoch[8] Train-loss=194.694945\n",
+ "INFO:root:Epoch[8] Time cost=4.776\n",
+ "INFO:root:Epoch[9] Train-loss=193.699284\n",
+ "INFO:root:Epoch[9] Time cost=4.756\n",
+ "INFO:root:Epoch[10] Train-loss=193.036517\n",
+ "INFO:root:Epoch[10] Time cost=4.757\n",
+ "INFO:root:Epoch[11] Train-loss=192.555736\n",
+ "INFO:root:Epoch[11] Time cost=4.678\n",
+ "INFO:root:Epoch[12] Train-loss=192.020813\n",
+ "INFO:root:Epoch[12] Time cost=4.630\n",
+ "INFO:root:Epoch[13] Train-loss=191.648876\n",
+ "INFO:root:Epoch[13] Time cost=5.158\n",
+ "INFO:root:Epoch[14] Train-loss=191.057798\n",
+ "INFO:root:Epoch[14] Time cost=4.781\n",
+ "INFO:root:Epoch[15] Train-loss=190.315835\n",
+ "INFO:root:Epoch[15] Time cost=5.117\n",
+ "INFO:root:Epoch[16] Train-loss=189.311271\n",
+ "INFO:root:Epoch[16] Time cost=4.707\n",
+ "INFO:root:Epoch[17] Train-loss=187.285967\n",
+ "INFO:root:Epoch[17] Time cost=4.745\n",
+ "INFO:root:Epoch[18] Train-loss=185.271324\n",
+ "INFO:root:Epoch[18] Time cost=4.692\n",
+ "INFO:root:Epoch[19] Train-loss=183.510888\n",
+ "INFO:root:Epoch[19] Time cost=4.762\n",
+ "INFO:root:Epoch[20] Train-loss=181.756008\n",
+ "INFO:root:Epoch[20] Time cost=4.838\n",
+ "INFO:root:Epoch[21] Train-loss=180.546818\n",
+ "INFO:root:Epoch[21] Time cost=4.764\n",
+ "INFO:root:Epoch[22] Train-loss=179.479776\n",
+ "INFO:root:Epoch[22] Time cost=4.791\n",
+ "INFO:root:Epoch[23] Train-loss=178.352077\n",
+ "INFO:root:Epoch[23] Time cost=4.981\n",
+ "INFO:root:Epoch[24] Train-loss=177.385084\n",
+ "INFO:root:Epoch[24] Time cost=5.292\n",
+ "INFO:root:Epoch[25] Train-loss=175.920123\n",
+ "INFO:root:Epoch[25] Time cost=5.097\n",
+ "INFO:root:Epoch[26] Train-loss=174.377171\n",
+ "INFO:root:Epoch[26] Time cost=4.907\n",
+ "INFO:root:Epoch[27] Train-loss=172.590589\n",
+ "INFO:root:Epoch[27] Time cost=4.484\n",
+ "INFO:root:Epoch[28] Train-loss=170.933683\n",
+ "INFO:root:Epoch[28] Time cost=4.348\n",
+ "INFO:root:Epoch[29] Train-loss=169.866807\n",
+ "INFO:root:Epoch[29] Time cost=4.647\n",
+ "INFO:root:Epoch[30] Train-loss=169.182084\n",
+ "INFO:root:Epoch[30] Time cost=5.034\n",
+ "INFO:root:Epoch[31] Train-loss=168.121719\n",
+ "INFO:root:Epoch[31] Time cost=5.615\n",
+ "INFO:root:Epoch[32] Train-loss=167.389992\n",
+ "INFO:root:Epoch[32] Time cost=4.733\n",
+ "INFO:root:Epoch[33] Train-loss=166.189067\n",
+ "INFO:root:Epoch[33] Time cost=5.041\n",
+ "INFO:root:Epoch[34] Train-loss=163.783392\n",
+ "INFO:root:Epoch[34] Time cost=5.168\n",
+ "INFO:root:Epoch[35] Train-loss=162.167959\n",
+ "INFO:root:Epoch[35] Time cost=5.019\n",
+ "INFO:root:Epoch[36] Train-loss=161.192039\n",
+ "INFO:root:Epoch[36] Time cost=5.064\n",
+ "INFO:root:Epoch[37] Train-loss=160.307114\n",
+ "INFO:root:Epoch[37] Time cost=5.180\n",
+ "INFO:root:Epoch[38] Train-loss=159.591957\n",
+ "INFO:root:Epoch[38] Time cost=5.440\n",
+ "INFO:root:Epoch[39] Train-loss=159.109593\n",
+ "INFO:root:Epoch[39] Time cost=5.119\n",
+ "INFO:root:Epoch[40] Train-loss=158.463844\n",
+ "INFO:root:Epoch[40] Time cost=5.299\n",
+ "INFO:root:Epoch[41] Train-loss=158.037287\n",
+ "INFO:root:Epoch[41] Time cost=4.856\n",
+ "INFO:root:Epoch[42] Train-loss=157.598576\n",
+ "INFO:root:Epoch[42] Time cost=5.227\n",
+ "INFO:root:Epoch[43] Train-loss=157.097344\n",
+ "INFO:root:Epoch[43] Time cost=5.237\n",
+ "INFO:root:Epoch[44] Train-loss=156.594472\n",
+ "INFO:root:Epoch[44] Time cost=4.783\n",
+ "INFO:root:Epoch[45] Train-loss=156.177069\n",
+ "INFO:root:Epoch[45] Time cost=4.834\n",
+ "INFO:root:Epoch[46] Train-loss=155.825302\n",
+ "INFO:root:Epoch[46] Time cost=4.902\n",
+ "INFO:root:Epoch[47] Train-loss=155.318117\n",
+ "INFO:root:Epoch[47] Time cost=4.966\n",
+ "INFO:root:Epoch[48] Train-loss=154.890766\n",
+ "INFO:root:Epoch[48] Time cost=5.012\n",
+ "INFO:root:Epoch[49] Train-loss=154.504158\n",
+ "INFO:root:Epoch[49] Time cost=4.844\n",
+ "INFO:root:Epoch[50] Train-loss=154.035214\n",
+ "INFO:root:Epoch[50] Time cost=4.736\n",
+ "INFO:root:Epoch[51] Train-loss=153.692903\n",
+ "INFO:root:Epoch[51] Time cost=5.057\n",
+ "INFO:root:Epoch[52] Train-loss=153.257554\n",
+ "INFO:root:Epoch[52] Time cost=5.044\n",
+ "INFO:root:Epoch[53] Train-loss=152.849715\n",
+ "INFO:root:Epoch[53] Time cost=4.783\n",
+ "INFO:root:Epoch[54] Train-loss=152.483047\n",
+ "INFO:root:Epoch[54] Time cost=4.842\n",
+ "INFO:root:Epoch[55] Train-loss=152.091617\n",
+ "INFO:root:Epoch[55] Time cost=5.044\n",
+ "INFO:root:Epoch[56] Train-loss=151.715490\n",
+ "INFO:root:Epoch[56] Time cost=5.029\n",
+ "INFO:root:Epoch[57] Train-loss=151.362293\n",
+ "INFO:root:Epoch[57] Time cost=4.873\n",
+ "INFO:root:Epoch[58] Train-loss=151.003241\n",
+ "INFO:root:Epoch[58] Time cost=4.729\n",
+ "INFO:root:Epoch[59] Train-loss=150.619678\n",
+ "INFO:root:Epoch[59] Time cost=5.068\n",
+ "INFO:root:Epoch[60] Train-loss=150.296043\n",
+ "INFO:root:Epoch[60] Time cost=4.458\n",
+ "INFO:root:Epoch[61] Train-loss=149.964152\n",
+ "INFO:root:Epoch[61] Time cost=4.828\n",
+ "INFO:root:Epoch[62] Train-loss=149.694102\n",
+ "INFO:root:Epoch[62] Time cost=5.012\n",
+ "INFO:root:Epoch[63] Train-loss=149.290113\n",
+ "INFO:root:Epoch[63] Time cost=5.193\n",
+ "INFO:root:Epoch[64] Train-loss=148.934186\n",
+ "INFO:root:Epoch[64] Time cost=4.999\n",
+ "INFO:root:Epoch[65] Train-loss=148.657502\n",
+ "INFO:root:Epoch[65] Time cost=4.810\n",
+ "INFO:root:Epoch[66] Train-loss=148.331948\n",
+ "INFO:root:Epoch[66] Time cost=5.201\n",
+ "INFO:root:Epoch[67] Train-loss=148.018539\n",
+ "INFO:root:Epoch[67] Time cost=4.833\n",
+ "INFO:root:Epoch[68] Train-loss=147.746825\n",
+ "INFO:root:Epoch[68] Time cost=5.187\n",
+ "INFO:root:Epoch[69] Train-loss=147.406399\n",
+ "INFO:root:Epoch[69] Time cost=5.355\n",
+ "INFO:root:Epoch[70] Train-loss=147.181831\n",
+ "INFO:root:Epoch[70] Time cost=4.989\n",
+ "INFO:root:Epoch[71] Train-loss=146.860770\n",
+ "INFO:root:Epoch[71] Time cost=4.934\n",
+ "INFO:root:Epoch[72] Train-loss=146.604369\n",
+ "INFO:root:Epoch[72] Time cost=5.283\n",
+ "INFO:root:Epoch[73] Train-loss=146.351628\n",
+ "INFO:root:Epoch[73] Time cost=5.062\n",
+ "INFO:root:Epoch[74] Train-loss=146.102506\n",
+ "INFO:root:Epoch[74] Time cost=4.540\n",
+ "INFO:root:Epoch[75] Train-loss=145.828805\n",
+ "INFO:root:Epoch[75] Time cost=4.875\n",
+ "INFO:root:Epoch[76] Train-loss=145.571626\n",
+ "INFO:root:Epoch[76] Time cost=4.856\n",
+ "INFO:root:Epoch[77] Train-loss=145.365383\n",
+ "INFO:root:Epoch[77] Time cost=5.003\n",
+ "INFO:root:Epoch[78] Train-loss=145.101047\n",
+ "INFO:root:Epoch[78] Time cost=4.718\n",
+ "INFO:root:Epoch[79] Train-loss=144.810765\n",
+ "INFO:root:Epoch[79] Time cost=5.127\n",
+ "INFO:root:Epoch[80] Train-loss=144.619876\n",
+ "INFO:root:Epoch[80] Time cost=4.737\n",
+ "INFO:root:Epoch[81] Train-loss=144.399066\n",
+ "INFO:root:Epoch[81] Time cost=4.742\n",
+ "INFO:root:Epoch[82] Train-loss=144.220090\n",
+ "INFO:root:Epoch[82] Time cost=4.810\n",
+ "INFO:root:Epoch[83] Train-loss=143.904279\n",
+ "INFO:root:Epoch[83] Time cost=5.176\n",
+ "INFO:root:Epoch[84] Train-loss=143.734935\n",
+ "INFO:root:Epoch[84] Time cost=4.921\n",
+ "INFO:root:Epoch[85] Train-loss=143.499403\n",
+ "INFO:root:Epoch[85] Time cost=4.692\n",
+ "INFO:root:Epoch[86] Train-loss=143.304287\n",
+ "INFO:root:Epoch[86] Time cost=4.778\n",
+ "INFO:root:Epoch[87] Train-loss=143.096145\n",
+ "INFO:root:Epoch[87] Time cost=4.962\n",
+ "INFO:root:Epoch[88] Train-loss=142.877920\n",
+ "INFO:root:Epoch[88] Time cost=4.815\n",
+ "INFO:root:Epoch[89] Train-loss=142.677429\n",
+ "INFO:root:Epoch[89] Time cost=5.127\n",
+ "INFO:root:Epoch[90] Train-loss=142.499622\n",
+ "INFO:root:Epoch[90] Time cost=5.463\n",
+ "INFO:root:Epoch[91] Train-loss=142.300291\n",
+ "INFO:root:Epoch[91] Time cost=4.639\n",
+ "INFO:root:Epoch[92] Train-loss=142.111362\n",
+ "INFO:root:Epoch[92] Time cost=5.064\n",
+ "INFO:root:Epoch[93] Train-loss=141.912848\n",
+ "INFO:root:Epoch[93] Time cost=4.894\n",
+ "INFO:root:Epoch[94] Train-loss=141.723130\n",
+ "INFO:root:Epoch[94] Time cost=4.635\n",
+ "INFO:root:Epoch[95] Train-loss=141.516580\n",
+ "INFO:root:Epoch[95] Time cost=5.063\n",
+ "INFO:root:Epoch[96] Train-loss=141.362380\n",
+ "INFO:root:Epoch[96] Time cost=4.785\n",
+ "INFO:root:Epoch[97] Train-loss=141.178878\n",
+ "INFO:root:Epoch[97] Time cost=4.699\n",
+ "INFO:root:Epoch[98] Train-loss=141.004168\n",
+ "INFO:root:Epoch[98] Time cost=4.959\n",
+ "INFO:root:Epoch[99] Train-loss=140.865592\n",
+ "INFO:root:Epoch[99] Time cost=5.155\n"
+ ]
+ }
+ ],
+ "source": [
+ "# training the model, save training loss as a list.\n",
+ "training_loss=list()\n",
+ "\n",
+ "# initilize the parameters for training using Normal.\n",
+ "init = mx.init.Normal(0.01)\n",
+ "model.fit(nd_iter, # train data\n",
+ " initializer=init,\n",
+ " # if eval_data is supplied, test loss will also be reported\n",
+ " # eval_data = nd_iter_test,\n",
+ " optimizer='sgd', # use SGD to train\n",
+ " optimizer_params={'learning_rate':1e-3,'wd':1e-2}, \n",
+ " # save parameters for each epoch if model_prefix is supplied\n",
+ " epoch_end_callback = None if model_prefix==None else mx.callback.do_checkpoint(model_prefix, 1),\n",
+ " batch_end_callback = log_to_list(N/batch_size,training_loss), \n",
+ " num_epoch=100,\n",
+ " eval_metric = 'Loss')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "DEBUG:matplotlib.font_manager:findfont: Matching :family=sans-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=12.0 to DejaVu Sans ('/usr/local/lib/python3.5/dist-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf') with score of 0.050000\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "ELBO = [-training_loss[i] for i in range(len(training_loss))]\n",
+ "plt.plot(ELBO)\n",
+ "plt.ylabel('ELBO');plt.xlabel('epoch');plt.title(\"training curve for mini batches\")\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As expected, the ELBO is monotonically increasing over epoch, and we reproduced the results given in the paper [Auto-Encoding Variational Bayes](https://arxiv.org/abs/1312.6114/). Now we can extract/load the parameters and then feed the network forward to calculate $y$ which is the reconstructed image, and we can also calculate the ELBO for the test set. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "arg_params = model.get_params()[0]\n",
+ "nd_iter_test.reset()\n",
+ "test_batch = nd_iter_test.next()\n",
+ "\n",
+ "# if saved the parameters, can load them using `load_checkpoint` method at e.g. 100th epoch\n",
+ "# sym, arg_params, aux_params = mx.model.load_checkpoint(model_prefix, 100)\n",
+ "# assert sym.tojson() == output.tojson()\n",
+ "\n",
+ "e = y.bind(mx.cpu(), {'data': test_batch.data[0],\n",
+ " 'encoder_h_weight': arg_params['encoder_h_weight'],\n",
+ " 'encoder_h_bias': arg_params['encoder_h_bias'],\n",
+ " 'mu_weight': arg_params['mu_weight'],\n",
+ " 'mu_bias': arg_params['mu_bias'],\n",
+ " 'logvar_weight':arg_params['logvar_weight'],\n",
+ " 'logvar_bias':arg_params['logvar_bias'],\n",
+ " 'decoder_z_weight':arg_params['decoder_z_weight'],\n",
+ " 'decoder_z_bias':arg_params['decoder_z_bias'],\n",
+ " 'decoder_x_weight':arg_params['decoder_x_weight'],\n",
+ " 'decoder_x_bias':arg_params['decoder_x_bias'], \n",
+ " 'loss_label':label})\n",
+ "\n",
+ "x_fit = e.forward()\n",
+ "x_construction = x_fit[0].asnumpy()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# learning images on the test set\n",
+ "f, ((ax1, ax2, ax3, ax4)) = plt.subplots(1,4, sharex='col', sharey='row',figsize=(12,3))\n",
+ "ax1.imshow(np.reshape(image_test[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax1.set_title('True image')\n",
+ "ax2.imshow(np.reshape(x_construction[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax2.set_title('Learned image')\n",
+ "ax3.imshow(np.reshape(image_test[99,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax3.set_title('True image')\n",
+ "ax4.imshow(np.reshape(x_construction[99,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax4.set_title('Learned image')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[('loss', 140.17346005859375)]"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# calculate the ELBO which is minus the loss for test set\n",
+ "metric = mx.metric.Loss()\n",
+ "model.score(nd_iter_test, metric)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 4. All together: MXNet-based class VAE"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from VAE import VAE"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "One can directly call the class `VAE` to do the training:\n",
+ "\n",
+ "```VAE(n_latent=5,num_hidden_ecoder=400,num_hidden_decoder=400,x_train=None,x_valid=None,\n",
+ "batch_size=100,learning_rate=0.001,weight_decay=0.01,num_epoch=100,optimizer='sgd',model_prefix=None,\n",
+ "initializer = mx.init.Normal(0.01),likelihood=Bernoulli)```\n",
+ "\n",
+ "The outputs are the learned model and training loss."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "INFO:root:Epoch[0] Train-loss=383.478870\n",
+ "INFO:root:Epoch[0] Time cost=5.075\n",
+ "INFO:root:Epoch[1] Train-loss=211.923867\n",
+ "INFO:root:Epoch[1] Time cost=4.741\n",
+ "INFO:root:Epoch[2] Train-loss=206.789445\n",
+ "INFO:root:Epoch[2] Time cost=4.601\n",
+ "INFO:root:Epoch[3] Train-loss=204.428186\n",
+ "INFO:root:Epoch[3] Time cost=4.865\n",
+ "INFO:root:Epoch[4] Train-loss=202.417322\n",
+ "INFO:root:Epoch[4] Time cost=4.606\n",
+ "INFO:root:Epoch[5] Train-loss=200.635136\n",
+ "INFO:root:Epoch[5] Time cost=4.711\n",
+ "INFO:root:Epoch[6] Train-loss=199.009614\n",
+ "INFO:root:Epoch[6] Time cost=5.159\n",
+ "INFO:root:Epoch[7] Train-loss=197.565788\n",
+ "INFO:root:Epoch[7] Time cost=4.588\n",
+ "INFO:root:Epoch[8] Train-loss=196.524507\n",
+ "INFO:root:Epoch[8] Time cost=4.905\n",
+ "INFO:root:Epoch[9] Train-loss=195.725745\n",
+ "INFO:root:Epoch[9] Time cost=4.426\n",
+ "INFO:root:Epoch[10] Train-loss=194.902025\n",
+ "INFO:root:Epoch[10] Time cost=4.685\n",
+ "INFO:root:Epoch[11] Train-loss=194.026873\n",
+ "INFO:root:Epoch[11] Time cost=4.622\n",
+ "INFO:root:Epoch[12] Train-loss=193.350646\n",
+ "INFO:root:Epoch[12] Time cost=4.712\n",
+ "INFO:root:Epoch[13] Train-loss=192.737502\n",
+ "INFO:root:Epoch[13] Time cost=4.618\n",
+ "INFO:root:Epoch[14] Train-loss=192.338165\n",
+ "INFO:root:Epoch[14] Time cost=4.763\n",
+ "INFO:root:Epoch[15] Train-loss=191.888625\n",
+ "INFO:root:Epoch[15] Time cost=5.168\n",
+ "INFO:root:Epoch[16] Train-loss=191.170650\n",
+ "INFO:root:Epoch[16] Time cost=4.809\n",
+ "INFO:root:Epoch[17] Train-loss=190.307264\n",
+ "INFO:root:Epoch[17] Time cost=4.622\n",
+ "INFO:root:Epoch[18] Train-loss=188.988063\n",
+ "INFO:root:Epoch[18] Time cost=4.543\n",
+ "INFO:root:Epoch[19] Train-loss=187.616311\n",
+ "INFO:root:Epoch[19] Time cost=5.154\n",
+ "INFO:root:Epoch[20] Train-loss=186.352783\n",
+ "INFO:root:Epoch[20] Time cost=4.661\n",
+ "INFO:root:Epoch[21] Train-loss=185.428020\n",
+ "INFO:root:Epoch[21] Time cost=5.193\n",
+ "INFO:root:Epoch[22] Train-loss=184.543097\n",
+ "INFO:root:Epoch[22] Time cost=4.519\n",
+ "INFO:root:Epoch[23] Train-loss=184.029907\n",
+ "INFO:root:Epoch[23] Time cost=4.732\n",
+ "INFO:root:Epoch[24] Train-loss=183.643270\n",
+ "INFO:root:Epoch[24] Time cost=5.011\n",
+ "INFO:root:Epoch[25] Train-loss=183.246912\n",
+ "INFO:root:Epoch[25] Time cost=4.706\n",
+ "INFO:root:Epoch[26] Train-loss=183.065233\n",
+ "INFO:root:Epoch[26] Time cost=4.673\n",
+ "INFO:root:Epoch[27] Train-loss=182.680542\n",
+ "INFO:root:Epoch[27] Time cost=4.628\n",
+ "INFO:root:Epoch[28] Train-loss=182.428677\n",
+ "INFO:root:Epoch[28] Time cost=4.772\n",
+ "INFO:root:Epoch[29] Train-loss=182.219946\n",
+ "INFO:root:Epoch[29] Time cost=4.571\n",
+ "INFO:root:Epoch[30] Train-loss=182.070927\n",
+ "INFO:root:Epoch[30] Time cost=4.603\n",
+ "INFO:root:Epoch[31] Train-loss=181.837968\n",
+ "INFO:root:Epoch[31] Time cost=4.559\n",
+ "INFO:root:Epoch[32] Train-loss=181.624303\n",
+ "INFO:root:Epoch[32] Time cost=5.069\n",
+ "INFO:root:Epoch[33] Train-loss=181.534547\n",
+ "INFO:root:Epoch[33] Time cost=4.654\n",
+ "INFO:root:Epoch[34] Train-loss=181.239556\n",
+ "INFO:root:Epoch[34] Time cost=4.776\n",
+ "INFO:root:Epoch[35] Train-loss=181.098188\n",
+ "INFO:root:Epoch[35] Time cost=4.571\n",
+ "INFO:root:Epoch[36] Train-loss=180.820560\n",
+ "INFO:root:Epoch[36] Time cost=4.815\n",
+ "INFO:root:Epoch[37] Train-loss=180.828095\n",
+ "INFO:root:Epoch[37] Time cost=4.455\n",
+ "INFO:root:Epoch[38] Train-loss=180.495569\n",
+ "INFO:root:Epoch[38] Time cost=5.096\n",
+ "INFO:root:Epoch[39] Train-loss=180.389106\n",
+ "INFO:root:Epoch[39] Time cost=4.797\n",
+ "INFO:root:Epoch[40] Train-loss=180.200965\n",
+ "INFO:root:Epoch[40] Time cost=5.054\n",
+ "INFO:root:Epoch[41] Train-loss=179.851014\n",
+ "INFO:root:Epoch[41] Time cost=4.642\n",
+ "INFO:root:Epoch[42] Train-loss=179.719933\n",
+ "INFO:root:Epoch[42] Time cost=4.603\n",
+ "INFO:root:Epoch[43] Train-loss=179.431740\n",
+ "INFO:root:Epoch[43] Time cost=4.341\n",
+ "INFO:root:Epoch[44] Train-loss=179.235384\n",
+ "INFO:root:Epoch[44] Time cost=4.638\n",
+ "INFO:root:Epoch[45] Train-loss=179.108771\n",
+ "INFO:root:Epoch[45] Time cost=4.754\n",
+ "INFO:root:Epoch[46] Train-loss=178.714163\n",
+ "INFO:root:Epoch[46] Time cost=4.457\n",
+ "INFO:root:Epoch[47] Train-loss=178.508338\n",
+ "INFO:root:Epoch[47] Time cost=4.960\n",
+ "INFO:root:Epoch[48] Train-loss=178.288002\n",
+ "INFO:root:Epoch[48] Time cost=4.562\n",
+ "INFO:root:Epoch[49] Train-loss=178.083288\n",
+ "INFO:root:Epoch[49] Time cost=4.619\n",
+ "INFO:root:Epoch[50] Train-loss=177.791330\n",
+ "INFO:root:Epoch[50] Time cost=4.580\n",
+ "INFO:root:Epoch[51] Train-loss=177.570741\n",
+ "INFO:root:Epoch[51] Time cost=4.704\n",
+ "INFO:root:Epoch[52] Train-loss=177.287114\n",
+ "INFO:root:Epoch[52] Time cost=5.172\n",
+ "INFO:root:Epoch[53] Train-loss=177.122645\n",
+ "INFO:root:Epoch[53] Time cost=4.678\n",
+ "INFO:root:Epoch[54] Train-loss=176.816022\n",
+ "INFO:root:Epoch[54] Time cost=4.819\n",
+ "INFO:root:Epoch[55] Train-loss=176.670484\n",
+ "INFO:root:Epoch[55] Time cost=4.568\n",
+ "INFO:root:Epoch[56] Train-loss=176.459671\n",
+ "INFO:root:Epoch[56] Time cost=4.450\n",
+ "INFO:root:Epoch[57] Train-loss=176.174175\n",
+ "INFO:root:Epoch[57] Time cost=4.579\n",
+ "INFO:root:Epoch[58] Train-loss=175.935856\n",
+ "INFO:root:Epoch[58] Time cost=4.552\n",
+ "INFO:root:Epoch[59] Train-loss=175.739928\n",
+ "INFO:root:Epoch[59] Time cost=4.385\n",
+ "INFO:root:Epoch[60] Train-loss=175.579695\n",
+ "INFO:root:Epoch[60] Time cost=4.496\n",
+ "INFO:root:Epoch[61] Train-loss=175.403871\n",
+ "INFO:root:Epoch[61] Time cost=5.088\n",
+ "INFO:root:Epoch[62] Train-loss=175.157114\n",
+ "INFO:root:Epoch[62] Time cost=4.628\n",
+ "INFO:root:Epoch[63] Train-loss=174.953950\n",
+ "INFO:root:Epoch[63] Time cost=4.826\n",
+ "INFO:root:Epoch[64] Train-loss=174.743393\n",
+ "INFO:root:Epoch[64] Time cost=4.832\n",
+ "INFO:root:Epoch[65] Train-loss=174.554056\n",
+ "INFO:root:Epoch[65] Time cost=4.375\n",
+ "INFO:root:Epoch[66] Train-loss=174.366719\n",
+ "INFO:root:Epoch[66] Time cost=4.583\n",
+ "INFO:root:Epoch[67] Train-loss=174.160622\n",
+ "INFO:root:Epoch[67] Time cost=4.586\n",
+ "INFO:root:Epoch[68] Train-loss=173.981699\n",
+ "INFO:root:Epoch[68] Time cost=5.149\n",
+ "INFO:root:Epoch[69] Train-loss=173.751617\n",
+ "INFO:root:Epoch[69] Time cost=4.495\n",
+ "INFO:root:Epoch[70] Train-loss=173.548732\n",
+ "INFO:root:Epoch[70] Time cost=4.588\n",
+ "INFO:root:Epoch[71] Train-loss=173.380950\n",
+ "INFO:root:Epoch[71] Time cost=5.042\n",
+ "INFO:root:Epoch[72] Train-loss=173.158519\n",
+ "INFO:root:Epoch[72] Time cost=4.817\n",
+ "INFO:root:Epoch[73] Train-loss=172.970726\n",
+ "INFO:root:Epoch[73] Time cost=4.791\n",
+ "INFO:root:Epoch[74] Train-loss=172.782357\n",
+ "INFO:root:Epoch[74] Time cost=4.377\n",
+ "INFO:root:Epoch[75] Train-loss=172.581992\n",
+ "INFO:root:Epoch[75] Time cost=4.518\n",
+ "INFO:root:Epoch[76] Train-loss=172.385020\n",
+ "INFO:root:Epoch[76] Time cost=4.863\n",
+ "INFO:root:Epoch[77] Train-loss=172.198309\n",
+ "INFO:root:Epoch[77] Time cost=5.104\n",
+ "INFO:root:Epoch[78] Train-loss=172.022333\n",
+ "INFO:root:Epoch[78] Time cost=4.571\n",
+ "INFO:root:Epoch[79] Train-loss=171.816585\n",
+ "INFO:root:Epoch[79] Time cost=4.557\n",
+ "INFO:root:Epoch[80] Train-loss=171.643714\n",
+ "INFO:root:Epoch[80] Time cost=4.567\n",
+ "INFO:root:Epoch[81] Train-loss=171.460581\n",
+ "INFO:root:Epoch[81] Time cost=4.735\n",
+ "INFO:root:Epoch[82] Train-loss=171.284854\n",
+ "INFO:root:Epoch[82] Time cost=5.012\n",
+ "INFO:root:Epoch[83] Train-loss=171.113129\n",
+ "INFO:root:Epoch[83] Time cost=4.877\n",
+ "INFO:root:Epoch[84] Train-loss=170.947790\n",
+ "INFO:root:Epoch[84] Time cost=4.487\n",
+ "INFO:root:Epoch[85] Train-loss=170.766223\n",
+ "INFO:root:Epoch[85] Time cost=4.723\n",
+ "INFO:root:Epoch[86] Train-loss=170.602559\n",
+ "INFO:root:Epoch[86] Time cost=4.803\n",
+ "INFO:root:Epoch[87] Train-loss=170.448713\n",
+ "INFO:root:Epoch[87] Time cost=4.636\n",
+ "INFO:root:Epoch[88] Train-loss=170.273053\n",
+ "INFO:root:Epoch[88] Time cost=4.562\n",
+ "INFO:root:Epoch[89] Train-loss=170.099485\n",
+ "INFO:root:Epoch[89] Time cost=4.567\n",
+ "INFO:root:Epoch[90] Train-loss=169.934289\n",
+ "INFO:root:Epoch[90] Time cost=4.905\n",
+ "INFO:root:Epoch[91] Train-loss=169.768920\n",
+ "INFO:root:Epoch[91] Time cost=4.636\n",
+ "INFO:root:Epoch[92] Train-loss=169.620803\n",
+ "INFO:root:Epoch[92] Time cost=4.429\n",
+ "INFO:root:Epoch[93] Train-loss=169.448189\n",
+ "INFO:root:Epoch[93] Time cost=4.985\n",
+ "INFO:root:Epoch[94] Train-loss=169.295794\n",
+ "INFO:root:Epoch[94] Time cost=4.649\n",
+ "INFO:root:Epoch[95] Train-loss=169.143627\n",
+ "INFO:root:Epoch[95] Time cost=4.602\n",
+ "INFO:root:Epoch[96] Train-loss=168.989410\n",
+ "INFO:root:Epoch[96] Time cost=4.904\n",
+ "INFO:root:Epoch[97] Train-loss=168.841089\n",
+ "INFO:root:Epoch[97] Time cost=4.602\n",
+ "INFO:root:Epoch[98] Train-loss=168.694906\n",
+ "INFO:root:Epoch[98] Time cost=4.589\n",
+ "INFO:root:Epoch[99] Train-loss=168.527604\n",
+ "INFO:root:Epoch[99] Time cost=4.560\n",
+ "INFO:root:Epoch[100] Train-loss=168.385596\n",
+ "INFO:root:Epoch[100] Time cost=4.835\n",
+ "INFO:root:Epoch[101] Train-loss=168.246526\n",
+ "INFO:root:Epoch[101] Time cost=4.558\n",
+ "INFO:root:Epoch[102] Train-loss=168.093663\n",
+ "INFO:root:Epoch[102] Time cost=4.609\n",
+ "INFO:root:Epoch[103] Train-loss=167.938807\n",
+ "INFO:root:Epoch[103] Time cost=4.599\n",
+ "INFO:root:Epoch[104] Train-loss=167.814916\n",
+ "INFO:root:Epoch[104] Time cost=4.394\n",
+ "INFO:root:Epoch[105] Train-loss=167.676473\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "INFO:root:Epoch[105] Time cost=4.724\n",
+ "INFO:root:Epoch[106] Train-loss=167.560241\n",
+ "INFO:root:Epoch[106] Time cost=4.316\n",
+ "INFO:root:Epoch[107] Train-loss=167.424132\n",
+ "INFO:root:Epoch[107] Time cost=4.646\n",
+ "INFO:root:Epoch[108] Train-loss=167.284482\n",
+ "INFO:root:Epoch[108] Time cost=4.472\n",
+ "INFO:root:Epoch[109] Train-loss=167.184511\n",
+ "INFO:root:Epoch[109] Time cost=4.768\n",
+ "INFO:root:Epoch[110] Train-loss=167.037793\n",
+ "INFO:root:Epoch[110] Time cost=4.717\n",
+ "INFO:root:Epoch[111] Train-loss=166.916652\n",
+ "INFO:root:Epoch[111] Time cost=4.803\n",
+ "INFO:root:Epoch[112] Train-loss=166.796803\n",
+ "INFO:root:Epoch[112] Time cost=4.617\n",
+ "INFO:root:Epoch[113] Train-loss=166.655028\n",
+ "INFO:root:Epoch[113] Time cost=4.420\n",
+ "INFO:root:Epoch[114] Train-loss=166.561129\n",
+ "INFO:root:Epoch[114] Time cost=4.333\n",
+ "INFO:root:Epoch[115] Train-loss=166.434593\n",
+ "INFO:root:Epoch[115] Time cost=4.526\n",
+ "INFO:root:Epoch[116] Train-loss=166.322805\n",
+ "INFO:root:Epoch[116] Time cost=4.310\n",
+ "INFO:root:Epoch[117] Train-loss=166.195452\n",
+ "INFO:root:Epoch[117] Time cost=4.458\n",
+ "INFO:root:Epoch[118] Train-loss=166.073792\n",
+ "INFO:root:Epoch[118] Time cost=4.333\n",
+ "INFO:root:Epoch[119] Train-loss=165.967437\n",
+ "INFO:root:Epoch[119] Time cost=4.459\n",
+ "INFO:root:Epoch[120] Train-loss=165.876094\n",
+ "INFO:root:Epoch[120] Time cost=5.070\n",
+ "INFO:root:Epoch[121] Train-loss=165.748064\n",
+ "INFO:root:Epoch[121] Time cost=4.782\n",
+ "INFO:root:Epoch[122] Train-loss=165.656283\n",
+ "INFO:root:Epoch[122] Time cost=4.640\n",
+ "INFO:root:Epoch[123] Train-loss=165.540462\n",
+ "INFO:root:Epoch[123] Time cost=4.522\n",
+ "INFO:root:Epoch[124] Train-loss=165.448734\n",
+ "INFO:root:Epoch[124] Time cost=4.858\n",
+ "INFO:root:Epoch[125] Train-loss=165.347751\n",
+ "INFO:root:Epoch[125] Time cost=4.842\n",
+ "INFO:root:Epoch[126] Train-loss=165.230048\n",
+ "INFO:root:Epoch[126] Time cost=4.495\n",
+ "INFO:root:Epoch[127] Train-loss=165.147932\n",
+ "INFO:root:Epoch[127] Time cost=4.766\n",
+ "INFO:root:Epoch[128] Train-loss=165.036021\n",
+ "INFO:root:Epoch[128] Time cost=4.526\n",
+ "INFO:root:Epoch[129] Train-loss=164.977613\n",
+ "INFO:root:Epoch[129] Time cost=5.091\n",
+ "INFO:root:Epoch[130] Train-loss=164.881467\n",
+ "INFO:root:Epoch[130] Time cost=5.223\n",
+ "INFO:root:Epoch[131] Train-loss=164.785627\n",
+ "INFO:root:Epoch[131] Time cost=4.165\n",
+ "INFO:root:Epoch[132] Train-loss=164.707629\n",
+ "INFO:root:Epoch[132] Time cost=4.527\n",
+ "INFO:root:Epoch[133] Train-loss=164.598039\n",
+ "INFO:root:Epoch[133] Time cost=4.167\n",
+ "INFO:root:Epoch[134] Train-loss=164.502932\n",
+ "INFO:root:Epoch[134] Time cost=4.354\n",
+ "INFO:root:Epoch[135] Train-loss=164.422286\n",
+ "INFO:root:Epoch[135] Time cost=4.387\n",
+ "INFO:root:Epoch[136] Train-loss=164.344749\n",
+ "INFO:root:Epoch[136] Time cost=4.662\n",
+ "INFO:root:Epoch[137] Train-loss=164.264898\n",
+ "INFO:root:Epoch[137] Time cost=4.671\n",
+ "INFO:root:Epoch[138] Train-loss=164.178707\n",
+ "INFO:root:Epoch[138] Time cost=4.776\n",
+ "INFO:root:Epoch[139] Train-loss=164.109071\n",
+ "INFO:root:Epoch[139] Time cost=4.787\n",
+ "INFO:root:Epoch[140] Train-loss=163.993291\n",
+ "INFO:root:Epoch[140] Time cost=4.726\n",
+ "INFO:root:Epoch[141] Train-loss=163.956234\n",
+ "INFO:root:Epoch[141] Time cost=4.337\n",
+ "INFO:root:Epoch[142] Train-loss=163.845638\n",
+ "INFO:root:Epoch[142] Time cost=4.787\n",
+ "INFO:root:Epoch[143] Train-loss=163.790882\n",
+ "INFO:root:Epoch[143] Time cost=5.563\n",
+ "INFO:root:Epoch[144] Train-loss=163.723495\n",
+ "INFO:root:Epoch[144] Time cost=4.529\n",
+ "INFO:root:Epoch[145] Train-loss=163.634262\n",
+ "INFO:root:Epoch[145] Time cost=5.028\n",
+ "INFO:root:Epoch[146] Train-loss=163.552854\n",
+ "INFO:root:Epoch[146] Time cost=4.933\n",
+ "INFO:root:Epoch[147] Train-loss=163.501429\n",
+ "INFO:root:Epoch[147] Time cost=4.912\n",
+ "INFO:root:Epoch[148] Train-loss=163.444245\n",
+ "INFO:root:Epoch[148] Time cost=5.034\n",
+ "INFO:root:Epoch[149] Train-loss=163.348476\n",
+ "INFO:root:Epoch[149] Time cost=4.600\n",
+ "INFO:root:Epoch[150] Train-loss=163.256955\n",
+ "INFO:root:Epoch[150] Time cost=4.704\n",
+ "INFO:root:Epoch[151] Train-loss=163.216139\n",
+ "INFO:root:Epoch[151] Time cost=4.670\n",
+ "INFO:root:Epoch[152] Train-loss=163.144691\n",
+ "INFO:root:Epoch[152] Time cost=4.678\n",
+ "INFO:root:Epoch[153] Train-loss=163.050236\n",
+ "INFO:root:Epoch[153] Time cost=4.595\n",
+ "INFO:root:Epoch[154] Train-loss=162.991225\n",
+ "INFO:root:Epoch[154] Time cost=5.307\n",
+ "INFO:root:Epoch[155] Train-loss=162.907200\n",
+ "INFO:root:Epoch[155] Time cost=4.684\n",
+ "INFO:root:Epoch[156] Train-loss=162.838075\n",
+ "INFO:root:Epoch[156] Time cost=4.686\n",
+ "INFO:root:Epoch[157] Train-loss=162.759286\n",
+ "INFO:root:Epoch[157] Time cost=4.750\n",
+ "INFO:root:Epoch[158] Train-loss=162.725998\n",
+ "INFO:root:Epoch[158] Time cost=4.637\n",
+ "INFO:root:Epoch[159] Train-loss=162.635852\n",
+ "INFO:root:Epoch[159] Time cost=4.498\n",
+ "INFO:root:Epoch[160] Train-loss=162.563777\n",
+ "INFO:root:Epoch[160] Time cost=5.048\n",
+ "INFO:root:Epoch[161] Train-loss=162.527387\n",
+ "INFO:root:Epoch[161] Time cost=5.040\n",
+ "INFO:root:Epoch[162] Train-loss=162.395881\n",
+ "INFO:root:Epoch[162] Time cost=4.764\n",
+ "INFO:root:Epoch[163] Train-loss=162.353654\n",
+ "INFO:root:Epoch[163] Time cost=4.561\n",
+ "INFO:root:Epoch[164] Train-loss=162.285584\n",
+ "INFO:root:Epoch[164] Time cost=5.051\n",
+ "INFO:root:Epoch[165] Train-loss=162.204332\n",
+ "INFO:root:Epoch[165] Time cost=4.455\n",
+ "INFO:root:Epoch[166] Train-loss=162.147100\n",
+ "INFO:root:Epoch[166] Time cost=5.021\n",
+ "INFO:root:Epoch[167] Train-loss=162.051296\n",
+ "INFO:root:Epoch[167] Time cost=4.551\n",
+ "INFO:root:Epoch[168] Train-loss=161.978708\n",
+ "INFO:root:Epoch[168] Time cost=4.744\n",
+ "INFO:root:Epoch[169] Train-loss=161.927990\n",
+ "INFO:root:Epoch[169] Time cost=4.821\n",
+ "INFO:root:Epoch[170] Train-loss=161.883088\n",
+ "INFO:root:Epoch[170] Time cost=4.365\n",
+ "INFO:root:Epoch[171] Train-loss=161.785367\n",
+ "INFO:root:Epoch[171] Time cost=4.448\n",
+ "INFO:root:Epoch[172] Train-loss=161.716386\n",
+ "INFO:root:Epoch[172] Time cost=4.622\n",
+ "INFO:root:Epoch[173] Train-loss=161.656391\n",
+ "INFO:root:Epoch[173] Time cost=4.500\n",
+ "INFO:root:Epoch[174] Train-loss=161.598127\n",
+ "INFO:root:Epoch[174] Time cost=4.677\n",
+ "INFO:root:Epoch[175] Train-loss=161.518613\n",
+ "INFO:root:Epoch[175] Time cost=4.958\n",
+ "INFO:root:Epoch[176] Train-loss=161.418783\n",
+ "INFO:root:Epoch[176] Time cost=4.607\n",
+ "INFO:root:Epoch[177] Train-loss=161.407767\n",
+ "INFO:root:Epoch[177] Time cost=4.427\n",
+ "INFO:root:Epoch[178] Train-loss=161.319552\n",
+ "INFO:root:Epoch[178] Time cost=4.930\n",
+ "INFO:root:Epoch[179] Train-loss=161.234087\n",
+ "INFO:root:Epoch[179] Time cost=4.240\n",
+ "INFO:root:Epoch[180] Train-loss=161.187404\n",
+ "INFO:root:Epoch[180] Time cost=4.484\n",
+ "INFO:root:Epoch[181] Train-loss=161.123118\n",
+ "INFO:root:Epoch[181] Time cost=4.937\n",
+ "INFO:root:Epoch[182] Train-loss=160.999420\n",
+ "INFO:root:Epoch[182] Time cost=4.489\n",
+ "INFO:root:Epoch[183] Train-loss=160.955369\n",
+ "INFO:root:Epoch[183] Time cost=4.894\n",
+ "INFO:root:Epoch[184] Train-loss=160.908542\n",
+ "INFO:root:Epoch[184] Time cost=4.269\n",
+ "INFO:root:Epoch[185] Train-loss=160.846908\n",
+ "INFO:root:Epoch[185] Time cost=4.998\n",
+ "INFO:root:Epoch[186] Train-loss=160.765964\n",
+ "INFO:root:Epoch[186] Time cost=4.467\n",
+ "INFO:root:Epoch[187] Train-loss=160.687773\n",
+ "INFO:root:Epoch[187] Time cost=4.609\n",
+ "INFO:root:Epoch[188] Train-loss=160.652674\n",
+ "INFO:root:Epoch[188] Time cost=5.327\n",
+ "INFO:root:Epoch[189] Train-loss=160.551175\n",
+ "INFO:root:Epoch[189] Time cost=4.267\n",
+ "INFO:root:Epoch[190] Train-loss=160.477424\n",
+ "INFO:root:Epoch[190] Time cost=4.798\n",
+ "INFO:root:Epoch[191] Train-loss=160.501221\n",
+ "INFO:root:Epoch[191] Time cost=4.695\n",
+ "INFO:root:Epoch[192] Train-loss=160.370335\n",
+ "INFO:root:Epoch[192] Time cost=4.640\n",
+ "INFO:root:Epoch[193] Train-loss=160.279749\n",
+ "INFO:root:Epoch[193] Time cost=4.653\n",
+ "INFO:root:Epoch[194] Train-loss=160.242415\n",
+ "INFO:root:Epoch[194] Time cost=5.044\n",
+ "INFO:root:Epoch[195] Train-loss=160.197063\n",
+ "INFO:root:Epoch[195] Time cost=4.684\n",
+ "INFO:root:Epoch[196] Train-loss=160.132983\n",
+ "INFO:root:Epoch[196] Time cost=4.460\n",
+ "INFO:root:Epoch[197] Train-loss=160.083149\n",
+ "INFO:root:Epoch[197] Time cost=4.713\n",
+ "INFO:root:Epoch[198] Train-loss=160.025012\n",
+ "INFO:root:Epoch[198] Time cost=4.779\n",
+ "INFO:root:Epoch[199] Train-loss=159.945513\n",
+ "INFO:root:Epoch[199] Time cost=4.659\n"
+ ]
+ }
+ ],
+ "source": [
+ "# can initilize weights and biases with the learned parameters as follows: \n",
+ "# init = mx.initializer.Load(params)\n",
+ "\n",
+ "# call the VAE, output model contains the learned model and training loss\n",
+ "out = VAE(n_latent=2, x_train=image, x_valid=None, num_epoch=200) "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# encode test images to obtain mu and logvar which are used for sampling\n",
+ "[mu,logvar] = VAE.encoder(out,image_test)\n",
+ "# sample in the latent space\n",
+ "z = VAE.sampler(mu,logvar)\n",
+ "# decode from the latent space to obtain reconstructed images\n",
+ "x_construction = VAE.decoder(out,z)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "f, ((ax1, ax2, ax3, ax4)) = plt.subplots(1,4, sharex='col', sharey='row',figsize=(12,3))\n",
+ "ax1.imshow(np.reshape(image_test[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax1.set_title('True image')\n",
+ "ax2.imshow(np.reshape(x_construction[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax2.set_title('Learned image')\n",
+ "ax3.imshow(np.reshape(image_test[146,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax3.set_title('True image')\n",
+ "ax4.imshow(np.reshape(x_construction[146,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ "ax4.set_title('Learned image')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "DEBUG:matplotlib.font_manager:findfont: Matching :family=sans-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=15.0 to DejaVu Sans ('/usr/local/lib/python3.5/dist-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf') with score of 0.050000\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "z1 = z[:,0]\n",
+ "z2 = z[:,1]\n",
+ "\n",
+ "fig = plt.figure()\n",
+ "ax = fig.add_subplot(111)\n",
+ "ax.plot(z1,z2,'ko')\n",
+ "plt.title(\"latent space\")\n",
+ "\n",
+ "#np.where((z1>3) & (z2<2) & (z2>0))\n",
+ "#select the points from the latent space\n",
+ "a_vec = [2,5,7,789,25,9993]\n",
+ "for i in range(len(a_vec)):\n",
+ " ax.plot(z1[a_vec[i]],z2[a_vec[i]],'ro') \n",
+ " ax.annotate('z%d' %i, xy=(z1[a_vec[i]],z2[a_vec[i]]), \n",
+ " xytext=(z1[a_vec[i]],z2[a_vec[i]]),color = 'r',fontsize=15)\n",
+ "\n",
+ "\n",
+ "f, ((ax0, ax1, ax2, ax3, ax4,ax5)) = plt.subplots(1,6, sharex='col', sharey='row',figsize=(12,2.5))\n",
+ "for i in range(len(a_vec)):\n",
+ " eval('ax%d' %(i)).imshow(np.reshape(x_construction[a_vec[i],:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
+ " eval('ax%d' %(i)).set_title('z%d'%i)\n",
+ "\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Above is a plot of points in the 2D latent space and their corresponding decoded images, it can be seen that points that are close in the latent space get mapped to the same digit from the decoder, and we can see how it evolves from left to right."
+ ]
+ }
+ ],
+ "metadata": {
+ "anaconda-cloud": {},
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/example/bi-lstm-sort/README.md b/example/bi-lstm-sort/README.md
index 3bacc8686368..f00cc85caa30 100644
--- a/example/bi-lstm-sort/README.md
+++ b/example/bi-lstm-sort/README.md
@@ -1,24 +1,14 @@
-This is an example of using bidirection lstm to sort an array.
+# Bidirectionnal LSTM to sort an array.
-Run the training script by doing the following:
+This is an example of using bidirectionmal lstm to sort an array. Please refer to the notebook.
-```
-python lstm_sort.py --start-range 100 --end-range 1000 --cpu
-```
-You can provide the start-range and end-range for the numbers and whether to train on the cpu or not.
-By default the script tries to train on the GPU. The default start-range is 100 and end-range is 1000.
+We train a bidirectionnal LSTM to sort an array of integer.
-At last, test model by doing the following:
+For example:
-```
-python infer_sort.py 234 189 785 763 231
-```
+`500 30 999 10 130` should give us `10 30 130 500 999`
-This should output the sorted seq like the following:
-```
-189
-231
-234
-763
-785
-```
+![](https://cdn-images-1.medium.com/max/1200/1*6QnPUSv_t9BY9Fv8_aLb-Q.png)
+
+
+([Diagram source](http://colah.github.io/posts/2015-09-NN-Types-FP/))
\ No newline at end of file
diff --git a/example/bi-lstm-sort/bi-lstm-sort.ipynb b/example/bi-lstm-sort/bi-lstm-sort.ipynb
new file mode 100644
index 000000000000..085117674b58
--- /dev/null
+++ b/example/bi-lstm-sort/bi-lstm-sort.ipynb
@@ -0,0 +1,607 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Using a bi-lstm to sort a sequence of integers"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import random\n",
+ "import string\n",
+ "\n",
+ "import mxnet as mx\n",
+ "from mxnet import gluon, nd\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Data Preparation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "max_num = 999\n",
+ "dataset_size = 60000\n",
+ "seq_len = 5\n",
+ "split = 0.8\n",
+ "batch_size = 512\n",
+ "ctx = mx.gpu() if len(mx.test_utils.list_gpus()) > 0 else mx.cpu()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We are getting a dataset of **dataset_size** sequences of integers of length **seq_len** between **0** and **max_num**. We use **split*100%** of them for training and the rest for testing.\n",
+ "\n",
+ "\n",
+ "For example:\n",
+ "\n",
+ "50 10 200 999 30\n",
+ "\n",
+ "Should return\n",
+ "\n",
+ "10 30 50 200 999"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "X = mx.random.uniform(low=0, high=max_num, shape=(dataset_size, seq_len)).astype('int32').asnumpy()\n",
+ "Y = X.copy()\n",
+ "Y.sort() #Let's sort X to get the target"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Input [548, 592, 714, 843, 602]\n",
+ "Target [548, 592, 602, 714, 843]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"Input {}\\nTarget {}\".format(X[0].tolist(), Y[0].tolist()))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For the purpose of training, we encode the input as characters rather than numbers"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0123456789 \n",
+ "{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, ' ': 10}\n"
+ ]
+ }
+ ],
+ "source": [
+ "vocab = string.digits + \" \"\n",
+ "print(vocab)\n",
+ "vocab_idx = { c:i for i,c in enumerate(vocab)}\n",
+ "print(vocab_idx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We write a transform that will convert our numbers into text of maximum length **max_len**, and one-hot encode the characters.\n",
+ "For example:\n",
+ "\n",
+ "\"30 10\" corresponding indices are [3, 0, 10, 1, 0]\n",
+ "\n",
+ "We then one hot encode that and get a matrix representation of our input. We don't need to encode our target as the loss we are going to use support sparse labels"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Maximum length of the string: 19\n"
+ ]
+ }
+ ],
+ "source": [
+ "max_len = len(str(max_num))*seq_len+(seq_len-1)\n",
+ "print(\"Maximum length of the string: %s\" % max_len)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def transform(x, y):\n",
+ " x_string = ' '.join(map(str, x.tolist()))\n",
+ " x_string_padded = x_string + ' '*(max_len-len(x_string))\n",
+ " x = [vocab_idx[c] for c in x_string_padded]\n",
+ " y_string = ' '.join(map(str, y.tolist()))\n",
+ " y_string_padded = y_string + ' '*(max_len-len(y_string))\n",
+ " y = [vocab_idx[c] for c in y_string_padded]\n",
+ " return mx.nd.one_hot(mx.nd.array(x), len(vocab)), mx.nd.array(y)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "split_idx = int(split*len(X))\n",
+ "train_dataset = gluon.data.ArrayDataset(X[:split_idx], Y[:split_idx]).transform(transform)\n",
+ "test_dataset = gluon.data.ArrayDataset(X[split_idx:], Y[split_idx:]).transform(transform)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Input [548 592 714 843 602]\n",
+ "Transformed data Input \n",
+ "[[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]\n",
+ " [0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]\n",
+ " [0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]\n",
+ " [0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]\n",
+ " [0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]\n",
+ " [0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]\n",
+ " [1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
+ " [0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]]\n",
+ "\n",
+ "Target [548 592 602 714 843]\n",
+ "Transformed data Target \n",
+ "[ 5. 4. 8. 10. 5. 9. 2. 10. 6. 0. 2. 10. 7. 1. 4. 10. 8. 4.\n",
+ " 3.]\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"Input {}\".format(X[0]))\n",
+ "print(\"Transformed data Input {}\".format(train_dataset[0][0]))\n",
+ "print(\"Target {}\".format(Y[0]))\n",
+ "print(\"Transformed data Target {}\".format(train_dataset[0][1]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_data = gluon.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=20, last_batch='rollover')\n",
+ "test_data = gluon.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=5, last_batch='rollover')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Creating the network"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "net = gluon.nn.HybridSequential()\n",
+ "with net.name_scope():\n",
+ " net.add(\n",
+ " gluon.rnn.LSTM(hidden_size=128, num_layers=2, layout='NTC', bidirectional=True),\n",
+ " gluon.nn.Dense(len(vocab), flatten=False)\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "net.initialize(mx.init.Xavier(), ctx=ctx)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "loss = gluon.loss.SoftmaxCELoss()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We use a learning rate schedule to improve the convergence of the model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "schedule = mx.lr_scheduler.FactorScheduler(step=len(train_data)*10, factor=0.75)\n",
+ "schedule.base_lr = 0.01"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate':0.01, 'lr_scheduler':schedule})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Training loop"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Epoch [0] Loss: 1.6627886372227823, LR 0.01\n",
+ "Epoch [1] Loss: 1.210370733382854, LR 0.01\n",
+ "Epoch [2] Loss: 0.9692377131035987, LR 0.01\n",
+ "Epoch [3] Loss: 0.7976046623067653, LR 0.01\n",
+ "Epoch [4] Loss: 0.5714595343476983, LR 0.01\n",
+ "Epoch [5] Loss: 0.4458411196444897, LR 0.01\n",
+ "Epoch [6] Loss: 0.36039798817736035, LR 0.01\n",
+ "Epoch [7] Loss: 0.32665719377233626, LR 0.01\n",
+ "Epoch [8] Loss: 0.262064205702915, LR 0.01\n",
+ "Epoch [9] Loss: 0.22285924059279422, LR 0.0075\n",
+ "Epoch [10] Loss: 0.19018426854559717, LR 0.0075\n",
+ "Epoch [11] Loss: 0.1718730723604243, LR 0.0075\n",
+ "Epoch [12] Loss: 0.15736752171670237, LR 0.0075\n",
+ "Epoch [13] Loss: 0.14579375246737866, LR 0.0075\n",
+ "Epoch [14] Loss: 0.13546599733068587, LR 0.0075\n",
+ "Epoch [15] Loss: 0.12490207590955368, LR 0.0075\n",
+ "Epoch [16] Loss: 0.11803316300915133, LR 0.0075\n",
+ "Epoch [17] Loss: 0.10653189395336395, LR 0.0075\n",
+ "Epoch [18] Loss: 0.10514750379197141, LR 0.0075\n",
+ "Epoch [19] Loss: 0.09590611559279422, LR 0.005625\n",
+ "Epoch [20] Loss: 0.08146028108494256, LR 0.005625\n",
+ "Epoch [21] Loss: 0.07707348782965477, LR 0.005625\n",
+ "Epoch [22] Loss: 0.07206193436967566, LR 0.005625\n",
+ "Epoch [23] Loss: 0.07001185417175293, LR 0.005625\n",
+ "Epoch [24] Loss: 0.06797058351578252, LR 0.005625\n",
+ "Epoch [25] Loss: 0.0649358110224947, LR 0.005625\n",
+ "Epoch [26] Loss: 0.06219124286732775, LR 0.005625\n",
+ "Epoch [27] Loss: 0.06075144828634059, LR 0.005625\n",
+ "Epoch [28] Loss: 0.05711334495134251, LR 0.005625\n",
+ "Epoch [29] Loss: 0.054747099572039666, LR 0.00421875\n",
+ "Epoch [30] Loss: 0.0441775271233092, LR 0.00421875\n",
+ "Epoch [31] Loss: 0.041551097910454936, LR 0.00421875\n",
+ "Epoch [32] Loss: 0.04095017269093503, LR 0.00421875\n",
+ "Epoch [33] Loss: 0.04045371045457556, LR 0.00421875\n",
+ "Epoch [34] Loss: 0.038867686657195394, LR 0.00421875\n",
+ "Epoch [35] Loss: 0.038131744303601854, LR 0.00421875\n",
+ "Epoch [36] Loss: 0.039834817250569664, LR 0.00421875\n",
+ "Epoch [37] Loss: 0.03669035941996473, LR 0.00421875\n",
+ "Epoch [38] Loss: 0.03373505967728635, LR 0.00421875\n",
+ "Epoch [39] Loss: 0.03164981273894615, LR 0.0031640625\n",
+ "Epoch [40] Loss: 0.025532766055035336, LR 0.0031640625\n",
+ "Epoch [41] Loss: 0.022659448867148543, LR 0.0031640625\n",
+ "Epoch [42] Loss: 0.02307056112492338, LR 0.0031640625\n",
+ "Epoch [43] Loss: 0.02236944056571798, LR 0.0031640625\n",
+ "Epoch [44] Loss: 0.022204211963120328, LR 0.0031640625\n",
+ "Epoch [45] Loss: 0.02262336903430046, LR 0.0031640625\n",
+ "Epoch [46] Loss: 0.02253308448385685, LR 0.0031640625\n",
+ "Epoch [47] Loss: 0.025286573044797207, LR 0.0031640625\n",
+ "Epoch [48] Loss: 0.02439300988310127, LR 0.0031640625\n",
+ "Epoch [49] Loss: 0.017976388018181983, LR 0.002373046875\n",
+ "Epoch [50] Loss: 0.014343131095805067, LR 0.002373046875\n",
+ "Epoch [51] Loss: 0.013039355582379281, LR 0.002373046875\n",
+ "Epoch [52] Loss: 0.011884741885687715, LR 0.002373046875\n",
+ "Epoch [53] Loss: 0.011438189668858305, LR 0.002373046875\n",
+ "Epoch [54] Loss: 0.011447292693117832, LR 0.002373046875\n",
+ "Epoch [55] Loss: 0.014212571560068334, LR 0.002373046875\n",
+ "Epoch [56] Loss: 0.019900493724371797, LR 0.002373046875\n",
+ "Epoch [57] Loss: 0.02102568301748722, LR 0.002373046875\n",
+ "Epoch [58] Loss: 0.01346214400961044, LR 0.002373046875\n",
+ "Epoch [59] Loss: 0.010107964911359422, LR 0.0017797851562500002\n",
+ "Epoch [60] Loss: 0.008353193600972494, LR 0.0017797851562500002\n",
+ "Epoch [61] Loss: 0.007678258292218472, LR 0.0017797851562500002\n",
+ "Epoch [62] Loss: 0.007262124660167288, LR 0.0017797851562500002\n",
+ "Epoch [63] Loss: 0.00705223578087827, LR 0.0017797851562500002\n",
+ "Epoch [64] Loss: 0.006788556293774677, LR 0.0017797851562500002\n",
+ "Epoch [65] Loss: 0.006473606571238091, LR 0.0017797851562500002\n",
+ "Epoch [66] Loss: 0.006206096486842378, LR 0.0017797851562500002\n",
+ "Epoch [67] Loss: 0.00584477313021396, LR 0.0017797851562500002\n",
+ "Epoch [68] Loss: 0.005648705267137097, LR 0.0017797851562500002\n",
+ "Epoch [69] Loss: 0.006481769871204458, LR 0.0013348388671875003\n",
+ "Epoch [70] Loss: 0.008430448618341, LR 0.0013348388671875003\n",
+ "Epoch [71] Loss: 0.006877245421105242, LR 0.0013348388671875003\n",
+ "Epoch [72] Loss: 0.005671108281740578, LR 0.0013348388671875003\n",
+ "Epoch [73] Loss: 0.004832422162624116, LR 0.0013348388671875003\n",
+ "Epoch [74] Loss: 0.004441103402604448, LR 0.0013348388671875003\n",
+ "Epoch [75] Loss: 0.004216198591475791, LR 0.0013348388671875003\n",
+ "Epoch [76] Loss: 0.004041922989711967, LR 0.0013348388671875003\n",
+ "Epoch [77] Loss: 0.003937713643337818, LR 0.0013348388671875003\n",
+ "Epoch [78] Loss: 0.010251983049068046, LR 0.0013348388671875003\n",
+ "Epoch [79] Loss: 0.01829354052848004, LR 0.0010011291503906252\n",
+ "Epoch [80] Loss: 0.006723233448561802, LR 0.0010011291503906252\n",
+ "Epoch [81] Loss: 0.004397524798170049, LR 0.0010011291503906252\n",
+ "Epoch [82] Loss: 0.0038475305476087206, LR 0.0010011291503906252\n",
+ "Epoch [83] Loss: 0.003591177945441388, LR 0.0010011291503906252\n",
+ "Epoch [84] Loss: 0.003425112014175743, LR 0.0010011291503906252\n",
+ "Epoch [85] Loss: 0.0032633850549129728, LR 0.0010011291503906252\n",
+ "Epoch [86] Loss: 0.0031762316505959693, LR 0.0010011291503906252\n",
+ "Epoch [87] Loss: 0.0030452777096565734, LR 0.0010011291503906252\n",
+ "Epoch [88] Loss: 0.002950224184220837, LR 0.0010011291503906252\n",
+ "Epoch [89] Loss: 0.002821172171450676, LR 0.0007508468627929689\n",
+ "Epoch [90] Loss: 0.002725780961361337, LR 0.0007508468627929689\n",
+ "Epoch [91] Loss: 0.002660556359493986, LR 0.0007508468627929689\n",
+ "Epoch [92] Loss: 0.0026011724946319414, LR 0.0007508468627929689\n",
+ "Epoch [93] Loss: 0.0025355776256703317, LR 0.0007508468627929689\n",
+ "Epoch [94] Loss: 0.0024825221997626283, LR 0.0007508468627929689\n",
+ "Epoch [95] Loss: 0.0024245587435174497, LR 0.0007508468627929689\n",
+ "Epoch [96] Loss: 0.002365282145879602, LR 0.0007508468627929689\n",
+ "Epoch [97] Loss: 0.0023112583984719946, LR 0.0007508468627929689\n",
+ "Epoch [98] Loss: 0.002257173682780976, LR 0.0007508468627929689\n",
+ "Epoch [99] Loss: 0.002162747085094452, LR 0.0005631351470947267\n"
+ ]
+ }
+ ],
+ "source": [
+ "epochs = 100\n",
+ "for e in range(epochs):\n",
+ " epoch_loss = 0.\n",
+ " for i, (data, label) in enumerate(train_data):\n",
+ " data = data.as_in_context(ctx)\n",
+ " label = label.as_in_context(ctx)\n",
+ "\n",
+ " with mx.autograd.record():\n",
+ " output = net(data)\n",
+ " l = loss(output, label)\n",
+ "\n",
+ " l.backward()\n",
+ " trainer.step(data.shape[0])\n",
+ " \n",
+ " epoch_loss += l.mean()\n",
+ " \n",
+ " print(\"Epoch [{}] Loss: {}, LR {}\".format(e, epoch_loss.asscalar()/(i+1), trainer.learning_rate))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Testing"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We get a random element from the testing set"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "n = random.randint(0, len(test_data)-1)\n",
+ "\n",
+ "x_orig = X[split_idx+n]\n",
+ "y_orig = Y[split_idx+n]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_pred(x):\n",
+ " x, _ = transform(x, x)\n",
+ " output = net(x.as_in_context(ctx).expand_dims(axis=0))\n",
+ "\n",
+ " # Convert output back to string\n",
+ " pred = ''.join([vocab[int(o)] for o in output[0].argmax(axis=1).asnumpy().tolist()])\n",
+ " return pred"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Printing the result"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "X 611 671 275 871 944\n",
+ "Predicted 275 611 671 871 944\n",
+ "Label 275 611 671 871 944\n"
+ ]
+ }
+ ],
+ "source": [
+ "x_ = ' '.join(map(str,x_orig))\n",
+ "label = ' '.join(map(str,y_orig))\n",
+ "print(\"X {}\\nPredicted {}\\nLabel {}\".format(x_, get_pred(x_orig), label))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also pick our own example, and the network manages to sort it without problem:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 66,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "10 30 130 500 999 \n"
+ ]
+ }
+ ],
+ "source": [
+ "print(get_pred(np.array([500, 30, 999, 10, 130])))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The model has even learned to generalize to examples not on the training set"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 64,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Only four numbers: 105 202 302 501 \n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"Only four numbers:\", get_pred(np.array([105, 302, 501, 202])))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "However we can see it has trouble with other edge cases:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 63,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Small digits: 8 0 42 28 \n",
+ "Small digits, 6 numbers: 10 0 20 82 71 115 \n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"Small digits:\", get_pred(np.array([10, 3, 5, 2, 8])))\n",
+ "print(\"Small digits, 6 numbers:\", get_pred(np.array([10, 33, 52, 21, 82, 10])))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This could be improved by adjusting the training dataset accordingly"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/example/bi-lstm-sort/infer_sort.py b/example/bi-lstm-sort/infer_sort.py
deleted file mode 100644
index f81c6c0ec62a..000000000000
--- a/example/bi-lstm-sort/infer_sort.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: disable=C0111,too-many-arguments,too-many-instance-attributes,too-many-locals,redefined-outer-name,fixme
-# pylint: disable=superfluous-parens, no-member, invalid-name
-import sys
-import os
-import argparse
-import numpy as np
-import mxnet as mx
-
-from sort_io import BucketSentenceIter, default_build_vocab
-from rnn_model import BiLSTMInferenceModel
-
-TRAIN_FILE = "sort.train.txt"
-TEST_FILE = "sort.test.txt"
-VALID_FILE = "sort.valid.txt"
-DATA_DIR = os.path.join(os.getcwd(), "data")
-SEQ_LEN = 5
-
-def MakeInput(char, vocab, arr):
- idx = vocab[char]
- tmp = np.zeros((1,))
- tmp[0] = idx
- arr[:] = tmp
-
-def main():
- tks = sys.argv[1:]
- assert len(tks) >= 5, "Please provide 5 numbers for sorting as sequence length is 5"
- batch_size = 1
- buckets = []
- num_hidden = 300
- num_embed = 512
- num_lstm_layer = 2
-
- num_epoch = 1
- learning_rate = 0.1
- momentum = 0.9
-
- contexts = [mx.context.cpu(i) for i in range(1)]
-
- vocab = default_build_vocab(os.path.join(DATA_DIR, TRAIN_FILE))
- rvocab = {}
- for k, v in vocab.items():
- rvocab[v] = k
-
- _, arg_params, __ = mx.model.load_checkpoint("sort", 1)
- for tk in tks:
- assert (tk in vocab), "{} not in range of numbers that the model trained for.".format(tk)
-
- model = BiLSTMInferenceModel(SEQ_LEN, len(vocab),
- num_hidden=num_hidden, num_embed=num_embed,
- num_label=len(vocab), arg_params=arg_params, ctx=contexts, dropout=0.0)
-
- data = np.zeros((1, len(tks)))
- for k in range(len(tks)):
- data[0][k] = vocab[tks[k]]
-
- data = mx.nd.array(data)
- prob = model.forward(data)
- for k in range(len(tks)):
- print(rvocab[np.argmax(prob, axis = 1)[k]])
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/example/bi-lstm-sort/lstm.py b/example/bi-lstm-sort/lstm.py
deleted file mode 100644
index 362481dd09ea..000000000000
--- a/example/bi-lstm-sort/lstm.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint:skip-file
-import sys
-import mxnet as mx
-import numpy as np
-from collections import namedtuple
-import time
-import math
-LSTMState = namedtuple("LSTMState", ["c", "h"])
-LSTMParam = namedtuple("LSTMParam", ["i2h_weight", "i2h_bias",
- "h2h_weight", "h2h_bias"])
-LSTMModel = namedtuple("LSTMModel", ["rnn_exec", "symbol",
- "init_states", "last_states", "forward_state", "backward_state",
- "seq_data", "seq_labels", "seq_outputs",
- "param_blocks"])
-
-def lstm(num_hidden, indata, prev_state, param, seqidx, layeridx, dropout=0.):
- """LSTM Cell symbol"""
- if dropout > 0.:
- indata = mx.sym.Dropout(data=indata, p=dropout)
- i2h = mx.sym.FullyConnected(data=indata,
- weight=param.i2h_weight,
- bias=param.i2h_bias,
- num_hidden=num_hidden * 4,
- name="t%d_l%d_i2h" % (seqidx, layeridx))
- h2h = mx.sym.FullyConnected(data=prev_state.h,
- weight=param.h2h_weight,
- bias=param.h2h_bias,
- num_hidden=num_hidden * 4,
- name="t%d_l%d_h2h" % (seqidx, layeridx))
- gates = i2h + h2h
- slice_gates = mx.sym.SliceChannel(gates, num_outputs=4,
- name="t%d_l%d_slice" % (seqidx, layeridx))
- in_gate = mx.sym.Activation(slice_gates[0], act_type="sigmoid")
- in_transform = mx.sym.Activation(slice_gates[1], act_type="tanh")
- forget_gate = mx.sym.Activation(slice_gates[2], act_type="sigmoid")
- out_gate = mx.sym.Activation(slice_gates[3], act_type="sigmoid")
- next_c = (forget_gate * prev_state.c) + (in_gate * in_transform)
- next_h = out_gate * mx.sym.Activation(next_c, act_type="tanh")
- return LSTMState(c=next_c, h=next_h)
-
-
-def bi_lstm_unroll(seq_len, input_size,
- num_hidden, num_embed, num_label, dropout=0.):
-
- embed_weight = mx.sym.Variable("embed_weight")
- cls_weight = mx.sym.Variable("cls_weight")
- cls_bias = mx.sym.Variable("cls_bias")
- last_states = []
- last_states.append(LSTMState(c = mx.sym.Variable("l0_init_c"), h = mx.sym.Variable("l0_init_h")))
- last_states.append(LSTMState(c = mx.sym.Variable("l1_init_c"), h = mx.sym.Variable("l1_init_h")))
- forward_param = LSTMParam(i2h_weight=mx.sym.Variable("l0_i2h_weight"),
- i2h_bias=mx.sym.Variable("l0_i2h_bias"),
- h2h_weight=mx.sym.Variable("l0_h2h_weight"),
- h2h_bias=mx.sym.Variable("l0_h2h_bias"))
- backward_param = LSTMParam(i2h_weight=mx.sym.Variable("l1_i2h_weight"),
- i2h_bias=mx.sym.Variable("l1_i2h_bias"),
- h2h_weight=mx.sym.Variable("l1_h2h_weight"),
- h2h_bias=mx.sym.Variable("l1_h2h_bias"))
-
- # embeding layer
- data = mx.sym.Variable('data')
- label = mx.sym.Variable('softmax_label')
- embed = mx.sym.Embedding(data=data, input_dim=input_size,
- weight=embed_weight, output_dim=num_embed, name='embed')
- wordvec = mx.sym.SliceChannel(data=embed, num_outputs=seq_len, squeeze_axis=1)
-
- forward_hidden = []
- for seqidx in range(seq_len):
- hidden = wordvec[seqidx]
- next_state = lstm(num_hidden, indata=hidden,
- prev_state=last_states[0],
- param=forward_param,
- seqidx=seqidx, layeridx=0, dropout=dropout)
- hidden = next_state.h
- last_states[0] = next_state
- forward_hidden.append(hidden)
-
- backward_hidden = []
- for seqidx in range(seq_len):
- k = seq_len - seqidx - 1
- hidden = wordvec[k]
- next_state = lstm(num_hidden, indata=hidden,
- prev_state=last_states[1],
- param=backward_param,
- seqidx=k, layeridx=1,dropout=dropout)
- hidden = next_state.h
- last_states[1] = next_state
- backward_hidden.insert(0, hidden)
-
- hidden_all = []
- for i in range(seq_len):
- hidden_all.append(mx.sym.Concat(*[forward_hidden[i], backward_hidden[i]], dim=1))
-
- hidden_concat = mx.sym.Concat(*hidden_all, dim=0)
- pred = mx.sym.FullyConnected(data=hidden_concat, num_hidden=num_label,
- weight=cls_weight, bias=cls_bias, name='pred')
-
- label = mx.sym.transpose(data=label)
- label = mx.sym.Reshape(data=label, target_shape=(0,))
- sm = mx.sym.SoftmaxOutput(data=pred, label=label, name='softmax')
-
- return sm
-
-
-def bi_lstm_inference_symbol(input_size, seq_len,
- num_hidden, num_embed, num_label, dropout=0.):
- seqidx = 0
- embed_weight=mx.sym.Variable("embed_weight")
- cls_weight = mx.sym.Variable("cls_weight")
- cls_bias = mx.sym.Variable("cls_bias")
- last_states = [LSTMState(c = mx.sym.Variable("l0_init_c"), h = mx.sym.Variable("l0_init_h")),
- LSTMState(c = mx.sym.Variable("l1_init_c"), h = mx.sym.Variable("l1_init_h"))]
- forward_param = LSTMParam(i2h_weight=mx.sym.Variable("l0_i2h_weight"),
- i2h_bias=mx.sym.Variable("l0_i2h_bias"),
- h2h_weight=mx.sym.Variable("l0_h2h_weight"),
- h2h_bias=mx.sym.Variable("l0_h2h_bias"))
- backward_param = LSTMParam(i2h_weight=mx.sym.Variable("l1_i2h_weight"),
- i2h_bias=mx.sym.Variable("l1_i2h_bias"),
- h2h_weight=mx.sym.Variable("l1_h2h_weight"),
- h2h_bias=mx.sym.Variable("l1_h2h_bias"))
- data = mx.sym.Variable("data")
- embed = mx.sym.Embedding(data=data, input_dim=input_size,
- weight=embed_weight, output_dim=num_embed, name='embed')
- wordvec = mx.sym.SliceChannel(data=embed, num_outputs=seq_len, squeeze_axis=1)
- forward_hidden = []
- for seqidx in range(seq_len):
- next_state = lstm(num_hidden, indata=wordvec[seqidx],
- prev_state=last_states[0],
- param=forward_param,
- seqidx=seqidx, layeridx=0, dropout=0.0)
- hidden = next_state.h
- last_states[0] = next_state
- forward_hidden.append(hidden)
-
- backward_hidden = []
- for seqidx in range(seq_len):
- k = seq_len - seqidx - 1
- next_state = lstm(num_hidden, indata=wordvec[k],
- prev_state=last_states[1],
- param=backward_param,
- seqidx=k, layeridx=1, dropout=0.0)
- hidden = next_state.h
- last_states[1] = next_state
- backward_hidden.insert(0, hidden)
-
- hidden_all = []
- for i in range(seq_len):
- hidden_all.append(mx.sym.Concat(*[forward_hidden[i], backward_hidden[i]], dim=1))
- hidden_concat = mx.sym.Concat(*hidden_all, dim=0)
- fc = mx.sym.FullyConnected(data=hidden_concat, num_hidden=num_label,
- weight=cls_weight, bias=cls_bias, name='pred')
- sm = mx.sym.SoftmaxOutput(data=fc, name='softmax')
- output = [sm]
- for state in last_states:
- output.append(state.c)
- output.append(state.h)
- return mx.sym.Group(output)
-
diff --git a/example/bi-lstm-sort/lstm_sort.py b/example/bi-lstm-sort/lstm_sort.py
deleted file mode 100644
index 3d7090a9a351..000000000000
--- a/example/bi-lstm-sort/lstm_sort.py
+++ /dev/null
@@ -1,142 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: disable=C0111,too-many-arguments,too-many-instance-attributes,too-many-locals,redefined-outer-name,fixme
-# pylint: disable=superfluous-parens, no-member, invalid-name
-import argparse
-import errno
-import logging
-import os
-import random
-import sys
-
-import numpy as np
-
-import mxnet as mx
-from lstm import bi_lstm_unroll
-from sort_io import BucketSentenceIter, default_build_vocab
-
-head = '%(asctime)-15s %(message)s'
-logging.basicConfig(level=logging.DEBUG, format=head)
-
-TRAIN_FILE = "sort.train.txt"
-TEST_FILE = "sort.test.txt"
-VALID_FILE = "sort.valid.txt"
-DATA_DIR = os.path.join(os.getcwd(), "data")
-SEQ_LEN = 5
-
-
-def gen_data(seq_len, start_range, end_range):
- if not os.path.exists(DATA_DIR):
- try:
- logging.info('create directory %s', DATA_DIR)
- os.makedirs(DATA_DIR)
- except OSError as exc:
- if exc.errno != errno.EEXIST:
- raise OSError('failed to create ' + DATA_DIR)
- vocab = [str(x) for x in range(start_range, end_range)]
- sw_train = open(os.path.join(DATA_DIR, TRAIN_FILE), "w")
- sw_test = open(os.path.join(DATA_DIR, TEST_FILE), "w")
- sw_valid = open(os.path.join(DATA_DIR, VALID_FILE), "w")
-
- for i in range(1000000):
- seq = " ".join([vocab[random.randint(0, len(vocab) - 1)] for j in range(seq_len)])
- k = i % 50
- if k == 0:
- sw_test.write(seq + "\n")
- elif k == 1:
- sw_valid.write(seq + "\n")
- else:
- sw_train.write(seq + "\n")
-
- sw_train.close()
- sw_test.close()
-
-def parse_args():
- parser = argparse.ArgumentParser(description="Parse args for lstm_sort example",
- formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument('--start-range', type=int, default=100,
- help='starting number of the range')
- parser.add_argument('--end-range', type=int, default=1000,
- help='Ending number of the range')
- parser.add_argument('--cpu', action='store_true',
- help='To use CPU for training')
- return parser.parse_args()
-
-
-def Perplexity(label, pred):
- label = label.T.reshape((-1,))
- loss = 0.
- for i in range(pred.shape[0]):
- loss += -np.log(max(1e-10, pred[i][int(label[i])]))
- return np.exp(loss / label.size)
-
-def main():
- args = parse_args()
- gen_data(SEQ_LEN, args.start_range, args.end_range)
- batch_size = 100
- buckets = []
- num_hidden = 300
- num_embed = 512
- num_lstm_layer = 2
-
- num_epoch = 1
- learning_rate = 0.1
- momentum = 0.9
-
- if args.cpu:
- contexts = [mx.context.cpu(i) for i in range(1)]
- else:
- contexts = [mx.context.gpu(i) for i in range(1)]
-
- vocab = default_build_vocab(os.path.join(DATA_DIR, TRAIN_FILE))
-
- def sym_gen(seq_len):
- return bi_lstm_unroll(seq_len, len(vocab),
- num_hidden=num_hidden, num_embed=num_embed,
- num_label=len(vocab))
-
- init_c = [('l%d_init_c'%l, (batch_size, num_hidden)) for l in range(num_lstm_layer)]
- init_h = [('l%d_init_h'%l, (batch_size, num_hidden)) for l in range(num_lstm_layer)]
- init_states = init_c + init_h
-
- data_train = BucketSentenceIter(os.path.join(DATA_DIR, TRAIN_FILE), vocab,
- buckets, batch_size, init_states)
- data_val = BucketSentenceIter(os.path.join(DATA_DIR, VALID_FILE), vocab,
- buckets, batch_size, init_states)
-
- if len(buckets) == 1:
- symbol = sym_gen(buckets[0])
- else:
- symbol = sym_gen
-
- model = mx.model.FeedForward(ctx=contexts,
- symbol=symbol,
- num_epoch=num_epoch,
- learning_rate=learning_rate,
- momentum=momentum,
- wd=0.00001,
- initializer=mx.init.Xavier(factor_type="in", magnitude=2.34))
-
- model.fit(X=data_train, eval_data=data_val,
- eval_metric = mx.metric.np(Perplexity),
- batch_end_callback=mx.callback.Speedometer(batch_size, 50),)
-
- model.save("sort")
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/example/bi-lstm-sort/rnn_model.py b/example/bi-lstm-sort/rnn_model.py
deleted file mode 100644
index 1079e90991bf..000000000000
--- a/example/bi-lstm-sort/rnn_model.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: disable=C0111,too-many-arguments,too-many-instance-attributes,too-many-locals,redefined-outer-name,fixme
-# pylint: disable=superfluous-parens, no-member, invalid-name
-import sys
-import numpy as np
-import mxnet as mx
-
-from lstm import LSTMState, LSTMParam, lstm, bi_lstm_inference_symbol
-
-class BiLSTMInferenceModel(object):
- def __init__(self,
- seq_len,
- input_size,
- num_hidden,
- num_embed,
- num_label,
- arg_params,
- ctx=mx.cpu(),
- dropout=0.):
- self.sym = bi_lstm_inference_symbol(input_size, seq_len,
- num_hidden,
- num_embed,
- num_label,
- dropout)
- batch_size = 1
- init_c = [('l%d_init_c'%l, (batch_size, num_hidden)) for l in range(2)]
- init_h = [('l%d_init_h'%l, (batch_size, num_hidden)) for l in range(2)]
-
- data_shape = [("data", (batch_size, seq_len, ))]
-
- input_shapes = dict(init_c + init_h + data_shape)
- self.executor = self.sym.simple_bind(ctx=mx.cpu(), **input_shapes)
-
- for key in self.executor.arg_dict.keys():
- if key in arg_params:
- arg_params[key].copyto(self.executor.arg_dict[key])
-
- state_name = []
- for i in range(2):
- state_name.append("l%d_init_c" % i)
- state_name.append("l%d_init_h" % i)
-
- self.states_dict = dict(zip(state_name, self.executor.outputs[1:]))
- self.input_arr = mx.nd.zeros(data_shape[0][1])
-
- def forward(self, input_data, new_seq=False):
- if new_seq == True:
- for key in self.states_dict.keys():
- self.executor.arg_dict[key][:] = 0.
- input_data.copyto(self.executor.arg_dict["data"])
- self.executor.forward()
- for key in self.states_dict.keys():
- self.states_dict[key].copyto(self.executor.arg_dict[key])
- prob = self.executor.outputs[0].asnumpy()
- return prob
-
-
diff --git a/example/bi-lstm-sort/sort_io.py b/example/bi-lstm-sort/sort_io.py
deleted file mode 100644
index 853d0ee87dbb..000000000000
--- a/example/bi-lstm-sort/sort_io.py
+++ /dev/null
@@ -1,255 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: disable=C0111,too-many-arguments,too-many-instance-attributes,too-many-locals,redefined-outer-name,fixme
-# pylint: disable=superfluous-parens, no-member, invalid-name
-from __future__ import print_function
-import sys
-import numpy as np
-import mxnet as mx
-
-# The interface of a data iter that works for bucketing
-#
-# DataIter
-# - default_bucket_key: the bucket key for the default symbol.
-#
-# DataBatch
-# - provide_data: same as DataIter, but specific to this batch
-# - provide_label: same as DataIter, but specific to this batch
-# - bucket_key: the key for the bucket that should be used for this batch
-
-def default_read_content(path):
- with open(path) as ins:
- content = ins.read()
- content = content.replace('\n', ' ').replace('. ', ' ')
- return content
-
-def default_build_vocab(path):
- content = default_read_content(path)
- content = content.split(' ')
-
- words = set([x for x in content if len(x) > 0])
- words = [x for x in words]
- words = sorted(words)
- the_vocab = {}
- idx = 1 # 0 is left for zero-padding
- the_vocab[' '] = 0 # put a dummy element here so that len(vocab) is correct
- for word in words:
- if len(word) == 0:
- continue
- if not word in the_vocab:
- the_vocab[word] = idx
- idx += 1
- return the_vocab
-
-def default_text2id(sentence, the_vocab):
- words = sentence.split(' ')
- words = [the_vocab[w] for w in words if len(w) > 0]
- return words
-
-def default_gen_buckets(sentences, batch_size, the_vocab):
- len_dict = {}
- max_len = -1
- for sentence in sentences:
- words = default_text2id(sentence, the_vocab)
- lw = len(words)
- if lw == 0:
- continue
- if lw > max_len:
- max_len = lw
- if lw in len_dict:
- len_dict[lw] += 1
- else:
- len_dict[lw] = 1
- print(len_dict)
-
- tl = 0
- buckets = []
- for l, n in len_dict.items(): # TODO: There are better heuristic ways to do this
- if n + tl >= batch_size:
- buckets.append(l)
- tl = 0
- else:
- tl += n
- if tl > 0:
- buckets.append(max_len)
- return buckets
-
-
-class SimpleBatch(object):
- def __init__(self, data_names, data, label_names, label, bucket_key):
- self.data = data
- self.label = label
- self.data_names = data_names
- self.label_names = label_names
- self.bucket_key = bucket_key
-
- self.pad = 0
- self.index = None # TODO: what is index?
-
- @property
- def provide_data(self):
- return [(n, x.shape) for n, x in zip(self.data_names, self.data)]
-
- @property
- def provide_label(self):
- return [(n, x.shape) for n, x in zip(self.label_names, self.label)]
-
-class DummyIter(mx.io.DataIter):
- "A dummy iterator that always return the same batch, used for speed testing"
- def __init__(self, real_iter):
- super(DummyIter, self).__init__()
- self.real_iter = real_iter
- self.provide_data = real_iter.provide_data
- self.provide_label = real_iter.provide_label
- self.batch_size = real_iter.batch_size
-
- for batch in real_iter:
- self.the_batch = batch
- break
-
- def __iter__(self):
- return self
-
- def next(self):
- return self.the_batch
-
-class BucketSentenceIter(mx.io.DataIter):
- def __init__(self, path, vocab, buckets, batch_size,
- init_states, data_name='data', label_name='label',
- seperate_char=' ', text2id=None, read_content=None):
- super(BucketSentenceIter, self).__init__()
-
- if text2id is None:
- self.text2id = default_text2id
- else:
- self.text2id = text2id
- if read_content is None:
- self.read_content = default_read_content
- else:
- self.read_content = read_content
- content = self.read_content(path)
- sentences = content.split(seperate_char)
-
- if len(buckets) == 0:
- buckets = default_gen_buckets(sentences, batch_size, vocab)
- print(buckets)
- self.vocab_size = len(vocab)
- self.data_name = data_name
- self.label_name = label_name
-
- buckets.sort()
- self.buckets = buckets
- self.data = [[] for _ in buckets]
-
- # pre-allocate with the largest bucket for better memory sharing
- self.default_bucket_key = max(buckets)
-
- for sentence in sentences:
- sentence = self.text2id(sentence, vocab)
- if len(sentence) == 0:
- continue
- for i, bkt in enumerate(buckets):
- if bkt >= len(sentence):
- self.data[i].append(sentence)
- break
- # we just ignore the sentence it is longer than the maximum
- # bucket size here
-
- # convert data into ndarrays for better speed during training
- data = [np.zeros((len(x), buckets[i])) for i, x in enumerate(self.data)]
- for i_bucket in range(len(self.buckets)):
- for j in range(len(self.data[i_bucket])):
- sentence = self.data[i_bucket][j]
- data[i_bucket][j, :len(sentence)] = sentence
- self.data = data
-
- # Get the size of each bucket, so that we could sample
- # uniformly from the bucket
- bucket_sizes = [len(x) for x in self.data]
-
- print("Summary of dataset ==================")
- for bkt, size in zip(buckets, bucket_sizes):
- print("bucket of len %3d : %d samples" % (bkt, size))
-
- self.batch_size = batch_size
- self.make_data_iter_plan()
-
- self.init_states = init_states
- self.init_state_arrays = [mx.nd.zeros(x[1]) for x in init_states]
-
- self.provide_data = [('data', (batch_size, self.default_bucket_key))] + init_states
- self.provide_label = [('softmax_label', (self.batch_size, self.default_bucket_key))]
-
- def make_data_iter_plan(self):
- "make a random data iteration plan"
- # truncate each bucket into multiple of batch-size
- bucket_n_batches = []
- for i in range(len(self.data)):
- bucket_n_batches.append(len(self.data[i]) / self.batch_size)
- self.data[i] = self.data[i][:int(bucket_n_batches[i]*self.batch_size)]
-
- bucket_plan = np.hstack([np.zeros(n, int)+i for i, n in enumerate(bucket_n_batches)])
- np.random.shuffle(bucket_plan)
-
- bucket_idx_all = [np.random.permutation(len(x)) for x in self.data]
-
- self.bucket_plan = bucket_plan
- self.bucket_idx_all = bucket_idx_all
- self.bucket_curr_idx = [0 for x in self.data]
-
- self.data_buffer = []
- self.label_buffer = []
- for i_bucket in range(len(self.data)):
- data = np.zeros((self.batch_size, self.buckets[i_bucket]))
- label = np.zeros((self.batch_size, self.buckets[i_bucket]))
- self.data_buffer.append(data)
- self.label_buffer.append(label)
-
- def __iter__(self):
- init_state_names = [x[0] for x in self.init_states]
-
- for i_bucket in self.bucket_plan:
- data = self.data_buffer[i_bucket]
- label = self.label_buffer[i_bucket]
-
- i_idx = self.bucket_curr_idx[i_bucket]
- idx = self.bucket_idx_all[i_bucket][i_idx:i_idx+self.batch_size]
- self.bucket_curr_idx[i_bucket] += self.batch_size
- data[:] = self.data[i_bucket][idx]
-
- for k in range(len(data)):
- label[k] = sorted(data[k])
- #count = len(data[k]) / 2
- #for j in range(count):
- # data[j+count] = data[j]
-
- #label[:, :-1] = data[:, 1:]
- #label[:, -1] = 0
-
- data_all = [mx.nd.array(data)] + self.init_state_arrays
- label_all = [mx.nd.array(label)]
- data_names = ['data'] + init_state_names
- label_names = ['softmax_label']
-
- data_batch = SimpleBatch(data_names, data_all, label_names, label_all,
- self.buckets[i_bucket])
-
- yield data_batch
-
- def reset(self):
- self.bucket_curr_idx = [0 for x in self.data]
diff --git a/example/capsnet/README.md b/example/capsnet/README.md
index 49a6dd107267..500c7df72515 100644
--- a/example/capsnet/README.md
+++ b/example/capsnet/README.md
@@ -1,66 +1,66 @@
-**CapsNet-MXNet**
-=========================================
-
-This example is MXNet implementation of [CapsNet](https://arxiv.org/abs/1710.09829):
-Sara Sabour, Nicholas Frosst, Geoffrey E Hinton. Dynamic Routing Between Capsules. NIPS 2017
-- The current `best test error is 0.29%` and `average test error is 0.303%`
-- The `average test error on paper is 0.25%`
-
-Log files for the error rate are uploaded in [repository](https://github.com/samsungsds-rnd/capsnet.mxnet).
-* * *
-## **Usage**
-Install scipy with pip
-```
-pip install scipy
-```
-Install tensorboard with pip
-```
-pip install tensorboard
-```
-
-On Single gpu
-```
-python capsulenet.py --devices gpu0
-```
-On Multi gpus
-```
-python capsulenet.py --devices gpu0,gpu1
-```
-Full arguments
-```
-python capsulenet.py --batch_size 100 --devices gpu0,gpu1 --num_epoch 100 --lr 0.001 --num_routing 3 --model_prefix capsnet
-```
-
-* * *
-## **Prerequisities**
-
-MXNet version above (0.11.0)
-scipy version above (0.19.0)
-
-***
-## **Results**
-Train time takes about 36 seconds for each epoch (batch_size=100, 2 gtx 1080 gpus)
-
-CapsNet classification test error on MNIST
-
-```
-python capsulenet.py --devices gpu0,gpu1 --lr 0.0005 --decay 0.99 --model_prefix lr_0_0005_decay_0_99 --batch_size 100 --num_routing 3 --num_epoch 200
-```
-
-![](result.PNG)
-
-| Trial | Epoch | train err(%) | test err(%) | train loss | test loss |
-| :---: | :---: | :---: | :---: | :---: | :---: |
-| 1 | 120 | 0.06 | 0.31 | 0.0056 | 0.0064 |
-| 2 | 167 | 0.03 | 0.29 | 0.0048 | 0.0058 |
-| 3 | 182 | 0.04 | 0.31 | 0.0046 | 0.0058 |
-| average | - | 0.043 | 0.303 | 0.005 | 0.006 |
-
-We achieved `the best test error rate=0.29%` and `average test error=0.303%`. It is the best accuracy and fastest training time result among other implementations(Keras, Tensorflow at 2017-11-23).
-The result on paper is `0.25% (average test error rate)`.
-
-| Implementation| test err(%) | ※train time/epoch | GPU Used|
-| :---: | :---: | :---: |:---: |
-| MXNet | 0.29 | 36 sec | 2 GTX 1080 |
-| tensorflow | 0.49 | ※ 10 min | Unknown(4GB Memory) |
-| Keras | 0.30 | 55 sec | 2 GTX 1080 Ti |
+**CapsNet-MXNet**
+=========================================
+
+This example is MXNet implementation of [CapsNet](https://arxiv.org/abs/1710.09829):
+Sara Sabour, Nicholas Frosst, Geoffrey E Hinton. Dynamic Routing Between Capsules. NIPS 2017
+- The current `best test error is 0.29%` and `average test error is 0.303%`
+- The `average test error on paper is 0.25%`
+
+Log files for the error rate are uploaded in [repository](https://github.com/samsungsds-rnd/capsnet.mxnet).
+* * *
+## **Usage**
+Install scipy with pip
+```
+pip install scipy
+```
+Install tensorboard and mxboard with pip
+```
+pip install mxboard tensorflow
+```
+
+On Single gpu
+```
+python capsulenet.py --devices gpu0
+```
+On Multi gpus
+```
+python capsulenet.py --devices gpu0,gpu1
+```
+Full arguments
+```
+python capsulenet.py --batch_size 100 --devices gpu0,gpu1 --num_epoch 100 --lr 0.001 --num_routing 3 --model_prefix capsnet
+```
+
+* * *
+## **Prerequisities**
+
+MXNet version above (1.2.0)
+scipy version above (0.19.0)
+
+***
+## **Results**
+Train time takes about 36 seconds for each epoch (batch_size=100, 2 gtx 1080 gpus)
+
+CapsNet classification test error on MNIST:
+
+```
+python capsulenet.py --devices gpu0,gpu1 --lr 0.0005 --decay 0.99 --model_prefix lr_0_0005_decay_0_99 --batch_size 100 --num_routing 3 --num_epoch 200
+```
+
+![](result.PNG)
+
+| Trial | Epoch | train err(%) | test err(%) | train loss | test loss |
+| :---: | :---: | :---: | :---: | :---: | :---: |
+| 1 | 120 | 0.06 | 0.31 | 0.0056 | 0.0064 |
+| 2 | 167 | 0.03 | 0.29 | 0.0048 | 0.0058 |
+| 3 | 182 | 0.04 | 0.31 | 0.0046 | 0.0058 |
+| average | - | 0.043 | 0.303 | 0.005 | 0.006 |
+
+We achieved `the best test error rate=0.29%` and `average test error=0.303%`. It is the best accuracy and fastest training time result among other implementations(Keras, Tensorflow at 2017-11-23).
+The result on paper is `0.25% (average test error rate)`.
+
+| Implementation| test err(%) | ※train time/epoch | GPU Used|
+| :---: | :---: | :---: |:---: |
+| MXNet | 0.29 | 36 sec | 2 GTX 1080 |
+| tensorflow | 0.49 | ※ 10 min | Unknown(4GB Memory) |
+| Keras | 0.30 | 55 sec | 2 GTX 1080 Ti |
diff --git a/example/capsnet/capsulenet.py b/example/capsnet/capsulenet.py
index 6b44c3dfca90..67108757bf39 100644
--- a/example/capsnet/capsulenet.py
+++ b/example/capsnet/capsulenet.py
@@ -1,348 +1,347 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-import mxnet as mx
-import numpy as np
-import os
-import re
-import urllib
-import gzip
-import struct
-import scipy.ndimage as ndi
-from capsulelayers import primary_caps, CapsuleLayer
-
-from tensorboard import SummaryWriter
-
-def margin_loss(y_true, y_pred):
- loss = y_true * mx.sym.square(mx.sym.maximum(0., 0.9 - y_pred)) +\
- 0.5 * (1 - y_true) * mx.sym.square(mx.sym.maximum(0., y_pred - 0.1))
- return mx.sym.mean(data=mx.sym.sum(loss, 1))
-
-
-def capsnet(batch_size, n_class, num_routing,recon_loss_weight):
- # data.shape = [batch_size, 1, 28, 28]
- data = mx.sym.Variable('data')
-
- input_shape = (1, 28, 28)
- # Conv2D layer
- # net.shape = [batch_size, 256, 20, 20]
- conv1 = mx.sym.Convolution(data=data,
- num_filter=256,
- kernel=(9, 9),
- layout='NCHW',
- name='conv1')
- conv1 = mx.sym.Activation(data=conv1, act_type='relu', name='conv1_act')
- # net.shape = [batch_size, 256, 6, 6]
-
- primarycaps = primary_caps(data=conv1,
- dim_vector=8,
- n_channels=32,
- kernel=(9, 9),
- strides=[2, 2],
- name='primarycaps')
- primarycaps.infer_shape(data=(batch_size, 1, 28, 28))
- # CapsuleLayer
- kernel_initializer = mx.init.Xavier(rnd_type='uniform', factor_type='avg', magnitude=3)
- bias_initializer = mx.init.Zero()
- digitcaps = CapsuleLayer(num_capsule=10,
- dim_vector=16,
- batch_size=batch_size,
- kernel_initializer=kernel_initializer,
- bias_initializer=bias_initializer,
- num_routing=num_routing)(primarycaps)
-
- # out_caps : (batch_size, 10)
- out_caps = mx.sym.sqrt(data=mx.sym.sum(mx.sym.square(digitcaps), 2))
- out_caps.infer_shape(data=(batch_size, 1, 28, 28))
-
- y = mx.sym.Variable('softmax_label', shape=(batch_size,))
- y_onehot = mx.sym.one_hot(y, n_class)
- y_reshaped = mx.sym.Reshape(data=y_onehot, shape=(batch_size, -4, n_class, -1))
- y_reshaped.infer_shape(softmax_label=(batch_size,))
-
- # inputs_masked : (batch_size, 16)
- inputs_masked = mx.sym.linalg_gemm2(y_reshaped, digitcaps, transpose_a=True)
- inputs_masked = mx.sym.Reshape(data=inputs_masked, shape=(-3, 0))
- x_recon = mx.sym.FullyConnected(data=inputs_masked, num_hidden=512, name='x_recon')
- x_recon = mx.sym.Activation(data=x_recon, act_type='relu', name='x_recon_act')
- x_recon = mx.sym.FullyConnected(data=x_recon, num_hidden=1024, name='x_recon2')
- x_recon = mx.sym.Activation(data=x_recon, act_type='relu', name='x_recon_act2')
- x_recon = mx.sym.FullyConnected(data=x_recon, num_hidden=np.prod(input_shape), name='x_recon3')
- x_recon = mx.sym.Activation(data=x_recon, act_type='sigmoid', name='x_recon_act3')
-
- data_flatten = mx.sym.flatten(data=data)
- squared_error = mx.sym.square(x_recon-data_flatten)
- recon_error = mx.sym.mean(squared_error)
- recon_error_stopped = recon_error
- recon_error_stopped = mx.sym.BlockGrad(recon_error_stopped)
- loss = mx.symbol.MakeLoss((1-recon_loss_weight)*margin_loss(y_onehot, out_caps)+recon_loss_weight*recon_error)
-
- out_caps_blocked = out_caps
- out_caps_blocked = mx.sym.BlockGrad(out_caps_blocked)
- return mx.sym.Group([out_caps_blocked, loss, recon_error_stopped])
-
-
-def download_data(url, force_download=False):
- fname = url.split("/")[-1]
- if force_download or not os.path.exists(fname):
- urllib.urlretrieve(url, fname)
- return fname
-
-
-def read_data(label_url, image_url):
- with gzip.open(download_data(label_url)) as flbl:
- magic, num = struct.unpack(">II", flbl.read(8))
- label = np.fromstring(flbl.read(), dtype=np.int8)
- with gzip.open(download_data(image_url), 'rb') as fimg:
- magic, num, rows, cols = struct.unpack(">IIII", fimg.read(16))
- image = np.fromstring(fimg.read(), dtype=np.uint8).reshape(len(label), rows, cols)
- return label, image
-
-
-def to4d(img):
- return img.reshape(img.shape[0], 1, 28, 28).astype(np.float32)/255
-
-
-class LossMetric(mx.metric.EvalMetric):
- def __init__(self, batch_size, num_gpu):
- super(LossMetric, self).__init__('LossMetric')
- self.batch_size = batch_size
- self.num_gpu = num_gpu
- self.sum_metric = 0
- self.num_inst = 0
- self.loss = 0.0
- self.batch_sum_metric = 0
- self.batch_num_inst = 0
- self.batch_loss = 0.0
- self.recon_loss = 0.0
- self.n_batch = 0
-
- def update(self, labels, preds):
- batch_sum_metric = 0
- batch_num_inst = 0
- for label, pred_outcaps in zip(labels[0], preds[0]):
- label_np = int(label.asnumpy())
- pred_label = int(np.argmax(pred_outcaps.asnumpy()))
- batch_sum_metric += int(label_np == pred_label)
- batch_num_inst += 1
- batch_loss = preds[1].asnumpy()
- recon_loss = preds[2].asnumpy()
- self.sum_metric += batch_sum_metric
- self.num_inst += batch_num_inst
- self.loss += batch_loss
- self.recon_loss += recon_loss
- self.batch_sum_metric = batch_sum_metric
- self.batch_num_inst = batch_num_inst
- self.batch_loss = batch_loss
- self.n_batch += 1
-
- def get_name_value(self):
- acc = float(self.sum_metric)/float(self.num_inst)
- mean_loss = self.loss / float(self.n_batch)
- mean_recon_loss = self.recon_loss / float(self.n_batch)
- return acc, mean_loss, mean_recon_loss
-
- def get_batch_log(self, n_batch):
- print("n_batch :"+str(n_batch)+" batch_acc:" +
- str(float(self.batch_sum_metric) / float(self.batch_num_inst)) +
- ' batch_loss:' + str(float(self.batch_loss)/float(self.batch_num_inst)))
- self.batch_sum_metric = 0
- self.batch_num_inst = 0
- self.batch_loss = 0.0
-
- def reset(self):
- self.sum_metric = 0
- self.num_inst = 0
- self.loss = 0.0
- self.recon_loss = 0.0
- self.n_batch = 0
-
-
-class SimpleLRScheduler(mx.lr_scheduler.LRScheduler):
- """A simple lr schedule that simply return `dynamic_lr`. We will set `dynamic_lr`
- dynamically based on performance on the validation set.
- """
-
- def __init__(self, learning_rate=0.001):
- super(SimpleLRScheduler, self).__init__()
- self.learning_rate = learning_rate
-
- def __call__(self, num_update):
- return self.learning_rate
-
-
-def do_training(num_epoch, optimizer, kvstore, learning_rate, model_prefix, decay):
- summary_writer = SummaryWriter(args.tblog_dir)
- lr_scheduler = SimpleLRScheduler(learning_rate)
- optimizer_params = {'lr_scheduler': lr_scheduler}
- module.init_params()
- module.init_optimizer(kvstore=kvstore,
- optimizer=optimizer,
- optimizer_params=optimizer_params)
- n_epoch = 0
- while True:
- if n_epoch >= num_epoch:
- break
- train_iter.reset()
- val_iter.reset()
- loss_metric.reset()
- for n_batch, data_batch in enumerate(train_iter):
- module.forward_backward(data_batch)
- module.update()
- module.update_metric(loss_metric, data_batch.label)
- loss_metric.get_batch_log(n_batch)
- train_acc, train_loss, train_recon_err = loss_metric.get_name_value()
- loss_metric.reset()
- for n_batch, data_batch in enumerate(val_iter):
- module.forward(data_batch)
- module.update_metric(loss_metric, data_batch.label)
- loss_metric.get_batch_log(n_batch)
- val_acc, val_loss, val_recon_err = loss_metric.get_name_value()
-
- summary_writer.add_scalar('train_acc', train_acc, n_epoch)
- summary_writer.add_scalar('train_loss', train_loss, n_epoch)
- summary_writer.add_scalar('train_recon_err', train_recon_err, n_epoch)
- summary_writer.add_scalar('val_acc', val_acc, n_epoch)
- summary_writer.add_scalar('val_loss', val_loss, n_epoch)
- summary_writer.add_scalar('val_recon_err', val_recon_err, n_epoch)
-
- print('Epoch[%d] train acc: %.4f loss: %.6f recon_err: %.6f' % (n_epoch, train_acc, train_loss, train_recon_err))
- print('Epoch[%d] val acc: %.4f loss: %.6f recon_err: %.6f' % (n_epoch, val_acc, val_loss, val_recon_err))
- print('SAVE CHECKPOINT')
-
- module.save_checkpoint(prefix=model_prefix, epoch=n_epoch)
- n_epoch += 1
- lr_scheduler.learning_rate = learning_rate * (decay ** n_epoch)
-
-
-def apply_transform(x,
- transform_matrix,
- fill_mode='nearest',
- cval=0.):
- x = np.rollaxis(x, 0, 0)
- final_affine_matrix = transform_matrix[:2, :2]
- final_offset = transform_matrix[:2, 2]
- channel_images = [ndi.interpolation.affine_transform(
- x_channel,
- final_affine_matrix,
- final_offset,
- order=0,
- mode=fill_mode,
- cval=cval) for x_channel in x]
- x = np.stack(channel_images, axis=0)
- x = np.rollaxis(x, 0, 0 + 1)
- return x
-
-
-def random_shift(x, width_shift_fraction, height_shift_fraction):
- tx = np.random.uniform(-height_shift_fraction, height_shift_fraction) * x.shape[2]
- ty = np.random.uniform(-width_shift_fraction, width_shift_fraction) * x.shape[1]
- shift_matrix = np.array([[1, 0, tx],
- [0, 1, ty],
- [0, 0, 1]])
- x = apply_transform(x, shift_matrix, 'nearest')
- return x
-
-def _shuffle(data, idx):
- """Shuffle the data."""
- shuffle_data = []
-
- for k, v in data:
- shuffle_data.append((k, mx.ndarray.array(v.asnumpy()[idx], v.context)))
-
- return shuffle_data
-
-class MNISTCustomIter(mx.io.NDArrayIter):
-
- def reset(self):
- # shuffle data
- if self.is_train:
- np.random.shuffle(self.idx)
- self.data = _shuffle(self.data, self.idx)
- self.label = _shuffle(self.label, self.idx)
- if self.last_batch_handle == 'roll_over' and self.cursor > self.num_data:
- self.cursor = -self.batch_size + (self.cursor%self.num_data)%self.batch_size
- else:
- self.cursor = -self.batch_size
- def set_is_train(self, is_train):
- self.is_train = is_train
- def next(self):
- if self.iter_next():
- if self.is_train:
- data_raw_list = self.getdata()
- data_shifted = []
- for data_raw in data_raw_list[0]:
- data_shifted.append(random_shift(data_raw.asnumpy(), 0.1, 0.1))
- return mx.io.DataBatch(data=[mx.nd.array(data_shifted)], label=self.getlabel(),
- pad=self.getpad(), index=None)
- else:
- return mx.io.DataBatch(data=self.getdata(), label=self.getlabel(), \
- pad=self.getpad(), index=None)
-
- else:
- raise StopIteration
-
-
-if __name__ == "__main__":
- # Read mnist data set
- path = 'http://yann.lecun.com/exdb/mnist/'
- (train_lbl, train_img) = read_data(
- path + 'train-labels-idx1-ubyte.gz', path + 'train-images-idx3-ubyte.gz')
- (val_lbl, val_img) = read_data(
- path + 't10k-labels-idx1-ubyte.gz', path + 't10k-images-idx3-ubyte.gz')
- # set batch size
- import argparse
- parser = argparse.ArgumentParser()
- parser.add_argument('--batch_size', default=100, type=int)
- parser.add_argument('--devices', default='gpu0', type=str)
- parser.add_argument('--num_epoch', default=100, type=int)
- parser.add_argument('--lr', default=0.001, type=float)
- parser.add_argument('--num_routing', default=3, type=int)
- parser.add_argument('--model_prefix', default='capsnet', type=str)
- parser.add_argument('--decay', default=0.9, type=float)
- parser.add_argument('--tblog_dir', default='tblog', type=str)
- parser.add_argument('--recon_loss_weight', default=0.392, type=float)
- args = parser.parse_args()
- for k, v in sorted(vars(args).items()):
- print("{0}: {1}".format(k, v))
- contexts = re.split(r'\W+', args.devices)
- for i, ctx in enumerate(contexts):
- if ctx[:3] == 'gpu':
- contexts[i] = mx.context.gpu(int(ctx[3:]))
- else:
- contexts[i] = mx.context.cpu()
- num_gpu = len(contexts)
-
- if args.batch_size % num_gpu != 0:
- raise Exception('num_gpu should be positive divisor of batch_size')
-
- # generate train_iter, val_iter
- train_iter = MNISTCustomIter(data=to4d(train_img), label=train_lbl, batch_size=args.batch_size, shuffle=True)
- train_iter.set_is_train(True)
- val_iter = MNISTCustomIter(data=to4d(val_img), label=val_lbl, batch_size=args.batch_size,)
- val_iter.set_is_train(False)
- # define capsnet
- final_net = capsnet(batch_size=args.batch_size/num_gpu, n_class=10, num_routing=args.num_routing, recon_loss_weight=args.recon_loss_weight)
- # set metric
- loss_metric = LossMetric(args.batch_size/num_gpu, 1)
-
- # run model
- module = mx.mod.Module(symbol=final_net, context=contexts, data_names=('data',), label_names=('softmax_label',))
- module.bind(data_shapes=train_iter.provide_data,
- label_shapes=val_iter.provide_label,
- for_training=True)
- do_training(num_epoch=args.num_epoch, optimizer='adam', kvstore='device', learning_rate=args.lr,
- model_prefix=args.model_prefix, decay=args.decay)
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import mxnet as mx
+import numpy as np
+import os
+import re
+import gzip
+import struct
+import scipy.ndimage as ndi
+from capsulelayers import primary_caps, CapsuleLayer
+
+from mxboard import SummaryWriter
+
+def margin_loss(y_true, y_pred):
+ loss = y_true * mx.sym.square(mx.sym.maximum(0., 0.9 - y_pred)) +\
+ 0.5 * (1 - y_true) * mx.sym.square(mx.sym.maximum(0., y_pred - 0.1))
+ return mx.sym.mean(data=mx.sym.sum(loss, 1))
+
+
+def capsnet(batch_size, n_class, num_routing,recon_loss_weight):
+ # data.shape = [batch_size, 1, 28, 28]
+ data = mx.sym.Variable('data')
+
+ input_shape = (1, 28, 28)
+ # Conv2D layer
+ # net.shape = [batch_size, 256, 20, 20]
+ conv1 = mx.sym.Convolution(data=data,
+ num_filter=256,
+ kernel=(9, 9),
+ layout='NCHW',
+ name='conv1')
+ conv1 = mx.sym.Activation(data=conv1, act_type='relu', name='conv1_act')
+ # net.shape = [batch_size, 256, 6, 6]
+
+ primarycaps = primary_caps(data=conv1,
+ dim_vector=8,
+ n_channels=32,
+ kernel=(9, 9),
+ strides=[2, 2],
+ name='primarycaps')
+ primarycaps.infer_shape(data=(batch_size, 1, 28, 28))
+ # CapsuleLayer
+ kernel_initializer = mx.init.Xavier(rnd_type='uniform', factor_type='avg', magnitude=3)
+ bias_initializer = mx.init.Zero()
+ digitcaps = CapsuleLayer(num_capsule=10,
+ dim_vector=16,
+ batch_size=batch_size,
+ kernel_initializer=kernel_initializer,
+ bias_initializer=bias_initializer,
+ num_routing=num_routing)(primarycaps)
+
+ # out_caps : (batch_size, 10)
+ out_caps = mx.sym.sqrt(data=mx.sym.sum(mx.sym.square(digitcaps), 2))
+ out_caps.infer_shape(data=(batch_size, 1, 28, 28))
+
+ y = mx.sym.Variable('softmax_label', shape=(batch_size,))
+ y_onehot = mx.sym.one_hot(y, n_class)
+ y_reshaped = mx.sym.Reshape(data=y_onehot, shape=(batch_size, -4, n_class, -1))
+ y_reshaped.infer_shape(softmax_label=(batch_size,))
+
+ # inputs_masked : (batch_size, 16)
+ inputs_masked = mx.sym.linalg_gemm2(y_reshaped, digitcaps, transpose_a=True)
+ inputs_masked = mx.sym.Reshape(data=inputs_masked, shape=(-3, 0))
+ x_recon = mx.sym.FullyConnected(data=inputs_masked, num_hidden=512, name='x_recon')
+ x_recon = mx.sym.Activation(data=x_recon, act_type='relu', name='x_recon_act')
+ x_recon = mx.sym.FullyConnected(data=x_recon, num_hidden=1024, name='x_recon2')
+ x_recon = mx.sym.Activation(data=x_recon, act_type='relu', name='x_recon_act2')
+ x_recon = mx.sym.FullyConnected(data=x_recon, num_hidden=np.prod(input_shape), name='x_recon3')
+ x_recon = mx.sym.Activation(data=x_recon, act_type='sigmoid', name='x_recon_act3')
+
+ data_flatten = mx.sym.flatten(data=data)
+ squared_error = mx.sym.square(x_recon-data_flatten)
+ recon_error = mx.sym.mean(squared_error)
+ recon_error_stopped = recon_error
+ recon_error_stopped = mx.sym.BlockGrad(recon_error_stopped)
+ loss = mx.symbol.MakeLoss((1-recon_loss_weight)*margin_loss(y_onehot, out_caps)+recon_loss_weight*recon_error)
+
+ out_caps_blocked = out_caps
+ out_caps_blocked = mx.sym.BlockGrad(out_caps_blocked)
+ return mx.sym.Group([out_caps_blocked, loss, recon_error_stopped])
+
+
+def download_data(url, force_download=False):
+ fname = url.split("/")[-1]
+ if force_download or not os.path.exists(fname):
+ mx.test_utils.download(url, fname)
+ return fname
+
+
+def read_data(label_url, image_url):
+ with gzip.open(download_data(label_url)) as flbl:
+ magic, num = struct.unpack(">II", flbl.read(8))
+ label = np.fromstring(flbl.read(), dtype=np.int8)
+ with gzip.open(download_data(image_url), 'rb') as fimg:
+ magic, num, rows, cols = struct.unpack(">IIII", fimg.read(16))
+ image = np.fromstring(fimg.read(), dtype=np.uint8).reshape(len(label), rows, cols)
+ return label, image
+
+
+def to4d(img):
+ return img.reshape(img.shape[0], 1, 28, 28).astype(np.float32)/255
+
+
+class LossMetric(mx.metric.EvalMetric):
+ def __init__(self, batch_size, num_gpu):
+ super(LossMetric, self).__init__('LossMetric')
+ self.batch_size = batch_size
+ self.num_gpu = num_gpu
+ self.sum_metric = 0
+ self.num_inst = 0
+ self.loss = 0.0
+ self.batch_sum_metric = 0
+ self.batch_num_inst = 0
+ self.batch_loss = 0.0
+ self.recon_loss = 0.0
+ self.n_batch = 0
+
+ def update(self, labels, preds):
+ batch_sum_metric = 0
+ batch_num_inst = 0
+ for label, pred_outcaps in zip(labels[0], preds[0]):
+ label_np = int(label.asnumpy())
+ pred_label = int(np.argmax(pred_outcaps.asnumpy()))
+ batch_sum_metric += int(label_np == pred_label)
+ batch_num_inst += 1
+ batch_loss = preds[1].asnumpy()
+ recon_loss = preds[2].asnumpy()
+ self.sum_metric += batch_sum_metric
+ self.num_inst += batch_num_inst
+ self.loss += batch_loss
+ self.recon_loss += recon_loss
+ self.batch_sum_metric = batch_sum_metric
+ self.batch_num_inst = batch_num_inst
+ self.batch_loss = batch_loss
+ self.n_batch += 1
+
+ def get_name_value(self):
+ acc = float(self.sum_metric)/float(self.num_inst)
+ mean_loss = self.loss / float(self.n_batch)
+ mean_recon_loss = self.recon_loss / float(self.n_batch)
+ return acc, mean_loss, mean_recon_loss
+
+ def get_batch_log(self, n_batch):
+ print("n_batch :"+str(n_batch)+" batch_acc:" +
+ str(float(self.batch_sum_metric) / float(self.batch_num_inst)) +
+ ' batch_loss:' + str(float(self.batch_loss)/float(self.batch_num_inst)))
+ self.batch_sum_metric = 0
+ self.batch_num_inst = 0
+ self.batch_loss = 0.0
+
+ def reset(self):
+ self.sum_metric = 0
+ self.num_inst = 0
+ self.loss = 0.0
+ self.recon_loss = 0.0
+ self.n_batch = 0
+
+
+class SimpleLRScheduler(mx.lr_scheduler.LRScheduler):
+ """A simple lr schedule that simply return `dynamic_lr`. We will set `dynamic_lr`
+ dynamically based on performance on the validation set.
+ """
+
+ def __init__(self, learning_rate=0.001):
+ super(SimpleLRScheduler, self).__init__()
+ self.learning_rate = learning_rate
+
+ def __call__(self, num_update):
+ return self.learning_rate
+
+
+def do_training(num_epoch, optimizer, kvstore, learning_rate, model_prefix, decay):
+ summary_writer = SummaryWriter(args.tblog_dir)
+ lr_scheduler = SimpleLRScheduler(learning_rate)
+ optimizer_params = {'lr_scheduler': lr_scheduler}
+ module.init_params()
+ module.init_optimizer(kvstore=kvstore,
+ optimizer=optimizer,
+ optimizer_params=optimizer_params)
+ n_epoch = 0
+ while True:
+ if n_epoch >= num_epoch:
+ break
+ train_iter.reset()
+ val_iter.reset()
+ loss_metric.reset()
+ for n_batch, data_batch in enumerate(train_iter):
+ module.forward_backward(data_batch)
+ module.update()
+ module.update_metric(loss_metric, data_batch.label)
+ loss_metric.get_batch_log(n_batch)
+ train_acc, train_loss, train_recon_err = loss_metric.get_name_value()
+ loss_metric.reset()
+ for n_batch, data_batch in enumerate(val_iter):
+ module.forward(data_batch)
+ module.update_metric(loss_metric, data_batch.label)
+ loss_metric.get_batch_log(n_batch)
+ val_acc, val_loss, val_recon_err = loss_metric.get_name_value()
+
+ summary_writer.add_scalar('train_acc', train_acc, n_epoch)
+ summary_writer.add_scalar('train_loss', train_loss, n_epoch)
+ summary_writer.add_scalar('train_recon_err', train_recon_err, n_epoch)
+ summary_writer.add_scalar('val_acc', val_acc, n_epoch)
+ summary_writer.add_scalar('val_loss', val_loss, n_epoch)
+ summary_writer.add_scalar('val_recon_err', val_recon_err, n_epoch)
+
+ print('Epoch[%d] train acc: %.4f loss: %.6f recon_err: %.6f' % (n_epoch, train_acc, train_loss, train_recon_err))
+ print('Epoch[%d] val acc: %.4f loss: %.6f recon_err: %.6f' % (n_epoch, val_acc, val_loss, val_recon_err))
+ print('SAVE CHECKPOINT')
+
+ module.save_checkpoint(prefix=model_prefix, epoch=n_epoch)
+ n_epoch += 1
+ lr_scheduler.learning_rate = learning_rate * (decay ** n_epoch)
+
+
+def apply_transform(x,
+ transform_matrix,
+ fill_mode='nearest',
+ cval=0.):
+ x = np.rollaxis(x, 0, 0)
+ final_affine_matrix = transform_matrix[:2, :2]
+ final_offset = transform_matrix[:2, 2]
+ channel_images = [ndi.interpolation.affine_transform(
+ x_channel,
+ final_affine_matrix,
+ final_offset,
+ order=0,
+ mode=fill_mode,
+ cval=cval) for x_channel in x]
+ x = np.stack(channel_images, axis=0)
+ x = np.rollaxis(x, 0, 0 + 1)
+ return x
+
+
+def random_shift(x, width_shift_fraction, height_shift_fraction):
+ tx = np.random.uniform(-height_shift_fraction, height_shift_fraction) * x.shape[2]
+ ty = np.random.uniform(-width_shift_fraction, width_shift_fraction) * x.shape[1]
+ shift_matrix = np.array([[1, 0, tx],
+ [0, 1, ty],
+ [0, 0, 1]])
+ x = apply_transform(x, shift_matrix, 'nearest')
+ return x
+
+def _shuffle(data, idx):
+ """Shuffle the data."""
+ shuffle_data = []
+
+ for k, v in data:
+ shuffle_data.append((k, mx.ndarray.array(v.asnumpy()[idx], v.context)))
+
+ return shuffle_data
+
+class MNISTCustomIter(mx.io.NDArrayIter):
+
+ def reset(self):
+ # shuffle data
+ if self.is_train:
+ np.random.shuffle(self.idx)
+ self.data = _shuffle(self.data, self.idx)
+ self.label = _shuffle(self.label, self.idx)
+ if self.last_batch_handle == 'roll_over' and self.cursor > self.num_data:
+ self.cursor = -self.batch_size + (self.cursor%self.num_data)%self.batch_size
+ else:
+ self.cursor = -self.batch_size
+ def set_is_train(self, is_train):
+ self.is_train = is_train
+ def next(self):
+ if self.iter_next():
+ if self.is_train:
+ data_raw_list = self.getdata()
+ data_shifted = []
+ for data_raw in data_raw_list[0]:
+ data_shifted.append(random_shift(data_raw.asnumpy(), 0.1, 0.1))
+ return mx.io.DataBatch(data=[mx.nd.array(data_shifted)], label=self.getlabel(),
+ pad=self.getpad(), index=None)
+ else:
+ return mx.io.DataBatch(data=self.getdata(), label=self.getlabel(), \
+ pad=self.getpad(), index=None)
+
+ else:
+ raise StopIteration
+
+
+if __name__ == "__main__":
+ # Read mnist data set
+ path = 'http://yann.lecun.com/exdb/mnist/'
+ (train_lbl, train_img) = read_data(
+ path + 'train-labels-idx1-ubyte.gz', path + 'train-images-idx3-ubyte.gz')
+ (val_lbl, val_img) = read_data(
+ path + 't10k-labels-idx1-ubyte.gz', path + 't10k-images-idx3-ubyte.gz')
+ # set batch size
+ import argparse
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--batch_size', default=100, type=int)
+ parser.add_argument('--devices', default='gpu0', type=str)
+ parser.add_argument('--num_epoch', default=100, type=int)
+ parser.add_argument('--lr', default=0.001, type=float)
+ parser.add_argument('--num_routing', default=3, type=int)
+ parser.add_argument('--model_prefix', default='capsnet', type=str)
+ parser.add_argument('--decay', default=0.9, type=float)
+ parser.add_argument('--tblog_dir', default='tblog', type=str)
+ parser.add_argument('--recon_loss_weight', default=0.392, type=float)
+ args = parser.parse_args()
+ for k, v in sorted(vars(args).items()):
+ print("{0}: {1}".format(k, v))
+ contexts = re.split(r'\W+', args.devices)
+ for i, ctx in enumerate(contexts):
+ if ctx[:3] == 'gpu':
+ contexts[i] = mx.context.gpu(int(ctx[3:]))
+ else:
+ contexts[i] = mx.context.cpu()
+ num_gpu = len(contexts)
+
+ if args.batch_size % num_gpu != 0:
+ raise Exception('num_gpu should be positive divisor of batch_size')
+
+ # generate train_iter, val_iter
+ train_iter = MNISTCustomIter(data=to4d(train_img), label=train_lbl, batch_size=int(args.batch_size), shuffle=True)
+ train_iter.set_is_train(True)
+ val_iter = MNISTCustomIter(data=to4d(val_img), label=val_lbl, batch_size=int(args.batch_size),)
+ val_iter.set_is_train(False)
+ # define capsnet
+ final_net = capsnet(batch_size=int(args.batch_size/num_gpu), n_class=10, num_routing=args.num_routing, recon_loss_weight=args.recon_loss_weight)
+ # set metric
+ loss_metric = LossMetric(args.batch_size/num_gpu, 1)
+
+ # run model
+ module = mx.mod.Module(symbol=final_net, context=contexts, data_names=('data',), label_names=('softmax_label',))
+ module.bind(data_shapes=train_iter.provide_data,
+ label_shapes=val_iter.provide_label,
+ for_training=True)
+ do_training(num_epoch=args.num_epoch, optimizer='adam', kvstore='device', learning_rate=args.lr,
+ model_prefix=args.model_prefix, decay=args.decay)
diff --git a/example/cnn_visualization/README.md b/example/cnn_visualization/README.md
deleted file mode 100644
index 10b91492600e..000000000000
--- a/example/cnn_visualization/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Visualzing CNN decisions
-
-This folder contains an MXNet Gluon implementation of [Grad-CAM](https://arxiv.org/abs/1610.02391) that helps visualize CNN decisions.
-
-A tutorial on how to use this from Jupyter notebook is available [here](https://mxnet.incubator.apache.org/tutorials/vision/cnn_visualization.html).
-
-You can also do the visualization from terminal:
-```
-$ python gradcam_demo.py hummingbird.jpg
-Predicted category : hummingbird (94)
-Original Image : hummingbird_orig.jpg
-Grad-CAM : hummingbird_gradcam.jpg
-Guided Grad-CAM : hummingbird_guided_gradcam.jpg
-Saliency Map : hummingbird_saliency.jpg
-```
-
-![Output of gradcam_demo.py](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/example/cnn_visualization/hummingbird_filenames.png)
diff --git a/example/cnn_visualization/gradcam_demo.py b/example/cnn_visualization/gradcam_demo.py
deleted file mode 100644
index d9ca5ddade8e..000000000000
--- a/example/cnn_visualization/gradcam_demo.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-import mxnet as mx
-from mxnet import gluon
-
-import argparse
-import os
-import numpy as np
-import cv2
-
-import vgg
-import gradcam
-
-# Receive image path from command line
-parser = argparse.ArgumentParser(description='Grad-CAM demo')
-parser.add_argument('img_path', metavar='image_path', type=str, help='path to the image file')
-
-args = parser.parse_args()
-
-# We'll use VGG-16 for visualization
-network = vgg.vgg16(pretrained=True, ctx=mx.cpu())
-# We'll resize images to 224x244 as part of preprocessing
-image_sz = (224, 224)
-
-def preprocess(data):
- """Preprocess the image before running it through the network"""
- data = mx.image.imresize(data, image_sz[0], image_sz[1])
- data = data.astype(np.float32)
- data = data/255
- # These mean values were obtained from
- # https://mxnet.incubator.apache.org/api/python/gluon/model_zoo.html
- data = mx.image.color_normalize(data,
- mean=mx.nd.array([0.485, 0.456, 0.406]),
- std=mx.nd.array([0.229, 0.224, 0.225]))
- data = mx.nd.transpose(data, (2,0,1)) # Channel first
- return data
-
-def read_image_mxnet(path):
- with open(path, 'rb') as fp:
- img_bytes = fp.read()
- return mx.img.imdecode(img_bytes)
-
-def read_image_cv(path):
- return cv2.resize(cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2RGB), image_sz)
-
-# synset.txt contains the names of Imagenet categories
-# Load the file to memory and create a helper method to query category_index -> category name
-synset_url = "http://data.mxnet.io/models/imagenet/synset.txt"
-synset_file_name = "synset.txt"
-mx.test_utils.download(synset_url, fname=synset_file_name)
-
-synset = []
-with open('synset.txt', 'r') as f:
- synset = [l.rstrip().split(' ', 1)[1].split(',')[0] for l in f]
-
-def get_class_name(cls_id):
- return "%s (%d)" % (synset[cls_id], cls_id)
-
-def run_inference(net, data):
- """Run the input image through the network and return the predicted category as integer"""
- out = net(data)
- return out.argmax(axis=1).asnumpy()[0].astype(int)
-
-def visualize(net, img_path, conv_layer_name):
- """Create Grad-CAM visualizations using the network 'net' and the image at 'img_path'
- conv_layer_name is the name of the top most layer of the feature extractor"""
- image = read_image_mxnet(img_path)
- image = preprocess(image)
- image = image.expand_dims(axis=0)
-
- pred_str = get_class_name(run_inference(net, image))
-
- orig_img = read_image_cv(img_path)
- vizs = gradcam.visualize(net, image, orig_img, conv_layer_name)
- return (pred_str, (orig_img, *vizs))
-
-# Create Grad-CAM visualization for the user provided image
-last_conv_layer_name = 'vgg0_conv2d12'
-cat, vizs = visualize(network, args.img_path, last_conv_layer_name)
-
-print("{0:20}: {1:80}".format("Predicted category", cat))
-
-# Write the visualiations into file
-img_name = os.path.split(args.img_path)[1].split('.')[0]
-suffixes = ['orig', 'gradcam', 'guided_gradcam', 'saliency']
-image_desc = ['Original Image', 'Grad-CAM', 'Guided Grad-CAM', 'Saliency Map']
-
-for i, img in enumerate(vizs):
- img = img.astype(np.float32)
- if len(img.shape) == 3:
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
- out_file_name = "%s_%s.jpg" % (img_name, suffixes[i])
- cv2.imwrite(out_file_name, img)
- print("{0:20}: {1:80}".format(image_desc[i], out_file_name))
-
diff --git a/example/cnn_visualization/vgg.py b/example/cnn_visualization/vgg.py
deleted file mode 100644
index b6215a334e3b..000000000000
--- a/example/cnn_visualization/vgg.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-import mxnet as mx
-from mxnet import gluon
-
-import os
-from mxnet.gluon.model_zoo import model_store
-
-from mxnet.initializer import Xavier
-from mxnet.gluon.nn import MaxPool2D, Flatten, Dense, Dropout, BatchNorm
-from gradcam import Activation, Conv2D
-
-class VGG(mx.gluon.HybridBlock):
- def __init__(self, layers, filters, classes=1000, batch_norm=False, **kwargs):
- super(VGG, self).__init__(**kwargs)
- assert len(layers) == len(filters)
- with self.name_scope():
- self.features = self._make_features(layers, filters, batch_norm)
- self.features.add(Dense(4096, activation='relu',
- weight_initializer='normal',
- bias_initializer='zeros'))
- self.features.add(Dropout(rate=0.5))
- self.features.add(Dense(4096, activation='relu',
- weight_initializer='normal',
- bias_initializer='zeros'))
- self.features.add(Dropout(rate=0.5))
- self.output = Dense(classes,
- weight_initializer='normal',
- bias_initializer='zeros')
-
- def _make_features(self, layers, filters, batch_norm):
- featurizer = mx.gluon.nn.HybridSequential(prefix='')
- for i, num in enumerate(layers):
- for _ in range(num):
- featurizer.add(Conv2D(filters[i], kernel_size=3, padding=1,
- weight_initializer=Xavier(rnd_type='gaussian',
- factor_type='out',
- magnitude=2),
- bias_initializer='zeros'))
- if batch_norm:
- featurizer.add(BatchNorm())
- featurizer.add(Activation('relu'))
- featurizer.add(MaxPool2D(strides=2))
- return featurizer
-
- def hybrid_forward(self, F, x):
- x = self.features(x)
- x = self.output(x)
- return x
-
-vgg_spec = {11: ([1, 1, 2, 2, 2], [64, 128, 256, 512, 512]),
- 13: ([2, 2, 2, 2, 2], [64, 128, 256, 512, 512]),
- 16: ([2, 2, 3, 3, 3], [64, 128, 256, 512, 512]),
- 19: ([2, 2, 4, 4, 4], [64, 128, 256, 512, 512])}
-
-def get_vgg(num_layers, pretrained=False, ctx=mx.cpu(),
- root=os.path.join('~', '.mxnet', 'models'), **kwargs):
- layers, filters = vgg_spec[num_layers]
- net = VGG(layers, filters, **kwargs)
- if pretrained:
- from mxnet.gluon.model_zoo.model_store import get_model_file
- batch_norm_suffix = '_bn' if kwargs.get('batch_norm') else ''
- net.load_params(get_model_file('vgg%d%s'%(num_layers, batch_norm_suffix),
- root=root), ctx=ctx)
- return net
-
-def vgg16(**kwargs):
- return get_vgg(16, **kwargs)
-
diff --git a/example/deep-embedded-clustering/README.md b/example/deep-embedded-clustering/README.md
index 90803d2ed12d..3972f90bda4a 100644
--- a/example/deep-embedded-clustering/README.md
+++ b/example/deep-embedded-clustering/README.md
@@ -1,9 +1,18 @@
# DEC Implementation
This is based on the paper `Unsupervised deep embedding for clustering analysis` by Junyuan Xie, Ross Girshick, and Ali Farhadi
+Abstract:
+
+Clustering is central to many data-driven application domains and has been studied extensively in terms of distance functions and grouping algorithms. Relatively little work has focused on learning representations for clustering. In this paper, we propose Deep Embedded Clustering (DEC), a method that simultaneously learns feature representations and cluster assignments using deep neural networks. DEC learns a mapping from the data space to a lower-dimensional feature space in which it iteratively optimizes a clustering objective. Our experimental evaluations on image and text corpora show significant improvement over state-of-the-art methods.
+
+
## Prerequisite
- Install Scikit-learn: `python -m pip install --user sklearn`
- Install SciPy: `python -m pip install --user scipy`
+## Data
+
+The script is using MNIST dataset.
+
## Usage
-run `python dec.py`
\ No newline at end of file
+run `python dec.py`
diff --git a/example/deep-embedded-clustering/data.py b/example/deep-embedded-clustering/data.py
index 9fd472e6a8b1..3649990dbbb9 100644
--- a/example/deep-embedded-clustering/data.py
+++ b/example/deep-embedded-clustering/data.py
@@ -19,20 +19,21 @@
from __future__ import print_function
import os
+
+import mxnet as mx
import numpy as np
-from sklearn.datasets import fetch_mldata
def get_mnist():
""" Gets MNIST dataset """
np.random.seed(1234) # set seed for deterministic ordering
- data_path = os.path.dirname(os.path.abspath(os.path.expanduser(__file__)))
- data_path = os.path.join(data_path, '../../data')
- mnist = fetch_mldata('MNIST original', data_home=data_path)
- p = np.random.permutation(mnist.data.shape[0])
- X = mnist.data[p].astype(np.float32)*0.02
- Y = mnist.target[p]
+ mnist_data = mx.test_utils.get_mnist()
+ X = np.concatenate([mnist_data['train_data'], mnist_data['test_data']])
+ Y = np.concatenate([mnist_data['train_label'], mnist_data['test_label']])
+ p = np.random.permutation(X.shape[0])
+ X = X[p].reshape((X.shape[0], -1)).astype(np.float32)*5
+ Y = Y[p]
return X, Y
diff --git a/example/deep-embedded-clustering/dec.py b/example/deep-embedded-clustering/dec.py
index 44e582d9f94d..d7594703cfc3 100644
--- a/example/deep-embedded-clustering/dec.py
+++ b/example/deep-embedded-clustering/dec.py
@@ -19,9 +19,6 @@
from __future__ import print_function
import sys
import os
-# code to automatically download dataset
-curr_path = os.path.dirname(os.path.abspath(os.path.expanduser(__file__)))
-sys.path = [os.path.join(curr_path, "../autoencoder")] + sys.path
import mxnet as mx
import numpy as np
import data
@@ -33,14 +30,14 @@
import logging
def cluster_acc(Y_pred, Y):
- from sklearn.utils.linear_assignment_ import linear_assignment
- assert Y_pred.size == Y.size
- D = max(Y_pred.max(), Y.max())+1
- w = np.zeros((D,D), dtype=np.int64)
- for i in range(Y_pred.size):
- w[Y_pred[i], int(Y[i])] += 1
- ind = linear_assignment(w.max() - w)
- return sum([w[i,j] for i,j in ind])*1.0/Y_pred.size, w
+ from sklearn.utils.linear_assignment_ import linear_assignment
+ assert Y_pred.size == Y.size
+ D = max(Y_pred.max(), Y.max())+1
+ w = np.zeros((D,D), dtype=np.int64)
+ for i in range(Y_pred.size):
+ w[Y_pred[i], int(Y[i])] += 1
+ ind = linear_assignment(w.max() - w)
+ return sum([w[i,j] for i,j in ind])*1.0/Y_pred.size, w
class DECModel(model.MXModel):
class DECLoss(mx.operator.NumpyOp):
@@ -87,9 +84,9 @@ def setup(self, X, num_centers, alpha, save_to='dec_model'):
ae_model = AutoEncoderModel(self.xpu, [X.shape[1],500,500,2000,10], pt_dropout=0.2)
if not os.path.exists(save_to+'_pt.arg'):
ae_model.layerwise_pretrain(X_train, 256, 50000, 'sgd', l_rate=0.1, decay=0.0,
- lr_scheduler=mx.misc.FactorScheduler(20000,0.1))
+ lr_scheduler=mx.lr_scheduler.FactorScheduler(20000,0.1))
ae_model.finetune(X_train, 256, 100000, 'sgd', l_rate=0.1, decay=0.0,
- lr_scheduler=mx.misc.FactorScheduler(20000,0.1))
+ lr_scheduler=mx.lr_scheduler.FactorScheduler(20000,0.1))
ae_model.save(save_to+'_pt.arg')
logging.log(logging.INFO, "Autoencoder Training error: %f"%ae_model.eval(X_train))
logging.log(logging.INFO, "Autoencoder Validation error: %f"%ae_model.eval(X_val))
@@ -160,6 +157,8 @@ def refresh(i):
def mnist_exp(xpu):
X, Y = data.get_mnist()
+ if not os.path.isdir('data'):
+ os.makedirs('data')
dec_model = DECModel(xpu, X, 10, 1.0, 'data/mnist')
acc = []
for i in [10*(2**j) for j in range(9)]:
diff --git a/example/gluon/actor_critic.py b/example/gluon/actor_critic/actor_critic.py
similarity index 100%
rename from example/gluon/actor_critic.py
rename to example/gluon/actor_critic/actor_critic.py
diff --git a/example/gluon/dc_gan/README.md b/example/gluon/dc_gan/README.md
new file mode 100644
index 000000000000..5aacd78a3ed5
--- /dev/null
+++ b/example/gluon/dc_gan/README.md
@@ -0,0 +1,52 @@
+# DCGAN in MXNet
+
+[Deep Convolutional Generative Adversarial Networks(DCGAN)](https://arxiv.org/abs/1511.06434) implementation with Apache MXNet GLUON.
+This implementation uses [inception_score](https://github.com/openai/improved-gan) to evaluate the model.
+
+You can use this reference implementation on the MNIST and CIFAR-10 datasets.
+
+
+#### Generated image output examples from the CIFAR-10 dataset
+![Generated image output examples from the CIFAR-10 dataset](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/example/gluon/DCGAN/fake_img_iter_13900.png)
+
+#### Generated image output examples from the MNIST dataset
+![Generated image output examples from the MNIST dataset](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/example/gluon/DCGAN/fake_img_iter_21700.png)
+
+#### inception_score in cpu and gpu (the real image`s score is around 3.3)
+CPU & GPU
+
+![inception score with CPU](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/example/gluon/DCGAN/inception_score_cifar10_cpu.png)
+![inception score with GPU](https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/example/gluon/DCGAN/inception_score_cifar10.png)
+
+## Quick start
+Use the following code to see the configurations you can set:
+```bash
+python dcgan.py -h
+```
+
+
+ optional arguments:
+ -h, --help show this help message and exit
+ --dataset DATASET dataset to use. options are cifar10 and mnist.
+ --batch-size BATCH_SIZE input batch size, default is 64
+ --nz NZ size of the latent z vector, default is 100
+ --ngf NGF the channel of each generator filter layer, default is 64.
+ --ndf NDF the channel of each descriminator filter layer, default is 64.
+ --nepoch NEPOCH number of epochs to train for, default is 25.
+ --niter NITER save generated images and inception_score per niter iters, default is 100.
+ --lr LR learning rate, default=0.0002
+ --beta1 BETA1 beta1 for adam. default=0.5
+ --cuda enables cuda
+ --netG NETG path to netG (to continue training)
+ --netD NETD path to netD (to continue training)
+ --outf OUTF folder to output images and model checkpoints
+ --check-point CHECK_POINT
+ save results at each epoch or not
+ --inception_score INCEPTION_SCORE
+ To record the inception_score, default is True.
+
+
+Use the following Python script to train a DCGAN model with default configurations using the CIFAR-10 dataset and record metrics with `inception_score`:
+```bash
+python dcgan.py
+```
diff --git a/example/utils/__init__.py b/example/gluon/dc_gan/__init__.py
similarity index 100%
rename from example/utils/__init__.py
rename to example/gluon/dc_gan/__init__.py
diff --git a/example/gluon/dc_gan/dcgan.py b/example/gluon/dc_gan/dcgan.py
new file mode 100644
index 000000000000..970c35d54df4
--- /dev/null
+++ b/example/gluon/dc_gan/dcgan.py
@@ -0,0 +1,340 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import matplotlib as mpl
+mpl.use('Agg')
+from matplotlib import pyplot as plt
+
+import argparse
+import mxnet as mx
+from mxnet import gluon
+from mxnet.gluon import nn
+from mxnet import autograd
+import numpy as np
+import logging
+from datetime import datetime
+import os
+import time
+
+from inception_score import get_inception_score
+
+
+def fill_buf(buf, i, img, shape):
+ """
+ Reposition the images generated by the generator so that it can be saved as picture matrix.
+ :param buf: the images metric
+ :param i: index of each image
+ :param img: images generated by generator once
+ :param shape: each image`s shape
+ :return: Adjust images for output
+ """
+ n = buf.shape[0]//shape[1]
+ m = buf.shape[1]//shape[0]
+
+ sx = (i%m)*shape[0]
+ sy = (i//m)*shape[1]
+ buf[sy:sy+shape[1], sx:sx+shape[0], :] = img
+ return None
+
+
+def visual(title, X, name):
+ """
+ Image visualization and preservation
+ :param title: title
+ :param X: images to visualized
+ :param name: saved picture`s name
+ :return:
+ """
+ assert len(X.shape) == 4
+ X = X.transpose((0, 2, 3, 1))
+ X = np.clip((X - np.min(X))*(255.0/(np.max(X) - np.min(X))), 0, 255).astype(np.uint8)
+ n = np.ceil(np.sqrt(X.shape[0]))
+ buff = np.zeros((int(n*X.shape[1]), int(n*X.shape[2]), int(X.shape[3])), dtype=np.uint8)
+ for i, img in enumerate(X):
+ fill_buf(buff, i, img, X.shape[1:3])
+ buff = buff[:, :, ::-1]
+ plt.imshow(buff)
+ plt.title(title)
+ plt.savefig(name)
+
+
+parser = argparse.ArgumentParser()
+parser = argparse.ArgumentParser(description='Train a DCgan model for image generation '
+ 'and then use inception_score to metric the result.')
+parser.add_argument('--dataset', type=str, default='cifar10', help='dataset to use. options are cifar10 and mnist.')
+parser.add_argument('--batch-size', type=int, default=64, help='input batch size, default is 64')
+parser.add_argument('--nz', type=int, default=100, help='size of the latent z vector, default is 100')
+parser.add_argument('--ngf', type=int, default=64, help='the channel of each generator filter layer, default is 64.')
+parser.add_argument('--ndf', type=int, default=64, help='the channel of each descriminator filter layer, default is 64.')
+parser.add_argument('--nepoch', type=int, default=25, help='number of epochs to train for, default is 25.')
+parser.add_argument('--niter', type=int, default=10, help='save generated images and inception_score per niter iters, default is 100.')
+parser.add_argument('--lr', type=float, default=0.0002, help='learning rate, default=0.0002')
+parser.add_argument('--beta1', type=float, default=0.5, help='beta1 for adam. default=0.5')
+parser.add_argument('--cuda', action='store_true', help='enables cuda')
+parser.add_argument('--netG', default='', help="path to netG (to continue training)")
+parser.add_argument('--netD', default='', help="path to netD (to continue training)")
+parser.add_argument('--outf', default='./results', help='folder to output images and model checkpoints')
+parser.add_argument('--check-point', default=True, help="save results at each epoch or not")
+parser.add_argument('--inception_score', type=bool, default=True, help='To record the inception_score, default is True.')
+
+opt = parser.parse_args()
+print(opt)
+
+logging.basicConfig(level=logging.DEBUG)
+
+nz = int(opt.nz)
+ngf = int(opt.ngf)
+ndf = int(opt.ndf)
+niter = opt.niter
+nc = 3
+if opt.cuda:
+ ctx = mx.gpu(0)
+else:
+ ctx = mx.cpu()
+batch_size = opt.batch_size
+check_point = bool(opt.check_point)
+outf = opt.outf
+dataset = opt.dataset
+
+if not os.path.exists(outf):
+ os.makedirs(outf)
+
+
+def transformer(data, label):
+ # resize to 64x64
+ data = mx.image.imresize(data, 64, 64)
+ # transpose from (64, 64, 3) to (3, 64, 64)
+ data = mx.nd.transpose(data, (2, 0, 1))
+ # normalize to [-1, 1]
+ data = data.astype(np.float32)/128 - 1
+ # if image is greyscale, repeat 3 times to get RGB image.
+ if data.shape[0] == 1:
+ data = mx.nd.tile(data, (3, 1, 1))
+ return data, label
+
+
+# get dataset with the batch_size num each time
+def get_dataset(dataset):
+ # mnist
+ if dataset == "mnist":
+ train_data = gluon.data.DataLoader(
+ gluon.data.vision.MNIST('./data', train=True, transform=transformer),
+ batch_size, shuffle=True, last_batch='discard')
+
+ val_data = gluon.data.DataLoader(
+ gluon.data.vision.MNIST('./data', train=False, transform=transformer),
+ batch_size, shuffle=False)
+ # cifar10
+ elif dataset == "cifar10":
+ train_data = gluon.data.DataLoader(
+ gluon.data.vision.CIFAR10('./data', train=True, transform=transformer),
+ batch_size, shuffle=True, last_batch='discard')
+
+ val_data = gluon.data.DataLoader(
+ gluon.data.vision.CIFAR10('./data', train=False, transform=transformer),
+ batch_size, shuffle=False)
+
+ return train_data, val_data
+
+
+def get_netG():
+ # build the generator
+ netG = nn.Sequential()
+ with netG.name_scope():
+ # input is Z, going into a convolution
+ netG.add(nn.Conv2DTranspose(ngf * 8, 4, 1, 0, use_bias=False))
+ netG.add(nn.BatchNorm())
+ netG.add(nn.Activation('relu'))
+ # state size. (ngf*8) x 4 x 4
+ netG.add(nn.Conv2DTranspose(ngf * 4, 4, 2, 1, use_bias=False))
+ netG.add(nn.BatchNorm())
+ netG.add(nn.Activation('relu'))
+ # state size. (ngf*4) x 8 x 8
+ netG.add(nn.Conv2DTranspose(ngf * 2, 4, 2, 1, use_bias=False))
+ netG.add(nn.BatchNorm())
+ netG.add(nn.Activation('relu'))
+ # state size. (ngf*2) x 16 x 16
+ netG.add(nn.Conv2DTranspose(ngf, 4, 2, 1, use_bias=False))
+ netG.add(nn.BatchNorm())
+ netG.add(nn.Activation('relu'))
+ # state size. (ngf) x 32 x 32
+ netG.add(nn.Conv2DTranspose(nc, 4, 2, 1, use_bias=False))
+ netG.add(nn.Activation('tanh'))
+ # state size. (nc) x 64 x 64
+
+ return netG
+
+
+def get_netD():
+ # build the discriminator
+ netD = nn.Sequential()
+ with netD.name_scope():
+ # input is (nc) x 64 x 64
+ netD.add(nn.Conv2D(ndf, 4, 2, 1, use_bias=False))
+ netD.add(nn.LeakyReLU(0.2))
+ # state size. (ndf) x 32 x 32
+ netD.add(nn.Conv2D(ndf * 2, 4, 2, 1, use_bias=False))
+ netD.add(nn.BatchNorm())
+ netD.add(nn.LeakyReLU(0.2))
+ # state size. (ndf*2) x 16 x 16
+ netD.add(nn.Conv2D(ndf * 4, 4, 2, 1, use_bias=False))
+ netD.add(nn.BatchNorm())
+ netD.add(nn.LeakyReLU(0.2))
+ # state size. (ndf*4) x 8 x 8
+ netD.add(nn.Conv2D(ndf * 8, 4, 2, 1, use_bias=False))
+ netD.add(nn.BatchNorm())
+ netD.add(nn.LeakyReLU(0.2))
+ # state size. (ndf*8) x 4 x 4
+ netD.add(nn.Conv2D(2, 4, 1, 0, use_bias=False))
+ # state size. 2 x 1 x 1
+
+ return netD
+
+
+def get_configurations(netG, netD):
+ # loss
+ loss = gluon.loss.SoftmaxCrossEntropyLoss()
+
+ # initialize the generator and the discriminator
+ netG.initialize(mx.init.Normal(0.02), ctx=ctx)
+ netD.initialize(mx.init.Normal(0.02), ctx=ctx)
+
+ # trainer for the generator and the discriminator
+ trainerG = gluon.Trainer(netG.collect_params(), 'adam', {'learning_rate': opt.lr, 'beta1': opt.beta1})
+ trainerD = gluon.Trainer(netD.collect_params(), 'adam', {'learning_rate': opt.lr, 'beta1': opt.beta1})
+
+ return loss, trainerG, trainerD
+
+
+def ins_save(inception_score):
+ # draw the inception_score curve
+ length = len(inception_score)
+ x = np.arange(0, length)
+ plt.figure(figsize=(8.0, 6.0))
+ plt.plot(x, inception_score)
+ plt.xlabel("iter/100")
+ plt.ylabel("inception_score")
+ plt.savefig("inception_score.png")
+
+
+# main function
+def main():
+ print("|------- new changes!!!!!!!!!")
+ # to get the dataset and net configuration
+ train_data, val_data = get_dataset(dataset)
+ netG = get_netG()
+ netD = get_netD()
+ loss, trainerG, trainerD = get_configurations(netG, netD)
+
+ # set labels
+ real_label = mx.nd.ones((opt.batch_size,), ctx=ctx)
+ fake_label = mx.nd.zeros((opt.batch_size,), ctx=ctx)
+
+ metric = mx.metric.Accuracy()
+ print('Training... ')
+ stamp = datetime.now().strftime('%Y_%m_%d-%H_%M')
+
+ iter = 0
+
+ # to metric the network
+ loss_d = []
+ loss_g = []
+ inception_score = []
+
+ for epoch in range(opt.nepoch):
+ tic = time.time()
+ btic = time.time()
+ for data, _ in train_data:
+ ############################
+ # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
+ ###########################
+ # train with real_t
+ data = data.as_in_context(ctx)
+ noise = mx.nd.random.normal(0, 1, shape=(opt.batch_size, nz, 1, 1), ctx=ctx)
+
+ with autograd.record():
+ output = netD(data)
+ # reshape output from (opt.batch_size, 2, 1, 1) to (opt.batch_size, 2)
+ output = output.reshape((opt.batch_size, 2))
+ errD_real = loss(output, real_label)
+
+ metric.update([real_label, ], [output, ])
+
+ with autograd.record():
+ fake = netG(noise)
+ output = netD(fake.detach())
+ output = output.reshape((opt.batch_size, 2))
+ errD_fake = loss(output, fake_label)
+ errD = errD_real + errD_fake
+
+ errD.backward()
+ metric.update([fake_label,], [output,])
+
+ trainerD.step(opt.batch_size)
+
+ ############################
+ # (2) Update G network: maximize log(D(G(z)))
+ ###########################
+ with autograd.record():
+ output = netD(fake)
+ output = output.reshape((-1, 2))
+ errG = loss(output, real_label)
+
+ errG.backward()
+
+ trainerG.step(opt.batch_size)
+
+ name, acc = metric.get()
+ logging.info('discriminator loss = %f, generator loss = %f, binary training acc = %f at iter %d epoch %d'
+ % (mx.nd.mean(errD).asscalar(), mx.nd.mean(errG).asscalar(), acc, iter, epoch))
+ if iter % niter == 0:
+ visual('gout', fake.asnumpy(), name=os.path.join(outf, 'fake_img_iter_%d.png' % iter))
+ visual('data', data.asnumpy(), name=os.path.join(outf, 'real_img_iter_%d.png' % iter))
+ # record the metric data
+ loss_d.append(errD)
+ loss_g.append(errG)
+ if opt.inception_score:
+ score, _ = get_inception_score(fake)
+ inception_score.append(score)
+
+ iter = iter + 1
+ btic = time.time()
+
+ name, acc = metric.get()
+ metric.reset()
+ logging.info('\nbinary training acc at epoch %d: %s=%f' % (epoch, name, acc))
+ logging.info('time: %f' % (time.time() - tic))
+
+ # save check_point
+ if check_point:
+ netG.save_parameters(os.path.join(outf,'generator_epoch_%d.params' %epoch))
+ netD.save_parameters(os.path.join(outf,'discriminator_epoch_%d.params' % epoch))
+
+ # save parameter
+ netG.save_parameters(os.path.join(outf, 'generator.params'))
+ netD.save_parameters(os.path.join(outf, 'discriminator.params'))
+
+ # visualization the inception_score as a picture
+ if opt.inception_score:
+ ins_save(inception_score)
+
+
+if __name__ == '__main__':
+ if opt.inception_score:
+ print("Use inception_score to metric this DCgan model, the reusult is save as a picture named \"inception_score.png\"!")
+ main()
+
diff --git a/example/gluon/dc_gan/inception_score.py b/example/gluon/dc_gan/inception_score.py
new file mode 100644
index 000000000000..e23513f5055e
--- /dev/null
+++ b/example/gluon/dc_gan/inception_score.py
@@ -0,0 +1,110 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from mxnet.gluon.model_zoo import vision as models
+import mxnet as mx
+from mxnet import nd
+import numpy as np
+import math
+import sys
+
+import cv2
+
+
+inception_model = None
+
+
+def get_inception_score(images, splits=10):
+ """
+ Inception_score function.
+ The images will be divided into 'splits' parts, and calculate each inception_score separately,
+ then return the mean and std of inception_scores of these parts.
+ :param images: Images(num x c x w x h) that needs to calculate inception_score.
+ :param splits:
+ :return: mean and std of inception_score
+ """
+ assert (images.shape[1] == 3)
+
+ # load inception model
+ if inception_model is None:
+ _init_inception()
+
+ # resize images to adapt inception model(inceptionV3)
+ if images.shape[2] != 299:
+ images = resize(images, 299, 299)
+
+ preds = []
+ bs = 4
+ n_batches = int(math.ceil(float(images.shape[0])/float(bs)))
+
+ # to get the predictions/picture of inception model
+ for i in range(n_batches):
+ sys.stdout.write(".")
+ sys.stdout.flush()
+ inps = images[(i * bs):min((i + 1) * bs, len(images))]
+ # inps size. bs x 3 x 299 x 299
+ pred = nd.softmax(inception_model(inps))
+ # pred size. bs x 1000
+ preds.append(pred.asnumpy())
+
+ # list to array
+ preds = np.concatenate(preds, 0)
+ scores = []
+
+ # to calculate the inception_score each split.
+ for i in range(splits):
+ # extract per split image pred
+ part = preds[(i * preds.shape[0] // splits):((i + 1) * preds.shape[0] // splits), :]
+ kl = part * (np.log(part) - np.log(np.expand_dims(np.mean(part, 0), 0)))
+ kl = np.mean(np.sum(kl, 1))
+ scores.append(np.exp(kl))
+
+ return np.mean(scores), np.std(scores)
+
+
+def _init_inception():
+ global inception_model
+ inception_model = models.inception_v3(pretrained=True)
+ print("success import inception model, and the model is inception_v3!")
+
+
+def resize(images, w, h):
+ nums = images.shape[0]
+ res = nd.random.uniform(0, 255, (nums, 3, w, h))
+ for i in range(nums):
+ img = images[i, :, :, :]
+ img = mx.nd.transpose(img, (1, 2, 0))
+ # Replace 'mx.image.imresize()' with 'cv2.resize()' because : Operator _cvimresize is not implemented for GPU.
+ # img = mx.image.imresize(img, w, h)
+ img = cv2.resize(img.asnumpy(), (299, 299))
+ img = nd.array(img)
+ img = mx.nd.transpose(img, (2, 0, 1))
+ res[i, :, :, :] = img
+
+ return res
+
+
+if __name__ == '__main__':
+ if inception_model is None:
+ _init_inception()
+ # dummy data
+ images = nd.random.uniform(0, 255, (64, 3, 64, 64))
+ print(images.shape[0])
+ # resize(images,299,299)
+
+ score = get_inception_score(images)
+ print(score)
diff --git a/example/gluon/dcgan.py b/example/gluon/dcgan.py
deleted file mode 100644
index 8ac9c522cf59..000000000000
--- a/example/gluon/dcgan.py
+++ /dev/null
@@ -1,236 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-import matplotlib as mpl
-mpl.use('Agg')
-from matplotlib import pyplot as plt
-
-import argparse
-import mxnet as mx
-from mxnet import gluon
-from mxnet.gluon import nn
-from mxnet import autograd
-import numpy as np
-import logging
-from datetime import datetime
-import os
-import time
-
-def fill_buf(buf, i, img, shape):
- n = buf.shape[0]//shape[1]
- m = buf.shape[1]//shape[0]
-
- sx = (i%m)*shape[0]
- sy = (i//m)*shape[1]
- buf[sy:sy+shape[1], sx:sx+shape[0], :] = img
- return None
-
-def visual(title, X, name):
- assert len(X.shape) == 4
- X = X.transpose((0, 2, 3, 1))
- X = np.clip((X - np.min(X))*(255.0/(np.max(X) - np.min(X))), 0, 255).astype(np.uint8)
- n = np.ceil(np.sqrt(X.shape[0]))
- buff = np.zeros((int(n*X.shape[1]), int(n*X.shape[2]), int(X.shape[3])), dtype=np.uint8)
- for i, img in enumerate(X):
- fill_buf(buff, i, img, X.shape[1:3])
- buff = buff[:,:,::-1]
- plt.imshow(buff)
- plt.title(title)
- plt.savefig(name)
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument('--dataset', type=str, default='cifar10', help='dataset to use. options are cifar10 and imagenet.')
-parser.add_argument('--batch-size', type=int, default=64, help='input batch size')
-parser.add_argument('--nz', type=int, default=100, help='size of the latent z vector')
-parser.add_argument('--ngf', type=int, default=64)
-parser.add_argument('--ndf', type=int, default=64)
-parser.add_argument('--nepoch', type=int, default=25, help='number of epochs to train for')
-parser.add_argument('--lr', type=float, default=0.0002, help='learning rate, default=0.0002')
-parser.add_argument('--beta1', type=float, default=0.5, help='beta1 for adam. default=0.5')
-parser.add_argument('--cuda', action='store_true', help='enables cuda')
-parser.add_argument('--ngpu', type=int, default=1, help='number of GPUs to use')
-parser.add_argument('--netG', default='', help="path to netG (to continue training)")
-parser.add_argument('--netD', default='', help="path to netD (to continue training)")
-parser.add_argument('--outf', default='./results', help='folder to output images and model checkpoints')
-parser.add_argument('--check-point', default=True, help="save results at each epoch or not")
-
-opt = parser.parse_args()
-print(opt)
-
-logging.basicConfig(level=logging.DEBUG)
-ngpu = int(opt.ngpu)
-nz = int(opt.nz)
-ngf = int(opt.ngf)
-ndf = int(opt.ndf)
-nc = 3
-if opt.cuda:
- ctx = mx.gpu(0)
-else:
- ctx = mx.cpu()
-check_point = bool(opt.check_point)
-outf = opt.outf
-
-if not os.path.exists(outf):
- os.makedirs(outf)
-
-
-def transformer(data, label):
- # resize to 64x64
- data = mx.image.imresize(data, 64, 64)
- # transpose from (64, 64, 3) to (3, 64, 64)
- data = mx.nd.transpose(data, (2,0,1))
- # normalize to [-1, 1]
- data = data.astype(np.float32)/128 - 1
- # if image is greyscale, repeat 3 times to get RGB image.
- if data.shape[0] == 1:
- data = mx.nd.tile(data, (3, 1, 1))
- return data, label
-
-train_data = gluon.data.DataLoader(
- gluon.data.vision.MNIST('./data', train=True, transform=transformer),
- batch_size=opt.batch_size, shuffle=True, last_batch='discard')
-
-val_data = gluon.data.DataLoader(
- gluon.data.vision.MNIST('./data', train=False, transform=transformer),
- batch_size=opt.batch_size, shuffle=False)
-
-
-# build the generator
-netG = nn.Sequential()
-with netG.name_scope():
- # input is Z, going into a convolution
- netG.add(nn.Conv2DTranspose(ngf * 8, 4, 1, 0, use_bias=False))
- netG.add(nn.BatchNorm())
- netG.add(nn.Activation('relu'))
- # state size. (ngf*8) x 4 x 4
- netG.add(nn.Conv2DTranspose(ngf * 4, 4, 2, 1, use_bias=False))
- netG.add(nn.BatchNorm())
- netG.add(nn.Activation('relu'))
- # state size. (ngf*8) x 8 x 8
- netG.add(nn.Conv2DTranspose(ngf * 2, 4, 2, 1, use_bias=False))
- netG.add(nn.BatchNorm())
- netG.add(nn.Activation('relu'))
- # state size. (ngf*8) x 16 x 16
- netG.add(nn.Conv2DTranspose(ngf, 4, 2, 1, use_bias=False))
- netG.add(nn.BatchNorm())
- netG.add(nn.Activation('relu'))
- # state size. (ngf*8) x 32 x 32
- netG.add(nn.Conv2DTranspose(nc, 4, 2, 1, use_bias=False))
- netG.add(nn.Activation('tanh'))
- # state size. (nc) x 64 x 64
-
-# build the discriminator
-netD = nn.Sequential()
-with netD.name_scope():
- # input is (nc) x 64 x 64
- netD.add(nn.Conv2D(ndf, 4, 2, 1, use_bias=False))
- netD.add(nn.LeakyReLU(0.2))
- # state size. (ndf) x 32 x 32
- netD.add(nn.Conv2D(ndf * 2, 4, 2, 1, use_bias=False))
- netD.add(nn.BatchNorm())
- netD.add(nn.LeakyReLU(0.2))
- # state size. (ndf) x 16 x 16
- netD.add(nn.Conv2D(ndf * 4, 4, 2, 1, use_bias=False))
- netD.add(nn.BatchNorm())
- netD.add(nn.LeakyReLU(0.2))
- # state size. (ndf) x 8 x 8
- netD.add(nn.Conv2D(ndf * 8, 4, 2, 1, use_bias=False))
- netD.add(nn.BatchNorm())
- netD.add(nn.LeakyReLU(0.2))
- # state size. (ndf) x 4 x 4
- netD.add(nn.Conv2D(2, 4, 1, 0, use_bias=False))
-
-# loss
-loss = gluon.loss.SoftmaxCrossEntropyLoss()
-
-# initialize the generator and the discriminator
-netG.initialize(mx.init.Normal(0.02), ctx=ctx)
-netD.initialize(mx.init.Normal(0.02), ctx=ctx)
-
-# trainer for the generator and the discriminator
-trainerG = gluon.Trainer(netG.collect_params(), 'adam', {'learning_rate': opt.lr, 'beta1': opt.beta1})
-trainerD = gluon.Trainer(netD.collect_params(), 'adam', {'learning_rate': opt.lr, 'beta1': opt.beta1})
-
-# ============printing==============
-real_label = mx.nd.ones((opt.batch_size,), ctx=ctx)
-fake_label = mx.nd.zeros((opt.batch_size,), ctx=ctx)
-
-metric = mx.metric.Accuracy()
-print('Training... ')
-stamp = datetime.now().strftime('%Y_%m_%d-%H_%M')
-
-iter = 0
-for epoch in range(opt.nepoch):
- tic = time.time()
- btic = time.time()
- for data, _ in train_data:
- ############################
- # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
- ###########################
- # train with real_t
- data = data.as_in_context(ctx)
- noise = mx.nd.random.normal(0, 1, shape=(opt.batch_size, nz, 1, 1), ctx=ctx)
-
- with autograd.record():
- output = netD(data)
- output = output.reshape((opt.batch_size, 2))
- errD_real = loss(output, real_label)
- metric.update([real_label,], [output,])
-
- fake = netG(noise)
- output = netD(fake.detach())
- output = output.reshape((opt.batch_size, 2))
- errD_fake = loss(output, fake_label)
- errD = errD_real + errD_fake
- errD.backward()
- metric.update([fake_label,], [output,])
-
- trainerD.step(opt.batch_size)
-
- ############################
- # (2) Update G network: maximize log(D(G(z)))
- ###########################
- with autograd.record():
- output = netD(fake)
- output = output.reshape((-1, 2))
- errG = loss(output, real_label)
- errG.backward()
-
- trainerG.step(opt.batch_size)
-
- name, acc = metric.get()
- # logging.info('speed: {} samples/s'.format(opt.batch_size / (time.time() - btic)))
- logging.info('discriminator loss = %f, generator loss = %f, binary training acc = %f at iter %d epoch %d' %(mx.nd.mean(errD).asscalar(), mx.nd.mean(errG).asscalar(), acc, iter, epoch))
- if iter % 1 == 0:
- visual('gout', fake.asnumpy(), name=os.path.join(outf,'fake_img_iter_%d.png' %iter))
- visual('data', data.asnumpy(), name=os.path.join(outf,'real_img_iter_%d.png' %iter))
-
- iter = iter + 1
- btic = time.time()
-
- name, acc = metric.get()
- metric.reset()
- logging.info('\nbinary training acc at epoch %d: %s=%f' % (epoch, name, acc))
- logging.info('time: %f' % (time.time() - tic))
-
- if check_point:
- netG.save_parameters(os.path.join(outf,'generator_epoch_%d.params' %epoch))
- netD.save_parameters(os.path.join(outf,'discriminator_epoch_%d.params' % epoch))
-
-netG.save_parameters(os.path.join(outf, 'generator.params'))
-netD.save_parameters(os.path.join(outf, 'discriminator.params'))
diff --git a/example/gluon/kaggle_k_fold_cross_validation.py b/example/gluon/house_prices/kaggle_k_fold_cross_validation.py
similarity index 100%
rename from example/gluon/kaggle_k_fold_cross_validation.py
rename to example/gluon/house_prices/kaggle_k_fold_cross_validation.py
diff --git a/example/gluon/learning_rate_manipulation.py b/example/gluon/learning_rate_manipulation.py
deleted file mode 100644
index be1ffc290242..000000000000
--- a/example/gluon/learning_rate_manipulation.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-
-# This example demonstrates how to manipulate the learning rate of an optimizer
-# in gluon. The example uses linear regression as a case study.
-
-from __future__ import print_function
-import numpy as np
-import mxnet as mx
-from mxnet import autograd
-from mxnet import gluon
-
-# Generate synthetic data.
-X = np.random.randn(10000, 2)
-Y = 2 * X[:, 0] - 3.4 * X[:, 1] + 4.2 + .01 * np.random.normal(size=10000)
-
-net = gluon.nn.Sequential()
-# The output dimension is 1.
-net.add(gluon.nn.Dense(1))
-net.initialize()
-loss = gluon.loss.L2Loss()
-
-# Initialize the learning rate as 0.1.
-trainer = gluon.Trainer(net.collect_params(), 'sgd',
- optimizer_params={'learning_rate': 0.1})
-net.initialize(mx.init.Xavier(magnitude=2.24),
- force_reinit=True)
-train_data = mx.io.NDArrayIter(X, Y, batch_size=10, shuffle=True)
-
-for epoch in range(5):
- train_data.reset()
- for i, batch in enumerate(train_data):
- data = batch.data[0]
- label = batch.label[0].reshape((-1, 1))
- with autograd.record():
- output = net(data)
- mse = loss(output, label)
- mse.backward()
- trainer.step(data.shape[0])
- # After the second epoch, decay the learning rate of the optimizer every
- # epoch.
- if epoch > 1:
- trainer.set_learning_rate(trainer.learning_rate * 0.9)
- print('Epoch:', epoch, 'Learning rate:', trainer.learning_rate)
-
-for para_name, para_value in net.collect_params().items():
- # Print all the parameter values after training.
- print(para_name, para_value.data().asnumpy()[0])
diff --git a/example/gluon/lstm_crf.py b/example/gluon/lstm_crf/lstm_crf.py
similarity index 95%
rename from example/gluon/lstm_crf.py
rename to example/gluon/lstm_crf/lstm_crf.py
index 3e95c054fedc..9c2218577312 100644
--- a/example/gluon/lstm_crf.py
+++ b/example/gluon/lstm_crf/lstm_crf.py
@@ -21,8 +21,8 @@
import mxnet.optimizer as optim
import sys
-# This example demonstrates how LSTM-CRF model can be implemented in Gluon to perform
-# noun-phrase chunking as a sequence labeling task.
+# This example demonstrates how the LSTM-CRF model can be implemented
+# in Gluon to perform noun-phrase chunking as a sequence labeling task.
mx.random.seed(1)
@@ -208,7 +208,9 @@ def forward(self, sentence): # dont confuse this with _forward_alg above.
# Make sure prepare_sequence from earlier in the LSTM section is loaded
for epoch in range(300): # again, normally you would NOT do 300 epochs, it is toy data
- for sentence, tags in training_data:
+
+ neg_log_likelihood_acc = 0.
+ for i, (sentence, tags) in enumerate(training_data):
# Step 1. Get our inputs ready for the network, that is,
# turn them into Variables of word indices.
# Remember to use autograd to record the calculation.
@@ -223,6 +225,8 @@ def forward(self, sentence): # dont confuse this with _forward_alg above.
# calling optimizer.step()
neg_log_likelihood.backward()
optimizer.step(1)
+ neg_log_likelihood_acc += neg_log_likelihood.mean()
+ print("Epoch [{}], Negative Log Likelihood {:.4f}".format(epoch, neg_log_likelihood_acc.asscalar()/(i+1)))
# Check predictions after training
precheck_sent = prepare_sequence(training_data[0][0], word2idx)
diff --git a/example/gluon/mnist.py b/example/gluon/mnist/mnist.py
similarity index 100%
rename from example/gluon/mnist.py
rename to example/gluon/mnist/mnist.py
diff --git a/example/gluon/sn_gan/data.py b/example/gluon/sn_gan/data.py
index 333125dbe9fe..782f74ffca5d 100644
--- a/example/gluon/sn_gan/data.py
+++ b/example/gluon/sn_gan/data.py
@@ -17,7 +17,7 @@
# This example is inspired by https://github.com/jason71995/Keras-GAN-Library,
# https://github.com/kazizzad/DCGAN-Gluon-MxNet/blob/master/MxnetDCGAN.ipynb
-# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dcgan.py
+# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dc_gan/dcgan.py
import numpy as np
diff --git a/example/gluon/sn_gan/model.py b/example/gluon/sn_gan/model.py
index 38f87ebddc8a..6040adb4eeac 100644
--- a/example/gluon/sn_gan/model.py
+++ b/example/gluon/sn_gan/model.py
@@ -17,7 +17,7 @@
# This example is inspired by https://github.com/jason71995/Keras-GAN-Library,
# https://github.com/kazizzad/DCGAN-Gluon-MxNet/blob/master/MxnetDCGAN.ipynb
-# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dcgan.py
+# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dc_gan/dcgan.py
import mxnet as mx
from mxnet import nd
diff --git a/example/gluon/sn_gan/train.py b/example/gluon/sn_gan/train.py
index 1cba1f57d0a0..5faf3a2a02a8 100644
--- a/example/gluon/sn_gan/train.py
+++ b/example/gluon/sn_gan/train.py
@@ -17,7 +17,7 @@
# This example is inspired by https://github.com/jason71995/Keras-GAN-Library,
# https://github.com/kazizzad/DCGAN-Gluon-MxNet/blob/master/MxnetDCGAN.ipynb
-# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dcgan.py
+# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dc_gan/dcgan.py
import os
diff --git a/example/gluon/sn_gan/utils.py b/example/gluon/sn_gan/utils.py
index d3f1b8626a1a..1a77a6e90ec0 100644
--- a/example/gluon/sn_gan/utils.py
+++ b/example/gluon/sn_gan/utils.py
@@ -17,7 +17,7 @@
# This example is inspired by https://github.com/jason71995/Keras-GAN-Library,
# https://github.com/kazizzad/DCGAN-Gluon-MxNet/blob/master/MxnetDCGAN.ipynb
-# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dcgan.py
+# https://github.com/apache/incubator-mxnet/blob/master/example/gluon/dc_gan/dcgan.py
import math
diff --git a/example/gluon/super_resolution.py b/example/gluon/super_resolution/super_resolution.py
similarity index 100%
rename from example/gluon/super_resolution.py
rename to example/gluon/super_resolution/super_resolution.py
diff --git a/example/gluon/tree_lstm/README.md b/example/gluon/tree_lstm/README.md
new file mode 100644
index 000000000000..e14ab4c70afc
--- /dev/null
+++ b/example/gluon/tree_lstm/README.md
@@ -0,0 +1,29 @@
+
+# Tree-Structured Long Short-Term Memory Networks
+This is a [MXNet Gluon](https://mxnet.io/) implementation of Tree-LSTM as described in the paper [Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks](http://arxiv.org/abs/1503.00075) by Kai Sheng Tai, Richard Socher, and Christopher Manning.
+
+### Requirements
+- Python (tested on **3.6.5**, should work on **>=2.7**)
+- Java >= 8 (for Stanford CoreNLP utilities)
+- Other dependencies are in `requirements.txt`
+Note: Currently works with MXNet 1.3.0.
+
+### Usage
+Before delving into how to run the code, here is a quick overview of the contents:
+ - Use the script `fetch_and_preprocess.sh` to download the [SICK dataset](http://alt.qcri.org/semeval2014/task1/index.php?id=data-and-tools), [Stanford Parser](http://nlp.stanford.edu/software/lex-parser.shtml) and [Stanford POS Tagger](http://nlp.stanford.edu/software/tagger.shtml), and [Glove word vectors](http://nlp.stanford.edu/projects/glove/) (Common Crawl 840) -- **Warning:** this is a 2GB download!), and additionally preprocess the data, i.e. generate dependency parses using [Stanford Neural Network Dependency Parser](http://nlp.stanford.edu/software/nndep.shtml).
+- `main.py`does the actual heavy lifting of training the model and testing it on the SICK dataset. For a list of all command-line arguments, have a look at `python main.py -h`.
+- The first run caches GLOVE embeddings for words in the SICK vocabulary. In later runs, only the cache is read in during later runs.
+
+Next, these are the different ways to run the code here to train a TreeLSTM model.
+#### Local Python Environment
+If you have a working Python3 environment, simply run the following sequence of steps:
+
+```
+- bash fetch_and_preprocess.sh
+- python main.py
+```
+
+
+### Acknowledgments
+- The Gluon version is ported from this implementation [dasguptar/treelstm.pytorch](https://github.com/dasguptar/treelstm.pytorch)
+- Shout-out to [Kai Sheng Tai](https://github.com/kaishengtai/) for the [original LuaTorch implementation](https://github.com/stanfordnlp/treelstm), and to the [Pytorch team](https://github.com/pytorch/pytorch#the-team) for the fun library.
diff --git a/example/gluon/tree_lstm/dataset.py b/example/gluon/tree_lstm/dataset.py
index 02c57c0cc809..5d6b766042d6 100644
--- a/example/gluon/tree_lstm/dataset.py
+++ b/example/gluon/tree_lstm/dataset.py
@@ -115,7 +115,7 @@ def load_embedding(self, f, reset=[]):
self.add(word)
if word in self.tok2idx:
vectors[word] = [float(x) for x in tokens[1:]]
- dim = len(vectors.values()[0])
+ dim = len(list(vectors.values())[0])
def to_vector(tok):
if tok in vectors and tok not in reset:
return vectors[tok]
@@ -154,7 +154,7 @@ def __init__(self, path, vocab, num_classes, shuffle=True):
def reset(self):
if self.shuffle:
- mask = range(self.size)
+ mask = list(range(self.size))
random.shuffle(mask)
self.l_sentences = [self.l_sentences[i] for i in mask]
self.r_sentences = [self.r_sentences[i] for i in mask]
diff --git a/example/gluon/tree_lstm/fetch_and_preprocess.sh b/example/gluon/tree_lstm/fetch_and_preprocess.sh
index f372392830d0..a9b9d28612f3 100755
--- a/example/gluon/tree_lstm/fetch_and_preprocess.sh
+++ b/example/gluon/tree_lstm/fetch_and_preprocess.sh
@@ -18,8 +18,8 @@
# under the License.
set -e
-python2.7 scripts/download.py
+python scripts/download.py
CLASSPATH="lib:lib/stanford-parser/stanford-parser.jar:lib/stanford-parser/stanford-parser-3.5.1-models.jar"
javac -cp $CLASSPATH lib/*.java
-python2.7 scripts/preprocess-sick.py
+python scripts/preprocess-sick.py
diff --git a/example/gluon/tree_lstm/main.py b/example/gluon/tree_lstm/main.py
index ad5d59f7a47d..53af3fa019e9 100644
--- a/example/gluon/tree_lstm/main.py
+++ b/example/gluon/tree_lstm/main.py
@@ -152,7 +152,6 @@ def train(epoch, ctx, train_data, dev_data):
net.embed.weight.set_data(vocab.embed.as_in_context(ctx[0]))
train_data.set_context(ctx[0])
dev_data.set_context(ctx[0])
-
# set up trainer for optimizing the network.
trainer = gluon.Trainer(net.collect_params(), optimizer, {'learning_rate': opt.lr, 'wd': opt.wd})
diff --git a/example/gluon/tree_lstm/scripts/download.py b/example/gluon/tree_lstm/scripts/download.py
index 7ea930370175..6537ef1ff655 100644
--- a/example/gluon/tree_lstm/scripts/download.py
+++ b/example/gluon/tree_lstm/scripts/download.py
@@ -24,7 +24,6 @@
"""
from __future__ import print_function
-import urllib2
import sys
import os
import shutil
diff --git a/example/image-classification/benchmark_score.py b/example/image-classification/benchmark_score.py
index 05e4b487f380..e81a30bd6439 100644
--- a/example/image-classification/benchmark_score.py
+++ b/example/image-classification/benchmark_score.py
@@ -21,26 +21,49 @@
from common import find_mxnet
from common.util import get_gpus
import mxnet as mx
+import mxnet.gluon.model_zoo.vision as models
from importlib import import_module
import logging
+import argparse
import time
import numpy as np
logging.basicConfig(level=logging.DEBUG)
+parser = argparse.ArgumentParser(description='SymbolAPI-based CNN inference performance benchmark')
+parser.add_argument('--network', type=str, default='all',
+ choices=['all', 'alexnet', 'vgg-16', 'resnetv1-50', 'resnet-50',
+ 'resnet-152', 'inception-bn', 'inception-v3',
+ 'inception-v4', 'inception-resnet-v2', 'mobilenet',
+ 'densenet121', 'squeezenet1.1'])
+parser.add_argument('--batch-size', type=int, default=0,
+ help='Batch size to use for benchmarking. Example: 32, 64, 128.'
+ 'By default, runs benchmark for batch sizes - 1, 32, 64, 128, 256')
+
+opt = parser.parse_args()
+
def get_symbol(network, batch_size, dtype):
- image_shape = (3,299,299) if network == 'inception-v3' else (3,224,224)
+ image_shape = (3,299,299) if network in ['inception-v3', 'inception-v4'] else (3,224,224)
num_layers = 0
- if 'resnet' in network:
+ if network == 'inception-resnet-v2':
+ network = network
+ elif 'resnet' in network:
num_layers = int(network.split('-')[1])
- network = 'resnet'
+ network = network.split('-')[0]
if 'vgg' in network:
num_layers = int(network.split('-')[1])
network = 'vgg'
- net = import_module('symbols.'+network)
- sym = net.get_symbol(num_classes=1000,
- image_shape=','.join([str(i) for i in image_shape]),
- num_layers=num_layers,
- dtype=dtype)
+ if network in ['densenet121', 'squeezenet1.1']:
+ sym = models.get_model(network)
+ sym.hybridize()
+ data = mx.sym.var('data')
+ sym = sym(data)
+ sym = mx.sym.SoftmaxOutput(sym, name='softmax')
+ else:
+ net = import_module('symbols.'+network)
+ sym = net.get_symbol(num_classes=1000,
+ image_shape=','.join([str(i) for i in image_shape]),
+ num_layers=num_layers,
+ dtype=dtype)
return (sym, [('data', (batch_size,)+image_shape)])
def score(network, dev, batch_size, num_batches, dtype):
@@ -69,14 +92,31 @@ def score(network, dev, batch_size, num_batches, dtype):
return num_batches*batch_size/(time.time() - tic)
if __name__ == '__main__':
- networks = ['alexnet', 'vgg-16', 'inception-bn', 'inception-v3', 'resnet-50', 'resnet-152']
+ if opt.network == 'all':
+ networks = ['alexnet', 'vgg-16', 'resnetv1-50', 'resnet-50',
+ 'resnet-152', 'inception-bn', 'inception-v3',
+ 'inception-v4', 'inception-resnet-v2',
+ 'mobilenet', 'densenet121', 'squeezenet1.1']
+ logging.info('It may take some time to run all models, '
+ 'set --network to run a specific one')
+ else:
+ networks = [opt.network]
devs = [mx.gpu(0)] if len(get_gpus()) > 0 else []
# Enable USE_MKLDNN for better CPU performance
devs.append(mx.cpu())
- batch_sizes = [1, 2, 4, 8, 16, 32]
+ if opt.batch_size == 0:
+ batch_sizes = [1, 32, 64, 128, 256]
+ logging.info('run batchsize [1, 32, 64, 128, 256] by default, '
+ 'set --batch-size to run a specific one')
+ else:
+ batch_sizes = [opt.batch_size]
+
for net in networks:
logging.info('network: %s', net)
+ if net in ['densenet121', 'squeezenet1.1']:
+ logging.info('network: %s is converted from gluon modelzoo', net)
+ logging.info('you can run benchmark/python/gluon/benchmark_gluon.py for more models')
for d in devs:
logging.info('device: %s', d)
logged_fp16_warning = False
diff --git a/example/image-classification/symbols/resnetv1.py b/example/image-classification/symbols/resnetv1.py
new file mode 100755
index 000000000000..e5752f775447
--- /dev/null
+++ b/example/image-classification/symbols/resnetv1.py
@@ -0,0 +1,200 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+'''
+Adapted from https://github.com/tornadomeet/ResNet/blob/master/symbol_resnet.py
+(Original author Wei Wu) by Antti-Pekka Hynninen
+
+Implementing the original resnet ILSVRC 2015 winning network from:
+
+Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun. "Deep Residual Learning for Image Recognition"
+'''
+import mxnet as mx
+import numpy as np
+
+def residual_unit(data, num_filter, stride, dim_match, name, bottle_neck=True, bn_mom=0.9, workspace=256, memonger=False):
+ """Return ResNet Unit symbol for building ResNet
+ Parameters
+ ----------
+ data : str
+ Input data
+ num_filter : int
+ Number of output channels
+ bnf : int
+ Bottle neck channels factor with regard to num_filter
+ stride : tuple
+ Stride used in convolution
+ dim_match : Boolean
+ True means channel number between input and output is the same, otherwise means differ
+ name : str
+ Base name of the operators
+ workspace : int
+ Workspace used in convolution operator
+ """
+ if bottle_neck:
+ conv1 = mx.sym.Convolution(data=data, num_filter=int(num_filter*0.25), kernel=(1,1), stride=stride, pad=(0,0),
+ no_bias=True, workspace=workspace, name=name + '_conv1')
+ bn1 = mx.sym.BatchNorm(data=conv1, fix_gamma=False, eps=2e-5, momentum=bn_mom, name=name + '_bn1')
+ act1 = mx.sym.Activation(data=bn1, act_type='relu', name=name + '_relu1')
+ conv2 = mx.sym.Convolution(data=act1, num_filter=int(num_filter*0.25), kernel=(3,3), stride=(1,1), pad=(1,1),
+ no_bias=True, workspace=workspace, name=name + '_conv2')
+ bn2 = mx.sym.BatchNorm(data=conv2, fix_gamma=False, eps=2e-5, momentum=bn_mom, name=name + '_bn2')
+ act2 = mx.sym.Activation(data=bn2, act_type='relu', name=name + '_relu2')
+ conv3 = mx.sym.Convolution(data=act2, num_filter=num_filter, kernel=(1,1), stride=(1,1), pad=(0,0), no_bias=True,
+ workspace=workspace, name=name + '_conv3')
+ bn3 = mx.sym.BatchNorm(data=conv3, fix_gamma=False, eps=2e-5, momentum=bn_mom, name=name + '_bn3')
+
+ if dim_match:
+ shortcut = data
+ else:
+ conv1sc = mx.sym.Convolution(data=data, num_filter=num_filter, kernel=(1,1), stride=stride, no_bias=True,
+ workspace=workspace, name=name+'_conv1sc')
+ shortcut = mx.sym.BatchNorm(data=conv1sc, fix_gamma=False, eps=2e-5, momentum=bn_mom, name=name + '_sc')
+ if memonger:
+ shortcut._set_attr(mirror_stage='True')
+ return mx.sym.Activation(data=bn3 + shortcut, act_type='relu', name=name + '_relu3')
+ else:
+ conv1 = mx.sym.Convolution(data=data, num_filter=num_filter, kernel=(3,3), stride=stride, pad=(1,1),
+ no_bias=True, workspace=workspace, name=name + '_conv1')
+ bn1 = mx.sym.BatchNorm(data=conv1, fix_gamma=False, momentum=bn_mom, eps=2e-5, name=name + '_bn1')
+ act1 = mx.sym.Activation(data=bn1, act_type='relu', name=name + '_relu1')
+ conv2 = mx.sym.Convolution(data=act1, num_filter=num_filter, kernel=(3,3), stride=(1,1), pad=(1,1),
+ no_bias=True, workspace=workspace, name=name + '_conv2')
+ bn2 = mx.sym.BatchNorm(data=conv2, fix_gamma=False, momentum=bn_mom, eps=2e-5, name=name + '_bn2')
+
+ if dim_match:
+ shortcut = data
+ else:
+ conv1sc = mx.sym.Convolution(data=data, num_filter=num_filter, kernel=(1,1), stride=stride, no_bias=True,
+ workspace=workspace, name=name+'_conv1sc')
+ shortcut = mx.sym.BatchNorm(data=conv1sc, fix_gamma=False, momentum=bn_mom, eps=2e-5, name=name + '_sc')
+ if memonger:
+ shortcut._set_attr(mirror_stage='True')
+ return mx.sym.Activation(data=bn2 + shortcut, act_type='relu', name=name + '_relu3')
+
+def resnet(units, num_stages, filter_list, num_classes, image_shape, bottle_neck=True, bn_mom=0.9, workspace=256, dtype='float32', memonger=False):
+ """Return ResNet symbol of
+ Parameters
+ ----------
+ units : list
+ Number of units in each stage
+ num_stages : int
+ Number of stage
+ filter_list : list
+ Channel size of each stage
+ num_classes : int
+ Ouput size of symbol
+ dataset : str
+ Dataset type, only cifar10 and imagenet supports
+ workspace : int
+ Workspace used in convolution operator
+ dtype : str
+ Precision (float32 or float16)
+ """
+ num_unit = len(units)
+ assert(num_unit == num_stages)
+ data = mx.sym.Variable(name='data')
+ if dtype == 'float32':
+ data = mx.sym.identity(data=data, name='id')
+ else:
+ if dtype == 'float16':
+ data = mx.sym.Cast(data=data, dtype=np.float16)
+ (nchannel, height, width) = image_shape
+ if height <= 32: # such as cifar10
+ body = mx.sym.Convolution(data=data, num_filter=filter_list[0], kernel=(3, 3), stride=(1,1), pad=(1, 1),
+ no_bias=True, name="conv0", workspace=workspace)
+ # Is this BatchNorm supposed to be here?
+ body = mx.sym.BatchNorm(data=body, fix_gamma=False, eps=2e-5, momentum=bn_mom, name='bn0')
+ else: # often expected to be 224 such as imagenet
+ body = mx.sym.Convolution(data=data, num_filter=filter_list[0], kernel=(7, 7), stride=(2,2), pad=(3, 3),
+ no_bias=True, name="conv0", workspace=workspace)
+ body = mx.sym.BatchNorm(data=body, fix_gamma=False, eps=2e-5, momentum=bn_mom, name='bn0')
+ body = mx.sym.Activation(data=body, act_type='relu', name='relu0')
+ body = mx.sym.Pooling(data=body, kernel=(3, 3), stride=(2,2), pad=(1,1), pool_type='max')
+
+ for i in range(num_stages):
+ body = residual_unit(body, filter_list[i+1], (1 if i==0 else 2, 1 if i==0 else 2), False,
+ name='stage%d_unit%d' % (i + 1, 1), bottle_neck=bottle_neck, workspace=workspace,
+ memonger=memonger)
+ for j in range(units[i]-1):
+ body = residual_unit(body, filter_list[i+1], (1,1), True, name='stage%d_unit%d' % (i + 1, j + 2),
+ bottle_neck=bottle_neck, workspace=workspace, memonger=memonger)
+ # bn1 = mx.sym.BatchNorm(data=body, fix_gamma=False, eps=2e-5, momentum=bn_mom, name='bn1')
+ # relu1 = mx.sym.Activation(data=bn1, act_type='relu', name='relu1')
+ # Although kernel is not used here when global_pool=True, we should put one
+ pool1 = mx.sym.Pooling(data=body, global_pool=True, kernel=(7, 7), pool_type='avg', name='pool1')
+ flat = mx.sym.Flatten(data=pool1)
+ fc1 = mx.sym.FullyConnected(data=flat, num_hidden=num_classes, name='fc1')
+ if dtype == 'float16':
+ fc1 = mx.sym.Cast(data=fc1, dtype=np.float32)
+ return mx.sym.SoftmaxOutput(data=fc1, name='softmax')
+
+def get_symbol(num_classes, num_layers, image_shape, conv_workspace=256, dtype='float32', **kwargs):
+ """
+ Adapted from https://github.com/tornadomeet/ResNet/blob/master/symbol_resnet.py
+ (Original author Wei Wu) by Antti-Pekka Hynninen
+ Implementing the original resnet ILSVRC 2015 winning network from:
+ Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun. "Deep Residual Learning for Image Recognition"
+ """
+ image_shape = [int(l) for l in image_shape.split(',')]
+ (nchannel, height, width) = image_shape
+ if height <= 28:
+ num_stages = 3
+ if (num_layers-2) % 9 == 0 and num_layers >= 164:
+ per_unit = [(num_layers-2)//9]
+ filter_list = [16, 64, 128, 256]
+ bottle_neck = True
+ elif (num_layers-2) % 6 == 0 and num_layers < 164:
+ per_unit = [(num_layers-2)//6]
+ filter_list = [16, 16, 32, 64]
+ bottle_neck = False
+ else:
+ raise ValueError("no experiments done on num_layers {}, you can do it yourself".format(num_layers))
+ units = per_unit * num_stages
+ else:
+ if num_layers >= 50:
+ filter_list = [64, 256, 512, 1024, 2048]
+ bottle_neck = True
+ else:
+ filter_list = [64, 64, 128, 256, 512]
+ bottle_neck = False
+ num_stages = 4
+ if num_layers == 18:
+ units = [2, 2, 2, 2]
+ elif num_layers == 34:
+ units = [3, 4, 6, 3]
+ elif num_layers == 50:
+ units = [3, 4, 6, 3]
+ elif num_layers == 101:
+ units = [3, 4, 23, 3]
+ elif num_layers == 152:
+ units = [3, 8, 36, 3]
+ elif num_layers == 200:
+ units = [3, 24, 36, 3]
+ elif num_layers == 269:
+ units = [3, 30, 48, 8]
+ else:
+ raise ValueError("no experiments done on num_layers {}, you can do it yourself".format(num_layers))
+
+ return resnet(units = units,
+ num_stages = num_stages,
+ filter_list = filter_list,
+ num_classes = num_classes,
+ image_shape = image_shape,
+ bottle_neck = bottle_neck,
+ workspace = conv_workspace,
+ dtype = dtype)
diff --git a/example/image-classification/train_mnist.py b/example/image-classification/train_mnist.py
index 2bc4289318d9..17a5a372c0c2 100644
--- a/example/image-classification/train_mnist.py
+++ b/example/image-classification/train_mnist.py
@@ -72,6 +72,7 @@ def get_mnist_iter(args, kv):
help='the number of training examples')
parser.add_argument('--add_stn', action="store_true", default=False, help='Add Spatial Transformer Network Layer (lenet only)')
+ parser.add_argument('--image_shape', default='1, 28, 28', help='shape of training images')
fit.add_fit_args(parser)
parser.set_defaults(
diff --git a/example/memcost/README.md b/example/memcost/README.md
deleted file mode 100644
index 4c4e1fa977af..000000000000
--- a/example/memcost/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-Memory Cost of Deep Nets under Different Allocations
-====================================================
-This folder contains a script to show the memory cost of different allocation strategies,
-discussed in [Note on Memory Optimization](http://mxnet.io/architecture/note_memory.html).
-
-We use inception-bn as an example, with batch size of 32.
-
-How to See the cost
--------------------
-The possible options are gathered together in the [Makefile](Makefile).
-Type the following command to see the allocation cost. Look for the
-```Final message Total x MB allocated```
-- ```make no_optimization```
- - Shows the cost without any optimization.
-- ```make with_inplace```
- - Shows the cost with inplace optimization.
-- ```make with_sharing```
- - Shows the cost with memory allocating algorithm for sharing.
-- ```make with_both```
- - Shows the cost of memory allocation with both inplace and sharing optimization.
-- ```make forward_only```
- - Shows the cost of when we only want to run forward pass.
-
-Notes
------
-- You can change the symbol in the [inception_memcost.py](inception_memcost.py) to the net you interested in.
-- You will need to install mxnet or type make on the root folder before use the script.
-- The estimation is only on space cost of intermediate node.
- - The cost of temporal workspace is not estimated, so you will likely need more memory when running real nets.
-- The estimation does real allocation on CPU, the plan is the same on GPU.
diff --git a/example/memcost/inception_memcost.py b/example/memcost/inception_memcost.py
deleted file mode 100644
index c539e73b3c24..000000000000
--- a/example/memcost/inception_memcost.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: skip-file
-import sys
-sys.path.append('../../python/')
-import mxnet as mx
-import logging
-
-def ConvFactory(data, num_filter, kernel, stride=(1,1), pad=(0, 0), name=None, suffix=''):
- conv = mx.symbol.Convolution(data=data, num_filter=num_filter, kernel=kernel, stride=stride, pad=pad, name='conv_%s%s' %(name, suffix))
- bn = mx.symbol.BatchNorm(data=conv, name='bn_%s%s' %(name, suffix))
- act = mx.symbol.Activation(data=bn, act_type='relu', name='relu_%s%s' %(name, suffix))
- return act
-
-def InceptionFactoryA(data, num_1x1, num_3x3red, num_3x3, num_d3x3red, num_d3x3, pool, proj, name):
- # 1x1
- c1x1 = ConvFactory(data=data, num_filter=num_1x1, kernel=(1, 1), name=('%s_1x1' % name))
- # 3x3 reduce + 3x3
- c3x3r = ConvFactory(data=data, num_filter=num_3x3red, kernel=(1, 1), name=('%s_3x3' % name), suffix='_reduce')
- c3x3 = ConvFactory(data=c3x3r, num_filter=num_3x3, kernel=(3, 3), pad=(1, 1), name=('%s_3x3' % name))
- # double 3x3 reduce + double 3x3
- cd3x3r = ConvFactory(data=data, num_filter=num_d3x3red, kernel=(1, 1), name=('%s_double_3x3' % name), suffix='_reduce')
- cd3x3 = ConvFactory(data=cd3x3r, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), name=('%s_double_3x3_0' % name))
- cd3x3 = ConvFactory(data=cd3x3, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), name=('%s_double_3x3_1' % name))
- # pool + proj
- pooling = mx.symbol.Pooling(data=data, kernel=(3, 3), stride=(1, 1), pad=(1, 1), pool_type=pool, name=('%s_pool_%s_pool' % (pool, name)))
- cproj = ConvFactory(data=pooling, num_filter=proj, kernel=(1, 1), name=('%s_proj' % name))
- # concat
- concat = mx.symbol.Concat(*[c1x1, c3x3, cd3x3, cproj], name='ch_concat_%s_chconcat' % name)
- return concat
-
-def InceptionFactoryB(data, num_3x3red, num_3x3, num_d3x3red, num_d3x3, name):
- # 3x3 reduce + 3x3
- c3x3r = ConvFactory(data=data, num_filter=num_3x3red, kernel=(1, 1), name=('%s_3x3' % name), suffix='_reduce')
- c3x3 = ConvFactory(data=c3x3r, num_filter=num_3x3, kernel=(3, 3), pad=(1, 1), stride=(2, 2), name=('%s_3x3' % name))
- # double 3x3 reduce + double 3x3
- cd3x3r = ConvFactory(data=data, num_filter=num_d3x3red, kernel=(1, 1), name=('%s_double_3x3' % name), suffix='_reduce')
- cd3x3 = ConvFactory(data=cd3x3r, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), stride=(1, 1), name=('%s_double_3x3_0' % name))
- cd3x3 = ConvFactory(data=cd3x3, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), stride=(2, 2), name=('%s_double_3x3_1' % name))
- # pool + proj
- pooling = mx.symbol.Pooling(data=data, kernel=(3, 3), stride=(2, 2), pad=(1, 1), pool_type="max", name=('max_pool_%s_pool' % name))
- # concat
- concat = mx.symbol.Concat(*[c3x3, cd3x3, pooling], name='ch_concat_%s_chconcat' % name)
- return concat
-
-def inception(nhidden, grad_scale):
- # data
- data = mx.symbol.Variable(name="data")
- # stage 1
- conv1 = ConvFactory(data=data, num_filter=64, kernel=(7, 7), stride=(2, 2), pad=(3, 3), name='conv1')
- pool1 = mx.symbol.Pooling(data=conv1, kernel=(3, 3), stride=(2, 2), name='pool1', pool_type='max')
- # stage 2
- conv2red = ConvFactory(data=pool1, num_filter=64, kernel=(1, 1), stride=(1, 1), name='conv2red')
- conv2 = ConvFactory(data=conv2red, num_filter=192, kernel=(3, 3), stride=(1, 1), pad=(1, 1), name='conv2')
- pool2 = mx.symbol.Pooling(data=conv2, kernel=(3, 3), stride=(2, 2), name='pool2', pool_type='max')
- # stage 2
- in3a = InceptionFactoryA(pool2, 64, 64, 64, 64, 96, "avg", 32, '3a')
- in3b = InceptionFactoryA(in3a, 64, 64, 96, 64, 96, "avg", 64, '3b')
- in3c = InceptionFactoryB(in3b, 128, 160, 64, 96, '3c')
- # stage 3
- in4a = InceptionFactoryA(in3c, 224, 64, 96, 96, 128, "avg", 128, '4a')
- in4b = InceptionFactoryA(in4a, 192, 96, 128, 96, 128, "avg", 128, '4b')
- in4c = InceptionFactoryA(in4b, 160, 128, 160, 128, 160, "avg", 128, '4c')
- in4d = InceptionFactoryA(in4c, 96, 128, 192, 160, 192, "avg", 128, '4d')
- in4e = InceptionFactoryB(in4d, 128, 192, 192, 256, '4e')
- # stage 4
- in5a = InceptionFactoryA(in4e, 352, 192, 320, 160, 224, "avg", 128, '5a')
- in5b = InceptionFactoryA(in5a, 352, 192, 320, 192, 224, "max", 128, '5b')
- # global avg pooling
- avg = mx.symbol.Pooling(data=in5b, kernel=(7, 7), stride=(1, 1), name="global_pool", pool_type='avg')
- # linear classifier
- flatten = mx.symbol.Flatten(data=avg, name='flatten')
- fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=nhidden, name='fc1')
- softmax = mx.symbol.SoftmaxOutput(data=fc1, name='softmax')
- return softmax
-
-
-
-softmax = inception(1000, 1.0)
-batch_size = 32
-softmax = inception(1000, 1.0)
-
-if len(sys.argv) == 2:
- grad_req = sys.argv[1]
-else:
- grad_req = 'write'
-
-texec = softmax.simple_bind(ctx=mx.cpu(),
- data=(batch_size, 3, 224, 224),
- grad_req=grad_req)
-# We extract the memory cost from the execution plan
-print(texec.debug_str().split('\n')[-3])
diff --git a/example/module/utils/__init__.py b/example/module/utils/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/example/utils/get_data.py b/example/module/utils/get_data.py
similarity index 94%
rename from example/utils/get_data.py
rename to example/module/utils/get_data.py
index 861d16cdbad9..2a585ead385b 100644
--- a/example/utils/get_data.py
+++ b/example/module/utils/get_data.py
@@ -17,6 +17,7 @@
import os
import mxnet as mx
+import zipfile
def get_mnist(data_dir):
if not os.path.isdir(data_dir):
@@ -28,7 +29,7 @@ def get_mnist(data_dir):
(not os.path.exists('t10k-labels-idx1-ubyte')):
import urllib, zipfile
zippath = os.path.join(os.getcwd(), "mnist.zip")
- urllib.urlretrieve("http://data.mxnet.io/mxnet/data/mnist.zip", zippath)
+ mx.test_utils.download("http://data.mxnet.io/mxnet/data/mnist.zip", zippath)
zf = zipfile.ZipFile(zippath, "r")
zf.extractall()
zf.close()
@@ -45,7 +46,7 @@ def get_cifar10(data_dir):
import urllib, zipfile, glob
dirname = os.getcwd()
zippath = os.path.join(dirname, "cifar10.zip")
- urllib.urlretrieve("http://data.mxnet.io/mxnet/data/cifar10.zip", zippath)
+ mx.test_utils.download("http://data.mxnet.io/mxnet/data/cifar10.zip", zippath)
zf = zipfile.ZipFile(zippath, "r")
zf.extractall()
zf.close()
@@ -56,7 +57,6 @@ def get_cifar10(data_dir):
os.rmdir(os.path.join(dirname, "cifar"))
os.chdir(cwd)
-# data
def get_cifar10_iterator(args, kv):
data_shape = (3, 28, 28)
data_dir = args.data_dir
diff --git a/example/multi-task/README.md b/example/multi-task/README.md
index 9034814c3b50..b7756fe378a7 100644
--- a/example/multi-task/README.md
+++ b/example/multi-task/README.md
@@ -1,10 +1,13 @@
# Mulit-task learning example
-This is a simple example to show how to use mxnet for multi-task learning. It uses MNIST as an example and mocks up the multi-label task.
+This is a simple example to show how to use mxnet for multi-task learning. It uses MNIST as an example, trying to predict jointly the digit and whether this digit is odd or even.
-## Usage
-First, you need to write a multi-task iterator on your own. The iterator needs to generate multiple labels according to your applications, and the label names should be specified in the `provide_label` function, which needs to be consist with the names of output layers.
+For example:
-Then, if you want to show metrics of different tasks separately, you need to write your own metric class and specify the `num` parameter. In the `update` function of metric, calculate the metrics separately for different tasks.
+![](https://camo.githubusercontent.com/ed3cf256f47713335dc288f32f9b0b60bf1028b7/68747470733a2f2f7777772e636c61737365732e63732e756368696361676f2e6564752f617263686976652f323031332f737072696e672f31323330302d312f70612f7061312f64696769742e706e67)
-The example script uses gpu as device by default, if gpu is not available for your environment, you can change `device` to be `mx.cpu()`.
+Should be jointly classified as 4, and Even.
+
+In this example we don't expect the tasks to contribute to each other much, but for example multi-task learning has been successfully applied to the domain of image captioning. In [A Multi-task Learning Approach for Image Captioning](https://www.ijcai.org/proceedings/2018/0168.pdf) by Wei Zhao, Benyou Wang, Jianbo Ye, Min Yang, Zhou Zhao, Ruotian Luo, Yu Qiao, they train a network to jointly classify images and generate text captions
+
+Please refer to the notebook for a fully worked example.
diff --git a/example/multi-task/example_multi_task.py b/example/multi-task/example_multi_task.py
deleted file mode 100644
index 9e898494a14b..000000000000
--- a/example/multi-task/example_multi_task.py
+++ /dev/null
@@ -1,159 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: skip-file
-import mxnet as mx
-from mxnet.test_utils import get_mnist_iterator
-import numpy as np
-import logging
-import time
-
-logging.basicConfig(level=logging.DEBUG)
-
-def build_network():
- data = mx.symbol.Variable('data')
- fc1 = mx.symbol.FullyConnected(data = data, name='fc1', num_hidden=128)
- act1 = mx.symbol.Activation(data = fc1, name='relu1', act_type="relu")
- fc2 = mx.symbol.FullyConnected(data = act1, name = 'fc2', num_hidden = 64)
- act2 = mx.symbol.Activation(data = fc2, name='relu2', act_type="relu")
- fc3 = mx.symbol.FullyConnected(data = act2, name='fc3', num_hidden=10)
- sm1 = mx.symbol.SoftmaxOutput(data = fc3, name = 'softmax1')
- sm2 = mx.symbol.SoftmaxOutput(data = fc3, name = 'softmax2')
-
- softmax = mx.symbol.Group([sm1, sm2])
-
- return softmax
-
-class Multi_mnist_iterator(mx.io.DataIter):
- '''multi label mnist iterator'''
-
- def __init__(self, data_iter):
- super(Multi_mnist_iterator, self).__init__()
- self.data_iter = data_iter
- self.batch_size = self.data_iter.batch_size
-
- @property
- def provide_data(self):
- return self.data_iter.provide_data
-
- @property
- def provide_label(self):
- provide_label = self.data_iter.provide_label[0]
- # Different labels should be used here for actual application
- return [('softmax1_label', provide_label[1]), \
- ('softmax2_label', provide_label[1])]
-
- def hard_reset(self):
- self.data_iter.hard_reset()
-
- def reset(self):
- self.data_iter.reset()
-
- def next(self):
- batch = self.data_iter.next()
- label = batch.label[0]
-
- return mx.io.DataBatch(data=batch.data, label=[label, label], \
- pad=batch.pad, index=batch.index)
-
-class Multi_Accuracy(mx.metric.EvalMetric):
- """Calculate accuracies of multi label"""
-
- def __init__(self, num=None):
- self.num = num
- super(Multi_Accuracy, self).__init__('multi-accuracy')
-
- def reset(self):
- """Resets the internal evaluation result to initial state."""
- self.num_inst = 0 if self.num is None else [0] * self.num
- self.sum_metric = 0.0 if self.num is None else [0.0] * self.num
-
- def update(self, labels, preds):
- mx.metric.check_label_shapes(labels, preds)
-
- if self.num is not None:
- assert len(labels) == self.num
-
- for i in range(len(labels)):
- pred_label = mx.nd.argmax_channel(preds[i]).asnumpy().astype('int32')
- label = labels[i].asnumpy().astype('int32')
-
- mx.metric.check_label_shapes(label, pred_label)
-
- if self.num is None:
- self.sum_metric += (pred_label.flat == label.flat).sum()
- self.num_inst += len(pred_label.flat)
- else:
- self.sum_metric[i] += (pred_label.flat == label.flat).sum()
- self.num_inst[i] += len(pred_label.flat)
-
- def get(self):
- """Gets the current evaluation result.
-
- Returns
- -------
- names : list of str
- Name of the metrics.
- values : list of float
- Value of the evaluations.
- """
- if self.num is None:
- return super(Multi_Accuracy, self).get()
- else:
- return zip(*(('%s-task%d'%(self.name, i), float('nan') if self.num_inst[i] == 0
- else self.sum_metric[i] / self.num_inst[i])
- for i in range(self.num)))
-
- def get_name_value(self):
- """Returns zipped name and value pairs.
-
- Returns
- -------
- list of tuples
- A (name, value) tuple list.
- """
- if self.num is None:
- return super(Multi_Accuracy, self).get_name_value()
- name, value = self.get()
- return list(zip(name, value))
-
-
-batch_size=100
-num_epochs=100
-device = mx.gpu(0)
-lr = 0.01
-
-network = build_network()
-train, val = get_mnist_iterator(batch_size=batch_size, input_shape = (784,))
-train = Multi_mnist_iterator(train)
-val = Multi_mnist_iterator(val)
-
-
-model = mx.mod.Module(
- context = device,
- symbol = network,
- label_names = ('softmax1_label', 'softmax2_label'))
-
-model.fit(
- train_data = train,
- eval_data = val,
- eval_metric = Multi_Accuracy(num=2),
- num_epoch = num_epochs,
- optimizer_params = (('learning_rate', lr), ('momentum', 0.9), ('wd', 0.00001)),
- initializer = mx.init.Xavier(factor_type="in", magnitude=2.34),
- batch_end_callback = mx.callback.Speedometer(batch_size, 50))
-
diff --git a/example/multi-task/multi-task-learning.ipynb b/example/multi-task/multi-task-learning.ipynb
new file mode 100644
index 000000000000..6e03e2b61f8c
--- /dev/null
+++ b/example/multi-task/multi-task-learning.ipynb
@@ -0,0 +1,454 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Task Learning Example"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This is a simple example to show how to use mxnet for multi-task learning.\n",
+ "\n",
+ "The network is jointly going to learn whether a number is odd or even and to actually recognize the digit.\n",
+ "\n",
+ "\n",
+ "For example\n",
+ "\n",
+ "- 1 : 1 and odd\n",
+ "- 2 : 2 and even\n",
+ "- 3 : 3 and odd\n",
+ "\n",
+ "etc\n",
+ "\n",
+ "In this example we don't expect the tasks to contribute to each other much, but for example multi-task learning has been successfully applied to the domain of image captioning. In [A Multi-task Learning Approach for Image Captioning](https://www.ijcai.org/proceedings/2018/0168.pdf) by Wei Zhao, Benyou Wang, Jianbo Ye, Min Yang, Zhou Zhao, Ruotian Luo, Yu Qiao, they train a network to jointly classify images and generate text captions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import logging\n",
+ "import random\n",
+ "import time\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import mxnet as mx\n",
+ "from mxnet import gluon, nd, autograd\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 99,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "batch_size = 128\n",
+ "epochs = 5\n",
+ "ctx = mx.gpu() if len(mx.test_utils.list_gpus()) > 0 else mx.cpu()\n",
+ "lr = 0.01"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Data\n",
+ "\n",
+ "We get the traditionnal MNIST dataset and add a new label to the existing one. For each digit we return a new label that stands for Odd or Even"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "![](https://upload.wikimedia.org/wikipedia/commons/2/27/MnistExamples.png)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_dataset = gluon.data.vision.MNIST(train=True)\n",
+ "test_dataset = gluon.data.vision.MNIST(train=False)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def transform(x,y):\n",
+ " x = x.transpose((2,0,1)).astype('float32')/255.\n",
+ " y1 = y\n",
+ " y2 = y % 2 #odd or even\n",
+ " return x, np.float32(y1), np.float32(y2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We assign the transform to the original dataset"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_dataset_t = train_dataset.transform(transform)\n",
+ "test_dataset_t = test_dataset.transform(transform)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We load the datasets DataLoaders"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_data = gluon.data.DataLoader(train_dataset_t, shuffle=True, last_batch='rollover', batch_size=batch_size, num_workers=5)\n",
+ "test_data = gluon.data.DataLoader(test_dataset_t, shuffle=False, last_batch='rollover', batch_size=batch_size, num_workers=5)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Input shape: (28, 28, 1), Target Labels: (5.0, 1.0)\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"Input shape: {}, Target Labels: {}\".format(train_dataset[0][0].shape, train_dataset_t[0][1:]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Multi-task Network\n",
+ "\n",
+ "The output of the featurization is passed to two different outputs layers"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 135,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class MultiTaskNetwork(gluon.HybridBlock):\n",
+ " \n",
+ " def __init__(self):\n",
+ " super(MultiTaskNetwork, self).__init__()\n",
+ " \n",
+ " self.shared = gluon.nn.HybridSequential()\n",
+ " with self.shared.name_scope():\n",
+ " self.shared.add(\n",
+ " gluon.nn.Dense(128, activation='relu'),\n",
+ " gluon.nn.Dense(64, activation='relu'),\n",
+ " gluon.nn.Dense(10, activation='relu')\n",
+ " )\n",
+ " self.output1 = gluon.nn.Dense(10) # Digist recognition\n",
+ " self.output2 = gluon.nn.Dense(1) # odd or even\n",
+ "\n",
+ " \n",
+ " def hybrid_forward(self, F, x):\n",
+ " y = self.shared(x)\n",
+ " output1 = self.output1(y)\n",
+ " output2 = self.output2(y)\n",
+ " return output1, output2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can use two different losses, one for each output"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 136,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "loss_digits = gluon.loss.SoftmaxCELoss()\n",
+ "loss_odd_even = gluon.loss.SigmoidBCELoss()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We create and initialize the network"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 137,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mx.random.seed(42)\n",
+ "random.seed(42)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 138,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "net = MultiTaskNetwork()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 139,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "net.initialize(mx.init.Xavier(), ctx=ctx)\n",
+ "net.hybridize() # hybridize for speed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 140,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate':lr})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Evaluate Accuracy\n",
+ "We need to evaluate the accuracy of each task separately"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 141,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def evaluate_accuracy(net, data_iterator):\n",
+ " acc_digits = mx.metric.Accuracy(name='digits')\n",
+ " acc_odd_even = mx.metric.Accuracy(name='odd_even')\n",
+ " \n",
+ " for i, (data, label_digit, label_odd_even) in enumerate(data_iterator):\n",
+ " data = data.as_in_context(ctx)\n",
+ " label_digit = label_digit.as_in_context(ctx)\n",
+ " label_odd_even = label_odd_even.as_in_context(ctx).reshape(-1,1)\n",
+ "\n",
+ " output_digit, output_odd_even = net(data)\n",
+ " \n",
+ " acc_digits.update(label_digit, output_digit.softmax())\n",
+ " acc_odd_even.update(label_odd_even, output_odd_even.sigmoid() > 0.5)\n",
+ " return acc_digits.get(), acc_odd_even.get()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Training Loop"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We need to balance the contribution of each loss to the overall training and do so by tuning this alpha parameter within [0,1]."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 142,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "alpha = 0.5 # Combine losses factor"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 143,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Epoch [0], Acc Digits 0.8945 Loss Digits 0.3409\n",
+ "Epoch [0], Acc Odd/Even 0.9561 Loss Odd/Even 0.1152\n",
+ "Epoch [0], Testing Accuracies (('digits', 0.9487179487179487), ('odd_even', 0.9770633012820513))\n",
+ "Epoch [1], Acc Digits 0.9576 Loss Digits 0.1475\n",
+ "Epoch [1], Acc Odd/Even 0.9804 Loss Odd/Even 0.0559\n",
+ "Epoch [1], Testing Accuracies (('digits', 0.9642427884615384), ('odd_even', 0.9826722756410257))\n",
+ "Epoch [2], Acc Digits 0.9681 Loss Digits 0.1124\n",
+ "Epoch [2], Acc Odd/Even 0.9852 Loss Odd/Even 0.0418\n",
+ "Epoch [2], Testing Accuracies (('digits', 0.9580328525641025), ('odd_even', 0.9846754807692307))\n",
+ "Epoch [3], Acc Digits 0.9734 Loss Digits 0.0961\n",
+ "Epoch [3], Acc Odd/Even 0.9884 Loss Odd/Even 0.0340\n",
+ "Epoch [3], Testing Accuracies (('digits', 0.9670472756410257), ('odd_even', 0.9839743589743589))\n",
+ "Epoch [4], Acc Digits 0.9762 Loss Digits 0.0848\n",
+ "Epoch [4], Acc Odd/Even 0.9894 Loss Odd/Even 0.0310\n",
+ "Epoch [4], Testing Accuracies (('digits', 0.9652887658227848), ('odd_even', 0.9858583860759493))\n"
+ ]
+ }
+ ],
+ "source": [
+ "for e in range(epochs):\n",
+ " # Accuracies for each task\n",
+ " acc_digits = mx.metric.Accuracy(name='digits')\n",
+ " acc_odd_even = mx.metric.Accuracy(name='odd_even')\n",
+ " # Accumulative losses\n",
+ " l_digits_ = 0.\n",
+ " l_odd_even_ = 0. \n",
+ " \n",
+ " for i, (data, label_digit, label_odd_even) in enumerate(train_data):\n",
+ " data = data.as_in_context(ctx)\n",
+ " label_digit = label_digit.as_in_context(ctx)\n",
+ " label_odd_even = label_odd_even.as_in_context(ctx).reshape(-1,1)\n",
+ " \n",
+ " with autograd.record():\n",
+ " output_digit, output_odd_even = net(data)\n",
+ " l_digits = loss_digits(output_digit, label_digit)\n",
+ " l_odd_even = loss_odd_even(output_odd_even, label_odd_even)\n",
+ "\n",
+ " # Combine the loss of each task\n",
+ " l_combined = (1-alpha)*l_digits + alpha*l_odd_even\n",
+ " \n",
+ " l_combined.backward()\n",
+ " trainer.step(data.shape[0])\n",
+ " \n",
+ " l_digits_ += l_digits.mean()\n",
+ " l_odd_even_ += l_odd_even.mean()\n",
+ " acc_digits.update(label_digit, output_digit.softmax())\n",
+ " acc_odd_even.update(label_odd_even, output_odd_even.sigmoid() > 0.5)\n",
+ " \n",
+ " print(\"Epoch [{}], Acc Digits {:.4f} Loss Digits {:.4f}\".format(\n",
+ " e, acc_digits.get()[1], l_digits_.asscalar()/(i+1)))\n",
+ " print(\"Epoch [{}], Acc Odd/Even {:.4f} Loss Odd/Even {:.4f}\".format(\n",
+ " e, acc_odd_even.get()[1], l_odd_even_.asscalar()/(i+1)))\n",
+ " print(\"Epoch [{}], Testing Accuracies {}\".format(e, evaluate_accuracy(net, test_data)))\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Testing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 144,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_random_data():\n",
+ " idx = random.randint(0, len(test_dataset))\n",
+ "\n",
+ " img = test_dataset[idx][0]\n",
+ " data, _, _ = test_dataset_t[idx]\n",
+ " data = data.as_in_context(ctx).expand_dims(axis=0)\n",
+ "\n",
+ " plt.imshow(img.squeeze().asnumpy(), cmap='gray')\n",
+ " \n",
+ " return data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 152,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Predicted digit: [9.], odd: [1.]\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "data = get_random_data()\n",
+ "\n",
+ "digit, odd_even = net(data)\n",
+ "\n",
+ "digit = digit.argmax(axis=1)[0].asnumpy()\n",
+ "odd_even = (odd_even.sigmoid()[0] > 0.5).asnumpy()\n",
+ "\n",
+ "print(\"Predicted digit: {}, odd: {}\".format(digit, odd_even))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/example/multivariate_time_series/README.md b/example/multivariate_time_series/README.md
index 704c86ae770e..87baca36d35f 100644
--- a/example/multivariate_time_series/README.md
+++ b/example/multivariate_time_series/README.md
@@ -3,6 +3,8 @@
- This repo contains an MXNet implementation of [this](https://arxiv.org/pdf/1703.07015.pdf) state of the art time series forecasting model.
- You can find my blog post on the model [here](https://opringle.github.io/2018/01/05/deep_learning_multivariate_ts.html)
+- A Gluon implementation is available [here](https://github.com/safrooze/LSTNet-Gluon)
+
![](./docs/model_architecture.png)
## Running the code
@@ -22,7 +24,7 @@
## Hyperparameters
-The default arguements in `lstnet.py` achieve equivolent performance to the published results. For other datasets, the following hyperparameters provide a good starting point:
+The default arguements in `lstnet.py` achieve equivalent performance to the published results. For other datasets, the following hyperparameters provide a good starting point:
- q = {2^0, 2^1, ... , 2^9} (1 week is typical value)
- Convolutional num filters = {50, 100, 200}
diff --git a/example/named_entity_recognition/README.md b/example/named_entity_recognition/README.md
index 260c19d5ffb4..c914a6985dfe 100644
--- a/example/named_entity_recognition/README.md
+++ b/example/named_entity_recognition/README.md
@@ -11,9 +11,16 @@ To reproduce the preprocessed training data:
1. Download and unzip the data: https://www.kaggle.com/abhinavwalia95/entity-annotated-corpus/downloads/ner_dataset.csv
2. Move ner_dataset.csv into `./data`
-3. create `./preprocessed_data` directory
3. `$ cd src && python preprocess.py`
To train the model:
-- `$ cd src && python ner.py`
\ No newline at end of file
+- `$ cd src && python ner.py`
+
+To run inference using trained model:
+
+1. Recreate the bucketing module using `sym_gen` defined in `ner.py`
+2. Loading saved parameters using `module.set_params()`
+
+Refer to the `test` function in the [Bucketing Module example](https://github.com/apache/incubator-mxnet/blob/master/example/rnn/bucketing/cudnn_rnn_bucketing.py)
+and this [issue](https://github.com/apache/incubator-mxnet/issues/5008) on Bucketing Module Prediction
\ No newline at end of file
diff --git a/example/named_entity_recognition/src/metrics.py b/example/named_entity_recognition/src/metrics.py
index 40c5015e81be..d3d73782c62e 100644
--- a/example/named_entity_recognition/src/metrics.py
+++ b/example/named_entity_recognition/src/metrics.py
@@ -27,7 +27,7 @@ def load_obj(name):
with open(name + '.pkl', 'rb') as f:
return pickle.load(f)
-tag_dict = load_obj("../preprocessed_data/tag_to_index")
+tag_dict = load_obj("../data/tag_to_index")
not_entity_index = tag_dict["O"]
def classifer_metrics(label, pred):
diff --git a/example/named_entity_recognition/src/ner.py b/example/named_entity_recognition/src/ner.py
index 561db4c43d9e..7f5dd84527cc 100644
--- a/example/named_entity_recognition/src/ner.py
+++ b/example/named_entity_recognition/src/ner.py
@@ -34,7 +34,7 @@
parser = argparse.ArgumentParser(description="Deep neural network for multivariate time series forecasting",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-parser.add_argument('--data-dir', type=str, default='../preprocessed_data',
+parser.add_argument('--data-dir', type=str, default='../data',
help='relative path to input data')
parser.add_argument('--output-dir', type=str, default='../results',
help='directory to save model files to')
diff --git a/example/nce-loss/README.md b/example/nce-loss/README.md
index 70730b477291..56e43525a7ca 100644
--- a/example/nce-loss/README.md
+++ b/example/nce-loss/README.md
@@ -29,7 +29,7 @@ The dataset used in the following examples is [text8](http://mattmahoney.net/dc/
* word2vec.py: a CBOW word2vec example using nce loss. You need to [download the text8 dataset](#dataset-download) before running this script. Command to start training on CPU (pass -g for training on GPU):
```
-python word2vec.py
+python wordvec.py
```
diff --git a/example/notebooks/README.md b/example/notebooks/README.md
deleted file mode 100644
index 27ff7fabbe6d..000000000000
--- a/example/notebooks/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Moved to
-https://github.com/dmlc/mxnet-notebooks/tree/master/python/moved-from-mxnet/
-
-This folder will be removed soon.
diff --git a/example/numpy-ops/numpy_softmax.py b/example/numpy-ops/numpy_softmax.py
deleted file mode 100644
index 88d247349292..000000000000
--- a/example/numpy-ops/numpy_softmax.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: skip-file
-import mxnet as mx
-from mxnet.test_utils import get_mnist_iterator
-import numpy as np
-import logging
-
-
-class NumpySoftmax(mx.operator.NumpyOp):
- def __init__(self):
- super(NumpySoftmax, self).__init__(False)
-
- def list_arguments(self):
- return ['data', 'label']
-
- def list_outputs(self):
- return ['output']
-
- def infer_shape(self, in_shape):
- data_shape = in_shape[0]
- label_shape = (in_shape[0][0],)
- output_shape = in_shape[0]
- return [data_shape, label_shape], [output_shape]
-
- def forward(self, in_data, out_data):
- x = in_data[0]
- y = out_data[0]
- y[:] = np.exp(x - x.max(axis=1).reshape((x.shape[0], 1)))
- y /= y.sum(axis=1).reshape((x.shape[0], 1))
-
- def backward(self, out_grad, in_data, out_data, in_grad):
- l = in_data[1]
- l = l.reshape((l.size,)).astype(np.int)
- y = out_data[0]
- dx = in_grad[0]
- dx[:] = y
- dx[np.arange(l.shape[0]), l] -= 1.0
-
-# define mlp
-
-data = mx.symbol.Variable('data')
-fc1 = mx.symbol.FullyConnected(data = data, name='fc1', num_hidden=128)
-act1 = mx.symbol.Activation(data = fc1, name='relu1', act_type="relu")
-fc2 = mx.symbol.FullyConnected(data = act1, name = 'fc2', num_hidden = 64)
-act2 = mx.symbol.Activation(data = fc2, name='relu2', act_type="relu")
-fc3 = mx.symbol.FullyConnected(data = act2, name='fc3', num_hidden=10)
-#mlp = mx.symbol.Softmax(data = fc3, name = 'mlp')
-mysoftmax = NumpySoftmax()
-mlp = mysoftmax(data=fc3, name = 'softmax')
-
-# data
-
-train, val = get_mnist_iterator(batch_size=100, input_shape = (784,))
-
-# train
-
-logging.basicConfig(level=logging.DEBUG)
-
-# MXNET_CPU_WORKER_NTHREADS must be greater than 1 for custom op to work on CPU
-context=mx.cpu()
-# Uncomment this line to train on GPU instead of CPU
-# context=mx.gpu(0)
-
-mod = mx.mod.Module(mlp, context=context)
-
-mod.fit(train_data=train, eval_data=val, optimizer='sgd',
- optimizer_params={'learning_rate':0.1, 'momentum': 0.9, 'wd': 0.00001},
- num_epoch=10, batch_end_callback=mx.callback.Speedometer(100, 100))
diff --git a/example/onnx/super_resolution.py b/example/onnx/super_resolution.py
deleted file mode 100644
index fcb8ccc88edb..000000000000
--- a/example/onnx/super_resolution.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-"""Testing super_resolution model conversion"""
-from __future__ import absolute_import as _abs
-from __future__ import print_function
-from collections import namedtuple
-import logging
-import numpy as np
-from PIL import Image
-import mxnet as mx
-from mxnet.test_utils import download
-import mxnet.contrib.onnx as onnx_mxnet
-
-# set up logger
-logging.basicConfig()
-LOGGER = logging.getLogger()
-LOGGER.setLevel(logging.INFO)
-
-def import_onnx():
- """Import the onnx model into mxnet"""
- model_url = 'https://s3.amazonaws.com/onnx-mxnet/examples/super_resolution.onnx'
- download(model_url, 'super_resolution.onnx')
-
- LOGGER.info("Converting onnx format to mxnet's symbol and params...")
- sym, arg_params, aux_params = onnx_mxnet.import_model('super_resolution.onnx')
- LOGGER.info("Successfully Converted onnx format to mxnet's symbol and params...")
- return sym, arg_params, aux_params
-
-def get_test_image():
- """Download and process the test image"""
- # Load test image
- input_image_dim = 224
- img_url = 'https://s3.amazonaws.com/onnx-mxnet/examples/super_res_input.jpg'
- download(img_url, 'super_res_input.jpg')
- img = Image.open('super_res_input.jpg').resize((input_image_dim, input_image_dim))
- img_ycbcr = img.convert("YCbCr")
- img_y, img_cb, img_cr = img_ycbcr.split()
- input_image = np.array(img_y)[np.newaxis, np.newaxis, :, :]
- return input_image, img_cb, img_cr
-
-def perform_inference(sym, arg_params, aux_params, input_img, img_cb, img_cr):
- """Perform inference on image using mxnet"""
- metadata = onnx_mxnet.get_model_metadata('super_resolution.onnx')
- data_names = [input_name[0] for input_name in metadata.get('input_tensor_data')]
- # create module
- mod = mx.mod.Module(symbol=sym, data_names=data_names, label_names=None)
- mod.bind(for_training=False, data_shapes=[(data_names[0], input_img.shape)])
- mod.set_params(arg_params=arg_params, aux_params=aux_params)
-
- # run inference
- batch = namedtuple('Batch', ['data'])
- mod.forward(batch([mx.nd.array(input_img)]))
-
- # Save the result
- img_out_y = Image.fromarray(np.uint8(mod.get_outputs()[0][0][0].
- asnumpy().clip(0, 255)), mode='L')
-
- result_img = Image.merge(
- "YCbCr", [img_out_y,
- img_cb.resize(img_out_y.size, Image.BICUBIC),
- img_cr.resize(img_out_y.size, Image.BICUBIC)]).convert("RGB")
- output_img_dim = 672
- assert result_img.size == (output_img_dim, output_img_dim)
- LOGGER.info("Super Resolution example success.")
- result_img.save("super_res_output.jpg")
- return result_img
-
-if __name__ == '__main__':
- MX_SYM, MX_ARG_PARAM, MX_AUX_PARAM = import_onnx()
- INPUT_IMG, IMG_CB, IMG_CR = get_test_image()
- perform_inference(MX_SYM, MX_ARG_PARAM, MX_AUX_PARAM, INPUT_IMG, IMG_CB, IMG_CR)
diff --git a/example/python-howto/README.md b/example/python-howto/README.md
deleted file mode 100644
index 29652408e02d..000000000000
--- a/example/python-howto/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-Python Howto Examples
-=====================
-
-* [Configuring Net to Get Multiple Ouputs](multiple_outputs.py)
-* [Configuring Image Record Iterator](data_iter.py)
-* [Monitor Intermediate Outputs in the Network](monitor_weights.py)
-* Set break point in C++ code of the symbol using gdb under Linux:
-
- * Build mxnet with following values:
-
- ```
- DEBUG=1
- USE_CUDA=0 # to make sure convolution-inl.h will be used
- USE_CUDNN=0 # to make sure convolution-inl.h will be used
- ```
-
- * run python under gdb: ```gdb --args python debug_conv.py```
- * in gdb set break point on particular line of the code and run execution:
-
-```
-(gdb) break src/operator/convolution-inl.h:120
-(gdb) run
-Breakpoint 1, mxnet::op::ConvolutionOp::Forward (this=0x12219d0, ctx=..., in_data=std::vector of length 3, capacity 4 = {...}, req=std::vector of length 1, capacity 1 = {...}, out_data=std::vector of length 1, capacity 1 = {...},
- aux_args=std::vector of length 0, capacity 0) at src/operator/./convolution-inl.h:121
-121 data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1]);
-(gdb) list
-116 }
-117 Tensor data = in_data[conv::kData].get(s);
-118 Shape<3> wmat_shape =
-119 Shape3(param_.num_group,
-120 param_.num_filter / param_.num_group,
-121 data.shape_[1] / param_.num_group * param_.kernel[0] * param_.kernel[1]);
-122 Tensor wmat =
-123 in_data[conv::kWeight].get_with_shape(wmat_shape, s);
-124 Tensor out = out_data[conv::kOut].get(s);
-125 #if defined(__CUDACC__)
-```
diff --git a/example/python-howto/data_iter.py b/example/python-howto/data_iter.py
deleted file mode 100644
index 81c8988a8e51..000000000000
--- a/example/python-howto/data_iter.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-"""Create a Cifar data iterator.
-
-This example shows how to create a iterator reading from recordio,
-introducing image augmentations and using a backend thread to hide IO cost.
-All you need to do is to set some parameters.
-"""
-import mxnet as mx
-
-dataiter = mx.io.ImageRecordIter(
- # Dataset Parameter
- # Impulsary
- # indicating the data file, please check the data is already there
- path_imgrec="data/cifar/train.rec",
- # Dataset/Augment Parameter
- # Impulsary
- # indicating the image size after preprocessing
- data_shape=(3,28,28),
- # Batch Parameter
- # Impulsary
- # tells how many images in a batch
- batch_size=100,
- # Augmentation Parameter
- # Optional
- # when offers mean_img, each image will subtract the mean value at each pixel
- mean_img="data/cifar/cifar10_mean.bin",
- # Augmentation Parameter
- # Optional
- # randomly crop a patch of the data_shape from the original image
- rand_crop=True,
- # Augmentation Parameter
- # Optional
- # randomly mirror the image horizontally
- rand_mirror=True,
- # Augmentation Parameter
- # Optional
- # randomly shuffle the data
- shuffle=False,
- # Backend Parameter
- # Optional
- # Preprocessing thread number
- preprocess_threads=4,
- # Backend Parameter
- # Optional
- # Prefetch buffer size
- prefetch_buffer=4,
- # Backend Parameter,
- # Optional
- # Whether round batch,
- round_batch=True)
-
-batchidx = 0
-for dbatch in dataiter:
- data = dbatch.data[0]
- label = dbatch.label[0]
- pad = dbatch.pad
- index = dbatch.index
- print("Batch", batchidx)
- print(label.asnumpy().flatten())
- batchidx += 1
diff --git a/example/python-howto/monitor_weights.py b/example/python-howto/monitor_weights.py
deleted file mode 100644
index 929b0e7bf79d..000000000000
--- a/example/python-howto/monitor_weights.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: skip-file
-import mxnet as mx
-from mxnet.test_utils import get_mnist_iterator
-import numpy as np
-import logging
-
-# network
-data = mx.symbol.Variable('data')
-fc1 = mx.symbol.FullyConnected(data = data, name='fc1', num_hidden=128)
-act1 = mx.symbol.Activation(data = fc1, name='relu1', act_type="relu")
-fc2 = mx.symbol.FullyConnected(data = act1, name = 'fc2', num_hidden = 64)
-act2 = mx.symbol.Activation(data = fc2, name='relu2', act_type="relu")
-fc3 = mx.symbol.FullyConnected(data = act2, name='fc3', num_hidden=10)
-mlp = mx.symbol.SoftmaxOutput(data = fc3, name = 'softmax')
-
-# data
-train, val = get_mnist_iterator(batch_size=100, input_shape = (784,))
-
-# monitor
-def norm_stat(d):
- return mx.nd.norm(d)/np.sqrt(d.size)
-mon = mx.mon.Monitor(100, norm_stat)
-
-# train with monitor
-logging.basicConfig(level=logging.DEBUG)
-module = mx.module.Module(context=mx.cpu(), symbol=mlp)
-module.fit(train_data=train, eval_data=val, monitor=mon, num_epoch=2,
- batch_end_callback = mx.callback.Speedometer(100, 100),
- optimizer_params=(('learning_rate', 0.1), ('momentum', 0.9), ('wd', 0.00001)))
diff --git a/example/quantization/README.md b/example/quantization/README.md
index 63b65574d3ac..0e63cd84ede7 100644
--- a/example/quantization/README.md
+++ b/example/quantization/README.md
@@ -1,4 +1,305 @@
# Model Quantization with Calibration Examples
+
+This folder contains examples of quantizing a FP32 model with Intel® MKL-DNN or CUDNN.
+
+Contents
+
+* [1. Model Quantization with Intel® MKL-DNN](#1)
+* [2. Model Quantization with CUDNN](#2)
+
+Model Quantization with Intel® MKL-DNN
+
+Intel® MKL-DNN supports quantization with subgraph features on Intel® CPU Platform and can bring performance improvements on the [Intel® Xeon® Scalable Platform](https://www.intel.com/content/www/us/en/processors/xeon/scalable/xeon-scalable-platform.html). A new quantization script `imagenet_gen_qsym_mkldnn.py` has been designed to launch quantization for CNN models with Intel® MKL-DNN. This script integrates with [Gluon-CV modelzoo](https://gluon-cv.mxnet.io/model_zoo/classification.html), so that more pre-trained models can be downloaded from Gluon-CV and then converted for quantization. This script also supports custom models.
+
+Calibration is used for generating a calibration table for the quantized symbol. The quantization script supports three methods:
+
+- **none:** No calibration will be used. The thresholds for quantization will be calculated on the fly. This will result in inference speed slowdown and loss of accuracy in general.
+- **naive:** Simply take min and max values of layer outputs as thresholds for quantization. In general, the inference accuracy worsens with more examples used in calibration. It is recommended to use `entropy` mode as it produces more accurate inference results.
+- **entropy:** Calculate KL divergence of the fp32 output and quantized output for optimal thresholds. This mode is expected to produce the best inference accuracy of all three kinds of quantized models if the calibration dataset is representative enough of the inference dataset.
+
+Use the following command to install [Gluon-CV](https://gluon-cv.mxnet.io/):
+
+```
+pip install gluoncv
+```
+
+The following models have been tested on Linux systems.
+
+| Model | Source | Dataset | FP32 Accuracy (top-1/top-5)| INT8 Accuracy (top-1/top-5)|
+|:---|:---|---|:---:|:---:|
+| [ResNet50-V1](#3) | [Gluon-CV](https://gluon-cv.mxnet.io/model_zoo/classification.html) | [Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec) | 75.87%/92.72% | 75.71%/92.65% |
+| [ResNet101-V1](#4) | [Gluon-CV](https://gluon-cv.mxnet.io/model_zoo/classification.html) | [Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec) | 77.3%/93.58% | 77.09%/93.41% |
+|[Squeezenet 1.0](#5)|[Gluon-CV](https://gluon-cv.mxnet.io/model_zoo/classification.html)|[Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec)|57.01%/79.71%|56.62%/79.55%|
+|[MobileNet 1.0](#6)|[Gluon-CV](https://gluon-cv.mxnet.io/model_zoo/classification.html)|[Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec)|69.76%/89.32%|69.61%/89.09%|
+|[Inception V3](#7)|[Gluon-CV](https://gluon-cv.mxnet.io/model_zoo/classification.html)|[Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec)|76.49%/93.10% |76.38%/93% |
+|[ResNet152-V2](#8)|[MXNet ModelZoo](http://data.mxnet.io/models/imagenet/resnet/152-layers/)|[Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec)|76.76%/93.03%|76.48%/92.96%|
+|[Inception-BN](#9)|[MXNet ModelZoo](http://data.mxnet.io/models/imagenet/inception-bn/)|[Validation Dataset](http://data.mxnet.io/data/val_256_q90.rec)|72.09%/90.60%|72.00%/90.53%|
+| [SSD-VGG](#10) | [example/ssd](https://github.com/apache/incubator-mxnet/tree/master/example/ssd) | VOC2007/2012 | 0.83 mAP | 0.82 mAP |
+
+ResNet50-V1
+
+The following command is to download the pre-trained model from Gluon-CV and transfer it into the symbolic model which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=resnet50_v1 --num-calib-batches=5 --calib-mode=naive
+```
+
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/resnet50_v1-symbol.json --param-file=./model/resnet50_v1-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/resnet50_v1-quantized-5batches-naive-symbol.json --param-file=./model/resnet50_v1-quantized-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/resnet50_v1-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/resnet50_v1-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+ResNet101-V1
+
+The following command is to download the pre-trained model from Gluon-CV and transfer it into the symbolic model which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=resnet101_v1 --num-calib-batches=5 --calib-mode=naive
+```
+
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/resnet101_v1-symbol.json --param-file=./model/resnet101_v1-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/resnet101_v1-quantized-5batches-naive-symbol.json --param-file=./model/resnet101_v1-quantized-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/resnet101_v1-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/resnet101_v1-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+SqueezeNet 1.0
+
+The following command is to download the pre-trained model from Gluon-CV and transfer it into the symbolic model which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=squeezenet1.0 --num-calib-batches=5 --calib-mode=naive
+```
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/squeezenet1.0-symbol.json --param-file=./model/squeezenet1.0-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/squeezenet1.0-quantized-5batches-naive-symbol.json --param-file=./model/squeezenet1.0-quantized-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/squeezenet1.0-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/squeezenet1.0-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+MobileNet 1.0
+
+The following command is to download the pre-trained model from Gluon-CV and transfer it into the symbolic model which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=mobilenet1.0 --num-calib-batches=5 --calib-mode=naive
+```
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/mobilenet1.0-symbol.json --param-file=./model/mobilenet1.0-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/mobilenet1.0-quantized-5batches-naive-symbol.json --param-file=./model/mobilenet1.0-quantized-0000.params --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/mobilenet1.0-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/mobilenet1.0-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+Inception-V3
+
+The following command is to download the pre-trained model from Gluon-CV and transfer it into the symbolic model which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=inceptionv3 --image-shape=3,299,299 --num-calib-batches=5 --calib-mode=naive
+```
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/inceptionv3-symbol.json --param-file=./model/inceptionv3-0000.params --image-shape=3,299,299 --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/inceptionv3-quantized-5batches-naive-symbol.json --param-file=./model/inceptionv3-quantized-0000.params --image-shape=3,299,299 --rgb-mean=123.68,116.779,103.939 --rgb-std=58.393,57.12,57.375 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/inceptionv3-symbol.json --image-shape=3,299,299 --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/inceptionv3-quantized-5batches-naive-symbol.json --image-shape=3,299,299 --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+ResNet152-V2
+
+The following command is to download the pre-trained model from the [MXNet ModelZoo](http://data.mxnet.io/models/imagenet/resnet/152-layers/) which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=imagenet1k-resnet-152 --num-calib-batches=5 --calib-mode=naive
+```
+
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/imagenet1k-resnet-152-symbol.json --param-file=./model/imagenet1k-resnet-152-0000.params --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/imagenet1k-resnet-152-quantized-5batches-naive-symbol.json --param-file=./model/imagenet1k-resnet-152-quantized-0000.params --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/imagenet1k-resnet-152-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/imagenet1k-resnet-152-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+Inception-BN
+
+The following command is to download the pre-trained model from the [MXNet ModelZoo](http://data.mxnet.io/models/imagenet/inception-bn/) which would be finally quantized. The [validation dataset](http://data.mxnet.io/data/val_256_q90.rec) is available for testing the pre-trained models:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=imagenet1k-inception-bn --num-calib-batches=5 --calib-mode=naive
+```
+
+The model would be automatically replaced in fusion and quantization format. It is then saved as the quantized symbol and parameter files in the `./model` directory. The following command is to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/imagenet1k-inception-bn-symbol.json --param-file=./model/imagenet1k-inception-bn-0000.params --rgb-mean=123.68,116.779,103.939 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/imagenet1k-inception-bn-quantized-5batches-naive-symbol.json --param-file=./model/imagenet1k-inception-bn-quantized-0000.params --rgb-mean=123.68,116.779,103.939 --num-skipped-batches=50 --batch-size=64 --num-inference-batches=500 --dataset=./data/val_256_q90.rec --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/imagenet1k-inception-bn-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+python imagenet_inference.py --symbol-file=./model/imagenet1k-inception-bn-quantized-5batches-naive-symbol.json --batch-size=64 --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+SSD-VGG
+
+Follow the [SSD example's instructions](https://github.com/apache/incubator-mxnet/tree/master/example/ssd#train-the-model) in [example/ssd](https://github.com/apache/incubator-mxnet/tree/master/example/ssd) to train a FP32 `SSD-VGG16_reduced_300x300` model based on Pascal VOC dataset. You can also download our [SSD-VGG16 pre-trained model](http://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/ssd_vgg16_reduced_300-dd479559.zip) and [packed binary data](http://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/dataset/ssd-val-fc19a535.zip). Extract the zip files, then rename the directories to `model` and `data` respectively. Then, rename the files in directories as follows.
+
+```
+data/
+|---val.rec
+|---val.lxt
+|---val.idx
+model/
+|---ssd_vgg16_reduced_300.params
+|---ssd_vgg16_reduced_300-symbol.json
+```
+
+Then, use the following command for quantization. By default, this script uses 5 batches (32 samples per batch) for naive calibration:
+
+```
+python quantization.py
+```
+
+After quantization, INT8 models will be saved in `model/` dictionary. Use the following command to launch inference.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python evaluate.py --cpu --num-batch 10 --batch-size 224 --deploy --prefix=./model/ssd_
+
+# Launch INT8 Inference
+python evaluate.py --cpu --num-batch 10 --batch-size 224 --deploy --prefix=./model/cqssd_
+
+# Launch dummy data Inference
+python benchmark_score.py --deploy --prefix=./model/ssd_
+python benchmark_score.py --deploy --prefix=./model/cqssd_
+```
+
+Custom Model
+
+This script also supports custom symbolic models. You can easily add some quantization layer configs in `imagenet_gen_qsym_mkldnn.py` like below:
+
+```
+elif args.model == 'custom':
+ # add rgb mean/std of your model.
+ rgb_mean = '0,0,0'
+ rgb_std = '0,0,0'
+ calib_layer = lambda name: name.endswith('_output')
+ # add layer names you donnot want to quantize.
+ # add conv/pool layer names that has negative inputs
+ # since Intel® MKL-DNN only support uint8 quantization temporary.
+ # add all fc layer names since Intel® MKL-DNN does not support temporary.
+ excluded_sym_names += ['layers']
+ # add your first conv layer names since Intel® MKL-DNN only support uint8 quantization temporary.
+ if exclude_first_conv:
+ excluded_sym_names += ['layers']
+```
+
+Some tips on quantization configs:
+
+1. First, you should prepare your data, symbol file (custom-symbol.json) and parameter file (custom-0000.params) of your fp32 symbolic model.
+2. Then, you should run the following command and verify that your fp32 symbolic model runs inference as expected.
+
+```
+# USE MKLDNN AS SUBGRAPH BACKEND
+export MXNET_SUBGRAPH_BACKEND=MKLDNN
+
+# Launch FP32 Inference
+python imagenet_inference.py --symbol-file=./model/custom-symbol.json --param-file=./model/custom-0000.params --rgb-mean=* --rgb-std=* --num-skipped-batches=* --batch-size=* --num-inference-batches=*--dataset=./data/* --ctx=cpu --data-nthreads=1
+```
+
+3. Then, you should add `rgb_mean`, `rgb_std` and `excluded_sym_names` in this script. Notice that you should exclude conv/pool layers that have negative data since Intel® MKL-DNN only supports `uint8` quantization temporarily. You should also exclude all fc layers in your model.
+
+4. Then, you can run the following command for quantization:
+
+```
+python imagenet_gen_qsym_mkldnn.py --model=custom --num-calib-batches=5 --calib-mode=naive
+```
+
+5. After quantization, the quantized symbol and parameter files will be saved in the `model/` directory.
+
+6. Finally, you can run INT8 inference:
+
+```
+# Launch INT8 Inference
+python imagenet_inference.py --symbol-file=./model/*.json --param-file=./model/*.params --rgb-mean=* --rgb-std=* --num-skipped-batches=* --batch-size=* --num-inference-batches=*--dataset=./data/* --ctx=cpu --data-nthreads=1
+
+# Launch dummy data Inference
+python imagenet_inference.py --symbol-file=./model/*.json --batch-size=* --num-inference-batches=500 --ctx=cpu --benchmark=True
+```
+
+Model Quantization with CUDNN
+
This folder contains examples of quantizing a FP32 model with or without calibration and using the calibrated
quantized for inference. Two pre-trained imagenet models are taken as examples for quantization. One is
[Resnet-152](http://data.mxnet.io/models/imagenet/resnet/152-layers/), and the other one is
@@ -19,4 +320,6 @@ the console to run model quantization for a specific configuration.
- `launch_inference.sh` This is a shell script that calculate the accuracies of all the quantized models generated
by invoking `launch_quantize.sh`.
-**NOTE**: This example has only been tested on Linux systems.
\ No newline at end of file
+**NOTE**:
+- This example has only been tested on Linux systems.
+- Performance is expected to decrease with GPU, however the memory footprint of a quantized model is smaller. The purpose of the quantization implementation is to minimize accuracy loss when converting FP32 models to INT8. MXNet community is working on improving the performance.
\ No newline at end of file
diff --git a/example/quantization/imagenet_gen_qsym.py b/example/quantization/imagenet_gen_qsym.py
index 85474b663fae..8a2818c4bca0 100644
--- a/example/quantization/imagenet_gen_qsym.py
+++ b/example/quantization/imagenet_gen_qsym.py
@@ -92,7 +92,7 @@ def save_params(fname, arg_params, aux_params, logger=None):
' thresholds. This mode is expected to produce the best inference accuracy of all three'
' kinds of quantized models if the calibration dataset is representative enough of the'
' inference dataset.')
- parser.add_argument('--quantized-dtype', type=str, default='int8',
+ parser.add_argument('--quantized-dtype', type=str, default='int8',
choices=['int8', 'uint8'],
help='quantization destination data type for input data')
args = parser.parse_args()
diff --git a/example/quantization/imagenet_gen_qsym_mkldnn.py b/example/quantization/imagenet_gen_qsym_mkldnn.py
new file mode 100644
index 000000000000..9056f7904296
--- /dev/null
+++ b/example/quantization/imagenet_gen_qsym_mkldnn.py
@@ -0,0 +1,322 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import os
+import logging
+from common import modelzoo
+import mxnet as mx
+import gluoncv
+from mxnet import gluon, nd, image
+from gluoncv import utils
+from gluoncv.model_zoo import get_model
+from mxnet.contrib.quantization import *
+from mxnet.base import SymbolHandle, check_call, _LIB, mx_uint, c_str_array
+import ctypes
+
+
+def download_calib_dataset(dataset_url, calib_dataset, logger=None):
+ if logger is not None:
+ logger.info('Downloading calibration dataset from %s to %s' % (dataset_url, calib_dataset))
+ mx.test_utils.download(dataset_url, calib_dataset)
+
+
+def download_model(model_name, logger=None):
+ dir_path = os.path.dirname(os.path.realpath(__file__))
+ model_path = os.path.join(dir_path, 'model')
+ if logger is not None:
+ logger.info('Downloading model %s... into path %s' % (model_name, model_path))
+ return modelzoo.download_model(args.model, os.path.join(dir_path, 'model'))
+
+def convert_from_gluon(model_name, image_shape, classes=1000, logger=None):
+ dir_path = os.path.dirname(os.path.realpath(__file__))
+ model_path = os.path.join(dir_path, 'model')
+ if logger is not None:
+ logger.info('Converting model from Gluon-CV ModelZoo %s... into path %s' % (model_name, model_path))
+ net = get_model(name=model_name, classes=classes, pretrained=True)
+ net.hybridize()
+ x = mx.sym.var('data')
+ y = net(x)
+ y = mx.sym.SoftmaxOutput(data=y, name='softmax')
+ symnet = mx.symbol.load_json(y.tojson())
+ params = net.collect_params()
+ args = {}
+ auxs = {}
+ for param in params.values():
+ v = param._reduce()
+ k = param.name
+ if 'running' in k:
+ auxs[k] = v
+ else:
+ args[k] = v
+ mod = mx.mod.Module(symbol=symnet, context=mx.cpu(),
+ label_names = ['softmax_label'])
+ mod.bind(for_training=False,
+ data_shapes=[('data', (1,) +
+ tuple([int(i) for i in image_shape.split(',')]))])
+ mod.set_params(arg_params=args, aux_params=auxs)
+ dst_dir = os.path.join(dir_path, 'model')
+ prefix = os.path.join(dir_path, 'model', model_name)
+ if not os.path.isdir(dst_dir):
+ os.mkdir(dst_dir)
+ mod.save_checkpoint(prefix, 0)
+ return prefix
+
+def save_symbol(fname, sym, logger=None):
+ if logger is not None:
+ logger.info('Saving symbol into file at %s' % fname)
+ sym.save(fname)
+
+
+def save_params(fname, arg_params, aux_params, logger=None):
+ if logger is not None:
+ logger.info('Saving params into file at %s' % fname)
+ save_dict = {('arg:%s' % k): v.as_in_context(cpu()) for k, v in arg_params.items()}
+ save_dict.update({('aux:%s' % k): v.as_in_context(cpu()) for k, v in aux_params.items()})
+ mx.nd.save(fname, save_dict)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Generate a calibrated quantized model from a FP32 model with Intel MKL-DNN support')
+ parser.add_argument('--model', type=str, choices=['resnet50_v1',
+ 'resnet101_v1',
+ 'inceptionv3',
+ 'squeezenet1.0',
+ 'mobilenet1.0',
+ 'imagenet1k-resnet-152',
+ 'imagenet1k-inception-bn',
+ 'custom'],
+ help='currently only supports imagenet1k-resnet-50_v1, imagenet1k-resnet-152 or imagenet1k-inception-bn.'
+ 'you can set to custom to load your pre-trained model.')
+ parser.add_argument('--use-gluon-model', type=bool, default=False,
+ help='If enabled, will download pretrained model from Gluon-CV '
+ 'and convert to symbolic model ')
+ parser.add_argument('--batch-size', type=int, default=32)
+ parser.add_argument('--label-name', type=str, default='softmax_label')
+ parser.add_argument('--calib-dataset', type=str, default='data/val_256_q90.rec',
+ help='path of the calibration dataset')
+ parser.add_argument('--image-shape', type=str, default='3,224,224')
+ parser.add_argument('--data-nthreads', type=int, default=60,
+ help='number of threads for data decoding')
+ parser.add_argument('--num-calib-batches', type=int, default=10,
+ help='number of batches for calibration')
+ parser.add_argument('--exclude-first-conv', action='store_true', default=True,
+ help='excluding quantizing the first conv layer since the'
+ ' input data may have negative value which doesn\'t support at moment' )
+ parser.add_argument('--shuffle-dataset', action='store_true', default=True,
+ help='shuffle the calibration dataset')
+ parser.add_argument('--shuffle-chunk-seed', type=int, default=3982304,
+ help='shuffling chunk seed, see'
+ ' https://mxnet.incubator.apache.org/api/python/io/io.html?highlight=imager#mxnet.io.ImageRecordIter'
+ ' for more details')
+ parser.add_argument('--shuffle-seed', type=int, default=48564309,
+ help='shuffling seed, see'
+ ' https://mxnet.incubator.apache.org/api/python/io/io.html?highlight=imager#mxnet.io.ImageRecordIter'
+ ' for more details')
+ parser.add_argument('--calib-mode', type=str, default='entropy',
+ help='calibration mode used for generating calibration table for the quantized symbol; supports'
+ ' 1. none: no calibration will be used. The thresholds for quantization will be calculated'
+ ' on the fly. This will result in inference speed slowdown and loss of accuracy'
+ ' in general.'
+ ' 2. naive: simply take min and max values of layer outputs as thresholds for'
+ ' quantization. In general, the inference accuracy worsens with more examples used in'
+ ' calibration. It is recommended to use `entropy` mode as it produces more accurate'
+ ' inference results.'
+ ' 3. entropy: calculate KL divergence of the fp32 output and quantized output for optimal'
+ ' thresholds. This mode is expected to produce the best inference accuracy of all three'
+ ' kinds of quantized models if the calibration dataset is representative enough of the'
+ ' inference dataset.')
+ parser.add_argument('--quantized-dtype', type=str, default='uint8',
+ choices=['int8', 'uint8'],
+ help='quantization destination data type for input data')
+ parser.add_argument('--enable-calib-quantize', type=bool, default=True,
+ help='If enabled, the quantize op will '
+ 'be calibrated offline if calibration mode is '
+ 'enabled')
+ args = parser.parse_args()
+ ctx = mx.cpu(0)
+ logging.basicConfig()
+ logger = logging.getLogger('logger')
+ logger.setLevel(logging.INFO)
+
+ logger.info('shuffle_dataset=%s' % args.shuffle_dataset)
+
+ calib_mode = args.calib_mode
+ logger.info('calibration mode set to %s' % calib_mode)
+
+ # download calibration dataset
+ if calib_mode != 'none':
+ download_calib_dataset('http://data.mxnet.io/data/val_256_q90.rec', args.calib_dataset)
+
+ # download model
+ if args.model in ['resnet50_v1', 'resnet101_v1', 'squeezenet1.0', 'mobilenet1.0', 'inceptionv3']:
+ logger.info('model %s is converted from GluonCV' % args.model)
+ args.use_gluon_model = True
+ if args.use_gluon_model == True:
+ prefix = convert_from_gluon(model_name=args.model, image_shape=args.image_shape, classes=1000, logger=logger)
+ epoch = 0
+ sym, arg_params, aux_params = mx.model.load_checkpoint(prefix, epoch)
+ elif args.model == 'custom':
+ dir_path = os.path.dirname(os.path.realpath(__file__))
+ prefix = os.path.join(dir_path, 'model', args.model)
+ epoch = 0
+ sym, arg_params, aux_params = mx.model.load_checkpoint(prefix, epoch)
+ else:
+ prefix, epoch = download_model(model_name=args.model, logger=logger)
+ sym, arg_params, aux_params = mx.model.load_checkpoint(prefix, epoch)
+
+ sym = sym.get_backend_symbol('MKLDNN')
+
+ # get batch size
+ batch_size = args.batch_size
+ logger.info('batch size = %d for calibration' % batch_size)
+
+ # get number of batches for calibration
+ num_calib_batches = args.num_calib_batches
+ if calib_mode == 'none':
+ logger.info('skip calibration step as calib_mode is none')
+ else:
+ logger.info('number of batches = %d for calibration' % num_calib_batches)
+
+ # get number of threads for decoding the dataset
+ data_nthreads = args.data_nthreads
+
+ # get image shape
+ image_shape = args.image_shape
+
+ exclude_first_conv = args.exclude_first_conv
+ excluded_sym_names = []
+ if args.model == 'imagenet1k-resnet-152':
+ rgb_mean = '0,0,0'
+ rgb_std = '1,1,1'
+ calib_layer = lambda name: name.endswith('_output')
+ excluded_sym_names += ['flatten0', 'fc1', 'pooling0']
+ if exclude_first_conv:
+ excluded_sym_names += ['conv0']
+ elif args.model == 'imagenet1k-inception-bn':
+ rgb_mean = '123.68,116.779,103.939'
+ rgb_std = '1,1,1'
+ calib_layer = lambda name: name.endswith('_output')
+ excluded_sym_names += ['flatten', 'fc1']
+ if exclude_first_conv:
+ excluded_sym_names += ['conv_1']
+ elif args.model in ['resnet50_v1', 'resnet101_v1']:
+ rgb_mean = '123.68,116.779,103.939'
+ rgb_std = '58.393, 57.12, 57.375'
+ calib_layer = lambda name: name.endswith('_output')
+ excluded_sym_names += ['resnetv10_dense0_fwd', 'resnetv10_pool0_fwd']
+ if exclude_first_conv:
+ excluded_sym_names += ['resnetv10_conv0_fwd']
+ elif args.model == 'squeezenet1.0':
+ rgb_mean = '123.68,116.779,103.939'
+ rgb_std = '58.393, 57.12, 57.375'
+ calib_layer = lambda name: name.endswith('_output')
+ excluded_sym_names += ['squeezenet0_flatten0_flatten0',
+ 'squeezenet0_pool0_fwd',
+ 'squeezenet0_pool1_fwd',
+ 'squeezenet0_pool2_fwd',
+ 'squeezenet0_pool3_fwd']
+ if exclude_first_conv:
+ excluded_sym_names += ['squeezenet0_conv0_fwd']
+ elif args.model == 'mobilenet1.0':
+ rgb_mean = '123.68,116.779,103.939'
+ rgb_std = '58.393, 57.12, 57.375'
+ calib_layer = lambda name: name.endswith('_output')
+ excluded_sym_names += ['mobilenet0_flatten0_flatten0',
+ 'mobilenet0_dense0_fwd',
+ 'mobilenet0_pool0_fwd']
+ if exclude_first_conv:
+ excluded_sym_names += ['mobilenet0_conv0_fwd']
+ elif args.model == 'inceptionv3':
+ rgb_mean = '123.68,116.779,103.939'
+ rgb_std = '58.393, 57.12, 57.375'
+ calib_layer = lambda name: name.endswith('_output')
+ excluded_sym_names += ['inception30_dense0_fwd',
+ 'inception30_pool0_fwd']
+ if exclude_first_conv:
+ excluded_sym_names += ['inception30_conv0_fwd']
+ elif args.model == 'custom':
+ # add rgb mean/std of your model.
+ rgb_mean = '0,0,0'
+ rgb_std = '0,0,0'
+ calib_layer = lambda name: name.endswith('_output')
+ # add layer names you donnot want to quantize.
+ # add conv/pool layer names that has negative inputs
+ # since Intel MKL-DNN only support uint8 quantization temporary.
+ # add all fc layer names since Intel MKL-DNN does not support temporary.
+ excluded_sym_names += ['layers']
+ # add your first conv layer names since Intel MKL-DNN only support uint8 quantization temporary.
+ if exclude_first_conv:
+ excluded_sym_names += ['layers']
+ else:
+ raise ValueError('model %s is not supported in this script' % args.model)
+
+ label_name = args.label_name
+ logger.info('label_name = %s' % label_name)
+
+ data_shape = tuple([int(i) for i in image_shape.split(',')])
+ logger.info('Input data shape = %s' % str(data_shape))
+
+ logger.info('rgb_mean = %s' % rgb_mean)
+ rgb_mean = [float(i) for i in rgb_mean.split(',')]
+ mean_args = {'mean_r': rgb_mean[0], 'mean_g': rgb_mean[1], 'mean_b': rgb_mean[2]}
+ logger.info('rgb_std = %s' % rgb_std)
+ rgb_std = [float(i) for i in rgb_std.split(',')]
+ std_args = {'std_r': rgb_std[0], 'std_g': rgb_std[1], 'std_b': rgb_std[2]}
+
+ if calib_mode == 'none':
+ logger.info('Quantizing FP32 model %s' % args.model)
+ qsym, qarg_params, aux_params = quantize_model(sym=sym, arg_params=arg_params, aux_params=aux_params,
+ ctx=ctx, excluded_sym_names=excluded_sym_names,
+ calib_mode=calib_mode, quantized_dtype=args.quantized_dtype,
+ logger=logger)
+ sym_name = '%s-symbol.json' % (prefix + '-quantized')
+ else:
+ logger.info('Creating ImageRecordIter for reading calibration dataset')
+ data = mx.io.ImageRecordIter(path_imgrec=args.calib_dataset,
+ label_width=1,
+ preprocess_threads=data_nthreads,
+ batch_size=batch_size,
+ data_shape=data_shape,
+ label_name=label_name,
+ rand_crop=False,
+ rand_mirror=False,
+ shuffle=args.shuffle_dataset,
+ shuffle_chunk_seed=args.shuffle_chunk_seed,
+ seed=args.shuffle_seed,
+ **mean_args,
+ **std_args)
+
+ qsym, qarg_params, aux_params = quantize_model(sym=sym, arg_params=arg_params, aux_params=aux_params,
+ ctx=ctx, excluded_sym_names=excluded_sym_names,
+ calib_mode=calib_mode, calib_data=data,
+ num_calib_examples=num_calib_batches * batch_size,
+ calib_layer=calib_layer, quantized_dtype=args.quantized_dtype,
+ label_names=(label_name,), calib_quantize_op = True,
+ logger=logger)
+ if calib_mode == 'entropy':
+ suffix = '-quantized-%dbatches-entropy' % num_calib_batches
+ elif calib_mode == 'naive':
+ suffix = '-quantized-%dbatches-naive' % num_calib_batches
+ else:
+ raise ValueError('unknow calibration mode %s received, only supports `none`, `naive`, and `entropy`'
+ % calib_mode)
+ sym_name = '%s-symbol.json' % (prefix + suffix)
+ qsym = qsym.get_backend_symbol('MKLDNN_POST_QUANTIZE')
+ save_symbol(sym_name, qsym, logger)
+ param_name = '%s-%04d.params' % (prefix + '-quantized', epoch)
+ save_params(param_name, qarg_params, aux_params, logger)
diff --git a/example/quantization/imagenet_inference.py b/example/quantization/imagenet_inference.py
index 85649530aa0b..3fdb52f40cb2 100644
--- a/example/quantization/imagenet_inference.py
+++ b/example/quantization/imagenet_inference.py
@@ -93,19 +93,51 @@ def score(sym, arg_params, aux_params, data, devs, label_name, max_num_examples,
if logger is not None:
logger.info('Finished inference with %d images' % num)
logger.info('Finished with %f images per second', speed)
+ logger.warn('Note: GPU performance is expected to be slower than CPU. Please refer quantization/README.md for details')
for m in metrics:
logger.info(m.get())
+def benchmark_score(symbol_file, ctx, batch_size, num_batches, logger=None):
+ # get mod
+ cur_path = os.path.dirname(os.path.realpath(__file__))
+ symbol_file_path = os.path.join(cur_path, symbol_file)
+ if logger is not None:
+ logger.info('Loading symbol from file %s' % symbol_file_path)
+ sym = mx.sym.load(symbol_file_path)
+ mod = mx.mod.Module(symbol=sym, context=ctx)
+ mod.bind(for_training = False,
+ inputs_need_grad = False,
+ data_shapes = [('data', (batch_size,)+data_shape)])
+ mod.init_params(initializer=mx.init.Xavier(magnitude=2.))
+
+ # get data
+ data = [mx.random.uniform(-1.0, 1.0, shape=shape, ctx=ctx) for _, shape in mod.data_shapes]
+ batch = mx.io.DataBatch(data, []) # empty label
+
+ # run
+ dry_run = 5 # use 5 iterations to warm up
+ for i in range(dry_run+num_batches):
+ if i == dry_run:
+ tic = time.time()
+ mod.forward(batch, is_train=False)
+ for output in mod.get_outputs():
+ output.wait_to_read()
+
+ # return num images per second
+ return num_batches*batch_size/(time.time() - tic)
+
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Score a model on a dataset')
parser.add_argument('--ctx', type=str, default='gpu')
+ parser.add_argument('--benchmark', type=bool, default=False, help='dummy data benchmark')
parser.add_argument('--symbol-file', type=str, required=True, help='symbol file path')
- parser.add_argument('--param-file', type=str, required=True, help='param file path')
+ parser.add_argument('--param-file', type=str, required=False, help='param file path')
parser.add_argument('--batch-size', type=int, default=32)
parser.add_argument('--label-name', type=str, default='softmax_label')
- parser.add_argument('--dataset', type=str, required=True, help='dataset path')
+ parser.add_argument('--dataset', type=str, required=False, help='dataset path')
parser.add_argument('--rgb-mean', type=str, default='0,0,0')
+ parser.add_argument('--rgb-std', type=str, default='1,1,1')
parser.add_argument('--image-shape', type=str, default='3,224,224')
parser.add_argument('--data-nthreads', type=int, default=60, help='number of threads for data decoding')
parser.add_argument('--num-skipped-batches', type=int, default=0, help='skip the number of batches for inference')
@@ -129,7 +161,7 @@ def score(sym, arg_params, aux_params, data, devs, label_name, max_num_examples,
ctx = mx.cpu(0)
else:
raise ValueError('ctx %s is not supported in this script' % args.ctx)
-
+
logging.basicConfig()
logger = logging.getLogger('logger')
logger.setLevel(logging.INFO)
@@ -145,6 +177,10 @@ def score(sym, arg_params, aux_params, data, devs, label_name, max_num_examples,
logger.info('rgb_mean = %s' % rgb_mean)
rgb_mean = [float(i) for i in rgb_mean.split(',')]
mean_args = {'mean_r': rgb_mean[0], 'mean_g': rgb_mean[1], 'mean_b': rgb_mean[2]}
+ rgb_std = args.rgb_std
+ logger.info('rgb_std = %s' % rgb_std)
+ rgb_std = [float(i) for i in rgb_std.split(',')]
+ std_args = {'std_r': rgb_std[0], 'std_g': rgb_std[1], 'std_b': rgb_std[2]}
label_name = args.label_name
logger.info('label_name = %s' % label_name)
@@ -153,32 +189,38 @@ def score(sym, arg_params, aux_params, data, devs, label_name, max_num_examples,
data_shape = tuple([int(i) for i in image_shape.split(',')])
logger.info('Input data shape = %s' % str(data_shape))
- dataset = args.dataset
- download_dataset('http://data.mxnet.io/data/val_256_q90.rec', dataset)
- logger.info('Dataset for inference: %s' % dataset)
-
- # creating data iterator
- data = mx.io.ImageRecordIter(path_imgrec=dataset,
- label_width=1,
- preprocess_threads=data_nthreads,
- batch_size=batch_size,
- data_shape=data_shape,
- label_name=label_name,
- rand_crop=False,
- rand_mirror=False,
- shuffle=True,
- shuffle_chunk_seed=3982304,
- seed=48564309,
- **mean_args)
-
- # loading model
- sym, arg_params, aux_params = load_model(symbol_file, param_file, logger)
-
- # make sure that fp32 inference works on the same images as calibrated quantized model
- logger.info('Skipping the first %d batches' % args.num_skipped_batches)
- data = advance_data_iter(data, args.num_skipped_batches)
-
- num_inference_images = args.num_inference_batches * batch_size
- logger.info('Running model %s for inference' % symbol_file)
- score(sym, arg_params, aux_params, data, [ctx], label_name,
- max_num_examples=num_inference_images, logger=logger)
+ if args.benchmark == False:
+ dataset = args.dataset
+ download_dataset('http://data.mxnet.io/data/val_256_q90.rec', dataset)
+ logger.info('Dataset for inference: %s' % dataset)
+
+ # creating data iterator
+ data = mx.io.ImageRecordIter(path_imgrec=dataset,
+ label_width=1,
+ preprocess_threads=data_nthreads,
+ batch_size=batch_size,
+ data_shape=data_shape,
+ label_name=label_name,
+ rand_crop=False,
+ rand_mirror=False,
+ shuffle=True,
+ shuffle_chunk_seed=3982304,
+ seed=48564309,
+ **mean_args,
+ **std_args)
+
+ # loading model
+ sym, arg_params, aux_params = load_model(symbol_file, param_file, logger)
+
+ # make sure that fp32 inference works on the same images as calibrated quantized model
+ logger.info('Skipping the first %d batches' % args.num_skipped_batches)
+ data = advance_data_iter(data, args.num_skipped_batches)
+
+ num_inference_images = args.num_inference_batches * batch_size
+ logger.info('Running model %s for inference' % symbol_file)
+ score(sym, arg_params, aux_params, data, [ctx], label_name,
+ max_num_examples=num_inference_images, logger=logger)
+ else:
+ logger.info('Running model %s for inference' % symbol_file)
+ speed = benchmark_score(symbol_file, ctx, batch_size, args.num_inference_batches, logger)
+ logger.info('batch size %2d, image/sec: %f', batch_size, speed)
diff --git a/example/rcnn/README.md b/example/rcnn/README.md
index ab3c8fb88c39..b5284183d160 100644
--- a/example/rcnn/README.md
+++ b/example/rcnn/README.md
@@ -32,6 +32,7 @@ Make a directory `data` and follow `py-faster-rcnn` for data preparation instruc
### Training and evaluation
Use `python3 train.py --dataset $Dataset$ --network $Network$ --pretrained $IMAGENET_MODEL_FILE$ --gpus $GPUS$` to train,
for example, `python3 train.py --dataset voc --network vgg16 --pretrained model/vgg16-0000.params --gpus 0,1`.
+use `python3 train.py --dataset voc --imageset 2007_trainval+2012_trainval --network vgg16 --pretrained model/vgg16-0000.params --gpus 0,1` to train on both of voc2007 and voc2012.
Use `python3 test.py --dataset $Dataset$ --network $Network$ --params $MODEL_FILE$ --gpu $GPU$` to evaluate,
for example, `python3 test.py --dataset voc --network vgg16 --params model/vgg16-0010.params --gpu 0`.
diff --git a/example/rcnn/symnet/proposal_target.py b/example/rcnn/symnet/proposal_target.py
index 926720b28b5f..77cb78850176 100644
--- a/example/rcnn/symnet/proposal_target.py
+++ b/example/rcnn/symnet/proposal_target.py
@@ -35,7 +35,7 @@ def sample_rois(rois, gt_boxes, num_classes, rois_per_image, fg_rois_per_image,
:param fg_rois_per_image: foreground roi number
:param fg_overlap: overlap threshold for fg rois
:param box_stds: std var of bbox reg
- :return: (labels, rois, bbox_targets, bbox_weights)
+ :return: (rois, labels, bbox_targets, bbox_weights)
"""
overlaps = bbox_overlaps(rois[:, 1:], gt_boxes[:, :4])
gt_assignment = overlaps.argmax(axis=1)
diff --git a/example/reinforcement-learning/ddpg/README.md b/example/reinforcement-learning/ddpg/README.md
index 37f42a8292c4..2e299dd5daa3 100644
--- a/example/reinforcement-learning/ddpg/README.md
+++ b/example/reinforcement-learning/ddpg/README.md
@@ -1,6 +1,8 @@
# mx-DDPG
MXNet Implementation of DDPG
+## /!\ This example depends on RLLAB which is deprecated /!\
+
# Introduction
This is the MXNet implementation of [DDPG](https://arxiv.org/abs/1509.02971). It is tested in the rllab cart pole environment against rllab's native implementation and achieves comparably similar results. You can substitute with this anywhere you use rllab's DDPG with minor modifications.
diff --git a/example/reinforcement-learning/dqn/setup.sh b/example/reinforcement-learning/dqn/setup.sh
index 3069fef62ecc..012ff8fb1c07 100755
--- a/example/reinforcement-learning/dqn/setup.sh
+++ b/example/reinforcement-learning/dqn/setup.sh
@@ -26,11 +26,11 @@ pip install pygame
# Install arcade learning environment
if [[ "$OSTYPE" == "linux-gnu" ]]; then
- sudo apt-get install libsdl1.2-dev libsdl-gfx1.2-dev libsdl-image1.2-dev cmake
+ sudo apt-get install libsdl1.2-dev libsdl-gfx1.2-dev libsdl-image1.2-dev cmake ninja-build
elif [[ "$OSTYPE" == "darwin"* ]]; then
brew install sdl sdl_image sdl_mixer sdl_ttf portmidi
fi
-git clone git@github.com:mgbellemare/Arcade-Learning-Environment.git || true
+git clone https://github.com/mgbellemare/Arcade-Learning-Environment || true
pushd .
cd Arcade-Learning-Environment
mkdir -p build
@@ -43,6 +43,6 @@ popd
cp Arcade-Learning-Environment/ale.cfg .
# Copy roms
-git clone git@github.com:npow/atari.git || true
+git clone https://github.com/npow/atari || true
cp -R atari/roms .
diff --git a/example/restricted-boltzmann-machine/README.md b/example/restricted-boltzmann-machine/README.md
index 129120ba9961..a8769a51e05a 100644
--- a/example/restricted-boltzmann-machine/README.md
+++ b/example/restricted-boltzmann-machine/README.md
@@ -8,6 +8,58 @@ Here are some samples generated by the RBM with the default hyperparameters. The
+Usage:
+
+```
+python binary_rbm_gluon.py --help
+usage: binary_rbm_gluon.py [-h] [--num-hidden NUM_HIDDEN] [--k K]
+ [--batch-size BATCH_SIZE] [--num-epoch NUM_EPOCH]
+ [--learning-rate LEARNING_RATE]
+ [--momentum MOMENTUM]
+ [--ais-batch-size AIS_BATCH_SIZE]
+ [--ais-num-batch AIS_NUM_BATCH]
+ [--ais-intermediate-steps AIS_INTERMEDIATE_STEPS]
+ [--ais-burn-in-steps AIS_BURN_IN_STEPS] [--cuda]
+ [--no-cuda] [--device-id DEVICE_ID]
+ [--data-loader-num-worker DATA_LOADER_NUM_WORKER]
+
+Restricted Boltzmann machine learning MNIST
+
+optional arguments:
+ -h, --help show this help message and exit
+ --num-hidden NUM_HIDDEN
+ number of hidden units
+ --k K number of Gibbs sampling steps used in the PCD
+ algorithm
+ --batch-size BATCH_SIZE
+ batch size
+ --num-epoch NUM_EPOCH
+ number of epochs
+ --learning-rate LEARNING_RATE
+ learning rate for stochastic gradient descent
+ --momentum MOMENTUM momentum for the stochastic gradient descent
+ --ais-batch-size AIS_BATCH_SIZE
+ batch size for AIS to estimate the log-likelihood
+ --ais-num-batch AIS_NUM_BATCH
+ number of batches for AIS to estimate the log-
+ likelihood
+ --ais-intermediate-steps AIS_INTERMEDIATE_STEPS
+ number of intermediate distributions for AIS to
+ estimate the log-likelihood
+ --ais-burn-in-steps AIS_BURN_IN_STEPS
+ number of burn in steps for each intermediate
+ distributions of AIS to estimate the log-likelihood
+ --cuda train on GPU with CUDA
+ --no-cuda train on CPU
+ --device-id DEVICE_ID
+ GPU device id
+ --data-loader-num-worker DATA_LOADER_NUM_WORKER
+ number of multithreading workers for the data loader
+```
+Default:
+```
+Namespace(ais_batch_size=100, ais_burn_in_steps=10, ais_intermediate_steps=10, ais_num_batch=10, batch_size=80, cuda=True, data_loader_num_worker=4, device_id=0, k=30, learning_rate=0.1, momentum=0.3, num_epoch=130, num_hidden=500)
+```
[1] G E Hinton & R R Salakhutdinov, Reducing the Dimensionality of Data with Neural Networks Science **313**, 5786 (2006)
[2] R M Neal, Annealed importance sampling. Stat Comput **11** 2 (2001)
[3] R Salakhutdinov & I Murray, On the quantitative analysis of deep belief networks. In Proc. ICML '08 **25** (2008)
\ No newline at end of file
diff --git a/example/rnn-time-major/bucket_io.py b/example/rnn-time-major/bucket_io.py
deleted file mode 100644
index e689ff112678..000000000000
--- a/example/rnn-time-major/bucket_io.py
+++ /dev/null
@@ -1,264 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# pylint: disable=C0111,too-many-arguments,too-many-instance-attributes,too-many-locals,redefined-outer-name,fixme
-# pylint: disable=superfluous-parens, no-member, invalid-name
-
-from __future__ import print_function
-import numpy as np
-import mxnet as mx
-
-# The interface of a data iter that works for bucketing
-#
-# DataIter
-# - default_bucket_key: the bucket key for the default symbol.
-#
-# DataBatch
-# - provide_data: same as DataIter, but specific to this batch
-# - provide_label: same as DataIter, but specific to this batch
-# - bucket_key: the key for the bucket that should be used for this batch
-
-def default_read_content(path):
- with open(path) as ins:
- content = ins.read()
- content = content.replace('\n', ' ').replace('. ', ' ')
- return content
-
-def default_build_vocab(path):
- content = default_read_content(path)
- content = content.split(' ')
- idx = 1 # 0 is left for zero-padding
- the_vocab = {}
- the_vocab[' '] = 0 # put a dummy element here so that len(vocab) is correct
- for word in content:
- if len(word) == 0:
- continue
- if not word in the_vocab:
- the_vocab[word] = idx
- idx += 1
- return the_vocab
-
-def default_text2id(sentence, the_vocab):
- words = sentence.split(' ')
- words = [the_vocab[w] for w in words if len(w) > 0]
- return words
-
-def default_gen_buckets(sentences, batch_size, the_vocab):
- len_dict = {}
- max_len = -1
- for sentence in sentences:
- words = default_text2id(sentence, the_vocab)
- if len(words) == 0:
- continue
- if len(words) > max_len:
- max_len = len(words)
- if len(words) in len_dict:
- len_dict[len(words)] += 1
- else:
- len_dict[len(words)] = 1
- print(len_dict)
-
- tl = 0
- buckets = []
- for l, n in len_dict.items(): # TODO: There are better heuristic ways to do this
- if n + tl >= batch_size:
- buckets.append(l)
- tl = 0
- else:
- tl += n
- if tl > 0:
- buckets.append(max_len)
- return buckets
-
-class SimpleBatch(object):
- def __init__(self, data_names, data, data_layouts, label_names, label, label_layouts, bucket_key):
- self.data = data
- self.label = label
- self.data_names = data_names
- self.label_names = label_names
- self.data_layouts = data_layouts
- self.label_layouts = label_layouts
- self.bucket_key = bucket_key
-
- self.pad = 0
- self.index = None # TODO: what is index?
-
- @property
- def provide_data(self):
- return [mx.io.DataDesc(n, x.shape, layout=l) for n, x, l in zip(self.data_names, self.data, self.data_layouts)]
-
- @property
- def provide_label(self):
- return [mx.io.DataDesc(n, x.shape, layout=l) for n, x, l in zip(self.label_names, self.label, self.label_layouts)]
-
-class DummyIter(mx.io.DataIter):
- "A dummy iterator that always return the same batch, used for speed testing"
- def __init__(self, real_iter):
- super(DummyIter, self).__init__()
- self.real_iter = real_iter
- self.provide_data = real_iter.provide_data
- self.provide_label = real_iter.provide_label
- self.batch_size = real_iter.batch_size
-
- for batch in real_iter:
- self.the_batch = batch
- break
-
- def __iter__(self):
- return self
-
- def next(self):
- return self.the_batch
-
-class BucketSentenceIter(mx.io.DataIter):
- def __init__(self, path, vocab, buckets, batch_size,
- init_states, data_name='data', label_name='label',
- seperate_char=' ', text2id=None, read_content=None,
- time_major=True):
- super(BucketSentenceIter, self).__init__()
-
- if text2id is None:
- self.text2id = default_text2id
- else:
- self.text2id = text2id
- if read_content is None:
- self.read_content = default_read_content
- else:
- self.read_content = read_content
- content = self.read_content(path)
- sentences = content.split(seperate_char)
-
- if len(buckets) == 0:
- buckets = default_gen_buckets(sentences, batch_size, vocab)
-
- self.vocab_size = len(vocab)
- self.data_name = data_name
- self.label_name = label_name
- self.time_major = time_major
-
- buckets.sort()
- self.buckets = buckets
- self.data = [[] for _ in buckets]
-
- # pre-allocate with the largest bucket for better memory sharing
- self.default_bucket_key = max(buckets)
-
- for sentence in sentences:
- sentence = self.text2id(sentence, vocab)
- if len(sentence) == 0:
- continue
- for i, bkt in enumerate(buckets):
- if bkt >= len(sentence):
- self.data[i].append(sentence)
- break
- # we just ignore the sentence it is longer than the maximum
- # bucket size here
-
- # convert data into ndarrays for better speed during training
- data = [np.zeros((len(x), buckets[i])) for i, x in enumerate(self.data)]
- for i_bucket in range(len(self.buckets)):
- for j in range(len(self.data[i_bucket])):
- sentence = self.data[i_bucket][j]
- data[i_bucket][j, :len(sentence)] = sentence
- self.data = data
-
- # Get the size of each bucket, so that we could sample
- # uniformly from the bucket
- bucket_sizes = [len(x) for x in self.data]
-
- print("Summary of dataset ==================")
- for bkt, size in zip(buckets, bucket_sizes):
- print("bucket of len %3d : %d samples" % (bkt, size))
-
- self.batch_size = batch_size
- self.make_data_iter_plan()
-
- self.init_states = init_states
- self.init_state_arrays = [mx.nd.zeros(x[1]) for x in init_states]
-
- if self.time_major:
- self.provide_data = [mx.io.DataDesc('data', (self.default_bucket_key, batch_size), layout='TN')] + init_states
- self.provide_label = [mx.io.DataDesc('softmax_label', (self.default_bucket_key, batch_size), layout='TN')]
- else:
- self.provide_data = [('data', (batch_size, self.default_bucket_key))] + init_states
- self.provide_label = [('softmax_label', (self.batch_size, self.default_bucket_key))]
-
- def make_data_iter_plan(self):
- "make a random data iteration plan"
- # truncate each bucket into multiple of batch-size
- bucket_n_batches = []
- for i in range(len(self.data)):
- bucket_n_batches.append(len(self.data[i]) / self.batch_size)
- self.data[i] = self.data[i][:int(bucket_n_batches[i]*self.batch_size)]
-
- bucket_plan = np.hstack([np.zeros(int(n), int)+i for i, n in enumerate(bucket_n_batches)])
- np.random.shuffle(bucket_plan)
-
- bucket_idx_all = [np.random.permutation(len(x)) for x in self.data]
-
- self.bucket_plan = bucket_plan
- self.bucket_idx_all = bucket_idx_all
- self.bucket_curr_idx = [0 for x in self.data]
-
- self.data_buffer = []
- self.label_buffer = []
- for i_bucket in range(len(self.data)):
- if self.time_major:
- data = np.zeros((self.buckets[i_bucket], self.batch_size))
- label = np.zeros((self.buckets[i_bucket], self.batch_size))
- else:
- data = np.zeros((self.batch_size, self.buckets[i_bucket]))
- label = np.zeros((self.batch_size, self.buckets[i_bucket]))
-
- self.data_buffer.append(data)
- self.label_buffer.append(label)
-
- def __iter__(self):
- for i_bucket in self.bucket_plan:
- data = self.data_buffer[i_bucket]
- i_idx = self.bucket_curr_idx[i_bucket]
- idx = self.bucket_idx_all[i_bucket][i_idx:i_idx+self.batch_size]
- self.bucket_curr_idx[i_bucket] += self.batch_size
-
- init_state_names = [x[0] for x in self.init_states]
-
- if self.time_major:
- data[:] = self.data[i_bucket][idx].T
- else:
- data[:] = self.data[i_bucket][idx]
-
- label = self.label_buffer[i_bucket]
- if self.time_major:
- label[:-1, :] = data[1:, :]
- label[-1, :] = 0
- else:
- label[:, :-1] = data[:, 1:]
- label[:, -1] = 0
-
- data_all = [mx.nd.array(data)] + self.init_state_arrays
- label_all = [mx.nd.array(label)]
- data_names = ['data'] + init_state_names
- label_names = ['softmax_label']
-
- data_batch = SimpleBatch(data_names, data_all, [x.layout for x in self.provide_data],
- label_names, label_all, [x.layout for x in self.provide_label],
- self.buckets[i_bucket])
- yield data_batch
-
-
- def reset(self):
- self.bucket_curr_idx = [0 for x in self.data]
diff --git a/example/rnn-time-major/get_sherlockholmes_data.sh b/example/rnn-time-major/get_sherlockholmes_data.sh
deleted file mode 100755
index 43c8669e003c..000000000000
--- a/example/rnn-time-major/get_sherlockholmes_data.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env bash
-
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-echo
-echo "NOTE: To continue, you need to review the licensing of the data sets used by this script"
-echo "See https://www.gutenberg.org/wiki/Gutenberg:The_Project_Gutenberg_License for the licensing"
-read -p "Please confirm you have reviewed the licensing [Y/n]:" -n 1 -r
-echo
-
-if [ $REPLY != "Y" ]
-then
- echo "License was not reviewed, aborting script."
- exit 1
-fi
-
-RNN_DIR=$(cd `dirname $0`; pwd)
-DATA_DIR="${RNN_DIR}/data/"
-
-if [[ ! -d "${DATA_DIR}" ]]; then
- echo "${DATA_DIR} doesn't exist, will create one";
- mkdir -p ${DATA_DIR}
-fi
-
-wget -P ${DATA_DIR} https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/sherlockholmes/sherlockholmes.train.txt;
-wget -P ${DATA_DIR} https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/sherlockholmes/sherlockholmes.valid.txt;
-wget -P ${DATA_DIR} https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/sherlockholmes/sherlockholmes.test.txt;
-wget -P ${DATA_DIR} https://raw.githubusercontent.com/dmlc/web-data/master/mxnet/tinyshakespeare/input.txt;
diff --git a/example/rnn-time-major/readme.md b/example/rnn-time-major/readme.md
deleted file mode 100644
index b30b8410b047..000000000000
--- a/example/rnn-time-major/readme.md
+++ /dev/null
@@ -1,24 +0,0 @@
-Time major data layout for RNN
-==============================
-
-This example demonstrates an RNN implementation with Time-major layout. This implementation shows 1.5x-2x speedups compared to Batch-major RNN.
-
-As example of Batch-major RNN is available in MXNet [RNN Bucketing example](https://github.com/apache/incubator-mxnet/tree/master/example/rnn/bucketing)
-
-## Running the example
-- Prerequisite: an instance with GPU compute resources is required to run MXNet RNN
-- Make the shell script ```get_sherlockholmes_data.sh``` executable:
- ```bash
- chmod +x get_sherlockholmes_data.sh
- ```
-- Run ```get_sherlockholmes_data.sh``` to download the sherlockholmes dataset, and follow the instructions to review the license:
- ```bash
- ./get_sherlockholmes_data.sh
- ```
- The sherlockholmes data sets will be downloaded into ./data directory, and available for the example to train on.
-- Run the example:
- ```bash
- python rnn_cell_demo.py
- ```
-
- If everything goes well, console will plot training speed and perplexity that you can compare to the batch major RNN.
diff --git a/example/rnn-time-major/rnn_cell_demo.py b/example/rnn-time-major/rnn_cell_demo.py
deleted file mode 100644
index 80b281b3bdbb..000000000000
--- a/example/rnn-time-major/rnn_cell_demo.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-"""A simple demo of new RNN cell with Sherlock Holmes language model."""
-
-################################################################################
-# Speed test (time major is 1.5~2 times faster than batch major).
-#
-# -- This script (time major) -----
-# 2016-10-10 18:43:21,890 Epoch[0] Batch [50] Speed: 1717.76 samples/sec Train-Perplexity=4311.345018
-# 2016-10-10 18:43:25,959 Epoch[0] Batch [100] Speed: 1573.17 samples/sec Train-Perplexity=844.092421
-# 2016-10-10 18:43:29,807 Epoch[0] Batch [150] Speed: 1663.17 samples/sec Train-Perplexity=498.080716
-# 2016-10-10 18:43:33,871 Epoch[0] Batch [200] Speed: 1574.84 samples/sec Train-Perplexity=455.051252
-# 2016-10-10 18:43:37,720 Epoch[0] Batch [250] Speed: 1662.87 samples/sec Train-Perplexity=410.500066
-# 2016-10-10 18:43:40,766 Epoch[0] Batch [300] Speed: 2100.81 samples/sec Train-Perplexity=274.317460
-# 2016-10-10 18:43:44,571 Epoch[0] Batch [350] Speed: 1682.45 samples/sec Train-Perplexity=350.132577
-# 2016-10-10 18:43:48,377 Epoch[0] Batch [400] Speed: 1681.41 samples/sec Train-Perplexity=320.674884
-# 2016-10-10 18:43:51,253 Epoch[0] Train-Perplexity=336.210212
-# 2016-10-10 18:43:51,253 Epoch[0] Time cost=33.529
-# 2016-10-10 18:43:53,373 Epoch[0] Validation-Perplexity=282.453883
-#
-# -- ../rnn/rnn_cell_demo.py (batch major) -----
-# 2016-10-10 18:44:34,133 Epoch[0] Batch [50] Speed: 1004.50 samples/sec Train-Perplexity=4398.428571
-# 2016-10-10 18:44:39,874 Epoch[0] Batch [100] Speed: 1114.85 samples/sec Train-Perplexity=771.401960
-# 2016-10-10 18:44:45,528 Epoch[0] Batch [150] Speed: 1132.03 samples/sec Train-Perplexity=525.207444
-# 2016-10-10 18:44:51,564 Epoch[0] Batch [200] Speed: 1060.37 samples/sec Train-Perplexity=453.741140
-# 2016-10-10 18:44:57,865 Epoch[0] Batch [250] Speed: 1015.78 samples/sec Train-Perplexity=411.914237
-# 2016-10-10 18:45:04,032 Epoch[0] Batch [300] Speed: 1037.92 samples/sec Train-Perplexity=381.302188
-# 2016-10-10 18:45:10,153 Epoch[0] Batch [350] Speed: 1045.49 samples/sec Train-Perplexity=363.326871
-# 2016-10-10 18:45:16,062 Epoch[0] Batch [400] Speed: 1083.21 samples/sec Train-Perplexity=377.929014
-# 2016-10-10 18:45:19,993 Epoch[0] Train-Perplexity=294.675899
-# 2016-10-10 18:45:19,993 Epoch[0] Time cost=52.604
-# 2016-10-10 18:45:21,401 Epoch[0] Validation-Perplexity=294.345659
-################################################################################
-
-import os
-import numpy as np
-import mxnet as mx
-
-from bucket_io import BucketSentenceIter, default_build_vocab
-
-data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data'))
-
-
-def Perplexity(label, pred):
- """ Calculates prediction perplexity
-
- Args:
- label (mx.nd.array): labels array
- pred (mx.nd.array): prediction array
-
- Returns:
- float: calculated perplexity
-
- """
-
- # collapse the time, batch dimension
- label = label.reshape((-1,))
- pred = pred.reshape((-1, pred.shape[-1]))
-
- loss = 0.
- for i in range(pred.shape[0]):
- loss += -np.log(max(1e-10, pred[i][int(label[i])]))
- return np.exp(loss / label.size)
-
-
-if __name__ == '__main__':
- batch_size = 128
- buckets = [10, 20, 30, 40, 50, 60]
- num_hidden = 200
- num_embed = 200
- num_lstm_layer = 2
-
- num_epoch = 2
- learning_rate = 0.01
- momentum = 0.0
-
- # Update count per available GPUs
- gpu_count = 1
- contexts = [mx.context.gpu(i) for i in range(gpu_count)]
-
- vocab = default_build_vocab(os.path.join(data_dir, 'sherlockholmes.train.txt'))
-
- init_h = [mx.io.DataDesc('LSTM_state', (num_lstm_layer, batch_size, num_hidden), layout='TNC')]
- init_c = [mx.io.DataDesc('LSTM_state_cell', (num_lstm_layer, batch_size, num_hidden), layout='TNC')]
- init_states = init_c + init_h
-
- data_train = BucketSentenceIter(os.path.join(data_dir, 'sherlockholmes.train.txt'),
- vocab, buckets, batch_size, init_states,
- time_major=True)
- data_val = BucketSentenceIter(os.path.join(data_dir, 'sherlockholmes.valid.txt'),
- vocab, buckets, batch_size, init_states,
- time_major=True)
-
- def sym_gen(seq_len):
- """ Generates the MXNet symbol for the RNN
-
- Args:
- seq_len (int): input sequence length
-
- Returns:
- tuple: tuple containing symbol, data_names, label_names
-
- """
- data = mx.sym.Variable('data')
- label = mx.sym.Variable('softmax_label')
- embed = mx.sym.Embedding(data=data, input_dim=len(vocab),
- output_dim=num_embed, name='embed')
-
- # TODO(tofix)
- # currently all the LSTM parameters are concatenated as
- # a huge vector, and named '_parameters'. By default
- # mxnet initializer does not know how to initilize this
- # guy because its name does not ends with _weight or _bias
- # or anything familiar. Here we just use a temp workaround
- # to create a variable and name it as LSTM_bias to get
- # this demo running. Note by default bias is initialized
- # as zeros, so this is not a good scheme. But calling it
- # LSTM_weight is not good, as this is 1D vector, while
- # the initialization scheme of a weight parameter needs
- # at least two dimensions.
- rnn_params = mx.sym.Variable('LSTM_bias')
-
- # RNN cell takes input of shape (time, batch, feature)
- rnn = mx.sym.RNN(data=embed, state_size=num_hidden,
- num_layers=num_lstm_layer, mode='lstm',
- name='LSTM',
- # The following params can be omitted
- # provided we do not need to apply the
- # workarounds mentioned above
- parameters=rnn_params)
-
- # the RNN cell output is of shape (time, batch, dim)
- # if we need the states and cell states in the last time
- # step (e.g. when building encoder-decoder models), we
- # can set state_outputs=True, and the RNN cell will have
- # extra outputs: rnn['LSTM_output'], rnn['LSTM_state']
- # and for LSTM, also rnn['LSTM_state_cell']
-
- # now we collapse the time and batch dimension to do the
- # final linear logistic regression prediction
- hidden = mx.sym.Reshape(data=rnn, shape=(-1, num_hidden))
-
- pred = mx.sym.FullyConnected(data=hidden, num_hidden=len(vocab),
- name='pred')
-
- # reshape to be of compatible shape as labels
- pred_tm = mx.sym.Reshape(data=pred, shape=(seq_len, -1, len(vocab)))
-
- sm = mx.sym.SoftmaxOutput(data=pred_tm, label=label, preserve_shape=True,
- name='softmax')
-
- data_names = ['data', 'LSTM_state', 'LSTM_state_cell']
- label_names = ['softmax_label']
-
- return sm, data_names, label_names
-
- if len(buckets) == 1:
- mod = mx.mod.Module(*sym_gen(buckets[0]), context=contexts)
- else:
- mod = mx.mod.BucketingModule(sym_gen,
- default_bucket_key=data_train.default_bucket_key,
- context=contexts)
-
- import logging
- head = '%(asctime)-15s %(message)s'
- logging.basicConfig(level=logging.DEBUG, format=head)
-
- mod.fit(data_train, eval_data=data_val, num_epoch=num_epoch,
- eval_metric=mx.metric.np(Perplexity),
- batch_end_callback=mx.callback.Speedometer(batch_size, 50),
- initializer=mx.init.Xavier(factor_type="in", magnitude=2.34),
- optimizer='sgd',
- optimizer_params={'learning_rate': learning_rate,
- 'momentum': momentum, 'wd': 0.00001})
diff --git a/example/rnn/README.md b/example/rnn/README.md
index a0846fa3da85..1d1df6ed7687 100644
--- a/example/rnn/README.md
+++ b/example/rnn/README.md
@@ -1,6 +1,11 @@
Recurrent Neural Network Examples
===========
+For more current implementations of NLP and RNN models with MXNet, please visit [gluon-nlp](http://gluon-nlp.mxnet.io/index.html)
+
+------
+
+
This directory contains functions for creating recurrent neural networks
models using high level mxnet.rnn interface.
diff --git a/example/rnn/bucketing/README.md b/example/rnn/bucketing/README.md
index 9bbeefd21e4e..7b7883d79ad1 100644
--- a/example/rnn/bucketing/README.md
+++ b/example/rnn/bucketing/README.md
@@ -2,6 +2,15 @@ RNN Example
===========
This folder contains RNN examples using high level mxnet.rnn interface.
+--------------
+
+## Gluon Implementation
+
+You can check this improved [Gluon implementation](http://gluon-nlp.mxnet.io/model_zoo/language_model/index.html#word-language-model) in gluon-nlp, the largest LSTM model reaches a perplexity of 65.62.
+
+--------------
+
+
## Data
1) Review the license for the Sherlock Holmes dataset and ensure that you agree to it. Then uncomment the lines in the 'get_sherlockholmes_data.sh' script that download the dataset.
@@ -23,11 +32,11 @@ This folder contains RNN examples using high level mxnet.rnn interface.
For Python2 (GPU support only): can take 50+ minutes on AWS-EC2-p2.16xlarge
- $ python [cudnn_lstm_bucketing.py](cudnn_lstm_bucketing.py) --gpus 0,1,2,3
+ $ python [cudnn_rnn_bucketing.py](cudnn_rnn_bucketing.py) --gpus 0,1,2,3
For Python3 (GPU support only): can take 50+ minutes on AWS-EC2-p2.16xlarge
- $ python3 [cudnn_lstm_bucketing.py](cudnn_lstm_bucketing.py) --gpus 0,1,2,3
+ $ python3 [cudnn_rnn_bucketing.py](cudnn_rnn_bucketing.py) --gpus 0,1,2,3
### Performance Note:
diff --git a/example/rnn/large_word_lm/data.py b/example/rnn/large_word_lm/data.py
index b9cc3e8a89ea..0ca500628d02 100644
--- a/example/rnn/large_word_lm/data.py
+++ b/example/rnn/large_word_lm/data.py
@@ -174,7 +174,7 @@ def __init__(self, data_file, vocab, batch_size, bptt):
self._iter = self._dataset.iterate_once(batch_size, bptt)
def iter_next(self):
- data = self._iter.next()
+ data = next(self._iter)
if data is None:
return False
self._next_data = mx.nd.array(data[0], dtype=np.int32)
diff --git a/example/rnn/large_word_lm/readme.md b/example/rnn/large_word_lm/readme.md
deleted file mode 100644
index 465aaa1c44b3..000000000000
--- a/example/rnn/large_word_lm/readme.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Large-Scale Language Model
-This example implements the baseline model in
-[Exploring the Limits of Language Modeling](https://arxiv.org/abs/1602.02410) on the
-[Google 1-Billion Word](https://github.com/ciprian-chelba/1-billion-word-language-modeling-benchmark) (GBW) dataset.
-
-This example reaches 48.0 test perplexity after 6 training epochs on a 1-layer, 2048-unit, 512-projection LSTM Language Model.
-It reaches 44.2 test perplexity after 35 epochs of training.
-
-The main differences with the original implementation include:
-* Synchronized gradient updates instead of asynchronized updates
-
-Each epoch for training (excluding time for evaluation on test set) takes around 80 minutes on a p3.8xlarge instance, which comes with 4 Volta V100 GPUs.
-
-# Setup dataset and build sampler
-1. Download 1-Billion Word Dataset: [Link](http://www.statmt.org/lm-benchmark/1-billion-word-language-modeling-benchmark-r13output.tar.gz)
-2. Download pre-processed vocabulary file which maps tokens into ids.
-3. Build sampler with cython by running `make` in the current directory. If you do not have cython installed, run `pip install cython`
-
-# Run the Script
-```
-usage: train.py [-h] [--data DATA] [--test TEST] [--vocab VOCAB]
- [--emsize EMSIZE] [--nhid NHID] [--num-proj NUM_PROJ]
- [--nlayers NLAYERS] [--epochs EPOCHS]
- [--batch-size BATCH_SIZE] [--dropout DROPOUT] [--eps EPS]
- [--bptt BPTT] [--k K] [--gpus GPUS]
- [--log-interval LOG_INTERVAL] [--seed SEED]
- [--checkpoint-dir CHECKPOINT_DIR] [--lr LR] [--clip CLIP]
- [--rescale-embed RESCALE_EMBED]
-
-Language Model on GBW
-
-optional arguments:
- -h, --help show this help message and exit
- --data DATA location of the training data
- --test TEST location of the test data
- --vocab VOCAB location of the corpus vocabulary file
- --emsize EMSIZE size of word embeddings
- --nhid NHID number of hidden units per layer
- --num-proj NUM_PROJ number of projection units per layer
- --nlayers NLAYERS number of LSTM layers
- --epochs EPOCHS number of epoch for training
- --batch-size BATCH_SIZE
- batch size per gpu
- --dropout DROPOUT dropout applied to layers (0 = no dropout)
- --eps EPS epsilon for adagrad
- --bptt BPTT sequence length
- --k K number of noise samples for estimation
- --gpus GPUS list of gpus to run, e.g. 0 or 0,2,5. empty means
- using gpu(0).
- --log-interval LOG_INTERVAL
- report interval
- --seed SEED random seed
- --checkpoint-dir CHECKPOINT_DIR
- dir for checkpoint
- --lr LR initial learning rate
- --clip CLIP gradient clipping by global norm.
- --rescale-embed RESCALE_EMBED
- scale factor for the gradients of the embedding layer
-```
-
-To reproduce the result, run
-```
-train.py --gpus=0,1,2,3 --clip=10 --lr=0.2 --dropout=0.1 --eps=1 --rescale-embed=256
---test=/path/to/heldout-monolingual.tokenized.shuffled/news.en.heldout-00000-of-00050
---data=/path/to/training-monolingual.tokenized.shuffled/*
-```
diff --git a/example/rnn/word_lm/README.md b/example/rnn/word_lm/README.md
index beed6fc8d895..ab0a8d704b9c 100644
--- a/example/rnn/word_lm/README.md
+++ b/example/rnn/word_lm/README.md
@@ -16,7 +16,7 @@ The Sherlock Holmes data is a copyright free copy of Sherlock Holmes from[(Proje
Example runs and the results:
```
-python train.py --tied --nhid 650 --emsize 650 --dropout 0.5 # Test ppl of 75.4
+python train.py --tied --nhid 650 --emsize 650 --dropout 0.5 # Test ppl of 44.26
```
```
diff --git a/example/sparse/linear_classification/data.py b/example/sparse/linear_classification/data.py
index 02984734fb99..bc5619a4bfb6 100644
--- a/example/sparse/linear_classification/data.py
+++ b/example/sparse/linear_classification/data.py
@@ -24,10 +24,9 @@ def get_avazu_data(data_dir, data_name, url):
os.mkdir(data_dir)
os.chdir(data_dir)
if (not os.path.exists(data_name)):
- print("Dataset " + data_name + " not present. Downloading now ...")
- import urllib
+ print("Dataset " + data_name + " not present. Downloading now ...")
zippath = os.path.join(data_dir, data_name + ".bz2")
- urllib.urlretrieve(url + data_name + ".bz2", zippath)
+ mx.test_utils.download(url + data_name + ".bz2", zippath)
os.system("bzip2 -d %r" % data_name + ".bz2")
print("Dataset " + data_name + " is now present.")
os.chdir("..")
diff --git a/example/speech_recognition/README.md b/example/speech_recognition/README.md
index f95fddf2103e..6f01911e1300 100644
--- a/example/speech_recognition/README.md
+++ b/example/speech_recognition/README.md
@@ -28,7 +28,9 @@ With rich functionalities and convenience explained above, you can build your ow
pip install soundfile
-- Warp CTC: Follow [this instruction](https://github.com/dmlc/mxnet/tree/master/example/warpctc) to install Baidu's Warp CTC.
+- Warp CTC: Follow [this instruction](https://github.com/baidu-research/warp-ctc) to compile Baidu's Warp CTC. (Note: If you are using V100, make sure to use this [fix](https://github.com/baidu-research/warp-ctc/pull/118))
+- You need to compile MXNet with WarpCTC, follow the instructions [here](https://github.com/apache/incubator-mxnet/tree/master/example/ctc)
+- You might need to set `LD_LIBRARY_PATH` to the right path if MXNet fails to find your `libwarpctc.so`
- **We strongly recommend that you first test a model of small networks.**
diff --git a/example/speech_recognition/label_util.py b/example/speech_recognition/label_util.py
index dab1d1ef1b40..8563736052bc 100644
--- a/example/speech_recognition/label_util.py
+++ b/example/speech_recognition/label_util.py
@@ -29,7 +29,7 @@ class LabelUtil:
# dataPath
def __init__(self):
- self._log = LogUtil().getlogger()
+ self._log = LogUtil.getInstance().getlogger()
self._log.debug("LabelUtil init")
def load_unicode_set(self, unicodeFilePath):
diff --git a/example/speech_recognition/log_util.py b/example/speech_recognition/log_util.py
index e61407f5f4d5..65c465811fde 100644
--- a/example/speech_recognition/log_util.py
+++ b/example/speech_recognition/log_util.py
@@ -17,48 +17,44 @@
import logging
import logging.handlers
+from singleton import Singleton
+@Singleton
+class LogUtil:
-class SingletonType(type):
- def __call__(cls, *args, **kwargs):
- try:
- return cls.__instance
- except AttributeError:
- cls.__instance = super(SingletonType, cls).__call__(*args, **kwargs)
- return cls.__instance
-
-
-class LogUtil(object):
- __metaclass__ = SingletonType
_logger = None
_filename = None
- def __init__(self, filename=None):
- self._filename = filename
-
- # logger
- self._logger = logging.getLogger('logger')
- # remove default handler
- self._logger.propagate = False
-
- stream_handler = logging.StreamHandler()
- stream_formatter = logging.Formatter('[%(levelname)8s][%(asctime)s.%(msecs)03d] %(message)s',
- datefmt='%Y/%m/%d %H:%M:%S')
- stream_handler.setFormatter(stream_formatter)
-
- if self._filename is not None:
- file_max_bytes = 10 * 1024 * 1024
-
- file_handler = logging.handlers.RotatingFileHandler(filename='./log/' + self._filename,
- maxBytes=file_max_bytes,
- backupCount=10)
- file_formatter = logging.Formatter('[%(levelname)8s][%(asctime)s.%(msecs)03d] %(message)s',
- datefmt='%Y/%m/%d %H:%M:%S')
- file_handler.setFormatter(file_formatter)
- self._logger.addHandler(file_handler)
-
- self._logger.addHandler(stream_handler)
- self._logger.setLevel(logging.DEBUG)
-
- def getlogger(self):
+ def getlogger(self, filename=None):
+ if self._logger is not None and filename is not None:
+ self._logger.warning('Filename %s ignored, logger is already instanciated with %s' % (filename, self._filename))
+ if self._logger is None:
+ self._filename = filename
+
+ # logger
+ self._logger = logging.getLogger('logger')
+ # remove default handler
+ self._logger.propagate = False
+
+ stream_handler = logging.StreamHandler()
+ stream_formatter = logging.Formatter('[%(levelname)8s][%(asctime)s.%(msecs)03d] %(message)s',
+ datefmt='%Y/%m/%d %H:%M:%S')
+ stream_handler.setFormatter(stream_formatter)
+
+ if self._filename is not None:
+ file_max_bytes = 10 * 1024 * 1024
+
+ file_handler = logging.handlers.RotatingFileHandler(filename='./log/' + self._filename,
+ maxBytes=file_max_bytes,
+ backupCount=10)
+ file_formatter = logging.Formatter('[%(levelname)8s][%(asctime)s.%(msecs)03d] %(message)s',
+ datefmt='%Y/%m/%d %H:%M:%S')
+ file_handler.setFormatter(file_formatter)
+ self._logger.addHandler(file_handler)
+
+ self._logger.addHandler(stream_handler)
+ self._logger.setLevel(logging.DEBUG)
+
+
return self._logger
+
diff --git a/example/speech_recognition/main.py b/example/speech_recognition/main.py
index e45026343de7..b2ea42eca0b8 100644
--- a/example/speech_recognition/main.py
+++ b/example/speech_recognition/main.py
@@ -38,6 +38,8 @@
os.environ['MXNET_ENGINE_TYPE'] = "ThreadedEnginePerDevice"
os.environ['MXNET_ENABLE_GPU_P2P'] = "0"
+logUtil = LogUtil.getInstance()
+
class WHCS:
width = 0
height = 0
@@ -91,7 +93,7 @@ def load_data(args):
max_duration = args.config.getfloat('data', 'max_duration')
language = args.config.get('data', 'language')
- log = LogUtil().getlogger()
+ log = logUtil.getlogger()
labelUtil = LabelUtil.getInstance()
if mode == "train" or mode == "load":
data_json = args.config.get('data', 'train_json')
@@ -276,7 +278,7 @@ def load_model(args, contexts, data_train):
mx.random.seed(hash(datetime.now()))
# set log file name
log_filename = args.config.get('common', 'log_filename')
- log = LogUtil(filename=log_filename).getlogger()
+ log = logUtil.getlogger(filename=log_filename)
# set parameters from data section(common)
mode = args.config.get('common', 'mode')
diff --git a/example/speech_recognition/singleton.py b/example/speech_recognition/singleton.py
index 01717e4df068..fdb20c06b149 100644
--- a/example/speech_recognition/singleton.py
+++ b/example/speech_recognition/singleton.py
@@ -18,23 +18,41 @@
import logging as log
+
class Singleton:
+ """
+ A non-thread-safe helper class to ease implementing singletons.
+ This should be used as a decorator -- not a metaclass -- to the
+ class that should be a singleton.
+
+ The decorated class can define one `__init__` function that
+ takes only the `self` argument. Also, the decorated class cannot be
+ inherited from. Other than that, there are no restrictions that apply
+ to the decorated class.
+
+ To get the singleton instance, use the `instance` method. Trying
+ to use `__call__` will result in a `TypeError` being raised.
+
+ """
+
def __init__(self, decorated):
- log.debug("Singleton Init %s" % decorated)
self._decorated = decorated
def getInstance(self):
+ """
+ Returns the singleton instance. Upon its first call, it creates a
+ new instance of the decorated class and calls its `__init__` method.
+ On all subsequent calls, the already created instance is returned.
+
+ """
try:
return self._instance
except AttributeError:
self._instance = self._decorated()
return self._instance
- def __new__(cls, *args, **kwargs):
- print("__new__")
- cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
- return cls._instance
-
def __call__(self):
- raise TypeError("Singletons must be accessed through 'getInstance()'")
+ raise TypeError('Singletons must be accessed through `getInstance()`.')
+ def __instancecheck__(self, inst):
+ return isinstance(inst, self._decorated)
\ No newline at end of file
diff --git a/example/speech_recognition/stt_datagenerator.py b/example/speech_recognition/stt_datagenerator.py
index 8fafa7909377..e1f8f13b7baf 100644
--- a/example/speech_recognition/stt_datagenerator.py
+++ b/example/speech_recognition/stt_datagenerator.py
@@ -28,6 +28,8 @@
from stt_bi_graphemes_util import generate_bi_graphemes_label
from multiprocessing import cpu_count, Process, Manager
+logUtil = LogUtil.getInstance()
+
class DataGenerator(object):
def __init__(self, save_dir, model_name, step=10, window=20, max_freq=8000, desc_file=None):
"""
@@ -86,7 +88,7 @@ def load_metadata_from_desc_file(self, desc_file, partition='train',
max_duration (float): In seconds, the maximum duration of
utterances to train or test on
"""
- logger = LogUtil().getlogger()
+ logger = logUtil.getlogger()
logger.info('Reading description file: {} for partition: {}'
.format(desc_file, partition))
audio_paths, durations, texts = [], [], []
@@ -245,7 +247,7 @@ def sample_normalize(self, k_samples=1000, overwrite=False):
Params:
k_samples (int): Use this number of samples for estimation
"""
- log = LogUtil().getlogger()
+ log = logUtil.getlogger()
log.info("Calculating mean and std from samples")
# if k_samples is negative then it goes through total dataset
if k_samples < 0:
diff --git a/example/speech_recognition/stt_io_iter.py b/example/speech_recognition/stt_io_iter.py
index 6c9bacd1a526..216dae8ac68e 100644
--- a/example/speech_recognition/stt_io_iter.py
+++ b/example/speech_recognition/stt_io_iter.py
@@ -86,7 +86,7 @@ def __init__(self, count, datagen, batch_size, num_label, init_states, seq_lengt
audio_paths = audio_paths
texts = texts
- self.trainDataList = zip(durations, audio_paths, texts)
+ self.trainDataList = list(zip(durations, audio_paths, texts))
# to shuffle data
if not sort_by_duration:
random.shuffle(self.trainDataList)
@@ -103,11 +103,11 @@ def __iter__(self):
texts = []
for i in range(self.batch_size):
try:
- duration, audio_path, text = self.trainDataIter.next()
+ duration, audio_path, text = next(self.trainDataIter)
except:
random.shuffle(self.trainDataList)
self.trainDataIter = iter(self.trainDataList)
- duration, audio_path, text = self.trainDataIter.next()
+ duration, audio_path, text = next(self.trainDataIter)
audio_paths.append(audio_path)
texts.append(text)
if self.is_first_epoch:
diff --git a/example/speech_recognition/stt_metric.py b/example/speech_recognition/stt_metric.py
index ec74fc063dc6..26609627ea58 100644
--- a/example/speech_recognition/stt_metric.py
+++ b/example/speech_recognition/stt_metric.py
@@ -51,7 +51,7 @@ def __init__(self, batch_size, num_gpu, is_epoch_end=False, is_logging=True):
def update(self, labels, preds):
check_label_shapes(labels, preds)
if self.is_logging:
- log = LogUtil().getlogger()
+ log = LogUtil.getInstance().getlogger()
labelUtil = LabelUtil.getInstance()
self.batch_loss = 0.
diff --git a/example/speech_recognition/stt_utils.py b/example/speech_recognition/stt_utils.py
index 0539d59f37af..cc0247223316 100644
--- a/example/speech_recognition/stt_utils.py
+++ b/example/speech_recognition/stt_utils.py
@@ -15,16 +15,12 @@
# specific language governing permissions and limitations
# under the License.
-import logging
import os
import os.path
import numpy as np
-import soundfile
from numpy.lib.stride_tricks import as_strided
-
-
-logger = logging.getLogger(__name__)
+import soundfile
def calc_feat_dim(window, max_freq):
diff --git a/example/speech_recognition/train.py b/example/speech_recognition/train.py
index b1ae50b07558..e585bfd05e65 100644
--- a/example/speech_recognition/train.py
+++ b/example/speech_recognition/train.py
@@ -51,7 +51,7 @@ def do_training(args, module, data_train, data_val, begin_epoch=0):
from distutils.dir_util import mkpath
from log_util import LogUtil
- log = LogUtil().getlogger()
+ log = LogUtil.getInstance().getlogger()
mkpath(os.path.dirname(get_checkpoint_path(args)))
#seq_len = args.config.get('arch', 'max_t_count')
diff --git a/example/ssd/README.md b/example/ssd/README.md
index cc034689c7b1..f70823de4808 100644
--- a/example/ssd/README.md
+++ b/example/ssd/README.md
@@ -4,6 +4,14 @@ SSD is an unified framework for object detection with a single network.
You can use the code to train/evaluate/test for object detection task.
+-------------------
+
+## Gluon Implementation
+
+You can find a Gluon implementation on [gluon-cv](https://gluon-cv.mxnet.io/build/examples_detection/train_ssd_voc.html).
+
+-------------------
+
### Disclaimer
This is a re-implementation of original SSD which is based on caffe. The official
repository is available [here](https://github.com/weiliu89/caffe/tree/ssd).
@@ -94,6 +102,7 @@ will open a window that will display the camera output together with the detecti
with the detection threshold to get more or less detections.
### Train the model
+* Note that we recommend to use gluon-cv to train the model, please refer to [gluon-cv ssd](https://gluon-cv.mxnet.io/build/examples_detection/train_ssd_voc.html).
This example only covers training on Pascal VOC dataset. Other datasets should
be easily supported by adding subclass derived from class `Imdb` in `dataset/imdb.py`.
See example of `dataset/pascal_voc.py` for details.
diff --git a/example/ssd/benchmark_score.py b/example/ssd/benchmark_score.py
index caeb208e79c2..01a0eb9528da 100644
--- a/example/ssd/benchmark_score.py
+++ b/example/ssd/benchmark_score.py
@@ -34,12 +34,19 @@
parser.add_argument('--batch_size', '-b', type=int, default=0)
parser.add_argument('--shape', '-w', type=int, default=300)
parser.add_argument('--class_num', '-class', type=int, default=20)
+parser.add_argument('--prefix', dest='prefix', help='load model prefix',
+ default=os.path.join(os.getcwd(), 'model', 'ssd_'), type=str)
+parser.add_argument('--deploy', dest='deploy', help='Load network from model',
+ action='store_true', default=False)
def get_data_shapes(batch_size):
image_shape = (3, 300, 300)
return [('data', (batch_size,)+image_shape)]
+def get_label_shapes(batch_size):
+ return [('label', (batch_size,) + (42, 6))]
+
def get_data(batch_size):
data_shapes = get_data_shapes(batch_size)
data = [mx.random.uniform(-1.0, 1.0, shape=shape, ctx=mx.cpu()) for _, shape in data_shapes]
@@ -53,6 +60,7 @@ def get_data(batch_size):
image_shape = args.shape
num_classes = args.class_num
b = args.batch_size
+ prefix = args.prefix
supported_image_shapes = [300, 512]
supported_networks = ['vgg16_reduced', 'inceptionv3', 'resnet50']
@@ -68,18 +76,27 @@ def get_data(batch_size):
batch_sizes = [b]
data_shape = (3, image_shape, image_shape)
- net = get_symbol(network, data_shape[1], num_classes=num_classes,
- nms_thresh=0.4, force_suppress=True)
+
+ if args.deploy == True:
+ prefix += network + '_' + str(data_shape[1]) + '-symbol.json'
+ net = mx.sym.load(prefix)
+ else:
+ net = get_symbol(network, data_shape[1], num_classes=num_classes,
+ nms_thresh=0.4, force_suppress=True)
+ if not 'label' in net.list_arguments():
+ label = mx.sym.Variable(name='label')
+ net = mx.sym.Group([net, label])
num_batches = 100
dry_run = 5 # use 5 iterations to warm up
for bs in batch_sizes:
batch = get_data(bs)
- mod = mx.mod.Module(net, label_names=None, context=mx.cpu())
+ mod = mx.mod.Module(net, label_names=('label',), context=mx.cpu())
mod.bind(for_training = False,
inputs_need_grad = False,
- data_shapes = get_data_shapes(bs))
+ data_shapes = get_data_shapes(bs),
+ label_shapes = get_label_shapes(bs))
mod.init_params(initializer=mx.init.Xavier(magnitude=2.))
# get data
diff --git a/example/ssd/config/config.py b/example/ssd/config/config.py
index b084888f4ba5..8d44a0d992c3 100644
--- a/example/ssd/config/config.py
+++ b/example/ssd/config/config.py
@@ -16,7 +16,7 @@
# under the License.
import os
-from config.utils import DotDict, namedtuple_with_defaults, zip_namedtuple, config_as_dict
+from .utils import DotDict, namedtuple_with_defaults, zip_namedtuple, config_as_dict
RandCropper = namedtuple_with_defaults('RandCropper',
'min_crop_scales, max_crop_scales, \
diff --git a/example/ssd/evaluate.py b/example/ssd/evaluate.py
index d1a83cca342f..bbe9feab333c 100644
--- a/example/ssd/evaluate.py
+++ b/example/ssd/evaluate.py
@@ -30,6 +30,8 @@ def parse_args():
default="", type=str)
parser.add_argument('--network', dest='network', type=str, default='vgg16_reduced',
help='which network to use')
+ parser.add_argument('--num-batch', dest='num_batch', type=int, default=5,
+ help='evaluation number batches')
parser.add_argument('--batch-size', dest='batch_size', type=int, default=32,
help='evaluation batch size')
parser.add_argument('--num-class', dest='num_class', type=int, default=20,
@@ -97,7 +99,7 @@ def parse_args():
prefix = args.prefix + args.network
else:
prefix = args.prefix
- evaluate_net(network, args.rec_path, num_class,
+ evaluate_net(network, args.rec_path, num_class, args.num_batch,
(args.mean_r, args.mean_g, args.mean_b), args.data_shape,
prefix, args.epoch, ctx, batch_size=args.batch_size,
path_imglist=args.list_path, nms_thresh=args.nms_thresh,
diff --git a/example/ssd/evaluate/evaluate_net.py b/example/ssd/evaluate/evaluate_net.py
index fabe54f00c75..35e253d44bba 100644
--- a/example/ssd/evaluate/evaluate_net.py
+++ b/example/ssd/evaluate/evaluate_net.py
@@ -24,10 +24,15 @@
from config.config import cfg
from evaluate.eval_metric import MApMetric, VOC07MApMetric
import logging
+import time
from symbol.symbol_factory import get_symbol
+from symbol import symbol_builder
+from mxnet.base import SymbolHandle, check_call, _LIB, mx_uint, c_str_array
+import ctypes
+from mxnet.contrib.quantization import *
-def evaluate_net(net, path_imgrec, num_classes, mean_pixels, data_shape,
- model_prefix, epoch, ctx=mx.cpu(), batch_size=1,
+def evaluate_net(net, path_imgrec, num_classes, num_batch, mean_pixels, data_shape,
+ model_prefix, epoch, ctx=mx.cpu(), batch_size=32,
path_imglist="", nms_thresh=0.45, force_nms=False,
ovp_thresh=0.5, use_difficult=False, class_names=None,
voc07_metric=False):
@@ -106,6 +111,23 @@ class names in string, must correspond to num_classes if set
metric = VOC07MApMetric(ovp_thresh, use_difficult, class_names)
else:
metric = MApMetric(ovp_thresh, use_difficult, class_names)
- results = mod.score(eval_iter, metric, num_batch=None)
+
+ num = num_batch * batch_size
+ data = [mx.random.uniform(-1.0, 1.0, shape=shape, ctx=ctx) for _, shape in mod.data_shapes]
+ batch = mx.io.DataBatch(data, []) # empty label
+
+ dry_run = 5 # use 5 iterations to warm up
+ for i in range(dry_run):
+ mod.forward(batch, is_train=False)
+ for output in mod.get_outputs():
+ output.wait_to_read()
+
+ tic = time.time()
+ results = mod.score(eval_iter, metric, num_batch=num_batch)
+ speed = num / (time.time() - tic)
+ if logger is not None:
+ logger.info('Finished inference with %d images' % num)
+ logger.info('Finished with %f images per second', speed)
+
for k, v in results:
print("{}: {}".format(k, v))
diff --git a/example/ssd/quantization.py b/example/ssd/quantization.py
new file mode 100644
index 000000000000..5cb74ba11a89
--- /dev/null
+++ b/example/ssd/quantization.py
@@ -0,0 +1,168 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import print_function
+import os
+import sys
+import importlib
+import mxnet as mx
+from dataset.iterator import DetRecordIter
+from config.config import cfg
+from evaluate.eval_metric import MApMetric, VOC07MApMetric
+import argparse
+import logging
+import time
+from symbol.symbol_factory import get_symbol
+from symbol import symbol_builder
+from mxnet.base import SymbolHandle, check_call, _LIB, mx_uint, c_str_array
+import ctypes
+from mxnet.contrib.quantization import *
+
+def save_symbol(fname, sym, logger=None):
+ if logger is not None:
+ logger.info('Saving symbol into file at %s' % fname)
+ sym.save(fname)
+
+
+def save_params(fname, arg_params, aux_params, logger=None):
+ if logger is not None:
+ logger.info('Saving params into file at %s' % fname)
+ save_dict = {('arg:%s' % k): v.as_in_context(cpu()) for k, v in arg_params.items()}
+ save_dict.update({('aux:%s' % k): v.as_in_context(cpu()) for k, v in aux_params.items()})
+ mx.nd.save(fname, save_dict)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Generate a calibrated quantized SSD model from a FP32 model')
+ parser.add_argument('--batch-size', type=int, default=32)
+ parser.add_argument('--num-calib-batches', type=int, default=5,
+ help='number of batches for calibration')
+ parser.add_argument('--exclude-first-conv', action='store_true', default=True,
+ help='excluding quantizing the first conv layer since the'
+ ' number of channels is usually not a multiple of 4 in that layer'
+ ' which does not satisfy the requirement of cuDNN')
+ parser.add_argument('--shuffle-dataset', action='store_true', default=True,
+ help='shuffle the calibration dataset')
+ parser.add_argument('--shuffle-chunk-seed', type=int, default=3982304,
+ help='shuffling chunk seed, see'
+ ' https://mxnet.incubator.apache.org/api/python/io/io.html?highlight=imager#mxnet.io.ImageRecordIter'
+ ' for more details')
+ parser.add_argument('--shuffle-seed', type=int, default=48564309,
+ help='shuffling seed, see'
+ ' https://mxnet.incubator.apache.org/api/python/io/io.html?highlight=imager#mxnet.io.ImageRecordIter'
+ ' for more details')
+ parser.add_argument('--calib-mode', type=str, default='naive',
+ help='calibration mode used for generating calibration table for the quantized symbol; supports'
+ ' 1. none: no calibration will be used. The thresholds for quantization will be calculated'
+ ' on the fly. This will result in inference speed slowdown and loss of accuracy'
+ ' in general.'
+ ' 2. naive: simply take min and max values of layer outputs as thresholds for'
+ ' quantization. In general, the inference accuracy worsens with more examples used in'
+ ' calibration. It is recommended to use `entropy` mode as it produces more accurate'
+ ' inference results.'
+ ' 3. entropy: calculate KL divergence of the fp32 output and quantized output for optimal'
+ ' thresholds. This mode is expected to produce the best inference accuracy of all three'
+ ' kinds of quantized models if the calibration dataset is representative enough of the'
+ ' inference dataset.')
+ parser.add_argument('--quantized-dtype', type=str, default='uint8',
+ choices=['int8', 'uint8'],
+ help='quantization destination data type for input data')
+
+ args = parser.parse_args()
+ ctx = mx.cpu(0)
+ logging.basicConfig()
+ logger = logging.getLogger('logger')
+ logger.setLevel(logging.INFO)
+
+ logger.info('shuffle_dataset=%s' % args.shuffle_dataset)
+
+ calib_mode = args.calib_mode
+ logger.info('calibration mode set to %s' % calib_mode)
+
+ # load FP32 models
+ prefix, epoch = "./model/ssd_vgg16_reduced_300", 0
+ sym, arg_params, aux_params = mx.model.load_checkpoint("./model/ssd_vgg16_reduced_300", 0)
+
+ if not 'label' in sym.list_arguments():
+ label = mx.sym.Variable(name='label')
+ sym = mx.sym.Group([sym, label])
+
+ sym = sym.get_backend_symbol('MKLDNN')
+
+ # get batch size
+ batch_size = args.batch_size
+ logger.info('batch size = %d for calibration' % batch_size)
+
+ # get number of batches for calibration
+ num_calib_batches = args.num_calib_batches
+ if calib_mode != 'none':
+ logger.info('number of batches = %d for calibration' % num_calib_batches)
+
+ # get image shape
+ image_shape = '3,300,300'
+
+ # Quantization layer configs
+ exclude_first_conv = args.exclude_first_conv
+ excluded_sym_names = []
+ rgb_mean = '123,117,104'
+ calib_layer = lambda name: name.endswith('_output')
+ for i in range(1,19):
+ excluded_sym_names += ['flatten'+str(i)]
+ excluded_sym_names += ['relu4_3_cls_pred_conv',
+ 'relu7_cls_pred_conv',
+ 'relu4_3_loc_pred_conv']
+ if exclude_first_conv:
+ excluded_sym_names += ['conv1_1']
+
+ label_name = 'label'
+ logger.info('label_name = %s' % label_name)
+
+ data_shape = tuple([int(i) for i in image_shape.split(',')])
+ logger.info('Input data shape = %s' % str(data_shape))
+
+ logger.info('rgb_mean = %s' % rgb_mean)
+ rgb_mean = [float(i) for i in rgb_mean.split(',')]
+ mean_args = {'mean_r': rgb_mean[0], 'mean_g': rgb_mean[1], 'mean_b': rgb_mean[2]}
+
+ if calib_mode == 'none':
+ logger.info('Quantizing FP32 model %s' % args.model)
+ qsym, qarg_params, aux_params = quantize_model(sym=sym, arg_params=arg_params, aux_params=aux_params,
+ ctx=ctx, excluded_sym_names=excluded_sym_names,
+ calib_mode=calib_mode, quantized_dtype=args.quantized_dtype,
+ logger=logger)
+ sym_name = '%s-symbol.json' % ('./model/qssd_vgg16_reduced_300')
+ param_name = '%s-%04d.params' % ('./model/qssd_vgg16_reduced_300', epoch)
+ save_symbol(sym_name, qsym, logger)
+ else:
+ logger.info('Creating ImageRecordIter for reading calibration dataset')
+ eval_iter = DetRecordIter(os.path.join(os.getcwd(), 'data', 'val.rec'),
+ batch_size, data_shape, mean_pixels=(123, 117, 104),
+ path_imglist="", **cfg.valid)
+
+ qsym, qarg_params, aux_params = quantize_model(sym=sym, arg_params=arg_params, aux_params=aux_params,
+ ctx=ctx, excluded_sym_names=excluded_sym_names,
+ calib_mode=calib_mode, calib_data=eval_iter,
+ num_calib_examples=num_calib_batches * batch_size,
+ calib_layer=calib_layer, quantized_dtype=args.quantized_dtype,
+ label_names=(label_name,),
+ calib_quantize_op = True,
+ logger=logger)
+ sym_name = '%s-symbol.json' % ('./model/cqssd_vgg16_reduced_300')
+ param_name = '%s-%04d.params' % ('./model/cqssd_vgg16_reduced_300', epoch)
+ qsym = qsym.get_backend_symbol('MKLDNN_POST_QUANTIZE')
+ save_symbol(sym_name, qsym, logger)
+ save_params(param_name, qarg_params, aux_params, logger)
diff --git a/example/stochastic-depth/sd_cifar10.py b/example/stochastic-depth/sd_cifar10.py
index 7eb320287016..e995ea44f76f 100644
--- a/example/stochastic-depth/sd_cifar10.py
+++ b/example/stochastic-depth/sd_cifar10.py
@@ -78,9 +78,6 @@
import mxnet as mx
import logging
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from utils import get_data
-
import sd_module
def residual_module(death_rate, n_channel, name_scope, context, stride=1, bn_momentum=0.9):
@@ -199,13 +196,31 @@ def get_death_rate(i_res_block):
batch_end_callbacks = [mx.callback.Speedometer(batch_size, 50)]
epoch_end_callbacks = [mx.callback.do_checkpoint('sd-%d' % (n_residual_blocks * 6 + 2))]
-
-args = type('', (), {})()
-args.batch_size = batch_size
-args.data_dir = os.path.join(os.path.dirname(__file__), "data")
+data_dir = os.path.join(os.path.dirname(__file__), "data", "cifar")
kv = mx.kvstore.create(kv_store)
-train, val = get_data.get_cifar10_iterator(args, kv)
+mx.test_utils.get_cifar10()
+
+data_shape = (3, 28, 28)
+train = mx.io.ImageRecordIter(
+ path_imgrec = os.path.join(data_dir, "train.rec"),
+ mean_img = os.path.join(data_dir, "mean.bin"),
+ data_shape = data_shape,
+ batch_size = batch_size,
+ rand_crop = True,
+ rand_mirror = True,
+ num_parts = kv.num_workers,
+ part_index = kv.rank)
+
+val = mx.io.ImageRecordIter(
+ path_imgrec = os.path.join(data_dir, "test.rec"),
+ mean_img = os.path.join(data_dir, "mean.bin"),
+ rand_crop = False,
+ rand_mirror = False,
+ data_shape = data_shape,
+ batch_size = batch_size,
+ num_parts = kv.num_workers,
+ part_index = kv.rank)
logging.basicConfig(level=logging.DEBUG)
mod_seq.fit(train, val,
diff --git a/example/stochastic-depth/sd_mnist.py b/example/stochastic-depth/sd_mnist.py
index 7eb93741ff5a..6c95a23bf23c 100644
--- a/example/stochastic-depth/sd_mnist.py
+++ b/example/stochastic-depth/sd_mnist.py
@@ -25,9 +25,6 @@
import mxnet as mx
import logging
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from utils import get_data
-
import sd_module
def get_conv(
@@ -121,8 +118,7 @@ def get_conv(
batch_size = 100
basedir = os.path.dirname(__file__)
-get_data.get_mnist(os.path.join(basedir, "data"))
-
+mx.test_utils.get_mnist_ubyte()
train = mx.io.MNISTIter(
image=os.path.join(basedir, "data", "train-images-idx3-ubyte"),
label=os.path.join(basedir, "data", "train-labels-idx1-ubyte"),
diff --git a/example/svm_mnist/svm_mnist.py b/example/svm_mnist/svm_mnist.py
index 679540198d28..3fc0362f6b01 100644
--- a/example/svm_mnist/svm_mnist.py
+++ b/example/svm_mnist/svm_mnist.py
@@ -20,16 +20,23 @@
## Please read the README.md document for better reference ##
#############################################################
from __future__ import print_function
+
+import logging
+import random
+
import mxnet as mx
import numpy as np
from sklearn.datasets import fetch_mldata
from sklearn.decomposition import PCA
-# import matplotlib.pyplot as plt
-import logging
+
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
+np.random.seed(1234) # set seed for deterministic ordering
+mx.random.seed(1234)
+random.seed(1234)
+
# Network declaration as symbols. The following pattern was based
# on the article, but feel free to play with the number of nodes
# and with the activation function
@@ -41,60 +48,77 @@
fc3 = mx.symbol.FullyConnected(data = act2, name='fc3', num_hidden=10)
# Here we add the ultimate layer based on L2-SVM objective
-mlp = mx.symbol.SVMOutput(data=fc3, name='svm')
+mlp_svm_l2 = mx.symbol.SVMOutput(data=fc3, name='svm_l2')
+
+# With L1-SVM objective
+mlp_svm_l1 = mx.symbol.SVMOutput(data=fc3, name='svm_l1', use_linear=True)
-# To use L1-SVM objective, comment the line above and uncomment the line below
-# mlp = mx.symbol.SVMOutput(data=fc3, name='svm', use_linear=True)
+# Compare with softmax cross entropy loss
+mlp_softmax = mx.symbol.SoftmaxOutput(data=fc3, name='softmax')
+
+print("Preparing data...")
+mnist_data = mx.test_utils.get_mnist()
+X = np.concatenate([mnist_data['train_data'], mnist_data['test_data']])
+Y = np.concatenate([mnist_data['train_label'], mnist_data['test_label']])
+X = X.reshape((X.shape[0], -1)).astype(np.float32) * 255
# Now we fetch MNIST dataset, add some noise, as the article suggests,
# permutate and assign the examples to be used on our network
-mnist = fetch_mldata('MNIST original')
-mnist_pca = PCA(n_components=70).fit_transform(mnist.data)
+mnist_pca = PCA(n_components=70).fit_transform(X)
noise = np.random.normal(size=mnist_pca.shape)
mnist_pca += noise
-np.random.seed(1234) # set seed for deterministic ordering
p = np.random.permutation(mnist_pca.shape[0])
-X = mnist_pca[p]
-Y = mnist.target[p]
-X_show = mnist.data[p]
+X = mnist_pca[p] / 255.
+Y = Y[p]
+X_show = X[p]
# This is just to normalize the input and separate train set and test set
-X = X.astype(np.float32)/255
X_train = X[:60000]
X_test = X[60000:]
X_show = X_show[60000:]
Y_train = Y[:60000]
Y_test = Y[60000:]
-
+print("Data prepared.")
# Article's suggestion on batch size
batch_size = 200
-train_iter = mx.io.NDArrayIter(X_train, Y_train, batch_size=batch_size, label_name='svm_label')
-test_iter = mx.io.NDArrayIter(X_test, Y_test, batch_size=batch_size, label_name='svm_label')
-
-# Here we instatiate and fit the model for our data
-# The article actually suggests using 400 epochs,
-# But I reduced to 10, for convinience
-mod = mx.mod.Module(
- context = mx.cpu(0), # Run on CPU 0
- symbol = mlp, # Use the network we just defined
- label_names = ['svm_label'],
-)
-mod.fit(
- train_data=train_iter,
- eval_data=test_iter, # Testing data set. MXNet computes scores on test set every epoch
- batch_end_callback = mx.callback.Speedometer(batch_size, 200), # Logging module to print out progress
- num_epoch = 10, # Train for 10 epochs
- optimizer_params = {
- 'learning_rate': 0.1, # Learning rate
- 'momentum': 0.9, # Momentum for SGD with momentum
- 'wd': 0.00001, # Weight decay for regularization
- },
-)
-
-# Uncomment to view an example
-# plt.imshow((X_show[0].reshape((28,28))*255).astype(np.uint8), cmap='Greys_r')
-# plt.show()
-# print 'Result:', model.predict(X_test[0:1])[0].argmax()
-
-# Now it prints how good did the network did for this configuration
-print('Accuracy:', mod.score(test_iter, mx.metric.Accuracy())[0][1]*100, '%')
+
+ctx = mx.gpu() if len(mx.test_utils.list_gpus()) > 0 else mx.cpu()
+
+results = {}
+for output in [mlp_svm_l2, mlp_svm_l1, mlp_softmax]:
+
+ print("\nTesting with %s \n" % output.name)
+
+ label = output.name + "_label"
+
+ train_iter = mx.io.NDArrayIter(X_train, Y_train, batch_size=batch_size, label_name=label)
+ test_iter = mx.io.NDArrayIter(X_test, Y_test, batch_size=batch_size, label_name=label)
+
+ # Here we instatiate and fit the model for our data
+ # The article actually suggests using 400 epochs,
+ # But I reduced to 10, for convenience
+
+ mod = mx.mod.Module(
+ context = ctx,
+ symbol = output, # Use the network we just defined
+ label_names = [label],
+ )
+ mod.fit(
+ train_data=train_iter,
+ eval_data=test_iter, # Testing data set. MXNet computes scores on test set every epoch
+ batch_end_callback = mx.callback.Speedometer(batch_size, 200), # Logging module to print out progress
+ num_epoch = 10, # Train for 10 epochs
+ optimizer_params = {
+ 'learning_rate': 0.1, # Learning rate
+ 'momentum': 0.9, # Momentum for SGD with momentum
+ 'wd': 0.00001, # Weight decay for regularization
+ })
+ results[output.name] = mod.score(test_iter, mx.metric.Accuracy())[0][1]*100
+ print('Accuracy for %s:'%output.name, mod.score(test_iter, mx.metric.Accuracy())[0][1]*100, '%\n')
+
+for key, value in results.items():
+ print(key, value, "%s")
+
+#svm_l2 97.85 %s
+#svm_l1 98.15 %s
+#softmax 97.69 %s
\ No newline at end of file
diff --git a/example/svrg_module/README.md b/example/svrg_module/README.md
index 63e7ba2f2bfa..250995a57152 100644
--- a/example/svrg_module/README.md
+++ b/example/svrg_module/README.md
@@ -1,7 +1,9 @@
## SVRGModule Example
+
SVRGModule is an extension to the Module API that implements SVRG optimization, which stands for Stochastic
Variance Reduced Gradient. SVRG is an optimization technique that complements SGD and has several key
properties:
+
* Employs explicit variance reduction by using a different update rule compared to SGD.
* Ability to use relatively large learning rate, which leads to faster convergence compared to SGD.
* Guarantees for fast convergence for smooth and strongly convex functions.
@@ -18,7 +20,9 @@ training script.
##### Dataset
YearPredictionMSD: contains predictions of the release year of a song from audio features. It has over
-400,000 samples with 90 features. Please uncomment data downloading script from data_reader.py to download the data.
+400,000 samples with 90 features. It will be automatically downloaded on first execution and cached.
+
+YearPredictionMSD dataset: https://archive.ics.uci.edu/ml/datasets/yearpredictionmsd
#### Benchmarks:
An initial set of benchmarks has been performed on YearPredictionDatasetMSD with linear regression model. A jupyter
diff --git a/example/svrg_module/benchmarks/svrg_benchmark.ipynb b/example/svrg_module/benchmarks/svrg_benchmark.ipynb
index db02938af466..54ae81281db3 100644
--- a/example/svrg_module/benchmarks/svrg_benchmark.ipynb
+++ b/example/svrg_module/benchmarks/svrg_benchmark.ipynb
@@ -16,17 +16,28 @@
},
{
"cell_type": "code",
- "execution_count": 16,
+ "execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
- "import mxnet as mx\n",
- "from sklearn.datasets import load_svmlight_file\n",
- "import numpy as np\n",
+ "import os\n",
"import json\n",
+ "import sys\n",
"import tempfile\n",
- "import os\n",
- "from mxnet.contrib.svrg_optimization.svrg_module import SVRGModule\n"
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import matplotlib.patches as mpatches\n",
+ "import mxnet as mx\n",
+ "from mxnet.contrib.svrg_optimization.svrg_module import SVRGModule\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import seaborn as sns\n",
+ "from sklearn.datasets import load_svmlight_file\n",
+ "\n",
+ "sys.path.insert(0, \"../linear_regression\")\n",
+ "from data_reader import get_year_prediction_data\n",
+ "\n",
+ "%matplotlib inline"
]
},
{
@@ -39,47 +50,30 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Download data file\n",
- "# from subprocess import call\n",
- "# YearPredictionMSD dataset: https://archive.ics.uci.edu/ml/datasets/yearpredictionmsd\n",
- "# call(['wget', 'https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/YearPredictionMSD.bz2'])\n",
- "# call(['bzip2', '-d', 'YearPredictionMSD.bz2'])"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 30,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "Extracting data...\n",
"Reading data from disk...\n"
]
}
],
"source": [
- "feature_dim = 90\n",
- "print(\"Reading data from disk...\")\n",
- "train_features, train_labels = load_svmlight_file('YearPredictionMSD', n_features=feature_dim, dtype=np.float32)\n",
- "train_features = train_features.todense()\n",
- "\n",
- "# normalize the data: subtract means and divide by standard deviations\n",
- "label_mean = train_labels.mean()\n",
- "label_std = np.sqrt(np.square(train_labels - label_mean).mean())\n",
- "feature_means = train_features.mean(axis=0)\n",
- "feature_stds = np.sqrt(np.square(train_features - feature_means).mean(axis=0))\n",
- "\n",
- "train_features = (train_features - feature_means) / feature_stds\n",
- "train_labels = (train_labels - label_mean) / label_std\n",
- "\n",
+ "feature_dim, train_features, train_labels = get_year_prediction_data()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
"train_features = train_features[-5000:]\n",
- "train_labels = train_labels[-5000:]"
+ "train_labels = train_labels[-5000:]"
]
},
{
@@ -91,7 +85,7 @@
},
{
"cell_type": "code",
- "execution_count": 19,
+ "execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
@@ -119,7 +113,7 @@
},
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
@@ -162,7 +156,7 @@
},
{
"cell_type": "code",
- "execution_count": 25,
+ "execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
@@ -194,19 +188,6 @@
" "
]
},
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [],
- "source": [
- "import json\n",
- "import seaborn as sns\n",
- "import matplotlib.pyplot as plt\n",
- "import matplotlib.patches as mpatches\n",
- "import pandas as pd"
- ]
- },
{
"cell_type": "markdown",
"metadata": {},
@@ -217,7 +198,7 @@
},
{
"cell_type": "code",
- "execution_count": 31,
+ "execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
@@ -227,7 +208,7 @@
},
{
"cell_type": "code",
- "execution_count": 32,
+ "execution_count": 10,
"metadata": {},
"outputs": [
{
@@ -236,13 +217,13 @@
"Text(0.5,0,'Epochs')"
]
},
- "execution_count": 32,
+ "execution_count": 10,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
- "image/png": "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8pWv5Lbbbstzn/vc/NM//VP+8i//Mr/xG7+RK6+8Mqeffno+/elP54477kgpJQ8//HCS5A1veEMuuuiinHbaabnvvvvywhe+MLfffntXfwcdUQAAAACzbM2aNbnmmmty8cUX5xvf+EYOOuigrF27Np/97GczPDycK6+8Mueee26SpNls5iUveUmS5I477sgRRxyR1atXJ8mUgqgXvehFWbRoUdasWZORkZGsXbt2rIZ77703Bx54YPr6+vKa17wmn/rUp9Lf358kueaaa/K6170uJ5xwQs4555w8+uijeeyxx7r6O+iIAgAAAJhlz3zmM3PjjTfmqquuylvf+tacccYZednLXpZLLrkkBx98cE4++eQccMABSZK+vr40m80kSa11j5/V29ubJGk0Glm0aFFKKWP7w8PD6enpyfXXX58vfelL+fjHP573v//9+fKXv5zR0dFcd911WbJkSZf+6p3piAIAAACYZevXr09/f38uuOCCvOlNb8pNN92UX/3VX81NN92USy+9dGwsb7yjjjoq99xzT+69994kyRVXXDHjWh5//PE88sgjOfPMM/Oe97wnt9xyS5LkjDPOyPvf//6x67Yd7yYdUQAAAMB+5ZBFPV19090hi3Yfr3zve9/Lm9/85rEupQ984ANpNps5++yzc9lll+XDH/7whN9bsmRJ/uZv/iZr167N8uXLc8opp8y43sceeyznnntuBgcHU2vNu9/97iTJ+973vrz2ta/N8ccfn+Hh4Zx++un527/92xk/r1OZTovXQnbSSSfVG264Yb7LAAAAAObI7bffnqOPPnq+y5i2xx9/PEuXLk2tNa997Wtz5JFH5qKLLpq3eib6PUspN9ZaT9rdd43mAQAAAOzFLr300pxwwgk59thj88gjj+TCCy+c75KmzWgeAAAAwF7soosu2qkD6kMf+lDe+9737nDs1FNPzSWXXDKXpe0xQRQAAADAAvOqV70qr3rVq+a7jD1mNA8AAACAOSGIAgAAAGBOCKIAAAAAmBOCKAAAAADmhMXKAQAAgP3KN659TrZs2di1+y1evDzPO+2fu3a/fZmOKAAAAGC/0s0QajbuN97hhx+ejRtn9xlzRRAFAAAAwJzYb4OokZGh3H//Zbn+2+fm2m+emptuOj8/+cnnUmud79IAAACAfczmzZtz1lln5VnPelaOO+64XHHFFbnqqqty1FFH5bTTTsvrX//6nH322UmSTZs25YwzzsiJJ56YCy+8cJdZxb333pujjjoqr3nNa3Lcccfl/PPPzzXXXJNTTz01Rx55ZK6//vokyde+9rWccMIJOeGEE3LiiSfmscceS5L81V/9VU4++eQcf/zxecc73jHrv8N+GUSNjAzmlu+8Mv9y55/mscduzdDQj/Ozh/93bv3+G/KDH7xNGAUAAAB01ec///kcdthh+c53vpNbb701a9euzYUXXpirr7461157bTZs2DB27Z/8yZ/ktNNOy80335xzzjkn99133y7vfdddd+UNb3hDvvvd7+aOO+7I5ZdfnmuvvTZ//dd/nT//8z9Pkvz1X/91Lrnkktxyyy35xje+kSVLluSLX/xi7rzzzlx//fW55ZZbcuONN+brX//6rP4O+2UQdd/9/5CHH75+wnMPrP9YNm366twWBAAAAOzT1qxZk2uuuSYXX3xxvvGNb+SHP/xhjjjiiKxevTpJct55541d+/Wvfz0XXHBBkuSss87KU5/61F3ee/Xq1VmzZk0ajUaOPfbYPP/5z08pJWvWrMm9996bJDn11FPzh3/4h3nf+96Xhx9+OD09PfniF7+YL37xiznxxBPz7Gc/O3fccUfuvPPO2fkB2vbLt+Y9uP6Tuzy//sFPZvny/2OOqgEAAAD2dc985jNz44035qqrrspb3/rWvOAFL9jl9aWUKd+7t7d3bLvRaIztNxqNDA8PJ0ne8pa35KyzzspVV12VX/qlX8o111yTWmve+ta35sILL5zGXzQ9+2VH1ODQg7s8PzS4fo4qAQAAAOba4sXL5/x+69evT39/fy644IK86U1vyre+9a3cc889Yx1LV1xxxdi1p59+ej760Y8mSa6++ur87Gc/m3GNd999d9asWZOLL744J510Uu6444688IUvzAc/+ME8/vjjSZIHHnggP/3pT2f8rF3ZLzui+nqfnicHJ5+v7O07bA6rAQAAAObS80775zl/5ve+9728+c1vTqPRyKJFi/KBD3wgDz74YNauXZvly5fnlFNOGbv2He94R84777w8+9nPzq/8yq9k1apVM37+e97znnzlK19Js9nMMccckxe96EXp7e3N7bffnuc+97lJkqVLl+YjH/lIDj300Bk/bzJlf1uY+6STTqqf/OT/mbvv+a+TXvOsZ/1Dli/71bkrCgAA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SDW7vSQa9OREMCanMuZXqHN6sOpkSqwzRy97VJUqhxtZgjKbJaJZPIEiBPsdLryqYLbNHGawTeX/tXfn0ZZU1R3Hv1tmBAFBlElbZZAsxSE4ZQkiGGKIoigYXYpxTKLLiDFqNCbSSpyCQaMJsiIqTjESNYhxAMTglAgKStPIoGgrg6gIAm0HtOHkj1Pvdb2qvXfVk+7b1c3vs9Zdfe97vz7vVO1b55yqd+99pRQu/dUt3LT6Nu6/9ZaD23fFqlv46a2r2WOrzQe376pbfs2K/7uVnTbflH223jLdvp//+jdcuvIWttl0E/bbdqv0FyU3rb6NZTevYhMzHrrt1unz85bb6meeri6FB2+7lXvhfM7q2wsX3PQrVt52O/tus+WCV+qCLkSFdCFKRERERERERGTtGnshSn9LV0REREREREREZkIXokREREREREREZCZ0IUpERERERERERGZCF6JERERERERERGQmdCFKRERERERERERmQheiRERERERERERkJnQhSkREREREREREZkIXokREREREREREZCZ0IUpERERERERERGZCF6JERERERERERGQmdCFKRERERERERERmQheiRERERERERERkJnQhSkREREREREREZkIXokREREREREREZCZ0IUpERERERERERGZCF6JERERERERERGQmdCFKRERERERERERmQheiRERERERERERkJnQhSkREREREREREZkIXokREREREREREZCZ0IUpERERERERERGZCF6JERERERERERGQmdCFKRERERERERERmQheiRERERERERERkJnQhSkREREREREREZkIXokREREREREREZCZ0IUpERERERERERGbCSinruw8zZWY/B37U+fJOwHWLaGYx+Y05O5V+bGjZqfRjCtmp9GMK2an0YwrZqfRjQ8tOpR9TyE6lH1PITqUfU8hOpR8bWnYq/ZhCdir9mEJ2Kv2YQnYq/djQslPpxxSyU+nH2sjep5Ryj8H/XUq509+Ab62r/MacnUo/NrTsVPoxhexU+jGF7FT6MYXsVPqxoWWn0o8pZKfSjylkp9KPKWSn0o8NLTuVfkwhO5V+TCE7lX5MITuVfmxo2an0YwrZqfRjXW5f96a35omIiIiIiIiIyEzoQpSIiIiIiIiIiMyELkRV/7oO8xtzdir92NCyU+nHFLJT6ccUslPp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wObA/ut6A0VERESGWCllffdBREREZKNgZiuoF5pWt778KuA3wJuB3Uqz+DKz84B3A+cAK4DtSyk3N997C7BLKeW5ZnYZ8OpSyqedn1eAA0opX2senwpcUEp5q5l9CLgFeGMp5ap1sLkiIiIii6a35omIiIisXU8ppWzfur23+frVZeFvAH9EfQXUrsD1cxehWt/brbm/B3BF8vOubd1fBWzT3H81YMB5ZnaxmT3/t9weERERkbVGF6JEREREZmO3uc9vatwbuKa53d3Mtu187+rm/pXA/Rf7w0op15ZSXlRK2RX4M+BEM9vzt+u6iIiIyNqhC1EiIiIis7Ez8DIz28zMjgL2BT5XSrkS+B/gLWa2pZntB7wA+Gjz/04GjjOzvazaz8x2HPphZnaUme3ePLwBKMBta3ujRERERBZDH1YuIiIisnZ9xszaF3zOAj4NnAvsBVwH/BQ4spTyiybzTOAk6qujbgCOLaWc1XzvBGAL6geP7wRcChwxoh8PB95pZts1P++YUsoP78iGiYiIiNxR+rByERERkXXMzJ4LvLCU8pj13RcRERGR9UlvzRMRERERERERkZnQhSgREREREREREZkJvTVPRERERERERERmQq+IEhERERERERGRmdCFKBERERERERERmQldiBIRERERERERkZnQhSgREREREREREZkJXYgSEREREREREZGZ+H9/niiZpk4gWAAAAABJRU5ErkJggg==\n",
+ "image/png": "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0nz4wx/O8573vPzVX/1VrrvuulRV7rzzziTJ61//+px33nl57nOfm5tvvjkvfOELc+211w71d9ARBQAAALDCtmzZko997GO54IIL8pnPfCZHHHFEtm7dmr/+67/O9PR0PvzhD+clL3lJkqTb7eZlL3tZkuS6667L4x73uJx44olJsqQg6kUvelE2bNiQLVu2ZGZmJlu3bp2r4aabbsoRRxyRsbGxvOpVr8oHP/jBjI+PJ0k+/vGP57WvfW1OOeWUvPjFL853v/vd3HPPPUP9HXREAQAAAKywJzzhCbnyyitz8cUX501velNe8IIX5OUvf3n+4A/+II985CNz6qmn5vDDD0+SjI2Npdvt7vd3jY6OJkk6nU42bNiQqprbn56ezsjISC677LJ84hOfyAc+8IH8wR/8Qf72b/82s7Oz+cIXvpCxsbHl/8GL0BEFAAAAsMJuu+22jI+P59xzz83555+fK6+8Ms9//vNz5ZVX5l3vetfcWN583/M935Mbb7wxN910U5LkoosuWnYt99xzT+66666ceeaZ+d3f/d18+ctfTpL84A/+YN72trfNXXfVVVct+7vm0xEFAAAAHFKO3jAy1DfdHb1h7/HKV7/61Zx//vlzXUrvfOc70+12c/bZZ+e9731v3ve+9y1436ZNm/KOd7wjW7duzcTERE477bRl13v33XfnJS95SSYnJ9Nay+/8zu8kSd761rfmNa95TZ761Kdmeno6Z5xxRv7wD/9w2d83qFprQ33gge7UU09tl19++VqXAQAAAKySa6+9Nk960pPWuoz9ds899+Swww5Lay2vec1rctJJJ+W8885bs3oW+j2r6orW2ql7u9doHgAAAMAB7F3veldOOeWUnHzyybnrrrvy6le/eq1L2m9G8wAAAAAOYOedd95DOqDe85735Pd///d3O3b66afn7W9/+2qWts8EUQAAAADrzE/8xE/kJ37iJ9a6jH1mNA8AAACAVSGIAgAAAGBVCKIAAAAAWBWCKAAAAABWhcXKAQAAgEPKZz777DzwwB1De97GjUflec/9+6E972CmIwoAAAA4pAwzhFqJ5813wgkn5I47VvY7VosgCgAAAIBVccgGUTMzU7nllvfmsi++JJ/93Om58spX5B//8W/SWlvr0gAAAICDzL333puzzjorT3va0/KUpzwlF110US6++OI88YlPzDOf+cy87nWvy9lnn50k2bFjR37wB38wJ598cn7yJ39yj1nFTTfdlCc+8Yl55StfmSc84Ql5xStekY9//OM5/fTTc9JJJ+Wyyy5Lkvzd3/1dTjnllJxyyil5+tOfnrvvvjtJ8pu/+Zs57bTT8tSnPjW//Mu/vOK/wyEZRM3MTOaqL78y//sb/zF33/21TE19K9+58wv52tWvz9e//mZhFAAAADBUH/nIR3Lsscfmy1/+cr72ta9l69atefWrX51LLrkkV1xxRbZv3z537a/+6q/muc99bq6++uq89KUvzc0337zHZ19//fV5wxvekOuuuy7XXXddLrzwwnz2s5/Nb/3Wb+XXf/3XkyS/9Vu/lbe//e256qqr8pnPfCabNm3KRz/60XzjG9/IZZddlquuuipXXHFFPv3pT6/o73BIBlE33/Lfc+edly147tbb/jw7dnxqdQsCAAAADmpbtmzJxz72sVxwwQX5zGc+k3/4h3/I4x73uJx44olJknPOOWfu2k9/+tM599xzkyRnnXVWHvGIR+zx2SeeeGK2bNmSTqeTk08+OS94wQtSVdmyZUtuuummJMnpp5+en/u5n8tb3/rW3HnnnRkZGclHP/rRfPSjH83Tn/70POMZz8h1112Xb3zjGyvzA/Qdkm/Nu/22D+zx/G23fyBHHfV/rFI1AAAAwMHuCU94Qq688spcfPHFedOb3pQXvOAFQ3v26Ojo3Han05nb73Q6mZ6eTpK88Y1vzFlnnZWLL744p59+ei699NK01vKLv/iLefWrXz20WvbmkOyImpy6fY/npyZvW6VKAAAAgNW2ceNRq/682267LePj4zn33HNz/vnn53Of+1xuvPHGuY6liy66aO7aM844IxdeeGGS5JJLLsl3vvOdZdd4ww03ZMuWLbngggty2mmn5brrrssLX/jCvPvd784999yTJLn11lvzT//0T8v+rj05JDuixkYfnfsnF5+vHB07dhWrAQAAAFbT857796v+nV/96ldz/vnnp9PpZMOGDXnnO9+Z22+/PVu3bs3ExEROO+20uWt/+Zd/Oeecc05OPvnkfN/3fV+OP/74ZX//7/3e7+WTn/zk3Pjei170ooyOjubaa6/Nc57znCTJYYcdlj/90z/NMcccs+zvW0wdagtzn3rqqe0DH/i/csONv73oNU972n/PUUf+i9UrCgAAAFgx1157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GPEHaPf4SDHoYxqT4TgVhUaXF6ssU2Y1Tbv/vKpKndODDFQkWGKaxZMoQrDb4canCioSzNNe0BBCUOf0MBZQKLOaSTvA9jW7vPR4/RRajAfcvk6Pj31uLxlGA+UxyhSEM+ALUOd0Y9PpmJ9omXZfjwcUto+70EsS1UnWuBcoIHjMbx13EVAF8xMtEWn5+6MIwZYxF+MBhdk2S1SkrmZE/b/27jzakqq64/h3yySgDDIokzYRULIUjcEpCxDBGEOMoIKR5YSASXQZMA5EY4KtxCkYNGqQpaioOBE1qHFgkOAUAwoyNDII2sggKoIMIQ7IyR+n3ut6VXvvqifdt6ub32etu/re93593qnat845Ve/e+wK6ECUiIiIiIiIisnKNvRA1fPlYRERERERERERkJdCFKBERERERERERmQldiBIRERERERERkZnQhSgREREREREREZkJXYgSEREREREREZGZ0IUoERERERERERGZCV2IEhERERERERGRmdCFKBERERERERERmQldiBIRERERERERkZnQhSgREREREREREZkJXYgSEREREREREZGZ0IUoERERERERERGZCV2IEhERERERERGRmdCFKBERERERERERmQldiBIRERERERERkZnQhSgREREREREREZkJXYgSEREREREREZGZ0IUoERERERERERGZCV2IEhERERERERGRmdCFKBERERERERERmQldiBIRERERERERkZnQhSgREREREREREZkJXYgSEREREREREZGZ0IUoERERERERERGZCV2IEhERERERERGRmdCFKBERERERERERmQldiBIRERERERERkZmwUsrq7sNMmdnPgKs7X94SuHERzSwmvzZnp9KPNS07lX5MITuVfkwhO5V+TCE7lX6sadmp9GMK2an0YwrZqfRjCtmp9GNNy06lH1PITqUfU8hOpR9TyE6lH2tadir9mEJ2Kv1YGdkHlVK2GvzfpZR7/A34zqrKr83ZqfRjTctOpR9TyE6lH1PITqUfU8hOpR9rWnYq/ZhCdir9mEJ2Kv2YQnYq/VjTslPpxxSyU+nHFLJT6ccUslPpx5qWnUo/ppCdSj9W5fZ1b3prnoiIiIiIiIiIzIQuRImIiIiIiIiIyEzoQlT13lWYX5uzU+nHmpadSj+mkJ1KP6aQnUo/ppCdSj/WtOxU+jGF7FT6MYXsVPoxhexU+rGmZafSjylkp9KPKWSn0o8pZKfSjzUtO5V+TCE7lX6syu1b4B73YeUiIiIiIiIiIrJ66BVRIiIiIiIiIiIyE7oQJSIiIiIiIiIis3F3/uTe2nADngJcDlwJvHog+wHgp8CygdwOwH8B3wMuAY5MsvcGzgUubLKvH9HndYDvAv85IrscuBi4gIE/sQhsBnwKuAy4FHh8kHtI097c7VbgZUm7f9ts2zLg48C9k+yRTe4Sr02vBsD9gDOA7zf/bp5kD2ravgvYfaDdY5t9cRHwH8BmSfaYJncBcDqw7dBzBngFUIAtB/qxFLiutb/3y9oG/qbp9yXAPyftfrLV5nLggiT7SOB/5p5HwGOS7COAbzXPu88Dm2THhVe/JNurX5KN6hflezWMsl4Nk3Z79cva7dYvabdXvyTbq1+SjernjlXAjsA51DH0k8D6SfalTW7+eZ9kP0odm5dRn2frJdn3N1+7iDqG3SfKtvbzO4HbB/pwEvDD1n5+5EDegDcCV1DH0COS7Ndb7V4PnJpk9wXOb7LfAHZKsvs02WXAh4B1o7nDq12S7dUuyfZql2R7tRsz37Xrl7Tt1i/I9mqXZHu1S7K92iXZrHbL6czlxHOfl43mPi8bjZ1e1p37onw0/wVtL8Wf+9x28ec+r91o7vOy0dznZaOxs7e2imqX5KP6edmofl42WruE60Gndl67Ue3cdr3aJW1H9fOyUf28bK9+BOtdr35J1lu3RNmodlHeW7eka3QWrluidnv1y9rt1i9p11u3RFlv3RJl3WOv6VvvHIRg7guy7twXZN25L8i6c5+Xjea9oN2T8NctXjab97y8O/cFWXfuC7Lu3IdzTkg873nZaNz0su6xl+SjsTM8j6U/dnrtLsUfO9128ec9r91o3PSy0bjpZcNjb8xtdHBtvFEXgVcBv0c9eboQ+P0kvxfwKIYvRG0DPKq5f1/qAe62Sx0E5gaf9agD4+MG2n858DHGX4jacijXZD8EHN7cX799EA7swxuABwXf3446IG7YPD4FOCTIPqx5gm8ErAucSWvRHtWAOum9urn/auCtSXZX6kR2NgsHJS/7ZFYMhm8daLc96R0BnJA9Z6gXAU4DrmbhxOa1vRR45ZjnI/DEZr9t0DzeesxzF/gX4Oik3dOBP23u7wecnWS/DTyhuX8ocEx2XHj1S7K9+iXZqH5RvlfDKOvVMGm3V78k26tf1odu/ZJ2e/VLslH93LGKekw/u/n6CcCLk+wfAEtojUtJdr/me0ZdrGTttmt3HPV5FI6twO7AR1hxISpq9yTgQOd4ifIvBD4M3KtVv8ExHvg08Pyk3SuAXZuvv6Tpl5f9I+AaYJfm628ADmv9nAVzh1e7JNurXZLt1S7J9mqXte3VL2nbrV+Q7dUu60O3dkm7vdp5Weor1LPaefs9mvu8bDT3edlo7PSy7twX5b2xM2l7Kf7c52Wjuc/tQ3fsTNqN5j4vG42dvbVVVLskH9XPy0b187LR2sVdDwa189qNaudl3dpl/Qjq57Ud1c/LuvVr/az59W5WPyfr1i7IurVL8uHx181G9QvadesXZMP6eX3wahe069YuyEbHnnsOgr9uibLeuiXKeuuWKOutW8JzJvrrlqjdk+jMe0nWnfeyfrTanFu3RG176xYveyjO3EdwToh/zhBlvXOGKBuNm1HeO2cIz2PpnzNE7S6lf84QZb1zhjHn0nPnDFG73jlDlE3HzaHbPf2teY8Briyl/KCU8mvgE8D+UbiU8jXgpqFGSyk/LqWc39y/jXqVebsgW0optzcP12tuJWrbzLYH/gw4cagfi2Fmm1IvKry/6devSym/GPFf9wWuKqVcnWTWBTY0s3WpT+Drg9yuwDmllDtKKXcCXwWe0Q4ENdifuqig+feAKFtKubSUcnn3BwfZ05t+QL0yvH2SvbX1cGOaGibPmbcDR9Gp9djnWJJ9MfCWUsqvmsxPh9o1MwOeRZ00o2yh/nYQYFOaGgbZXYCvNffPAJ7ZZKPjole/KOvVL8lG9YvyvRoOHMsLarjI4z7K9uo31G67fkm2V78kG9UvGqv2of42D1bUz82WUr5bSlne2RdR9ovN9wr1lT/bJ9lbW/tiwxXN9rNmtg71N19HDfWBQJJ/MfCGUspdTe6nQ22b2SbNPjw1yXr187K/BX5dSrmi+fp8/bpzR7OverXzss229GqXZHu1S7K92mVte/WLspEg26vdULvt2iVZd+x0slsQ1C7hzn0eb+xMsu7YGWTduW+AO//dTe7cl+nOfQG3foHe2JmsrdzaRXmvfkm2V78k26vfwHpwQe0Ws3ZMsm7thtpu1y/J9uqXZN25r6W93h069uazI469dnbMsdfODx1/3TV6duyNWc972aFjr9ducuy1s0PHXjub1a57DvJjgrnPyV4fzX1B1p37gmw09/Wy0bznZZ1+Zll33htquzv3Bdmoft3s/+LPfdE5oXfsudng2Iuy0bEX5b1jLzuP7R57g+e8Q33GP/bSdjvHXpT1ahdlh8bN1D39QtR21Kuwc64lOHH8XZnZEurV9HOSzDpmdgH1LU5nlFLCLPAO6hP5rpFdKMDpZnaemf1lktsR+BnwQTP7rpmdaGYbj2j/2SSLuFLKdcDbgB9RB/9bSimnB/FlwJ5mtoWZbcSKtzANuX8p5cfN/RuA+4/4P4t1KPClLGBmbzSza4DnUK80R7n9getKKRcu4ue/1MwuMrMPmNnmSW4X6j48x8y+amaPHtH2nsBPSinfTzIvA45ttu9twGuS7CWsuKB7EE4NO8dFWr8xx9CIrFu/bj6rYTs7VEOnH2H9Otm0fsH2ufXrZNP6dbJh/bpjFfUVpb9oTd7zY+hixrUsa2brAc8DvpxlzeyD1OfPQ4F3JdmXAp9rPeeG+vDGpnZvN7MNBvIPBv7CzL5jZl8ys51H7IsDgK+0FqVe9nDgi2Z2bbMv3hLU41xgXTPbvWn7wFb9unPHFgS1c7KZMNutXZT1apfk3fol/fDq52Xd2mXbR6d2QdatnZO9kbh24M/l0dg5dt4fk22PnW42GTd7+WTsjPrhjZ1eNho7s+3rjp1eNho7vaw3dkZrq6h2i1mLjcnO1S/MOvVzs0Htsj50axdlo9oNbV+7flHWq1+UHVq7tNe7Q+vOdG08MhutOxfkk+NvQXZo3eL0I1t3trND605v+6J1Zzs7tO5sZ93aeecgwHk4c99izleGsu25L8t2574k25v3BvqwYN5Lsu68N2JfzM99SbY39wX1OAV/7ovOCb1jbzHnj2Oy7WMvzDvHnpsNjr2sH91jL8p6x97Q9rWPvSjrHXtRdvCcL1UW8fKpte1GfbKf2Hr8PODdA/9nCQNvzWtl70Md8J4xMr8Z9TNbHhZ8/6nA8c39vRn31rztyoqX610I7BXkdgfuBB7bPP5XBl5eR305843UQSHKbA6cBWxF/Y39qcBzk/xhzT77GvAe4B1DNaBOKO3v3zxUL5yXSCfZ11LfL2xjngfUA/b1XpZ69f8cYNPm8XL6L4/ubt/9qS9Bvhf1vdwfSLLLqCdyRn3F3w/n+p1s33uAVwz04Z3AM5v7zwLOTLIPpb6s8zzgdcDPs+NioH7uMRTUL8r26jd0fDo1nM8O1dDZvqx+3WxWv2j7vPp1283q182m9Wsyc2PVHtRXlc59fYfucwxnXOvus4Hs+/DHAS+7DnA88MIguxf1swrmXn59e9Yu9e2LBmxA/c3b0QP52+dq0TxXvj6iz1+aq03S7mdYMTa/ita85WQfT/0ch3OBf6K+x783d1BfGt6rnZft/Kz52o3IztduRHZB7YI+b+vVL2rbq1+S7dVuRJ/na5e026tdku3VrvWzenM5wdjpZVuZs1n41q4su2DszLLBuOn12R07g6w7dgZZd+wc2L4FY2fQrjt2Btne2Emwtkpql67FWPgWk6HsfP2Gsu36Bdljvdol29erXZKNaje0ffP1S9ru1S/JhnMfnfVuVD8vGx17A9lo3RKuu+kff/NZhtct3e3L1i3dbLZuibbPW7d0283WLd2sWzuCcxD8uS89X2HheDWUbc99Q9n5uS/IPh9/3ou2zZv3oqy7ZhnR5/bcF7XtzX1R1p37cM4JicfO8PyR/ryXZb1zvvTclNax52TfSzzvedsXzXteNho7s+3rznteu9G852UHzxmy2+jg2nhrnvindZ5Irxn4P0sYcSGqOcBOA16+yD4dTfC+bODN1Cv3y6lXge8ATl5E20uTth8ALG893hP4wkB7+wOnD2QOAt7fevx8mgX4iP6+CXjJUA2oHwy4TXN/G+DyoXox8kIU9X3O3wI2Gvs8AB7Y6d98Fng49dULy5vbndTfDDxgZNvdbe8+/jLwxNbjq4Ctku1bF/gJ9W1P2c+5hRULCwNuHdnfXYBzs+Miqp+XjeoXZZP6pcdnu4bdbFbDEe0uidrN6pdsX69+Qbtu/Ub0d0H9Ot87mrq4uJEVC6QFY2on+8rW4+UEn9vSzlIntVNpPr9gqN3ma3vhf5bP0U17N7RqdxetBelAu3t77bbz1A+M3LG1n28Z2L4tqSer7h9waO3jqzrPze+N7POTqb9t9OaOj3q1C7Int9qcr12W7dZuqN1u7YL8zV79Rra9N/Vilpv1ajewfQtqF2S/4NVuZH+fDJwSPC+WUp9v4dzXzbYen43zOTXdLMHYGbXb2r5oLlgK/CMD81/S9hKv7da+COe+YPvcuc9pN5z7Bvq7C/Wkyl1bRbWL8l79smy3fkPttusXZL8S1O7hI9pdkrT7hah2A9u3oH5J2736jdwX3bXLgvVuVD8vmx17XrZbu6F8dPy1swysPQfaXRK12zzO1p3e9kXrzm672boz6+987fDPQd6DP/el5yssnPvCLP25b/A8iGbuC7I/xJ/3xrS7d9Lu8QRrloHt68590T725r4xfXbnPppzQsbNewvOH8nnvfksA/Oe17Z37HWyRzJu3vPaXZK0+xLGzXvt7Rua9+baHTPvef0Nzxmi2z39rXnfBnY2sx3NbH3qyzw/d3cbNTOjvvf80lLKcQPZrcxss+b+hsAfUweGnlLKa0op25dSljR9PauU8tyk7Y3N7L5z96kH97Kg7RuAa8zsIc2X9qUOGpmDGX7p8Y+Ax5nZRs1+2Zf6WTRRn7du/n0g9er8xwbah1qzFzT3XwB8dsT/GWRmT6G+deJppZQ7BrI7tx7uT1zDi0spW5dSljR1vJb6gdE3JG1v03r4dIIaNk6lfngdZrYLK35rFHkScFkp5dokA/X9wU9o7u9D/WsVUX/nangv4B+oH+CXHRe9+i3yGHKzUf2SfK+GXjaqIXVw99rt1S/Zvqh+0b5YUL+k3V79kv0Q1c8bqy6lvhLnwOa/z9Vv9LgWZc3scOBPgINL8/kFQfZyM9uptf1Pa/6/lz2vlPKAVu3uKKXslPRhm1a7B9Ace8n2zdev2d9XDOyLA6kXXn45sI83bZ4PzH0t6fNc/TYA/o764bXe3PEcr3aLmWeirFc7Lws8z6td0vbmXv2SfvTql2xfr3YD+2JB7YLt29+rXdLfXu2ax9Fc7o2do+f9KOuNnUnWnfuC/LeDsfO2oG1v7Iy2zxs770j2RXfsjNr1xs5oX/TGzmRt5a5bFrMWi7Je/ZJsr35B9vxg7XJx0G6vdsm2ufPewL5YUL8k26tfsi/cua/RXe9m684xa2M3O2Ld2c1na8/57Ii1Z7fdbN3Z3b5s3enti2jd2c1m685uf6Paeecg38OZ+4JsdL7iZr25L8l6c5+XPc6b95J2vXVLtG29eS/rc/O9BXNfso97ApFKmgAABgdJREFUc1/S52ju884J3WMvyLq8bHbsBflo7utmPxQde0G77rEXbJ977CX7onfsBVn32Av6m42bw8ZesVpbb9T3OF5BvYr42oHsx6nvaf1N80Q6LMjtQf38gLk/6zj/5xed7G7UP998UfNk6731I/h/ezPw1jzqXwO8kBV/3nto+x5J/TONFzVP7s2T7MbUK+Kbjujr66kH6DLqX3zYIMl+nTqAXQjsO6YG1M86+UpzoJwJ3C/JPr25/yvqhYPTkuyV1M8Qm6vhCUn20832XUT985XbRdnO9ixn4cujvbY/Qv2zmBdRB+Btkuz61N/uL6P+KdR9sn5Q/4rFX4/Yx3tQX3Z5IfUlpn+YZI+kHlNXUD8PZe6quntcePVLsr36JdmoflG+V8Mo69UwabdXvyTbq1/Wh279knZ79UuyUf3csYo6zpzb7O9/p74cPMoe0dTvTupEd2KSvZM6Ls/17WgvS3358jebfbyM+kqfTaJ2O7W7fWDbzmq1ezIr/kpdlN+M+lv4i6m/VXtE1g/qb+meMjQfUJ/3Fzf1O7vZ51H2WOqC73I6fzq4O3d4tUuyvdol2V7tvGxUu7HzHf5bK9v9cOsXZHu1y/rQrV3Sbq92SdatHcFcjj92Rllv7IyyvbEzyUZz3+D6gxVjZ9S2N3ZGWW/sDPtAf+yM2vXGzigbjZ29tZVXu1ZfvHy0dvGy0dznZaP6petBFr46xGs3Wrd4WXfdkvWjW7+k7Wjt4mWj+vXWu1H9gmxUOy/r1i7JR/VL1+id+nntRvXzstG60+1DUDuv3ah2XtatXfO93jkIwdwXZN25L8i6c1+Qdec+LxvNe0G70brFy4bzXtQPnLkvaNud+4JsNPf1zgmJjz0vGx17XjY79rx8dOwNnccuZ8Wx57UbHXteNjr23D7gH3teu9Gx52XDY2/MbW6QFRERERERERERWaXu6W/NExERERERERGRGdGFKBERERERERERmQldiBIRERERERERkZnQhSgREREREREREZkJXYgSEREREREREZGZ0IUoERERkTWUmRUz22l190NERERkLF2IEhEREVlJzGy5mf2fmd3eur17dfdLREREZCrWXd0dEBEREVnL/Hkp5czV3QkRERGRKdIrokRERERWMTM7xMy+aWbvNrNbzOwyM9u39f1tzexzZnaTmV1pZi9qfW8dM/t7M7vKzG4zs/PMbIdW808ys++b2S/M7N/MzJr/t5OZfbX5eTea2SdnuMkiIiIiLr0iSkRERGQ2Hgt8CtgSeAbwGTPbsZRyE/AJYBmwLfBQ4Awzu6qUchbwcuBgYD/gCmA34I5Wu08FHg1sApwHfB74MnAMcDrwRGB9YPdVvYEiIiIiQ6yUsrr7ICIiIrJWMLPl1AtNd7a+/CrgN8CbgO1Ks/gys3OBdwFnA8uBzUoptzXfezOwTSnlEDO7HDiqlPJZ5+cVYM9Syjeax6cA55dS3mJmHwZ+CbyhlHLtKthcERERkUXTW/NEREREVq4DSimbtW7va75+XVn4G8Crqa+A2ha4ae4iVOt72zX3dwCuSn7eDa37dwD3ae4fBRhwrpldYmaH/o7bIyIiIrLS6EKUiIiIyGxsN/f5TY0HAtc3t/uZ2X0737uuuX8N8ODF/rBSyg2llBeVUrYF/go43sx2+t26LiIiIrJy6EKUiIiIyGxsDRxhZuuZ2UHArsAXSynXAP8NvNnM7m1muwGHASc3/+9E4Bgz29mq3cxsi6EfZmYHmdn2zcObgQLctbI3SkRERGQx9GHlIiIiIivX583st63HZwCfBc4BdgZuBH4CHFhK+XmTORg4gfrqqJuB15VSzmy+dxywAfWDx7cELgOePqIfjwbeYWabNj/vyFLKD+7OhomIiIjcXfqwchEREZFVzMwOAQ4vpeyxuvsiIiIisjrprXkiIiIiIiIiIjITuhAlIiIiIiIiIiIzobfmiYiIiIiIiIjITOgVUSIiIiIiIiIiMhO6ECUiIiIiIiIiIjOhC1EiIiIiIiIiIjITuhAlIiIiIiIiIiIzoQtRIiIiIiIiIiIyE/8PNRi49G6D8RwAAAAASUVORK5CYII=\n",
"text/plain": [
""
]
@@ -286,7 +267,7 @@
},
{
"cell_type": "code",
- "execution_count": 33,
+ "execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
@@ -298,7 +279,7 @@
},
{
"cell_type": "code",
- "execution_count": 34,
+ "execution_count": 13,
"metadata": {},
"outputs": [
{
@@ -307,13 +288,13 @@
"Text(0.5,0,'Epochs')"
]
},
- "execution_count": 34,
+ "execution_count": 13,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
- "image/png": "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99xzDwDbt2/n2muvZcKECUyYMIEXX3yx2XHmz5/P5z//eUIIXHTRRezevZtt27Yd1mbbtm3s2bOHiy++mBACn//85/nv//5vAKZMmUIiUV8N6aKLLmLz5s0n8at2DAMnSZIkSZLUKSxatIhBgwbxxhtv8Pbbb/OpT32Kl19+mYqKCgDmzZvH9ddfD9SHU2PGjOH3v/8948ePZ/r06SxcuJDly5ezffv24461fv16fvnLXzJ//nymTZvGZZddxltvvUVOTg6//OUv2blzJz/72c945513ePPNN/mnf/onAO6++25mzJjBK6+8wn/9139x++23NzvGli1bOOuss1KfBw8ezJYtW45qM3jw4GO2AXjqqae48sorU583btzIuHHjuPTSS1m2bNlx37e9GThJkiRJkqROoaioiCVLljBr1iyWLVtG3759mTp1Kv/zP/9DTU0Nv/zlL/nkJz8JQGZmJtdeey0Aa9asYfjw4QwbNgyAG2+88bhjXXnllWRlZVFUVERtbS1Tp05NzWHTpk306dOHnj17cvvtt/PTn/6U3NxcAJYsWcKXvvQliouLKSkpYc+ePezdu7fJMRpqMTUWQjjhNg888ACJRIKbb74ZgDPPPJN3332X119/nX/7t3/jpptuYs+ePcd95/bkKXWSJEmSJKlTOOecc1i5ciULFixg9uzZTJkyheuvv545c+bQv39/JkyYQO/evQHo2bMnmZmZQNOhzfFkZ2cDkJGRQVZWVirkycjIoKamhkQiwYoVK1i6dClz587lscce4ze/+Q11dXX87ne/Iycn57hjDB48mPfeey/1efPmzQwaNOioNo23yh3Z5umnn+YXv/gFS5cuTc0xOzs7Nf8PfehDnH322fzxj39k/PjxJ/w7nCqucJIkSZIkSU0akNW261SO19/WrVvJzc1l2rRp3HPPPbz22mt89KMf5bXXXuPJJ59Mbac70siRI9mwYQObNm0C6rfetda+ffsoLy/nqquu4pFHHqG0tBSor6v02GOPpdo13G9KSUkJP/zhD4kx8vLLL9O3b1/OPPPMw9qceeaZ9O7dm5dffpkYIz/84Q9Tq7gWLVrE//2//5ef//znqRVWUF9HqqFQ+oYNG1i7di3Dhw9v9Tu3JVc4SZIkSZKkJr01eUz7jvfWW8ycOTO16ujxxx8nMzOTa665hh/84Ac8/fTTTX4vJyeHf//3f2fq1KkUFBQwceLEVs9l7969fPKTn6SqqooYI9/85jcBePTRR7nzzjsZO3YsNTU1XHLJJc2eEnfVVVexYMECCgsLyc3N5fvf/37qWXFxcSqsevzxx/nCF77A/v37ufLKK1O1mr70pS9x4MABPv7xjwP1hcO//e1v88ILL3DfffeRSCTIzMzk29/+Nv3792/1O7elcDLLzjq78ePHx1dffbWjpyFJkiRJUpeyevVqzjvvvI6exknZt28fvXr1IsbInXfeyYgRI5gxY0ZHT6vLaurfQghhZYyxRfv23FInSZIkSZK6vCeffJLi4mJGjx5NeXk506dP7+gppTW31EmSJEmSpC5vxowZR61o+v73v8+3vvWtw+5NmjSJOXPmtOnY7TVOV+KWOkmSJEmSBHTtLXVqW26pkyRJkiRJUqdi4CRJkiRJkqQ2ZeAkSZIkSZKkNmXRcEmSJEmS1KRlyy/k4MEdbdZfjx4FfGTy79usP3VernCSJEmSJElNasuw6VT019jQoUPZsePU9d9SvXr1OqH2ixYt4txzz6WwsJCvf/3rTbY5cOAA119/PYWFhVx44YVs2rQp9eyhhx6isLCQc889l1/96lep+7feeisDBw5kzJgxJ/UerWXgJEmSJEmSdArV1tY2e//OO+9k4cKFrFq1ip/85CesWrXqqHbf+9736NevH+vWrWPGjBnMmjULgFWrVjF37lzeeecdFi1axN///d+nxvrCF77AokWLTt1LHUe3D5wqKzex5g/38dJLl/HiS5eyevVs9lWs7ehpSZIkSZKkI1RUVHD11Vdz/vnnM2bMGObNm8eCBQsYOXIkkydP5q677uKaa64BoKysjClTpjBu3DimT59OjLHZfjdt2sTIkSO5/fbbGTNmDDfffDNLlixh0qRJjBgxghUrVgDw29/+luLiYoqLixk3bhx79+4F4Bvf+AYTJkxg7Nix3H///S16l+eff57LLruMm266iaKioibbrFixgsLCQoYPH06PHj244YYbmD9//lHt5s+fzy233ALAZz/7WZYuXUqMkfnz53PDDTeQnZ3NsGHDKCwsTL3LJZdcQv/+/Vs011OhWwdO5eWvs+KVErZseYb9Ve9SVbWZrdv+k1de+SQ7d77Y0dOTJEmSJEmNLFq0iEGDBvHGG2/w9ttvM3XqVKZPn87ChQtZvnw527dv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\n",
+ "image/png": "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rLO48MILAejVqxdjxoxh+/btTaq7tSRU4JSZcSxwKrOPkyRJkiRJncqSJUs4++yzee+99/jggw+YOnUqd955J4sXL2b16tXs3r07Pvahhx7i8ssvZ+3atUybNo2tWxvfybRx40buvfdeioqKKCoq4oUXXmDlypU89thjPPzwwwA89thjzJs3j8LCQlasWEHPnj1ZunQpGzZsYNWqVRQWFrJ69WqWL1/epOdZs2YNTzzxBB9++GG917dv386QIUPinwcPHlxvMFR3XEpKCn369KG4uLhJ39+yZQu/+c1vuPjii+PnnnrqKcaPH88dd9zBvn37mvQspyuxAqe6K5wMnCRJkiRJ6lTy8vJ49dVXmTt3LitWrGDz5s0MHz6cYcOGATBjxoz42OXLlzNz5kwArr/+evr27dvo3MOGDSMvL4+kpCRyc3O5+uqrCSGQl5cX74k0efJk7rnnHp588kn2799PSkoKS5cuZenSpVxwwQVceOGFFBUVsWHDhiY9z6RJk+K1d4RDhw5x00038fjjj9O7d28AvvzlL/PRRx9RWFjIWWedxb333tsm906owCktbSDJybXNu9xSJ0mSJElS5zJy5EjWrFlDXl4eX//613nppZdabe709PT4cVJSUvxzUlJSvMfSfffdxw9+8APKy8uZPHkyRUVFRFHE/fffH+95tHHjRmbNmtWke2ZmZjZ6fdCgQXz88cfxz9u2bWPQoEGNjquqqqKkpITs7OxGv19ZWclNN93Erbfeyqc//en4mDPPPJPk5GSSkpL4q7/6q/h2wtaWUIFTCIHMzNrG4WVlm4iimg6uSJIkSZKkziu5f/92nW/Hjh1kZGQwc+ZM5syZw1tvvcWmTZviK5AWLFgQH3vllVfywgsvALB48eJW2Rr20UcfkZeXx9y5c5k4cSJFRUVMmTKFZ599lkOHDgG129t27drV4nsBTJw4kQ0bNrB582aOHDnC/PnzKSgoOGlcQUEBzz//PAAvvvgiV111FSEECgoKmD9/PhUVFWzevJkNGzYwadIkoihi1qxZjBkzhnvuuee4uXbu3Bk//vnPf864ceNa5VlOlNIms3ZimRnnceBAIdXVZVRU/IEePc7u6JIkSZIkSeqURq5c0a73e//995kzZw5JSUmkpqby9NNPs3PnTqZOnUpmZiYTJ06Mj33wwQeZMWMGubm5XHbZZZxzzjktvv/jjz/O66+/Ht92d+2115Kens769eu59NJLAcjKyuInP/kJAwcObPH9UlJSeOqpp5gyZQrV1dXccccd5ObmAvDAAw8wYcIECgoKmDVrFp///OfJycmhX79+zJ8/H4Dc3Fw++9nPMnbsWFJSUpg3bx7JycmsXLmS//iP/yAvL4/8/HwAHn74Ya677jq+9rWvUVhYSAiBoUOH8r3vfa/Fz1Gf0FgX965qwoQJ0bvvvlvvtd///nts/OhbAOSf/yOys69sz9IkSZIkSeq01q9fz5gxYzq6jOMcOnSIrKwsoijirrvuYsSIEcyePbujy+r26vu3EEJYHUXRhKZ8P6G21AFkxLbUAZSW2ThckiRJkqTO7JlnniE/P5/c3FxKSkq48847O7okNUFCbqk7qrTUxuGSJEmSJHVms2fPbvKKpuLiYq6++uqTzi9btozs7OxWq+l079NedXUmCRc49ew5hKSkNGpqjlBW6gonSZIkSZK6i+zsbAoLCzvdfdqrrs4k4bbUhZBMRs9hgFvqJEmSJEmS2kLCBU4AGZm12+oqK/dSWdny1yZKkiRJkiTpmIQMnDIz6jQOd1udJEmSJElSq0q4Hk4AmZnHNw4/44wmvdFPkiRJkqSE8u1vf5vS0tJWmy8zM5M5c+a02nzqvBJyhVNGZp0VTvZxkiRJkiSpXq0ZNrXFfHUNHTqUPXv2tNn8TZWVlXVa45csWcKoUaPIycnh0UcfrXdMRUUFt9xyCzk5OVx88cVs2bIlfu2RRx4hJyeHUaNG8corr8TPDx06lLy8PPLz85kwof0X2iTkCqfapuFJQA1lpRs7uhxJkiRJktSNVVVVkZJycgRTXV3NXXfdxauvvsrgwYOZOHEiBQUFjB079rhxP/zhD+nbty8bN25k/vz5zJ07lwULFrBu3Trmz5/P2rVr2bFjB3/2Z3/Ghx9+SHJyMgCvv/46/fv3b5dnPFFCrnBKTk6nZ8/BgCucJEmSJEnqLEpLS7n++us5//zzGTduHAsWLGDRokWMHj2aiy66iLvvvpsbbrgBgOLiYq655hpyc3P54he/SBRFDc67ZcsWRo8eze23387IkSO59dZbee2115g8eTIjRoxg1apVALz55pvk5+eTn5/PBRdcwMGDB4HarYUTJ05k/PjxPPjgg016ljfeeIMrrrii3gDpqFWrVpGTk8Pw4cNJS0tj+vTpLFy48KRxCxcu5LbbbgPg5ptvZtmyZURRxMKFC5k+fTrp6ekMGzaMnJyc+LN0tHYLnEIIU0MIvwshbAwh3FfP9XNDCMtCCL8NIbwRQhhc51p1CKEw9vdSa9RztHH44cPbqa4ua40pJUmSJElSCyxZsoSzzz6b9957jw8++ICpU6dy5513snjxYlavXs3u3bvjYx966CEuv/xy1q5dy7Rp09i6dWujc2/cuJF7772XoqIiioqKeOGFF1i5ciWPPfYYDz/8MACPPfYY8+bNo7CwkBUrVtCzZ0+WLl3Khg0bWLVqFYWFhaxevZpfnNQWAAAgAElEQVTly5c36XnWrFnDE088wYcffljv9e3btzNkyJD458GDB7N9+/ZGx6WkpNCnTx+Ki4sb/X4IgWuuuYaLLrqI73//+02qtzW1S+AUQkgG5gHXAmOBGSGEE+O9x4AfR1E0HvgH4JE618qjKMqP/RW0Rk0ZdRuHl21qjSklSZIkSVIL5OXl8eqrrzJ37lxWrFjB5s2bGT58OMOGDQNgxowZ8bHLly9n5syZAFx//fX07du30bmHDRtGXl4eSUlJ5ObmcvXVVxNCIC8vL94TafLkydxzzz08+eST7N+/n5SUFJYuXcrSpUu54IILuPDCCykqKmLDhg1Nep5JkybFa29vK1euZM2aNSxevJh58+Y1OSRrLe21wmkSsDGKok1RFB0B5gM3njBmLPDL2PHr9VxvVUdXOAGUlbqtTpIkSZKkjjZy5EjWrFlDXl4eX//613nppVbZ5ARAenp6/DgpKSn+OSkpiaqqKgDuu+8+fvCDH1BeXs7kyZMpKioiiiLuv/9+CgsLKSwsZOPGjcyaNatJ98zMzGz0+qBBg/j444/jn7dt28agQYMaHVdVVUVJSQnZ2dmNfv/ofwcOHMi0adPafatdewVOg4CP63zeFjtX13vAp2PH04BeIYTs2OceIYR3QwhvhxD+or4bhBD+Ojbm3bpL7BqSWXeFk43DJUmSJEk6yakCk9aeb8eOHWRkZDBz5kzmzJnDW2+9xaZNm+IrkBYsWBAfe+WVV/LCCy8AsHjxYvbt29fi+j766CPy8vKYO3cuEydOpKioiClTpvDss89y6NAhoHZ7265du1p8L4CJEyeyYcMGNm/ezJEjR5g/fz4FBSdv7CooKOD5558H4MUXX+Sqq64ihEBBQQHz58+noqKCzZs3s2HDBiZNmkRpaWm8/1RpaSlLly5l3LhxrVJzU3Wmt9T9LfBUCOF2YDmwHaiOXTs3iqLtIYThwC9DCO9HUXTcsqQoir4PfB9gwoQJDXcKi8nMPLbCycbhkiRJkiSdbM6cOe16v/fff585c+aQlJREamoqTz/9NDt37mTq1KlkZmYyceLE+NgHH3yQGTNmkJuby2WXXcY555zT4vs//vjjvP766/Ftd9deey3p6emsX7+eSy+9FICsrCx+8pOfMHDgwBbfLyUlhaeeeoopU6ZQXV3NHXfcQW5uLgAPPPAAEyZMoKCggFmzZvH5z3+enJwc+vXrx/z58wHIzc3ls5/9LGPHjiUlJYV58+aRnJzMH//4R6ZNmwbUroj63Oc+x9SpU1tc7+kIjXVxb7WbhHAp8M0oiqbEPt8PEEXRIw2MzwKKoigaXM+154BfRFH0YkP3mzBhQvTuu++esq4VKy/lyJFdZGTkcOklrzTpWSRJkiRJ6q7Wr1/PmDFjOrqM4xw6dIisrCyiKOKuu+5ixIgRzJ49u6PL6vbq+7cQQlgdRdGEpny/vbbUvQOMCCEMCyGkAdOB4zZihhD6hxCO1nM/8GzsfN8QQvrRMcBkYF1rFHV0W115+RZqaipbY0pJkiRJktSKnnnmGfLz88nNzaWkpIQ777yzo0tSE7TLlrooiqpCCF8FXgGSgWejKFobQvgH4N0oil4C/gR4JIQQUbul7q7Y18cA3wsh1FAbkD0aRVHrBE4ZOezb979EURXl5VuP6+skSZIkSZI63uzZs5u8oqm4uJirr776pPPLli0jOzu7nm80z+nep73q6kzarYdTFEWLgEUnnHugzvGLwEnb5KIo+hWQ1xY1ZWQOjx+Xlm00cJIkSZIkJbwoigghdHQZzZKdnU1hYWGnu0971dVaWqP9UnttqeuUMjOOBUxlpTYOlyRJkiQlth49elBcXNwqgYO6piiKKC4upkePHi2apzO9pa7dHfemOgMnSZIkSVKCGzx4MNu2bWP37t0dXYo6UI8ePRg8+KT3uJ2WhA6c0tIGkpycRXX1IUrLNnZ0OZIkSZIkdajU1FSGDRvW0WWoG0joLXUhhPgqp7KyTURRTQdXJEmSJEmS1PV1+xVOUXU1h5Yvp/StX0FNDRmXXEyvq64ipNQ+embGeRw4UEh1dRkVFX+gR4+zO7hiSZIkSZKkrq1bB05V+/bx8Ze+xOH3fhs/t++FF0gfOZIhzzxD6pkDj3szXWnpRgMnSZIkSZKkFurWW+p23nf/cWHTURUffsj2e+4BIKNu4/AyG4dLkiRJkiS1VLcNnCo2b+bQm282eL189WrK3/+AzIxjK5zKfFOdJEmSJElSi3XbwOnw2nWnHvPB+/TsOYSkpDTAFU6SJEmSJEmtodsGTkkZPZswJoMQksnoWfvKx9LSjW1dliRJkiRJUrfXbQOnzEsuIalXrwavh/R0sj75SQAyYo3DKyv3Ulm5r13qkyRJkiRJ6q66beCUlJHBgNn/p8Hr/b/yFZLPOAOAzIw6jcPt4yRJkiRJktQi3TZwAuj3uc9x1qOPkDJkSPxcSE/jE998kOy//qv4uczMY43D3VYnSZIkSZLUMt06cAI44y/+gpxXlpDcPxuAtHPPpe/06YQQ4mMyMuuscLJxuCRJkiRJUot0+8AJICQlkTrwTACq9hSfdL22aXjtT1HmCidJkiRJkqQWSYjACSB5QH8AqvftI6qqOv5acjo9ew4GXOEkSZIkSZLUUgkTOKX0rw2ciCKq9u496frRxuGHD2+nurqsPUuTJEmSJEnqVhIncMruHz+u3rPnpOsZdRuHl21ql5okSZIkSZK6o8QJnPofC5yqik/u43R0hRNAWanb6iRJkiRJkporcQKnAXUCp90nr3DKrLvCycbhkiRJkiRJzZYwgVNydnb8uKqeLXWZmcdWONk4XJIkSZIkqfkSJnBK6T8gflxdfHLglJLSi7S0gQCUuqVOkiRJkiSp2RIncDrFljqAzIzhAJSXb6GmprJd6pIkSZIkSepuEiZwSsrKIqSlAfVvqQPIiG2ri6Iqysu3tlttkiRJkiRJ3UnCBE4hhPib6up7Sx2c0Di8zMbhkiRJkiRJzZEwgRNA8tHAqYEVTpkZxwKnMvs4SZIkSZIkNUtCBU5HVzjVlJRQc+TISdePe1OdgZMkSZIkSVKzJGTgBFBdz7a6tLSBJCdnAXDw0HpqaqrarTZJkiRJkqTuIsECp+z4cX3b6vaXvAtEAJSW/o6Vb01my5bvEkXV7VWiJEmSJElSl5dQgVNynRVOJwZO+/a9zW9+M5Pq6tL4ucrKPXy06dsU/e4b7VajJEmSJElSV5dQgVNKA4FTFEVs2PgwUVT/FrodOxZw8FBRm9cnSZIkSZLUHSRs4FRdJ3AqL9/KwYNrG/3urj++3GZ1SZIkSZIkdScJGzhV7TnWNLzuNrqGVFUfapOaJEmSJEmSupvECpyy628a3rPnuSQnZTT63V5ZuW1WlyRJkiRJUneSUIFTUmYmIaM2WKobOKWkZHLW2Z9p8Htpaf0588zr27w+SZIkSZKk7iChAic4tq2u+oS31OWc9zWysz950vjk5F6cP/4ZkpN7tkt9kiRJkiRJXV3CBk5VJwROyck9OH/8D7kg/8dkZ/9J/PyQIbfRu/f49ixRkiRJkiSpS0u8wCnWx6mmtJSa8vLjroUQ6NdvMrljH4ufO3SoqF3rkyRJkiRJ6uoSL3AaUOdNdcXF9Y5JTe1Lz57nAnDgQCFRFLVLbZIkSZIkSd1BwgVOyf3rBE67dzc4rnfv8wE4cmQPFRU727wuSZIkSZKk7iLhAqeU7DqB0wl9nOrqEwucAEoOFLZpTZIkSZIkSd1J4gVOdbbUVTewpQ6gd+/8+PGBEgMnSZIkSZKkpkq8wOm4LXUNr3Dq1WsMIaQCUHLgvTavS5IkSZIkqbtIvMAp9pY6aHxLXVJSOr2yxgBw8OAH1NRUtXltkiRJkiRJ3UHCBU7HNQ0vbjhwAujdp7aPU03NYUpLP2zTuiRJkiRJkrqLhAucktLTSerVC4DqRrbUwfF9nGwcLkmSJEmS1DQJFzjBsT5OjW2pg+PfVHfAPk6SJEmSJElNktiBU3ExURQ1OK5nz6GkpPQBDJwkSZIkSZKaKiEDp+T+tY3Do8OHqSktbXBcCIHevccDUFq6kaqqg+1SnyRJkiRJUleWkIFTSv8B8eOq3bsbHdsn3scp4sCB99uwKkmSJEmSpO4hQQOnY2+qqy4ubnRsb/s4SZIkSZIknZYEDZyy48enahx+dEsdGDhJkiRJkiQ1RYIGTsdWOFXtbjxwSkvLpmePcwAoOfBeo03GJUmSJEmSlKCBU3LdwKm48cAJoHef2m11R47soqJiZ5vVJUmSJEmS1B0kZOB03AqnU2ypg+P7OJW4rU6SJEmSJKlRiRk49esXP64+xZY6qPumOvs4SZIkSZIknUpCBk4hNZXkvn0BqDrFW+oAsrLGEkIqYOAkSZIkSZJ0KgkZOMGxN9U1ZUtdcnI6WVmjAThw4H1qaqratDZJkiRJkqSuLGEDp6ONw6uKi5v05rmj2+pqasopLd3QprVJkiRJkiR1ZQkbOKX0H1B7UFlJTUnJKcfXbRzutjpJkiRJkqSGJUTgVFNTw+7du9m1axfV1dUApGRnx6+f7pvqDJwkSZIkSZIaltLRBbS1NWvW8Oabb1ISW8XUq1cvLr/8cs7rf3zglJ6T0+g8GRlDSUnpTVXVAUoOFLZpzZIkSZIkSV1Zt17h9Otf/5qXXnopHjYBHDx4kMWLF/NRnbfTVe059ZvqQkiKr3IqLd1AVdWh1i9YkiRJkiSpG+i2gVNFRQW//OUvG7z+3u9/Hz+u2rO7SXMe21YXcfDgBy0pT5IkSZIkqdvqtoHT5s2bqaioaPB6aVpa/Li6CT2cAPrU6eNUYh8nSZIkSZKkenXbwKmysrLR64d79IgfN2VLHZzYONw+TpIkSZIkSfXptoHT2Wef3ej1I2lpkFT7+E15Sx1AWlo2PXoMAeBAiSucJEmSJEmS6tNtA6fs7GxGjRrV4PVzhg0jObsfAFXFTVvhBMe21VUc+SOHK/7QsiIlSZIkSZK6oW4bOAHceOONDBky5KTzn/jEJ7j55ptJye4PNL1pOJywrc5VTpIkSZIkSSfp1oFTRkYGf/mXf8mMGTPi5wYNGsRf//Vf06tXL1L61wZO1cV7iaqrmzRn7z72cZIkSZIkSWpMtw6cAJKSkhg1ahRpsbfSJScnkxTr3XQ0cKKmhur9+5s0X6+sXEJIAXxTnSRJkiRJUn26feB0VM+ePQEoLy+Pn0vpnx0/bmrj8OTkHmRljQbg4MH3iaKmrYySJEmSJElKFAkdOCUfXeEEVO1uWuAEx/o4VVeXUVq6sZUqlCRJkiRJ6h4SMnCKogiAlP4D4teri5seOPWp0zi8xD5OkiRJkiRJx0m4wKm6uprKykqgeVvqAHr3zo8fHygxcJIkSZIkSaor4QInOLatLqWZW+oyMoaRktILgAMHf9tKFUqSJEmSJHUPCRk4lZWVAScETsXFTZ4rhCR69xoPwKFDH1JVVdpKVUqSJEmSJHV9CRk4HV3hlNSnD6SmAlC1Z/dpzdc73sephoMHP2iVGiVJkiRJkrqDhA6cQgikZNf2cao+jR5OAL371OnjZONwSZIkSZKkuIQJnDIyMuLHRwMnOLatrmpP07fUQd0VTlBywD5OkiRJkiRJRyVM4FTfCifg2AqnffuIYm+va4qkkEpycm3j8N27X2HVO3/Bjh0vEkVRK1UsSZIkSZLUNSV84JQ8oE7j8L17mzRXZeV+3l39WaqrD8bORBw8+D7ri+ZS9Lu/N3SSJEmSJEkJLeEDp+PeVNfEPk6bN/8bZWUb6722Y8cC9u37VTOrlCRJkiRJ6voMnLKPBU5NaRweRRE7//CzRsfs3Nn4dUmSJEmSpO4sYQKnlJQUUlNTgRMCpwGnt8KppqaCqqoDjY6pOLKrmVVKkiRJkiR1fQkTOMGxVU71NQ2Hpr2pLikpnbS0gae4zznNrFCSJEmSJKnrS8jAqaysLH4u+TR7OIUQGHT29EbHnOq6JEmSJElSd5aQgVN5eXn8TXIpAwbEr1ft2d2kec4990v07XtpvdfOGz6H3r3zWlipJEmSJElS15VQgVNGRgYA1dXVVFZWApCUmUlIT68934QtdQDJyenkn/8jxoz5fznjjEuBZADS0gYydOiXWr9wSZIkSZKkLiShAqf63lQXQiAltq2uKVvqjkpKSuXss27mogt/wsCBUwA4cmQXZWVbWq9gSZIkSZKkLijhAyegWYFTXdn9rogf7927spnVSZIkSZIkdQ8GThxrHF5z8CA1FRWnPW+/fpfHj4v3rmhBhZIkSZIkSV2fgRPHVjgBVDdjlVOPHmeTkZEDwL59b1NTU9mCKiVJkiRJkro2AyeOD5yav62udpVTdfUhSg4UNrNCSZIkSZKkrs/ACUjpnx0/ripu2pvqTtQvu24fJ7fVSZIkSZKkxGXgxLEeTgBVu5u3wqnvGZMIIQ2wcbgkSZIkSUpsCRs4lZWVxY+P31K3u1lzJydncMYZFwFw4MBvqazc18wqJUmSJEmSurZ2C5xCCFNDCL8LIWwMIdxXz/VzQwjLQgi/DSG8EUIYXOfabSGEDbG/25pbQ5OahjdzSx1Av35Ht9VF7N37q2bPI0mSJEmS1JW1S+AUQkgG5gHXAmOBGSGEsScMewz4cRRF44F/AB6Jfbcf8CBwMTAJeDCE0Lc5daSmppKamgqcEDhl1+nh1MwtdQDZ/er2cXJbnSRJkiRJSkzttcJpErAxiqJNURQdAeYDN54wZizwy9jx63WuTwFejaJobxRF+4BXganNLeToKqe6gVNSRgZJmZlA899SB5CVNZrU1NrwqnjvCqIoavZckiRJkiRJXVV7BU6DgI/rfN4WO1fXe8CnY8fTgF4hhOwmfpcQwl+HEN4NIby7e3fDfZjqC5wAkmNvqmvuW+pqa0giu9/lAFRU7KSs7KNmzyVJkiRJktRVdaam4X8LfDKE8Bvgk8B2oLqpX46i6PtRFE2IomjCgAEDGhzXUOCU0r/2Oy1Z4QR1+zjVrnKSJEmSJElKNO0VOG0HhtT5PDh2Li6Koh1RFH06iqILgL+PndvflO+ejqOBU1VVFZWVlfHzRxuHR2Vl1JSWNnd6+vWbHD+2j5MkSZIkSUpE7RU4vQOMCCEMCyGkAdOBl+oOCCH0DyEcred+4NnY8SvANSGEvrFm4dfEzjVLg2+qq9s4vAXb6tLTB5KVNRqAfft+TU1NRbPnkiRJkiRJ6oraJXCKoqgK+Cq1QdF64D+jKFobQviHEEJBbNifAL8LIXwInAn8c+y7e4F/pDa0egf4h9i5ZmkwcBrQP37c8m11tX2camrK2b9/dYvmkiRJkiRJ6mpS2utGURQtAhadcO6BOscvAi828N1nObbiqUUaCpyS665wamHglN3vSrZu/QFQu62uX7/LWjSfJEmSJElSV9KZmoa3i7qBU1lZWfz4aNNwaHng1KfPBJKS0gH7OEmSJEmSpMST0IFTQ1vqqlsYOCUnp3PGGZMAOHhoLRVHWjafJEmSJElSV5JwgVNGRkb8uMGm4Xua3zT8qOx+V8aP9+19q8XzSZIkSZIkdRUJFzg12MOpf+s1DYdjjcMBiveuaPF8kiRJkiRJXYWBU0xSWhpJffoArRM4ZWaOID3tTKC2j1MURS2eU5IkSZIkqSswcKrj6La6lvZwAgghxFc5HTmym0Olv2vxnJIkSZIkSV1BwgVOqamppKSkAPUETrFtdVV79rTKiqR+2VfEj/e6rU6SJEmSJCWIhAuc4Ngqp4YCp+jIEWoOHmzxffr1nQwEAPYWr2zxfJIkSZIkSV2BgVMdyf1b9011aWn96NUrF4D9Jauori4/xTckSZIkSZK6PgOnOlL6D4gfV+3Z3Sr36tevdltdTc0R9u9/p1XmlCRJkiRJ6swSOnCqrKyksrIyfv7oljponcbhANn96vZxcludJEmSJEnq/hIycMrIyIgf113llNLKW+oA+vS5gOTk2vsV2zhckiRJkiQlgIQMnI6ucIITA6djK5yqWmmFU1JSGn3PuASA0tIPOVzxh1aZV5IkSZIkqbMycKoTOCW3QeAE0C/bbXWSJEmSJClxGDjVXeHUrx+EAEBVcesFTnX7OO3c8SIHD64liqJWm1+SJEmSJKkzMXCqEziFlBSS+/YFoHp36wVOVdWHCSEVgP0l77DqnQLeeedGDh5c12r3kCRJkiRJ6iwMnOoETnCsj1Nrbak7fHgHhYUziaLK484fPLSWNb+ZSXn5tla5jyRJkiRJUmdh4HRS4FT7prqqvXuJampafK+PP36Oysr99V6rqiph68fPtvgekiRJkiRJnYmB0wmBU7xxeFUV1SUlLb7XnuLXG71efIrrkiRJkiRJXY2BU53AqfrgQSp37Ih/3vmNb3C4qKhF94qiqsav1zR+XZIkSZIkqatJyMApNTWV5ORkAMrKygCo3LWLLTd/hvJ3V8fHHXptGZtvupkDixY1+15nnHFx49f7Tmr23JIkSZIkSZ1RQgZOIQQyMjKAYyuc/vjwIxz5/e9PHlxdzY77/46qvXubda9zhvxl/A11J9eRwjlDZjVrXkmSJEmSpM4qIQMnOLatrry8nKq9ezn46qsNjo0qKih56aVm3ScraxTj854mNbXvSddGj3qEXr3GNmteSZIkSZKkzsrAqbycqj/8AaqrGx1f+fG2Zt+rf/8/ZfJlK8nNfZzs7Kvi52tqyhv5liRJkiRJUteU8IFTZWUlUd+TVx+dKGXgwBbdLzm5B584888ZPfof4+f+uOvlFs0pSZIkSZLUGSV84ARQlZVF5hVXNDw4OZk+BX/eKvftkf4J+vS5CID9+1dRUbG7VeaVJEmSJEnqLAycqN1W94m//zuSs7PrHXvm3K+RetZZrXbvMwdeFzuK2LV7SavNK0mSJEmS1BkYOFEbOKUNHcqwF/8/+n7+84Q61z7xzW/S7wtfaNV7Dxg4FQgA7Nq1qFXnliRJkiRJ6mgGTtQGTgCpZ53FJ/7+7zjza3Pi15Kb0N/pdPVI/wRn9JkAwP7971BRsavV7yFJkiRJktRRDJw4FjgdlTZsePz4yOZNbXL/gQOvjR25rU6SJEmSJHUvCRs4ZWRkxI/LysqOu5Y2fFj8uGJTWwVOdbbV/dFtdZIkSZIkqftI2MCpsRVOKQMGkJSVBcCRTZvb5P7p6WdyxhkTAdhf8i6HK/7QJveRJEmSJElqbwZOnBw4hRBIG167re7Ipk1EUdQmNQys87a63bvcVidJkiRJkroHAydODpwA0ocNBaCmrIyqXW3T1HvggGPb6v64a3Gb3EOSJEmSJKm9JWzglJqaSnJyMlB/4HRc4/A26uOUnj6AM86YBECJ2+okSZIkSVI3kbCBUwghvsqp3sCpbuPwzW3TxwngzPi2OtjlKidJkiRJktQNJGzgBDQaOKUPr7vCqe0CpwEDpnD0f8OuXb6tTpIkSZIkdX0GTtQfOKWecw7Etty11ZY6OLqtrvZtdSUlazh8eGeb3UuSJEmS/n/27jy87rLO///zPic52dMmTUv3jS5A2UFwAVzQERl1GEQFR/i6DDIjrjiLouN3fs44zlcdL2ccHFHHkXFEx0FcUVBxARwFSgulpRTolu5N0qZJmvWc8/n9kZM0LU1y2pwly/NxXefqJ5/7Pp/POy3+4eu67/ctSYVg4AT09vaSTCaPGoslEiTmzwfyu6UO4JRZfzh4vb/J0+okSZIkSdLEZuCU0d3d/bzxRGZbXXLPHtKHD+etjpmzhmyr23dP3t4jSZIkSZJUCFM6cKqsrBy87uzsfN74UY3Dt23LWx1liQbq6i4G4FDbWrq7d+ftXZIkSZIkSfk2pQOnoSucitk4HGCWp9VJkiRJkqRJwsAp43iBU2LJkRVOvVvz1zgcYNbMP2Dgn2OfgZMkSZIkSZrADJwyRguc8t04PJFooK7uhQC0ta2lq2tXXt8nSZIkSZKULwZOGccLnErq6ojX1QH531IHcMrQbXVNrnKSJEmSJEkTk4FTxvECJzhyUl3vtm1EqVRe65k58w8IIQ7A/v0/yeu7JEmSJEmS8sXAKWO4wKksc1Jd1NND3549ea0nkZhB3fSBbXVP0NW1M6/vkyRJkiRJygcDp4xhVzgtGXpSXX4bh4On1UmSJEmSpIlvSgdOiUSCWKz/r2D4LXVDGocXIHBqaHglA/8sz23+Rx548EI2PfMJenqb8/5uSZIkSZKkXJjSgVMIYXCV0/Bb6oaucMpv4/Aoiti8+dNAevBeX99Bdu68g9Wrr6Gnpymv75ckSZIkScqFKR04AVRWVgLQ2dl53PHSefMIpaVA/rfUHTz4v+zZ+93jjnV372DL1s/n9f2SJEmSJEm5MOUDp9FWOIV4nMTiRQD0bNuW11r27v3+iOP79v2QKMrvSXmSJEmSJEljZeCUCZx6e3tJpY4f5gw0Dk81N5M6dChvtfSO0qcpleoklTp+MCZJkiRJkjReGDhlc1LdkMbhvVvz18eponLJiOOJxCzi8aq8vV+SJEmSJCkXDJyyCJyGNg7vyWPj8HlzrwXC8OPzriOE4cclSZIkSZLGAwOnbFY4LRlyUt3W/DUOr65ewcoVf3vcsaqq5SxedFPe3i1JkiRJkpQrJcUuoNiyC5yObHXL5wongPnz30rttHPZtfObtLVvoKNjAwCxWAWxWFle3y1JkiRJkpQLrnDKInCKV1dRcsopAPRuyd8KpwG1NWdy+umf4uKLfkh93SUAtLevo719Q97fLUmSJEmSNFYGTlkETnCkcXjvjh1EfX15r2vAvHlvGbzetetbBXuvJEmSJEnSyZrygVNlZeXgdWdn57Dzygb6OCWT9O7Yme+yBjU0vIJEYiYAe/f9kGSyo2DvliRJkiRJOhlTPnDKeoXTkD5O+WwcfqxYrJS5c94IQCp1mH37flSwd0uSJEmSJJ0MA6cT3FIH0FOAPk5DzchMEUkAACAASURBVJ17LRCA/m11URQV9P2SJEmSJEknYsoHTolEglis/69hpMCpbOnSwevePJ9Ud6yKinnMmPFSANo7NtDe/mRB3y9JkiRJknQipnzgFEIYXOU0UuBUcsophEy/p0KcVHeseXOvG7y2ebgkSZIkSRrPpnzgBGQVOIVYjLLFiwHo2bq14NvaZsx4GWVlswHYu+9HJJPtBX2/JEmSJElStgycyC5wAkhkttWl29pItbTkva6hYrES5s59c//7013s2fv9gr5fkiRJkiQpWwZOHAmcenp6SKVSw84rZuNwIHNaXf8/2W6bh0uSJEmSpHHKwImjT6rr7u4edt5RjcO3bstnScdVXj6HhoZXANBxeBOH2tYUvAZJkiRJkqTRGDhxdOA00ra6xJKhJ9UVfoUTwLx5Ng+XJEmSJEnjm4ETUJk5fQ6gs7Nz2HmJRQshBAB6thYncJpRfynl5fMA2L//J/T1tRalDkmSJEmSpOEYOJH9CqdYeTml8/rDnt4tW/Ne1/GEEGfe3GsBSKd72LP3e0WpQ5IkSZIkaTgGTmQfOMGRxuF9u3aRHqHfUz7NmfNGQigB+rfV2TxckiRJkiSNJwZOnFjgVDbQxymK6N2+PZ9lDV9D2UwaGl4JQGfnZlpbHy1KHZIkSZIkScdj4MSJrnAqfuNwgPnz3jJ4vWv3nUWrQ5IkSZIk6VgGTpzgCqfMljqAniIGTnV1L6KiYiEA+/ffR2/vgaLVIkmSJEmSNJSBE2NY4bR1W75KGlUIscHm4VHUyzPP/h1NTb8gmTxctJokSZIkSZLAwAmAsrIyQgjA6IFTvL6e2LRpQHG31AHMnHkF0F/3vn0/ZN2TN/HQb19MY+O/20hckiRJkiQVjYETEEIYXOU0WuAUQqBsSf+2up6tW4nS6bzXdzxRFPH007cCRwdLqVQHzz73D+za/a2i1CVJkiRJkmTglFFZWQmMHjjBkW11UVcXyX378lrXcFpbH+Zg6++HHd+27TbS6WQBK5IkSZIkSepn4JQxsMKps7Nz1LmJJYsHr4vVOLzlwIMjjvf07OXw4WcKVI0kSZIkSdIRBk4ZA4FTT08PqVRqxLllQxuHb9ma17qGFY2+lS+iONv9JEmSJEnS1GbglDH0pLru7u4R5yaWDD2prjgrnOrqXjTieGnpDKqrlheoGkmSJEmSpCMMnDKGBk6j9XFKLJgPJSUA9BRphVN9/SXU1pw97PiiRTcSi5UVsCJJkiRJkqR+Bk4ZJxI4hdJSEgsXAtC7tTiBUwgxzjnnK9RNf+HzxmKxcubNfUsRqpIkSZIkSTJwGnQigRNAYukSAJL79pHqOJy3ukasIdHA+ed/k4te8ENWrvgE9fWXApBOd7N7z3eKUpMkSZIkSZKBU8aJBk5lR/VxKlLj8IyamlXMn/8nnH7apwihFIDGxq+STvcWtS5JkiRJkjQ1GThlnPgKp+I3Dj9Wefkc5sz+YwB6evayd+8PilyRJEmSJEmaigycMk54hVNmSx1Az5bxETgBLFr0Lgb+Wbc33k4UpYpbkCRJkiRJmnIMnDIqKysHr7Na4bTkSODUW6ST6o6nsnIJs2ZdAUBn51b2N/2syBVJkiRJkqSpxsApY+gKp87OzlHnx2triTc0AONnS92AxYv+bPB6+7Z/I4qiIlYjSZIkSZKmGgOnjLKyMkIIQHYrnADKMqucerZuI3nwYN5qO1E1NauYUX8ZAO0dGzhw4MEiVyRJkiRJkqYSA6eMEMLgKqdsAqfup56iZ1tmK10yybOXvZTdt36UVHt7PsvM2qJFfz54vW37l4pYiSRJkiRJmmoMnIbINnDqefZZtl9/A6mm5iM3+/o4dPfdNL7zT0n39uazzKxMn/4Cpk07H4DW1oc5dGhNkSuSJEmSJElThYHTENkGTk23fZH04cPHHetet472n/4057WdqBACixe9e/BnVzlJkiRJkqRCMXAaYiBw6u7uJp1OH3dOlErRfv/9Iz6n7ec/z3ltJ2PGjJdRXX0aAM3N99PRsanIFUmSJEmSpKnAwGmIoSfVdXd3H3dOlEpBX9+Iz4k6s2s6nm8hBBYtvGnwZ1c5SZIkSZKkQjBwGmJo4DTctrpYIkHZypUjPqf87LNyWtdYzJp1JRXlCwHYt+/HdHU1FrkiSZIkSZI02Rk4DZFN4ARQ/7a3DTsWysupe/Obc1nWmMRiJSxcdGPmpzTbG79S1HokSZIkSdLkV7DAKYRwRQhhUwjhuRDCh48zvjCE8KsQwtoQwroQwpWZ+4tDCF0hhMczn7ztC6usrBy8HilwmnbVHzHjppuedz8kEsz/13+ldM6cvNR3subMfgOJxEwA9uy5i56epiJXJEmSJEmSJrOCBE4hhDhwG/Aa4AzguhDCGcdM+xjwnSiKzgOuBb44ZGxzFEXnZj5/lq86h65w6uzsHHZeCIFZH/wAp977U+pvvHHwfsUFF1B9yUvyVd5Ji8fLWLjgHQCk0708uvqPeWzNW9i27d/o7T1Q5OokSZIkSdJkU6gVThcBz0VRtCWKol7g28AfHTMnAmoz19OA3QWqbVC2W+oGJBYv5pQP3ULZ8mUAdK9fTzTM6XbFNn36C4AAQE/PHlpbH2bzls/y8CNX0nH42eIWJ0mSJEmSJpVCBU7zgB1Dft6ZuTfU3wJvDSHsBH4CvHfI2JLMVrvfhBAuPd4LQgjvCiGsDiGsbmo6uS1jJxo4DX7v3PMASLe307t580m9O5+iKGLj0x+hP9M7Wm9vExs2fJAoev6YJEmSJEnSyRhPTcOvA74eRdF84ErgGyGEGLAHWJjZancLcGcIofbYL0dR9OUoii6MoujCmTNnnlQBJx04nXfe4HXn2rUn9e58am19lMMjrGLq6NhIW9v4q1uSJEmSJE1MhQqcdgELhvw8P3NvqHcC3wGIouh3QDnQEEVRTxRFLZn7jwGbgRX5KPJkA6fK848ETl1rxl9w09m1dfQ5naPPkSRJkiRJykahAqdHgeUhhCUhhAT9TcF/eMycRuBygBDC6fQHTk0hhJmZpuOEEJYCy4Et+SiyrKyMEPr7HJ1I4FS6aBHxurr+743DFU6J0vpR55QmZhSgEkmSJEmSNBUUJHCKoigJvAe4D9hI/2l0G0IInwghvD4z7UPAjSGEJ4BvAW+L+hsLXQasCyE8DtwF/FkURXk5Wi0Wi1FeXg6cWOAUQhjcVte7fTvJA+Pr5Lf6+ssoHSF0SiRmUl/34gJWJEmSJEmSJrOSQr0oiqKf0N8MfOi9jw+5fgp4yXG+913gu3kvMKOiooKurq4TCpygf1tdxy9/CfSvcqq5/PJ8lHdS4vEyTlv5dzy5/n1A6nnjS5d8kFgsUfjCJEmSJEnSpDSemoaPC5WVlcCJrXCCoxuHj8dtdbNmXcH553+TGTNeSgilQHxwrPXQI8UrTJIkSZIkTToFW+E0UQw0Du/q6iKdThOLZZfJla9aBaWl0NdH59rH81niSaub/gLqpr+AKIpIp3v4/cN/QHf3Lvbu/QELF/wpNTWnF7tESZIkSZI0CbjC6RhDT6rr7u7O+nux8nLKz+gPbLqffJKotzfnteVKCIF4vJylS2/J3InYvPnTRa1JkiRJkiRNHgZOxxgaOJ1wH6fzzgcg6u2l+6mnclpXPsw+5fVUV58BQMuBBzhw4H+LXJEkSZIkSZoMTipwCiFcGkJ4XoPvyWDglDqA1tbWE/ru0D5O43Vb3VAhxFh26l8N/vzc5v9HFKWLWJEkSZIkSZoMsgqcQgi/DiFckrn+C+Bu4K4Qwl/ns7hCW79+PQ8//PDgz9/85jf58Y9/TF9fX1bfrzjv3MHr8dg4/Hjq6y+hru7FALS3r2ff/nuKXJEkSZIkSZrosl3hdBbw+8z1TcDLgIuBd+ehpqJ46qmnuOuuu47q25ROp1m9ejX/8z//QxRFoz6jdNYsSufPB6Bz7ZqsvlNsIYSjVjlt2fw50unx239KkiRJkiSNf9kGTjEgHUJYCpREUbQhiqJGoD5/pRVOOp3m/vvvH3b8mWeeYceOHVk9a2BbXaqpmb5du3JSX77V1p7FKae8DoCu7kZ27fpWkSuSJEmSJEkTWbaB0/8Cnwc+DXwPIBM+teSproJqaWmhpWXkX2XTpk1ZPWsibqsDOHXphwihFICt2/6VZLK9yBVJkiRJkqSJKtvA6W1AN7AJ+Hjm3hnAF/JQU8GlUqlR5ySTyayeVTmkcfhECpwqKhYwf96fANDXd4DtjV8pckWSJEmSJGmiyipwiqKoKYqiv4qi6KNRFHVk7v04iqJ/ym95hTFjxgwqKipGnLNgwYKsnlW2YgWxykoAOtdMnMAJYPHidxOPVwPQ2Pg1enr2F7kiSZIkSZI0EWV7St37QwjnZq4vCiFsCSE8G0K4KL/lFUZpaSkXX3zxsOP19fWcdtppWT0rxONUnHsOAD3PPEOqoyMnNRZCIjGDRYveBUA63cXWrf9S5IokSZIkSdJElO2Wug8B2zLX/wjcBnwWmDSJxGWXXcYFF1zwvPv19fW89a1vpaSkJOtnVZyb2VaXTtO9bl2uSiyIhQveTiIxC4Dde75DS8sDdHRsIpXqKXJlkiRJkiRposg2cJoeRVFrCKEaOBf4fBRFtwPZLfuZAGKxGK973et4z3vew+LFiwfvX3311dTXn9hhfBVD+jhNtG118XglS5e8H4AoSvH4E2/n4Ueu5KHfvpgtW/6ZKBq935UkSZIkSZrasg2cdoYQLgbeBDwYRVEqhFADTLr0oaGhgXPOOWfw5wMHDpzwMyrOPQdCACZW4/ABJaW1z7uXTLayddu/8PTTHytCRZIkSZIkaSLJNnD6K+BHwCeAv8/cey3waD6KKrYZM2YMXre0tJzw9+M1NZQtXw5A1xNPEGVxCt54EUVpNm/+zLDju/d8h47DzxawIkmSJEmSNNFke0rdj6MomhVF0fwoigZCpu8BV+WvtOIZa+AER7bVpTs66Hluc07qKoSOjk10dTWOOKdp/30FqkaSJEmSJE1E2a5wIoSwNIRwawjhthDCrcC8KIq681hb0VRWVlJeXg6cfOBUef6RPk5da9fkpK5CSKU7s5jTVYBKJEmSJEnSRJVV4BRCuBJ4gv6G4Z3AOcDaEMIf5rG2ogkhDK5yamlpIYqiE37G0MbhE6mPU1XlcmKx8hHn1NacXaBqJEmSJEnSRJTtCqdPAVdFUfSmKIr+MoqiN9O/ne5T+SutuAYCp97eXjo6Ok74+6ULFhDPPKNz7eM5rS2fSktrmTv3jcOOl5XNoaHh8gJWJEmSJEmSJppsA6eFwK+PufdA5v6k1NDQMHh9MtvqQghUnHcuAH2NjSSbm3NWW74tO/UjNDS88rhjpaXTCSHrnZiSJEmSJGkKyjY5eAL4wDH33gesy20540cuGodXnnf+4HXnBNpWF4+XcfZZX+KCC77DwoU3Mm/eW6msWAJAR8dGdu3+dpErlCRJkiRJ41lJlvPeDfw4hPABoBFYACSB1+ersGLL5Ul1AF1rH6f2Va8ac12FEkJg+rQLmD7tAqD/9LpHHn09UZRk8+ZPM7PhVZSVzSxylZIkSZIkaTzKaoVTFEVPASuB64HbgBuA06IoWp/H2oqqvr5+8PpkA6fyVWcQSkuBidU4/Hiqq1eycME7AUgm23n2uU8WuSJJkiRJkjReZd2MJ4qiviiKfh1F0Z1RFP0aIISwJW+VFVkikaC2thaA5pPsvxQrK6N81SoAutevJ93Tk7P6imHJkvdSXj4fgH37fkRLy4NFrkiSJEmSJI1HY+n+HIDFOapjXBrYVnfw4EFSqdRJPaPi/P4+TlFfH90bnspZbcUQj1ewcsXfDv686ZmPk0p1F68gSZIkSZI0Lo31uLEoJ1WMUwOBUzqdprW19aSeMXBSHUz8bXUADQ0vZ+bMKwDo6mpk2/Z/K3JFkiRJkiRpvPF8+xHk5qS6IY3DH5/4gRPAihV/QzxeDcD27bdz+PDmIlckSZIkSZLGkxFPqQshfO1kvzsZ5CJwKmlooHThQvoaG+lcs5Yoiggh5KrEoigvm82pSz/IM8/+HVHUx9Ob/obzz/vmhP+9JEmSJElSboy2wmnXCJ/twD/ktboiy0XgBFCZ2VaXammhb8eOMdc1Hsyffz01NWcC0Nr6MHv33l3kiiRJkiRJ0ngx4iqlKIr+plCFjEfTp08nFouRTqfHFDhVnHceh37wQ6C/j1Ni4cJclVg0IcQ5beXf8+jqq4E0zz73j1RVrQQiKisXU1JSU+wSJUmSJElSkdjDaQTxeJy6ujpgbCucKob0cepcMzn6OAHU1p7F/PnXA9DXd4BHV/8Rj66+igcfupiNT99KMtlR5AolSZIkSVIxGDiNYmBbXVtbG729vSf1jLJly4hV9zfZ7nz4YVKHDuWsvmKbNeuK591Lp3vYvfu/efyJd5JOJ4tQlSRJkiRJKiYDp1E0NDQMXh84cOCknpFsbiGUlwPQu20bz7z4Jez8wAfp27s3JzUW0/bttw87dujQapqbf1HAaiRJkiRJ0nhg4DSKsTYOT7W2sv3660k1Nw+5maL93nvZ/tbrSR48mIsyiyKV6qKl5TcjztnfdG+BqpEkSZIkSePFiE3DB4QQbhhmqAfYCTwSRVFfzqoaR8YaOB345jfpa2w87ljfzp0c/MY3mPm+9510fcWUTvcC0YhzUqmuwhQjSZIkSZLGjawCJ+BdwAuAFvoDpnlAA7AWWAz0hhCuiqJoTT6KHIttXT18aUcTvznQRjqCS+qquWnBLFZUlWf1/bEGTu33/WzE8bZ775uwgVNJSS0VFYvo6to+7JxptecWsCJJkiRJkjQeZLulbg3w4SiK5kZRdFEURfOAvwYeBuYCXwO+kKcaT9qaQ4d51aOb+PquZrZ29bK9u5dv7jnAH6zexEMH27N6RnV1NYlEAoDmodvispTu7BzT+HgWQmDhwj8dYbyUuXPfWMCKJEmSJEnSeJBt4HQ98C/H3PsCcEMURWngU8CqXBY2Vuko4r0bG2lPpZ831p2OeM9TjfSlR94OBv2hysAqp5aWFqJo9O8MVb5q5L+W0cbHu3lzr2PhgncedyyK+mjveLrAFUmSJEmSpGLLNnDaD7zmmHtXAE2Z63IglauicuGRQ4fZ3NUz7Pje3j5+daAtq2cNBE7d3d10nuCKpPobrh95/PqRx8e7EALLl9/KCy/+OUuWvJ/58/8Pc+deNzi+8am/oq+vtYgVSpIkSZKkQsu2h9MHgP8OIawFdgALgPOAN2fGXwh8MfflnbzdPaP3MN+VxRx4fh+nqqqqrOuovOACZv/fj7P37z8JqaMzuVkf/jBVL7w462eNZ1VVS1m65EgvqnSqi737vk9P7z6e3vRxzlz1z4QQilihJEmSJEkqlKxWOEVR9FNgGfB1YCNwB7Asc58oiu6Louhv8lXkyZhbVjrqnHlZzIGxNw6vu+46lv38ZzS8770klp06eL/8tNNO+FkTxcqVf0t52VwA9u+/h337flTkiiRJkiRJUqFku6WOKIr2R1H0H1EUfTKKoq9FUbQ/n4WN1UXTqji1omzY8dmJUl5eX5vVs8YaOAGUzp3LzHe/m9kf/ejgvfb77z+pZ00EJSU1nHHGZ4D+VU2bnvk43d27i1uUJEmSJEkqiKwCpxDCohDCf4YQ1oUQtgz95LvAkxULgS+cvpDq+PN/xfJY4F/PWEhpLLstXrkInAZUXnghsdr+oKv9/l+ccBPyiaSu7oUsXPAOAJLJdp7a+Ff095iXJEmSJEmTWbYrnO4EEsBHgRuP+Yxb50+r4hcvWMl1s+sH7y2tKONnF67kkrqarJ9TXl4+2LdprIFTKC2l+qUvBSC5ew89T0/uU9yWLv0QVVUrADh48Hfs2HlHkSuSJEmSJEn5lm3gdBbwJ1EU/SiKovuHfvJZXC4srijj0ysXDP68oqqMFVXlJ/ycgVVOLS0tpNNjW6VTc/krBq/bfzHu/wrHJB4vY9UZnyOEBACbN3+ajo5NRa5KkiRJkiTlU7aB00PA2fksJJ9KY4GKWP+v2pY8ubBoIHBKpVK0tbWNqZ6qSy4llPY3LJ/MfZwG1NSczqlLPwhAOt3Lhqc+RHf3Xjo6NtHXd6jI1UmSJEmSpFwryXLes8B9IYS7gL1DB6Io+kTOq8qDaSVxunrTtCVTJ/X9hoaGweuWlhamT59+0rXEq6uofPGLOPybB+h5+ml6d+4iMX/eST9vIli48J00t/yK1tZH6OjYyG//9yUAhFDCrJlXsHz5xygrm1nkKiVJkiRJUi5ku8KpHrgPqAGWD/ksy1NdOVdbEgfg0EkGTrlsHA5Q84rLB687fjn5VzmFEGfZso8wcGrdgChKsm//j3lszbX09Y1t5ZgkSZIkSRofsgqcoii6fpjPDfkuMFdqS/p/1fZxEzi9HEJ/+NJ+/y/H/LyJoGn/T4Hjn8rX1bWNXbvuLGxBkiRJkiQpL4YNnEII84dcLxzuU5gyx25ghVNbMkU6On7oMZK6ujpCJiBqbm4ecz0lM2dScc45AHSuXk3y4MExP3O827//3pHHm35SoEokSZIkSVI+jbTCaeOQ623A1syfQz9b81JVHkzLBE5p4HDqxBuHl5SUDPZtysUKJ4DqgdPqUik6fvObnDxzPEumDo84nkp1FqgSSZIkSZKUTyMFTtOGXJcCicyfQz+J/JWWWzWZwAk46cbhA9vqWltbSSaTY6/p8lcOXndMgW11tbVnjTheUzPyuCRJkiRJmhiGDZyiKEoPuU4N9ylMmWM3LYeBE8CBAwfGXFPZ0iUkli4FoOOhh0h3d4/5mePZggVvH3F8zuyrC1SJJEmSJEnKp6yahocQFoUQ/jOEsC6EsGXoJ98F5krtkMBpvJxUB1CT2VYXdXVx+H9/l5Nnjlcz6i9h+fKPMdx/djt2fI0hOackSZIkSZqgSrKcdyewA/goMCEb7eR6hVPuAqfLafnKVwFo/+X9/afXTWILF7ydmQ2vYu/e79Hds5dEaQN7991Nd/duWg48wNZtt7F0yXuLXaYkSZIkSRqDbAOns4DLJtIWumPVjtPAqfzss4nPbCDV1EzHL39FlEoR4vHRvziBVVTMZ8mQUGnWrFez+rFrSKd72Lr1n5lWew4zZlxWxAolSZIkSdJYZLWlDngIODufheRbLrbU1dbWUlLSn9HlKnAKsRg1L+/fVpc6cICuxx/PyXMnkpqaM1i58hOZnyLWb/ggXV27ilqTJEmSJEk6edkGTs8C94UQvhhC+PjQTz6Ly6VcbKmLxWLU19cDuQucAGpeefngdfsUOK3ueObOuYa5c98MQDLZypPrbyad7ilyVZIkSZIk6WRkGzjVA/cBNcDyIZ9leaor52pyEDjBkW11hw8fpqura8x1AVS+8IXEKisBaL//F0RRlJPnTjQrlv9famrOBKC9/Umeefbvi1yRJEmSJEk6GVkFTlEUXT/M54Z8F5gruVjhBNDQ0DB4feDAgTHVNCCWSFB1WX/Por7tjfRu3pyT50408XgZZ515GyUl0wDYtetO9uy5u8hVSZIkSZKkEzVs4BRCmD/keuFwn8KUOXa56OEE+WkcDv2n1Q1o/8X9OXvuRFNRMZ9Vqz4HBACe3vRR1j15M4+teQvrnryZpqafE0Xp4hYpSZIkSZJGNNIKp41DrrcBWzN/Dv1szUtVeVAeC5SG/hCjPXnygUW+Aqfql14GmYbk7fdP3cAJoGHGy1iy+D0ApNO9NDXdS2vrwzQ13cu6J/+MJ9e/l3Q6WeQqJUmSJEnScEYKnKYNuS4FEpk/h34S+Sstt0IIg6uccrXCqbm5ecx1DYjX1lJ10QsA6H7ySfr27cvZsyeiGQ0vH3asqeledu68o4DVSJIkSZKkEzFs4BQN2bcURVFquE9hysyNgT5OY+nhVFlZSUVFBZDbFU4A1UO21XX8cmqeVjdg9+7vjDi+c9d/FagSSZIkSZJ0orJqGh5CiIcQ3h1C+O8Qwv0hhF8OfPJdYC7VlPT/umMJnODIKqeWlpacnihX84pXDF5P5T5OAJ2dW0Yc7+pqZILlnZIkSZIkTRlZBU7A54D3AY8AFwP3APOBh/JUV14MXeE0lqBoIHDq6+ujvb09J7UBlM6ZQ/mqVQAcfuQRUjl89kRTWlo/4nj/SXbZ/ucrSZIkSZIKKdv/x34NcEUURf8EpDJ//hFwWd4qy4OBHk69UUR3euyBE+RjW11mlVNfHx0PPJDTZ08kc2ZfNeJ4Q8MrCZkm8JIkSZIkaXzJNnCqBLZnrjtDCBVRFG0Ezs9PWfkxsMIJoD1HjcNzHTjVXP7KweuD3/o2XY8/TpQ++VP1JqqGhsuZOfPVw463tT1BMjl1V4BJkiRJkjSeZRs4PQ1cmLl+DPh4COHDwO68VJUnNUMCp1ydVJfrwCleN51QXg5A1+rVbLv2Oja/5jV0PDihdi+OWQgxzlz1LyxfdisVFQsBiMdrKS3t/7vv7HyO9evfRzqdLGaZkiRJkiTpOLINnD4IDCyz+RDwIuCNwJ/lo6h8GbrCaSyNw+vrj/QXymXglO7upvEd7yDq7j7qft/2Rnb8+Z/TuWZtzt41EcRiJSxc+E5e/KJf8YqXP8NLL1vDxRfdQ3nZXABaDjzAM8/+fzlt3C5JkiRJksZu1MAphBAHVgDrAaIo2hRF0cuiKLogiqJf57m+nKrN0QqnRCJBbW0tkNvAqe2ee+h9bvPxB5NJmr/4xZy9a6IJIU4IgbKymZxzzleJx6sB2LXrTnbs+FqRq5MkSZIkSUONGjhF/WfPfyGKop4C1JNXtTla4QRHttUdPHiQVGpszxrQ8etfjzh++Le/Jd3bm5N3TWTV1Ss568x/pT8LhWef+xT7m+4rclWSJEmSJGlAtlvq7gkhXJnXSgogV1vqABoaGgBIp9O0traO6VkDor5R+hFFEeQo3JroZsy4lJUrPpH5KWLDhltoNrhz7AAAIABJREFUa1tX1JokSZIkSVK/kiznxYC7QwgPATuAwaY5URS9Ix+F5UOuttTB8xuHD/35ZFVeeMGIq5zKV60iVlEx5vdMFvPmXUtX13a2N36ZdLqbx594JzNnvorOzm3E45XMmnkFs2e/jlisrNilSpIkSZI0pWQbOD0LfCafhRTC0MCpPceBUy5Me8MbaPn3r5E6ePD477zxxpy8ZzI59dS/pLOrkaame+nrO8Du3f89ONbS8it27fom5557B6WltUWsUpIkSZKkqWXEwCmEcF0URd+KouhvClVQPuVrhVNzc/OYnjWgpK6OBV/9Crve+z76du8+aqz6FS+n9opX5+Q9k0kIMZYv+yhNTT/jyEGKR7S1r+O55z7F6ad/qvDFSZIkSZI0RY3Ww+n2glRRILns4TRt2jRisf6/vlyeVFexahWn3ncv82/7V2a8612QeUfvtu1EUTTKt6empub7OF7YNGDvvh+QTLYXriBJkiRJkqa40QKnUJAqCqQqHhv8hdqSwwcU2YjH49TX1wO5DZwAQmkpNZdfzqxbPkjNq14FQO+WLXStXZvT90wWnYc3jzieTvfQ3b17xDmSJEmSJCl3Rguc4iGEl4cQXjHcpyBV5kgshMFtdWNd4QQMBk7t7e1s3bo1LyuQpl9zzeB16//clfPnTwalifrR55TWFaASSZIkSZIEozcNLwP+neFXOkXA0pxWlGe1JXEOJVNj7uG0YcMGtm7dOvjzHXfcwZw5c7jmmmtycmLdgKoXv4iSuXNI7t5D2733csqtHyFeU5Oz508Gs0+5im3bbht2vLJiKWVlswpYkSRJkiRJU9toK5wOR1G0NIqiJcN8JlTYBFBb0v8rt6dOPnDaunUrd911F319fUfd37NnD3fccQfd3d1jqnGoEI8z/eo3ABB1ddF2z09y9uzJoqpqKYsX/fmw451dW9i9x9VhkiRJkiQVymiB06QzsKVuLCucHnjggWG3z7W1tbE2x72Wpl/9xxD6F5m13mVwcjxLl36IM874J2pqVgEQi5VTW3vO4PjGjR9h337DOkmSJEmSCmFKNQ2HIyfVdabS9KVPvOdSKpU6aivd8WzePHIT6xNVOncuVZdcAkD3+vV0b9yY0+dPBiEE5sy+iote8ENe8fJneNlL1/OCC+9m2al/nZmRZsOGD9Lc/Kui1ilJkiRJ0lQwYuAURdGkaxY0sMIJctM4vFCOah5+13eLWMn4F0KckFkRtmjRu1i8+GYAoijJk+tv5uDB3xezPEmSJEmSJr0pu6UOTq6PUzweZ/HixSPOWbo0962tal7+MuKZU/EO/ehHpHPYJ2qyW7rkgyyY/zYA0ukenlj3Lg4dery4RUmSJEmSNIlN6cDpZPs4XXrppcOO1dTUcN55553Uc0cSEgmmXXUVAOm2Ntp//vOcv2OyCiGwfPnHmDvnTQCkUodZ+/gNrH7szfzq16v49W/O5sn176W9fUORK5UkSZIkaXKYcoHTtKFb6vpOLnA69dRTufrqq6moqDjqfkVFBTfccMPz7ufK9GveMHjd+j82Dz8RIQROO+3vmTXrD4H+0OnQodWk092kUofZv/8nPLr6Gg4c+N8iVypJkiRJ0sQ35QKnmqGB00lsqRtw9tlnc8stt/DGN76ReLz/mdOnT2fmzJljrnE4ZUuXUnHBBQB0PvIIvdu25e1dk1EIcU5b+XeEUHLc8Sjq5emnP0oUpQtcmSRJkiRJk8uUC5ym5WBL3YDS0lJWrVrFwoULAdi7dy/dee6tdFTz8O/endd3TUYHDjxIFCWHHe/qbqS1dXUBK5IkSZIkafKZ0oHTyW6pO9ZA4BRFETt37szJM4dT++o/IFZdDUDr979H1NeX1/dNNj29TaPO6e1rLkAlkiRJkiRNXlMucMrVlrqhFi1aNHi9ffv2nDxzOLHKSmpfm+lD1NRMxwMP5PV9k01lxeIs5iwadY4kSZIkSRrelAucjlrhNMYtdQPmz59PLNb/V9nY2JiTZ45k+jVvHLy2efiJqa+/lPLyecOOh1AybI8nSZIkSZKUnSkXONXmsIfTgEQiwZw5cwDYuXMnyeTwPYJyoXzVGZSdfjoAHQ88QN++fXl932QSi5Vw5plfoKSk9rjjUZTksTVvoa19fYErkyRJkiRp8phygVNN/Ejg1J6jwAmO9HFKpVLs3r07Z889nhAC0695Q/8P6TSHvve9vL5vsplWew4XX/xTFi96N9Nqz2PatAtZsvh91NW9BIBkspW1a9/KoUNrilypJEmSJEkT05QLnEpjgcp4/6+dqxVOcCRwgsJsq5v22tcSysoAaL3ru0TpdN7fOZmUl83m1FM/xIUX3sWFF/w3S5e+n3PP+SozZ74agGSynbWP/x8OHvx9kSuVJEmSJGnimXKBExzp45SrHk5wdOCU78bhAPFp06i9oj8c6du5k86HH877Oye7WCzBmav+hVNOeT0AqVQnjz/xDlpafkM63cehQ2s5ePARksn2IlcqSZIkSdL4VrDAKYRwRQhhUwjhuRDCh48zvjCE8KsQwtoQwroQwpVDxj6S+d6mEMKrx1rLwLa6XK5wqqqqoqGhAYAdO3aQLsCKo+nXXDN4vesv/pId776Z1ru/R7q3N+/vnqxisRJWnfFZ5s55EwDpdA+PP3EjDz50Masfu4Y1a6/joYdezLPPfYp0uq/I1UqSJEmSND4VJHAKIcSB24DXAGcA14UQzjhm2seA70RRdB5wLfDFzHfPyPy8CrgC+GLmeSdtYIVTezK3odCiRYsA6O7uZv/+/Tl99vGkk0kIAYBUSwsdv/wle269lW3XXkuqtTXv75+sQohz2mmfZP78GzJ3UiSThwbHU+lOGhu/ytObPlacAiVJkiRJGucKtcLpIuC5KIq2RFHUC3wb+KNj5kTAwNFh04CBztt/BHw7iqKeKIq2As9lnnfSaodsqUtH0VgedZRC9nFKdRxm1/s/AMepv+epjez9h3/I6/snuxBiLDv1w8Ri5cPO2bPnLg4f3lLAqiRJkiRJmhgKFTjNA3YM+Xln5t5Qfwu8NYSwE/gJ8N4T+O4JmVbaHzhFQEcqd6ucCtnHqe0n95Buaxth/KckDx7Maw2TXXv7k6TT3SPOaW65v0DVSJIkSZI0cYynpuHXAV+Pomg+cCXwjRBC1vWFEN4VQlgdQljd1NQ04tya+JHH5rJx+PTp06mt7V+k1djYSJTD1VPH6t2ydeQJySR9O3aMPEcjSqd7Rp+TGn2OJEmSJElTTaECp13AgiE/z8/cG+qdwHcAoij6HVAONGT5XaIo+nIURRdGUXThzJkzRyxmoIcT5DZwCiEMrnJqb2+nNY99lOLTp48+p64ub++fCmpqVhGLlY08p/bsAlUjSZIkSdLEUajA6VFgeQhhSQghQX8T8B8eM6cRuBwghHA6/YFTU2betSGEshDCEmA58MhYiqkdEjjl8qQ6ONI4HPK7ra72tX842DD8eCrOPZfEggXDjmt0paXTmTv3TSPO2b79S/T1Db+1UZIkSZKkqagggVMURUngPcB9wEb6T6PbEEL4RAjh9ZlpHwJuDCE8AXwLeFvUbwP9K5+eAu4Fbo6iaEwp0dDAqT3HgVOhGocn5s+n4eabjz8Yi3HKRz+at3dPJcuXfYRTZr32efcHDkpsbX2Yx9a8me7uPYUuTZIkSZKkcaukUC+Kougn9DcDH3rv40OunwJeMsx3Pwl8Mle15HOF08yZMykvL6e7uzvvjcNnvudmEosW0vIf/0HPUxuPDKTThPh4as81ccViZZx55j+zuOPdNDffTzrdx/TpF1JWNpcnnngHXd2NHD78DKsfu4Zzz/ka1dUri12yJEmSJElFNyVTiWl5DJxisdjgKqeWlhY6Ojpy+vxjTXvd61h6992ctu4J5n7uc4P3m790e17fO9VUV69k8eJ3s3Tp+6mvfwlVVUu44ML/oabmTAB6evby2Jo3c/Dgw6TTvRw48Fv27f8pnZ2jNHeXJEmSJGkSmpKBUz631MHR2+p2FOikuJBIUHvFq0mceioA7T/7GT3PPVeQd09VZYkGzj/vTmbMeCkAyWQ7ax+/ngcevIi1j9/A+vXv4Xe/fyWPP/EOenubi1ytJEmSJEmFM+UDp1yvcILCNQ4/VojFaLjpXYM/N3/5ywV791RVUlLF2Wfdzpw51wAQRSlSqfaj5rS0/Ia1j7+NdLq3GCVKkiRJklRwUzJwGrqlri0PgdOcOXMoKelvj5XPxuHHU3vllZRmTqdr+/E99Bb4/VNRLFbKaSs/RSIxc9g5HR0baWr6eQGrkiRJkiSpeKZk4FST5xVOJSUlzJ8/H4A9e/bQ09OT83cMJ5SUMOPGP+3/IZ2m5StfKdi7p7Jk8gC9vU0jzmlp+XVhipEkSZIkqcimZOBUHgskQgDy08MJjvRxiqKInTt35uUdw5l21VWUzJ4NQOv3f0Df7t0Fff9UFEXpnMyRJEmSJGkymJKBUwhhsI9TPlY4wdGNwwvZxwkglkgw453v7P+hr4+Wf/9aQd8/FSUSM6msXDrKnIYCVSNJkiRJUnFNycAJjvRxykcPJ4AFCxYQMquoCt3HCWD6G68hPmMGAK133UWyaeTtXhqbEAKLF/35iHMad/wHO3b+J1EUFagqSZIkSZKKY8oGTjWDgVN+tjmVlZUxO7OtbefOnSSTyby8Zzix8nJmvP1tAEQ9PbR8/esFff9UNGfO1Sxb9mFisbKj7peUTMtcpXjmmf+Pp5++lXS6cH29JEmSJEkqtCkbOA1d4ZSvFSeLFi0CIJlMsmfPnry8YyTTr72O2LT+sOPgt75N8uDBgtcw1SxaeCOXvOS3nH7a/2P58o9x/nnf5NJLVrNi+d8QQv9/c7v3fIc1a99KT0//qrPOzu20tq6mu2dvMUuXJEmSJClnpmzgNNDDqS+K6ErnJ3Aa2sepGNvq4tVV1N9wPQBRZycHv/GNgtcwFZWW1jF37jUsXPB26upeSCwWY8GCt3HuOf8xuNrp0KE1PPLoa/n9w6/ld79/BY+teTO//e0lPPHEjXR3Fz6clCRJkiQpl6Zw4HTkV8/3SXVQ+MbhA+rf+lZiVVUAHPivb5Jqby9KHYL6+pfwggu/R1XVcgB6e5s5fHjjkBkRzS2/ZM2at9DX11acIiVJkiRJyoEpHDjFB6/zdVJddXU1MzKNuxsbG0mn89MvaiTxadOoe8tbAEi3tXHwzm8VvAYdUVm5iAsvuIvy8nnDzunqbmT37m8XsCpJkiRJknJrygZO04YETvk6qQ6OrHLq7u6mubk5b+8ZSf3b/g+hvByAA1//OunOzqLUoX4lJdUkk10jzmlq+lmBqpEkSZIkKfembOBUU6DAaaBxOBRvW13JjBnUvflNAKQOHmT7DTew+68/zKEf/pB0b29Raprqomjkv/d02n8XSZIkSdLENWUDp0KvcILiNA4fUHvllYPX3es3cOgHP2D3X/01W//4avr27StaXVPVtGnnjTgej1fl7fRESZIkSZLybcoGToXo4QRQV1dHdXU1ULwVTlEUse8fPnXcsd7Nm9n9ob8ocEVauPBPRxxvPfQITz31F6RSbn+UJEmSJE08Bk7kd4VTCGFwW11bWxutra15e9dwutevp+uJJ4Yd71y9mu6nny5gRZpRfwkrV3yCEEqOuh9CKRAA2Lvv+zy6+g10dm4tQoWSJEmSJJ28ktGnTE6F2lIH/dvqNmzYAPSvcpo+fXpe33esbMKk7qefpvy00wpQjQbMn/8nzJz1avbt+xG9PfupqFjIKae8lvb2Dazf8H56e5s5fPgZHnn0Ks44/dNMm3Y+e/beTVfnNhJlM5l9ylVUVS0t9q8hSZIkSdLzTNnAqVBb6uDoxuGNjY2cc845eX3fseI1NaPPyWz7U2GVJRpYuODtR92rq3shF73gRzy5/r0cOrSaVKqDJ9e/m/4FienBedu23caSJR9g6ZL3FrZoSZIkSZJG4ZY68r/CadasWZSVlQGwadMmnnnmGVKp/L5zqKpLLiVWVTXseGxaLVUveUnB6tHoyspmcf55/8XCBe8ccjf9vHlbt36efft/WrjCJEmSJEnKwpQNnKriscFfPt+B0/bt2wcDpo6ODu68804+//nPs2XLlry+d0C8uopZfzl8Y/C6t/wJsYqKgtSi7MVipSxffiv19ZeOOG/Hjv8oUEWSJEmSJGVnygZOsRAGVznlM3Bqbm7mzjvvJJlMHnW/vb2dO++8k/379+ft3UPVXXst8z73T5QtX/68sc6HHyaKooLUoROX7Ds04nh7+5MFqkSSJEmSpOxM2cAJjmyry2cPp9/97nf09fUddyyZTPK73/0ub+8+Vu2VV7Lkhz9g+YMPsOyB31Bx7rkAdK1ZQ9uPflSwOnRi4vHKEcdjsbICVSJJkiRJUnYMnID25PN74+TK1q0jH2k/2niuhRAomTmT0lmzOOVjH4MQANj/mc+S6jhc0FqUnVmzrhxxPJk8zLZtXyKKCtcXTJIkSZKkkRg4kd8VTiET6IxHFWeuYvo11wCQbGqi5Uv/VuSKdDxz5lxNdfXpI8xIs3nLZ3hszXV0dTUWrC5JkiRJkoYzpQOnaZnAqSudpi+dnx5Gp5566pjG823mBz9ArLYWgJY7/pOeAq+40uji8QrOP++/mDPnGmIhkbkbmDHjFSxe9O7BLXWHDj3Gw4/8Ibt2fZuWlt/yxLqb+O1vL+X3D7+GrVu/QN8ovaAkSZIkScqVMBmbRV944YXR6tWrR533vo3b+c7egwBseMmZzEiU5LyWAwcOcPvtt9PT0/O8sUQiwU033cSMGTNy/t4TceAb/8W+T34SgKrLLmXB7beP65VZU1ky2U5Pzz5KS+tJJOoB6Dj8LE899SHa2zeM+N3KyiWcf/63KUs0FKJUSZIkSdIkE0J4LIqiC7OZ6wqnjHydVFdfX8/1119/3FDp9a9/fdHDJoC6664dPL3u8AMP0vHrXxe3IA2rpKSGqqplg2ETQHXVci684C4WL76Zkf4n3dm5leee+8cCVClJkiRJmuqmdOBUOyRwymcfp/nz53PzzTfztre9jfPPP3/w/oEDB/L2zhMRSkr6G4hn7PvUP5I+zoosjV+xWIJTl97C7NlXjThv/757SCZtDi9JkiRJyi8Dp4x8rXAaEIvFWLx4Ma961auIxfr/2jdsGHkLVCFVXXwRNa+5AoC+xkYOfP2OIlekkzPyiYvpqJe+vpYC1SJJkiRJmqoMnDLyHTgNqKioYNmyZQDs27ePpqamgrw3G6f85V8SyssBaP7Sl+jbu7fIFelElZfNGWVGAEoLUYokSZIkaQqb0oFTIXo4Hc+qVasGr9evX1+w946mdO5cGm56FwBRVxf7P/PZIlekEzVnzhvoD5WGE7H6satpar6/UCVJkiRJkqagKR04FaqH07FWrlxJPN7/7g0bNjCeTgqsf8c7KJ0/H4C2e+6h8U9vZNctt3DgjjtItbUVuTqNprJyCcuX3XrcsRD6/5vr7d3PunXvYsNTH6KvrxWAtrZ1bG/8Kjt2fJ3Ozm2FKleSJEmSNEmVFLuAYirGljqA8vJyVqxYwcaNG2lubmbfvn3Mnj27YO8fSaysjJkf+AC7/+IvADj80EMAtP3kpzR/6XYWfPnLVJx1ZjFL1CgWLnwH1dWnsWPnHXS0P0W8pJpTZl3J7Nl/zNZtX2DPnrsA2Lv3+xxoeZDSRAOHD2868oBn/445c67htJV/Tyzm9jtJkiRJ0omb0oFTsbbUQf+2uo0bNwL9q5zGS+AE0PHgg8e9nzp4kJ0338ypv/g5sUSiwFXpRNTXv5j6+hc/7/4Zp/8/Zs16DU8//VF6evbS29dC73GaiO/Zcxcl8WpWrPibQpQrSZIkSZpk3FKXUcgtdQArVqygtLR/9cj69evHzba65MGDtN1zz/Dj+/fT/rOfF7Ai5VrDjJfxwovvpWHGK0act2v3twa33EmSJEmSdCKmdOBUEz8SOLWnChs4JRIJVqxYAcDBgwfZvXt3Qd8/nN4tWyCZHHFOz6anC1SN8qWkpIbp0y8ccU463UNb27oCVSRJkiRJmkymdOBUEgtUxfv/Cg71FTZwAjjzzCO9kDZs2FDw9x9PrKYmizm1BahE+RZCNv2ZRjrxTpIkSZKk45vSgRMc6eNU6B5OAMuWLSOR6YU0Xk6rK1u+nLLly0acU/uaKwpUjfJpxoyXjjpn85bP0Na+vgDVSJIkSZImkykfONUMBE4F3lIHUFpaymmnnQbAoUOH2LlzZ8FrOFYIgVNuvRVKjt9PvnT+PErnzy9wVcqHqqpTmX3KVSPOaW/fwKOP/jHPPPtJksnDAHR0bGLb9tvZuu02WltXj4ugVJIkSZI0vkzpU+qguCucoH9b3bp1/X1y1q9fz4IFC4pSx1BVL3oRi+74Ok3//C90PvJI/82SEkgm6du5i4Pf+hb1b3lLcYtUTpx++j8QL6lm9+7/Jor6gP7+TnPnvIm2tnW0HnoUSLNjx9fYv/8nlJfP49Chx456xvTpF3HWmbeRSNQX4TeQJEmSJI1HYTKuTrjwwguj1atXZzX3reu28IuWNgKw62XnEAuF7VmTTCb57Gc/S3d3N9XV1dxyyy3EYuNn4VmqrY2op4fevXvZft1bIJkklJez5Ht3U7ZkSbHLU4709h6gre0JQqyU6dPOJx6vJIoi9uz5Ls8+9ymSyZFPq6urexHnn/dfBapWkiRJklQMIYTHoiga+QSqjPGTbBRJbWaFUwR0pNIFf39JSQmnn346AB0dHTQ2Nha8hpHEa2spmTmTyrPOouHP/wyAqLub3X/9YaJRTrPTxJFI1NPQ8HJm1F9CPF4J9G+vnDv3Gl70wp8xa9aVI37/4MHfeaKdJEmSJGmQgVMmcAI4VMRtdQPWrx+/DZobbrqJ8rPPBqB73Tqab7+9yBWpEBKJGf8/e/cdH2lV9n/8c0/JpM2kZ9OzPcv23qUqvYvSBFEEFbCAqPj8LNifx0dB8REElSIiIB2lKbC0ZVu2ZftmS7LJbja9zSSZTLl/f2Q3m5BksrskMynf9+vly9z3uebMdWfZMlfOuQ4ZGaF7PQE0NBzfqkIREREREREZ+UZ9wSmhS8EpUn2cxo4dS2xsx6qS7du3E4hAA/PjYdhsZP3Pf2NERwNQc/8DtG4ZugUyGTgWo/92bz5/cxgyERERERERkeFg1BecnNZj34JGX2QKPVarlalTpwLQ0tJCSUlJRPI4Ho5x40j/zrc7LgIBDn33uwRbWyOblAy6xMQF2GzOkDGlpQ+wZ++vO0+zO8o0TYJB32CmJyIiIiIiIkPMqC84JdiPrXBqjuDKomnTpnV+PZS31QEkXX01ccuXA9C+bx9Vv7knwhnJYLNaYxk79taQMabpo7T0AVat/iQVFS/g8exj27Zv8c6701jxzhTWrDmfQ4eeZSQeVCAiIiIiIiLdjfqC01Do4QSQn59PfHw8ADt27MA/hBtyG4ZB5s9/jjUhAYD6v/0N98qVEc5KBlte7peYNPH/Ybcndd6zWmLJy/0SEyd8r3MFVHt7Fdt33MnqNedyuPJFgkEvAG7PLnbs/C579vwyIvmLiIiIiIhI+PTfmGWEc1kj38MJwGKxMG3aNNasWUNbWxv79u1j8uTJEcunP/Yx6WTc/SMO3n4HAIe+810SPn05eL04Jk/Gdd55WI70pZKRwTAM8vK+SHb2tTQ1bcY0/bhcMzoLTZmZl7J3370cOvQ0Hec+9v776UDZX8jM/DTx8QXhS15ERERERETCatSvcBoKTcOP6rqtbtu2bRHM5Pi4zjsP10UXAhCoraXuoT9R99hfqfh/32fPmWfRsn59hDOUwWC1OkhKWkhy8tJufZ2iolI5ZcrPmTP7r/3OcfjwS4OZooiIiIiIiETYqC84uexDY0sdQE5ODi6XC4CdO3fi8w39RsvRU6f1ej/Q0EDZV76Kv64uzBlJpDkcGf3GtLfXhiETERERERERiZRRX3AaSiucjm6rA/B6vezZsyei+fTHNE0annqqz/FgczMNzz4XxoxkKIiOzsBqCb2dsqb2baqqXlcDcRERERERkRFq1BecnEOkh9NR06dP7/z69ddf57XXXqO8vDyCGfUt2NREe2lpyJjWos1hykaGCqs1lozMy0LG+Hx1bNl6K4Xrr6C+fi0+XyN79vyKDz5YytsrJrNq9TmUlT1KMDh0m+eLiIiIiIhI30Z90/BoqwWHxcAbNIdEwampqanz68bGRtasWcOaNWuYO3cuF154IRbL0KkRGlFRYBgQYpWKxREdxoxkqJg44Tu43TtobNzwkRE7Ca4ZNDZ13G9q2sSGjVdjtcYRCHg6o1pa9rC7+Kc0NK5n+rTfYRhD5797ERERERER6Z8+xQGuI9vqIt3DqaGhgWeffbbXsQ0bNrBu3bowZxSaJSaGuE8sDxnj/NSnwpSNDCU2Wzxz5zzBtKn3kJr6SZKSlpCXdxNLl/yb+fOfYf6850hMXNQZ37XY1FVV1avU1LwZrrRFRERERERkgKjgBLiObKtrjnDBaf369QQCfeewevXqIdfzJu1rX8dwOHoftFhwTJwQ3oRkyLBYosjIuIRZMx9k7py/MWniXcTE5AGQkDCbuXOeYNbMP2MY1pDzVBx+IRzpioiIiIiIyABSwYmhs8Lp8OHDIcfr6+tpb28PUzbHJ2bGdPIeeYToGTOO3Ty67S8Y5ODtdxBsaYlMcjKkGYZBSsrpmGbo33fetoowZSQiIiIiIiIDRQUnjp1U1+QPRHQFkaOvlUJHWCwWbLah13Yrdu4cxj3zDya88Tpjn/kHk95/j5i5cwHwFhdTcffdQ25llgwNhmEQEzM2ZEyzeyf79v8ev7+52/22tkPU16+lpaVk8BIUERERERGRk6KCE+A8UnDym9ASDEYsj2nTpoUcnzp1KlZr6O1HkRSVn0/MjBnYUlLIvvderCkDmhWCAAAgAElEQVQpADS9/E8ann46wtnJUJWTfU3IcdP0sX//b1n54emUlj6I272HjZtuYOWHn2DDxqtZtfosCtdfidu9K0wZi4iIiIiISH9UcOLYCieAZn/kCk4FBQWMHz++1zG73c7pp58e3oQ+BvuYdLLvuadze13lz39Ba1FRhLOSoSgn53rSUntvLp+YuBCrNRYAv7+BPXt/xZq151FX9363uMbGQtZvuIbW1vJBz1dERERERET6p4ITx3o4QWT7OFksFq6++mqWLl1KdHR0tzGXy0XKkRVDw0XcooWk33E7AKbPR/k3vom/vj7CWclQY7HYmTHjD0yf/ntSU8/C5ZpFxphLmTf3aebNfZKlS94hL/dGLJajW057Lwr7/Q2UHvhT+BIXERERERGRPhkjsbfO/PnzzcLCwuOO/23JYf57f0fD7n/OncSChLjBSu24+f1+mpubeeWVV9izZw8A119/fZ8roIYq0zQp/9rXcL/5FgBxy5aR+9CDGEN4a6AMTV5vJWvWXoTPV9tnjMORwfJlK8OYlYiIiIiIyOhhGMZ60zTnH0+sVjjRfYVTU4RPqjvKZrORlJTEsmXLOu+tWrUqghmdHMMwyPrlL7Hn5wHgWbmSmj/cT6C5mfaSEgJuT4QzlOHC4RhDVFToVX4+XyOBQFuYMhIREREREZG+qOBE9x5OQ6XgdNTYsWPJyMgAoLi4mOrq6ghndOKsTic5992HcWSbYM3997N70WL2nnsexUuXcuiu7+Gvq4twljIcJCTMCTkeDLby4arTKD3wZwKBFqDjlLudO79PYeEVbNr8RSoqXiAY9IUjXRERERERkVFLBSeOnVIHke3h1BvDMFiyZEnn9erVqyOYzcmLLigg/dvfPnbjyGmAZns7jS++SOl11xNwuyOUnQwXuTk3YBi2kDHt7TXs2fNLVn54GkVbbmXt2gs5eOhJGps2Ulv7Ltt33MnGTZ8nEGgNU9YiIiIiIiKjjwpOfPSUuqFVcAKYNm0aTqcTgM2bN+PxDM9taP7Kyj7H2vfupeGpp8KYjQxH8fGTmTH9/7Bau/dZs1gcTJzwXXKyr8MwogDw+eqorn4d6NmnrqFhDfv2/y4cKYuIiIiIiIxKKjgxdE6p64vNZmPhwoVARzPxE2mIPpQ0vfF66PHXQo+LAKSlfYrly1YypeDn5Od/lcmTf8SypR+Qn38zBQV3s2zpO+TmfgHDCN2Y/tChfxAM+sOUtYiIiIiIyOiighNDu4fTUfPmzcNutwOwdu1a/P7h90E52E+D8OAwXbkl4WezOcnOvoqJE+4kN+d6oqKSO8ccjjFMnvR9kpM/EXIOv78Rn69+sFMVEREREREZlVRwYmieUvdRsbGxzJ49GwCPx8OWLVsinNGJi546tZ/xU8KUiYwG0dHZ/cbs2n03Tc1bO68DAS8Vh19k564fsrv4Z9TVr8I0e27JExERERERkdBUcALirJbOb8RQ3FJ31KJFizq/Xr169bD7IJx8/XUhx61p6WHKREaDzIxL+42prn6ddesuYePG6zlU8SyrVp/N9u3f4uDBJygre4SNGz/Hps1fUINxERERERGRE6SCEx0nwR3dVjdUVzgBpKamUlBQAEBlZSX79++PcEYnJv4TnyD923eCYfQ6Xv/YYzS+/HKYs5KRKiFhLtnZn+t1zGZzER2d03ldV7+SHTu+i9db3iO2ru59dhf/dNDyFBERERERGYlCny8+ijhtVur9gSFdcAJYvHgxu3btAmDVqlWMHz8+whmdmJQbb8R51lk0vPgi/sOV2LOzwWql5r77ADj0vf/CEh+P88wzI5ypjAQFk+/G5ZxOWflfcbt3YrO5GDPmQsaNvYWoqFSqq9+k9MCDNDVtDjlPRcXzTJzwbez2pDBlLiIiIiIiMryp4HTEcFjhBDB27FgyMjI4fPgwxcXFVFdXk5aWFum0TkjU2LGkf/Ob3e4ZFgvVv/0tBAIc/Obt5D70EHGLF/Uxg8jxMQyDrKzPkJX1GUzTxPjI6rr09HNISzubbdvvpLLyxT7nMU0fbvcukpIWD3bKIiIiIiIiI4K21B3hGiYFJ8MwWLJkSef16tWrI5jNwEn58s0kf+ELAJjt7ZTfcgutW7b28yqR4/fRYlPX+/Hxk/t9fUnJAzQ2buq8DgRaOXjwKbZsuY2iLbdQVv44fr97wPIVEREREREZzlRwOuJowak1aNIeDEY4m9CmTZuG0+kEYPPmzXg8nghn9PEZhkH6d75NwhWfBiDY0kLZTTfh3bOHgNuDt7gYf01NhLOUkSo97ex+Y+rqP6Bw/adZV3gFZeWPs3rN+ezc9f+oqn6N6uo32L37blavOZeWluHVW01ERERERGQwqOB0xNGCE0CTf2gXnGw2GwsXLgTA7/dTWFgY4YwGhmEYZP74xzjP7vjwH2hoYP9nPsvupUvZd9HFFC//BAdu/BLePXsinKmMNLGx48jJ6fsUxaioYycoNjVtZPfuu2lrO9AjzuutYMvW24bdCZIiIiIiIiIDTQWnIxK6FZyG9rY6gHnz5mG32wFYu3Ytfr8/whkNDMNqJevX/0vs0o5tg2ZrK7S3d457Vq6k5JpraS8piVCGMlJNnvRDJoy/E7s9pfNeXNxkZs58iOXLVjJr5p9ISlra7zxu904aGkdGEVhERERERORkqeB0hNN27FvROAwKTrGxscyePRsAj8fD008/zbvvvktlZWWEM/v4LFFROM8+p8/xYFMTNQ/8MYwZyWhgGBbGjv0qy5d9wOJFb7B0yQoWLXyVtNSzMAwLqalnMnfO40wY/+1+53I3b+/1vt/vpr29RiugRERERERkxFPB6YiuK5yah0HBCaCgoKDz6+LiYlasWMEDDzzACy+8QCAwPJ6hL54VK0KON73xhj60y6CwWKKIi5tITExer43G451T+p1j77572Lv3N7S1HQKgqamIjZtu4N33ZvH+B4v4cNXplJU9imkO7e27IiIiIiIiJ8sW6QSGiq49nIbDCqdAIMBrr73W69jmzZtxOp188pOfDHNWAyfY0hJy3Gxrg2AQrNaQcSIDLTlpCXZ7Cj5fbZ8xgYCbktL7KSn9I4kJc2ls2oxp+jrH29rK2V38U1paD1Aw+YfhSFtERERERCSstMLpiOHWw2nXrl3U1vb9gXfdunW0d+l9NNxET58ecjxq8iQMFZskAiwWx5EiUc/VTwCJiYuxWuOPXAVpaCzsVmzqqrz8Mdye4sFJVEREREREJIJUcDrCOcwKTuXl5SHHvV4v1dXVYcpm4CVdfRVGVFSf44G6enwVFWHMSOSYMWMuZPash0lImNd5z+WcycwZDzBv7hMsX7aSgoKfEhs7vt+5qipfGcxURUREREREIkJb6o4YbiucbLb+f+mOJ2aoisrLI/u393Lwjm91bJ/7iEBNDaWfu468Rx8hKjc3AhnKaJeSciopKafi93uAIDabs3PMZosnJ/saXM6ZrCu8JOQ8dfWryfU1YrcndN5zu3dzuPJlfL564mInkJl5GXZ70mA9ioiIiIiIyIAbvhWJATbcejhNmTKF9957r8/x5ORk0tLSwpjRwHOeeSYTV7xN40sv0b5vP9bkJOLPOIPKn/6Mtq1b8R082FF0euQRHOPHRTpdGaVstrg+x2Jj87FYogkGexZNj2psXMcHK5cwJv0CsrKuoqr6DcrK/tItZt++e5k+/T5SU88YsLxFREREREQGk7bUHeEaZiucsrKymDZtWp/jp556KhbL8P/ltSUlkXLDDWT+5Mekf/ObxM6aRd4jDxMzdy4A/spKSq+7jrZduyOcqUhPNpuTzMxP9xsXDHqpOPw86zd8tkexCSAQbGHL1ltpbQ29lVZERERERGSoGP4ViQHitA6vghPAZZddxuLFi7Hb7T3GampqIpBReFidTvL+/CdiFy8GIFBby4Hrr6d161ZaCgupfeRR6p/+B/5h3MNKRo5JE+8iMXFRj/s2q5OZM/7I+PF3EB2d3e88waCXg4eeHIwURUREREREBpxhmmakcxhw8+fPNwsLC0/4dRPfK8IdCLI4IY4X504ahMwGR1tbG1VVVfj9fp5++mm8Xi9Wq5VbbrmFlJSUSKc3aIJtbZR/4xt43j2ytdBigWDwWIDNRurNN5H6ta9hGL2fKCYSDqYZoKZmBVXVrxMItJLgmkVm5hVERSV3GX+boi1fCTlPYsIC5s176iNzmzQ2rqet7RCO6EwSE+ZhGPpZgoiIiIiIDDzDMNabpjn/uGJVcDpm7ofbOOT1cUpcNCsWThmEzAbf6tWref311wGYPHky11xzTYQzGlxmezsHv/Utmv/zZp8xY/7reyRff30YsxI5caYZYMU70zHN9hBRBikpp5OV+RlSU8/A7d7Jtu130tKytzMiNnYcU0/5XxIS5gx+0iIiIiIiMqqcSMFJPwbv4mgfp+Gypa43CxYs6GwWvnv3boqLiyOc0eAyoqJwnnteyJjaP/8F0+8PU0YiJ8cwrKSnnd1PlElt7Qq2bL2F9z9YTOH6z3YrNgG0tOxn46bP09JSMmi5ioiIiIiI9EcFpy4SRkDByWq1cu6553Zev/766/hHeLGldf36kOP+qiraDxwIUzYiJ2/cuK9jszp7HYuOziE+/pTOa7+/EdP09RobCHg40EvzcRERERERkXBRwakL55GCU3MgSGAYbzWcMGECU6Z0bAmsra1l7dq1Ec5okHU5YbAvxgg4sU9Gvri4Ccyb9zTJScs671ks0WRnXc2iha+waOG/WLjgZXJyrqe/P75rat7tcc80TZqaijhU8SzVNW8RCHgH+hFEREREREQAsEU6gaEkoUvhwu0PkGAfvt+ec845h+LiYgKBAO+88w4zZszA6ex95cRwF/+JU6n/6+N9jlvi4rBlZoYxI5GTFx9fwJw5f8XrrcbnbyDakYXNFtc57nROo8A5jZqad2hr63vlntd7iO3bv82YjEtITlpCa2s527Z9k6bmos4Yuz2JyZN+SEbGxYP6TCIiIiIiMvpo2UcXri4Fp8ZhvK0OICkpiWXLOlZJtLe389Zbb0U4o8ETt2wpMfPn9Tke9Hgov+VWAm53GLMS+XgcjjTi4yZ1KzZ1lZy8tJ8ZTCoOP8+mTZ/ng5VLWbvuom7FJgCfr55t2++gtvb9AcpaRERERESkgwpOXXQtOA3nPk5HLV++HJfLBcCmTZsoLy+PcEaDw7BYyL3/fpznnguG0Xnf4nRiREcD4Fm5ktLPXYevsjJSaYoMqLzcG7FYovsYtRIVldZ51d5eQyDg6SPWpKTkDwOen4iIiIiIjG4qOHXRveAUjGAmAyMqKoqzzz526tVrr71GMDj8n6s3VpeLnN/ey8Q3/0PO//2evIf/wqSVHzDumX9gy+rYTufduZOSK6+ibdfuCGcr8vHFxY1n1qw/44ga0+1+VFQ6s2c/zPJlHzJ3zt/JyroSwwi9PbihcR2BQFu3e8Ggn+rq/7Bn76/Zv//3uN27BvwZRERERERk5DLMYdwcuy/z5883CwsLT/h1fztUy527ygB4dPo4zk1LGOjUws40TR599FFKS0sBuOSSS5gzZ06EswovX1UVZV/5Ct7tOwCwxMeT9atf4TtcQcuatWAxiP/EqbguOB+LwxHhbEVOTDDoo67ufVrbDhLtyCQl5VQslqhuMZs230Rt7dsh50lImEfGmItJTz8Xv9/D5qIv0dKyr1tMZsblTJnyCywW+4A/h4iIiIiIDH2GYaw3TXP+ccWq4HTMy1UN3LytBIDfTcnjyszkAc4sMg4fPsyDDz6IaZrExMSwbNky7HY748ePJy0trf8JRoCgx0P57bfjea/vXjVREyaQ9/DD2MekhzEzkcFXVvYYu4t/clyxBlYMSxTBYGuv4/n5X2XihDsHMj0RERERERkmTqTgFLYtdYZhnGsYxi7DMPYYhnFXL+P3Goax6cj/dhuG0dBlLNBl7OXBytFlO/btGAk9nI7KyMjoXNXU2trKm2++yWuvvcYf/vAHnnnmGXw+X4QzHHyWuDhy77+fhM9c0WdM+969HLrru2HMSiQ8MjMvx+Ho+6TGuLjJnV+bBPosNgGUlz9OINAyoPmJiIiIiMjIE7qxxwAxDMMK/AH4FFAOrDMM42XTNLcfjTFN8/Yu8V8Duu77ajVNc/Zg5znSmoZ31dLS+wfEbdu2YbPZuOyyy8KcUfgZNhuu88+n8Zln+4xpWbUa7969OCZMCGNmIoPLZnMyd87jbN36DZrd27rcT2Dy5B+SmXEpbW0VVFW9SumBP9HeXt3nXIGAm8bGzSQnL+m819p6gANlj1Jf/yEAycnLyc25gZiYnMF7KBERERERGdLCUnACFgJ7TNPcB2AYxlPAJcD2PuKvBn4Uptw6JYzQglNdXR07d+7sc7yoqIgzzjiDxMTEMGYVGe3Fxf3GeIuLVXCSESc2dhwLFrxEU9NG3J5i7PZEUpJPw2rtOOkuOjqTvLwb8fkb+z21btPmG0hJPpX09HOJikpny9Zbu52C5/EUU3HoWWbPeYwE16xBfS4RERERERmawrWlLhso63JdfuReD4Zh5APjgK4dbqMNwyg0DGO1YRiX9vG6m4/EFFZX9/3T+VC6rnBqHEEFp6MNw/timiYHDhwIUzaRZU3ovxG81eUKQyYi4WcYBgkJc8nOupL0tHM6i01dpaV+st95TNNPTe3bbN/xHTZtvqFbsekof6CZ7du/hWmOzJMxRUREREQktLD1cDoBVwHPmqbZteKTf6Qp1TXAbw3D6LH8xDTNh0zTnG+a5vyTbYTttB4rODUHRk7ByWLp/5fZMIwwZBJ58WecgRHT80N2V55VqzFH0K+/yIlwuWaSnn5+n+Pp6RcSH3/Kcc3V0rKfhoa1Pe4HAl7q6lZSU7MCr/fkfkAgIiIiIiJDW7i21B0Ecrtc5xy515urgFu73jBN8+CR/99nGMY7dPR32jvQSUZbLURbDNqCJo2+kVNwGDduHBaLhWCw95UGFouF8ePHhzmryLC6XKTfeSeVP/1ZnzG1f/oTrVu2kP3r/8WWmhrG7ESGhmlTf43DMYaDB5/qbCAeHZ3LxAnfZsyYC4COYtKePb+iuubfIecqP/h3HI5MYmPzO67Ln2Df/nvx+eoBMAwbGRmXUjD5bqzWmEF8KhERERERCadwFZzWAZMMwxhHR6HpKjpWK3VjGMYUIAlY1eVeEtBimqbXMIxUYBnwq8FK1Gmz0tbuH1E9nFwuFwsWLGDNmjW9jufk5BAXFxfmrCIn+dprsaWkUPPgQ3h37AAgZs4cYubNo/6JJzBbW2lZvZr9l11O9r33EDt/PqZp4q+qBr8PW0YGRpfVcCIjjcXiYPKk7zN+3Ddwe3ZjtUQTH38KhnFstWRs7Diysj7bb8GpquoVqqpeIT5+Cg5HFrW1b3cbN00/FRXP4vPVM2vmQ4PyPCIiIiIiEn5hKTiZpuk3DOM24A3ACjxsmuY2wzB+AhSapvnykdCrgKdM0zS7vPwU4EHDMIJ0bAH8766n2w20BJuV6nY/TSNsS9XZZ5+N1Wpl7dq1+P3+bmPl5eUcOnSIrKysCGUXfq5zz8V17rkEmprAYsEaHw9A4qWXUP71b9C+bx/+6mpKP38DCZddStu27Z3FKVtWJilfvJGka68ZNVsRZXSy2ZwkJszrczw5eTkORwZe7+F+53K7d+J29314QU3NWzQ1FeFyzTypXEVEREREZGgxutd2Rob58+ebhYWFJ/Xa89fvZkNTC8l2K9uXzxjgzCKvpaWFsrKO/u1VVVW89dZbAKSmpnLzzTcTFRUVyfSGhKDHQ8UPfkjTq6+GjEu5+WbS77g9TFmJDE11dSvZXHQTwaC3232LJZppU+/F72+kuvrf1Na9h2n6+5ilw9j8W5gw4Vud18FgO4cPv8jhyn/i9zUSFz+JnOzrSEiYPSjPIiIiIiIioRmGsf5Ij+3+Y1Vw6u6qTXt5p74ZqwHlp80a0StYgsEgTzzxBHv3drTDmj9/PhdeeGGEsxoaTNOk/m9PUPnzn/cdZLEw8a03sWdmhi8xkSHI7d5F6YE/UV/3IRgGyUnLyMv7EvHxkztj6uo+ZOOm60LOYxh20tLOJi3tUyQlLmTr1m/Q0LiuR9zkST8kN/fzA/4cIiIiIiIS2okUnMLVw2nYcNk7evMETGgJBokbwb16LBYLl156Kffffz+tra0UFhYyadIkCgoKIp1axBmGQczMfla4BYM0//vfJH9eH3xldIuPL2Da1F+HjElImIPN5sTvb+4zxjR9nT2fOnZQ937Qwe7in5KcvIy4uIkfI2sRERERERlMlv5DRpcE27EC00hqHN4Xp9PJJZdc0nn90ksv4Xa7I5jR0BE4ju9DwOMJQyYiw5/VGkNuzg19jtvtKURHdz3MtPdiUweTg4ee7nG3sWkzO3f9gM2bb2LX7rtpbh60dn8iIiIiItIPFZw+wtWl4NQ4CgpOAFOmTGHu3LlAR4+nl156iZG41fJEOSZNgn5WuFlidIy7yPEaN+5r5ORcB3TfquxyzWHRwn+xdMkKFi18lfz8r/Q7V2XlKxw69CxebyWmabJnz68oLLycgwf/Tk3t25SXP87adRdRUnL/ID2NiIiIiIiEoi11H+HqUmBo8o2OghPAueeeS0lJCXV1dRQXF7Nu3ToWLlwY6bQiyp6ejuu882j617/6jKn6zT1gQvINn8ewqH4rEophWCmYfDd5uV+ipvZtgoE2EhLmkpAwr7NfXnx8AeNjv0lZ2aMEg219ztXeXsmOnd8FwOHIwus91Gvc3n2/weWaTXLy0oF/IBERERER6ZM+IXfh8QfY6m7pvL7vQBXFnr4/8IwkUVFRfPrTn8ZypGjy73//m6qqqghnFXkZP/ohMfN6HgtvOBwdX/j9VP3qV5R99av46+vDnJ3I8BQTk0NuzvXk599MYuL8HoczWCx2MsZcfNzz9VVsOqr84OM97gUCbRw+/BJ7991DWdmjeL3Vx/1+IiIiIiLSP51Sd0RNu5/LN+5hd0v3ApPdMHhwWj7npyUOZIpD1nvvvcfbb78NQEJCAtnZ2QSDQXJycpgzZw5xcXERzjD8zEAA9/vv417xDqbPR+y8uTjPO4/GZ56h8te/AZ8PAFt6Ohk/+THeHTtoevU1As3NRE+ZQtJ1nyN+2bIIP4XI8OL1VrN+/WdpbTvQYyw7+3Pk5d5Abe171Na9T23tipBz2Wwupk27l6TExVit0dTXr2XL1lvx+eo6YwzDxsSJd5GX+4UBfxYRERERkZHiRE6pU8HpiK9uK+GFqoZex2IsBuuXTiPZPvJ3IAaDQR5++GHKy8t7jEVHR3PttdeSm5vbyytHp9YtWzn4rW/hO9DzQ3FXad+6g9SbbgpTViIjQ3t7LaUHHqLy8D/x+RuJj5tMTs51ZGRc1m1V1IerPklr6/5+57NYHLhcs2ls3IhptvcaM3PGA6SlnT1gzyAiIiIiMpKcSMFJW+qAOp+ff1b3XmwCaA2aPF85OrZLWSwW4uPjex1ra2vjqaeewndkRY9AzIzpjHv+OVznnx8yrvo39+DduzdMWYmMDFFRKUya+D2WL/+QM07fxoIFL5CZeXmPLXgZGZf0MUN3waCXhoY1fRabAEoP/KmX1/moqn6DPXt/TUnJ/Xg8+07sQURERERERiEVnIDytnb8/Sz02tfiDU8yEebxeNi9e3fI8e3bddR4V9b4eDJ+/jOwhV4B1/jCC2HKSGR0ycu9gbi4Sb2OxcdPZ/asx8jP/yrx8VP7nauxcQMNDesIBv0AeDx7Wb3mbLZsuYXS0gfYu+83rF7zKXbu+gGmOXoOlhAREREROVEjf4/YcUiL6v/bkH4cMSNBTU0NwWAwZIyaifcUbGgAvz9kjO9QRZiyERldbDYn8+Y+yb59v6Pi8PMEAh5sNhdZmZ9h3LivYbM5SUlZzsQJd7JhwzXUN6wJOd/6DVdhtcaTmLiAxsYN+P2NPWIOHvw7DkcG48beOliPJSIiIiIyrI2OKko/Mh1RnJoUz3v17l7HLcDlY5LCm1SExMTEDEjMaGNNSsKIjsZs6/tUQ19lJcH2dixRUWHMTGR0sNuTKCi4m0mTvn+k4BSPYVh7xKWnX9BvwQkgEHD324y8rOxR8vNuwmI59nvaNIPU1r1Hff1qDMNKSvJpJCYu6LENUERERERkpNOWuiN+NimHJFvPDycA/zU+k7wYR5gzioy0tDTGjBkTMmbatGlhymb4sERH47rwgpAxrevXs//Sy/CsWdtjbCQ27xeJBIvFht2e0GuxCSAz8zJiYyf0OmYYDiZN/AE5Odf3GdOVz1fHoYpn8Ps9QMfJeusKL2Pz5hs5cOBPlJb+kQ0br2bjpuvx+5tP/qFERERERIYhnVLXRWmrl9+XVvF8ZR0twY7vy1dy0rh7UvZApziklZSU8PjjjxMI9N6f5Oqrr6agoCDMWQ19/vp6Sj93He29NQe328B3bMtdwiWXkHLLV2l89lkaXnyRQHUN9txcEj/7GVI+/3kMrYISGTRebzU7d32fmpq3gI4/6+Pjp1JQcDeJCfM647bv+C8qKp7udz7DsJPgmk1b2yHavAd7jUlPv4AZ0+8bkPxFRERERCLlRE6pU8GpFxuaPJy/vhiAr+am8aOJo6vgBFBeXs6KFSvYe6R4EhcXh8fT8VN8u93OjTfeSEZGRiRTHJICzc3U//1Jml59lWBzM45TTiH5c9cSlZfH4Z/9HPeKLlt0LBbopV9W3KmfIPf++zH6aUIuIh9PW9shWlpLibInExc3uce2t/r6NWzYeM0AvZvB0iXvEhNz7O+TQMBLZeU/qal9CzPoJyFhHllZnyUqKnmA3lNEREREZGCp4PQxC05tgSAT3y/Cb8KyxHiemzNxALMbXtrb2wkEAkRHR/P666+zZk1H7xOXy8VNN92E0+mMcIbDS/Nbb3H4Zz/HXxG6gXjW//w3CZcc37NmIzYAACAASURBVFHvIjI4TNNkc9FNffZymjDh2wQDbdTVfUBj0yaOrpbqS1bWVeTl3khs7Dh8vlo2brwet2dXtxi7PZHZsx7B5Zo5UI8hIiIiIjJgVHD6mAUngLPW7WSbuw2XzcKu5TPU8BUIBoM8+eSTFBd3rP7Kzs7mhhtuwG63Rziz4SXo8VB82ukE3b03qQeIW7qUvIf/EsasRKQ3gUAbxXt+QUXFswSDXgBiYvKZNPEu0tLO7ow7UPYYxcU/Oa45o6LSMQwLXu/hXscdjgyWLlnRrRn5sXxaCQQ82O3JGIbaMIqIiIhIeJ1IwUn/Wu3DTGcsAE3+ICWt7RHOZmiwWCxcccUVpKenA3Dw4EFefPFFgr1sC5O+WeLiMPv5nvlqa8OUjYiEYrVGM6XgJyxftpp5c59m4YJ/smTxm92KTQBj0s/HMI5vG2x7e1WfxSYAr/cw1TVvdbvn9hRTVPQV3nl3Ju9/sIiVH36CkpI/Egz6+5hFRERERCSyVHDqw9GCE8Dm5pYIZjK0OBwOrrnmGuLi4gDYtm0b7777boSzGn4cY8eGHA9UVdG2a3d4khGRftntLhIT5+N0Tu11ZZHDkUZe3k19vn7c2K9zypT/ISPjUuz2/ns07dt3L6UH/kxj4yaamraxvvAzVNf8B+goVnu9h9m773/ZseM7OuVSRERERIYkbanrw4ZGD+dv6Ng6dktuOj+cmDUQqY0YZWVlPProo50n2V1wwQUYhkFraytjxoxh4sSJWCyqZ/al/h//4PAPfxQ6yGol+XPXknrbbViP9Mpq27Wb9tISbKlpxMyehaHvsciQYZomBw48ROmBP+Pz1QHgcGQybtzXyM66sjOusbGIwvWXncDMFo4Wmnozd+5TJCUu6HYvGPTT0LAGr7eSmNh8ElxztTVcRERERD429XAagIJT65HG4QETlifG8+wobhzely1btvDcc8/1OpaUlMSVV16pk+z6YAaDHLrrLppe/mePMVt6Ov6qqs5ra2oqKV/4Ak3//jdtmzd33o/Kzyfjpz8hbuHCsOQsIscnGPTi9hRjYCUubhIWS/etdqZpsnrN2bS07BuQ98vMvIKpp/xP53Vt3Qfs2HEXXu+xwwni4iYzbeo9OJ2nDMh7ioiIiMjopILTABScAM5cu5PtnjYSbFZ2Lp+unw734umnn2bHjh29jsXFxXHbbbcRExMT5qyGB9M0cb/zDo0vvIi/poao3BwSr7ySmNmzaXjuOap/cw+BhoaQcxgOB2OfforoKVPClLWIDIS6upVs2nwjpunrMTZxwnfJyLiMxsb11NS+Q0XFM/3OFx9XQGLSQqIdWezZew/Qc167PYlFC1/F4UgfiEcQERERkVFIBacBKjh9c8cBnjrcsS1i9eJTGBvj+NhzjjT3338/VV1W43zUOeecw5IlS8KY0cjhr6+n+ne/o+Gpp0PGuc4/n+x7fhOmrERkoDQ2bmT//vuorXsfMHHGTyM//2bGjLmwMyYY9PHByiX4fPUD8p5jx97KhPF3dF6bpkl19b85eOhJWltKiXKkkpFxGVmZV/R6Sp6IiIiIjG4nUnA6viN1RqmZzhieOnKQ0ObmFhWcPqKtrS1ksQmgtLRUBaeTZEtKIvPuu2ndtBnvzp19xrnVtF1kWEpImMPs2Y8QDHoxzQBWa2yPGIvFTnb2NZSU/KHXOQzDRkLCfJqbtxAIePp9z4qK50hJPhWncwYWSxS7d/+Y8oOPd463th2gsXED1VWvM2vWn7BY9PeeiIiIiJwcFZxCmNXlpLqi5lYuSU+KYDZDz/FsMbRarWHIZGSzOEJ/4Av6fJimqS2fIsNUf0WdcWNvw+3eRU3Nmx95XTQzpv8fqalnEAz6cbu3s37DVQSD3j7n8noPs37DlRiGnZiY3D77SNXVr6Ss7FHy87/c7b5pmjQ0rKHZvQObNY7U1LOIiko5zicVERERkdFEBacQpsbHYDUgYEJRc0uk0xlyHA4H+fn5lJaW9hkzbty4MGY0MsUuXEhrl2bhPbS3U/alm0j/zneILpjceds0Tcy2Ngy7HcOm3+oiw5XFEsXMGQ9QV7eSyqp/4fc344w/haysz+JwjDkSY8PlmklKyhlUV7/e75ym6eu3aXn5wae6FZxaWw9QtOUW3O5jffsMw874cd8gP/8rKnqLiIiISDfq4dSPM9buZIenjUSblR1qHN5DSUkJjz32GH39d5SVlcV1112nxuEfg6+ign0XXUzQ7Q4daLGQeMUVpN52K81vvkn9Xx+nvaQEw27HefbZpN52Kw4VAEVGtKamIgrXfwbT9PcYs1rjmTTxe7S0ltDYuIHGxg1A6H8DOJ3Tcblm43ROZ//+33U7+a6rU6b8kqyszw7EI4iIiIjIEKam4QNYcPrGjgM8faRx+JrFp5CvPk497Nq1i9dee42GLieq2e12fL6OU5IyMzO57rrriI3t2Z9Ejk/Lxo0cvP0O/IcPd96zxMeT9LnP4X73XbxdTwq02cDf88OmxeVi7JN/xzFhQjhSFpEIqal5m+077sLnq+28FxOTx/Rpv8Plmtl5b/Pmm6mpfWtA3jMmJo8li9/CMCyd9+rqV3Gg9CEaGgsxDBupKWeQn/8V4uMnh5hJRERERIYyFZwGsOD0l/Jq/l/xQQAemjaWi9MTB2TekSYYDFJeXk5rayvp6elYLBYee+wx6uo6inUZGRlcf/31Kjp9DKbPh/v992kvKcWWlorzzDOxxMVhBgI0vvQy1ffei7+6OuQc8aefTu4fHwhTxiISKcFgO7W17+FtryI2Jp+kpCXdikEA1TVvUVR0c59zREfn4PPVEQgc35byCRO+Q2rK6cTFTeLw4RfZvuM7fHQFlcUSw5w5j5GYMO+En0lEREREIk8FpwEsOBU2erhwQzEAt+Wl8/0JWQMy72jQ1NTEY489Rm1tx0/Zx4wZw/XXX09cXFyEMxuZgi0tlH7hC7RtLuo7yDCYvHoV1oSE8CUmIkOSaZrs2HkXFRXP9hhLSJjPnNmPYhh2PJ5idu3+EY2N649rXqs1/kiRKtjreFzcZBYtfLXbFnW/v5lDFc9SW/MOpuknMWkx2VlX4XCkndSziYiIiMjgOJGCk6X/kNFtanxM5zdJjcNPjMvl4oYbbiA1NRWAyspKHn30Uerq6igqKuKDDz5g69atnVvv5OOxxMbiGDs2dJBp4qusDEs+IjK0GYbBKVN+ybSp95CYsICoqDTi46cyadL3mTP7r1itMVgsNpzOU8jLu/G45w0E3PRVbALweHZT37C287q1tZw1ay+iuPhn1NV/QH3Davbv/y2r15xDU1OIArqIiIiIDGla4XQcTl+7k51qHH7S3G43jz32GNVHtnsZhtGtyXhsbCyXX345EydOjFSKI0bNQ3+i+p57QsZYXC6Srr6a5M9diy0tjZYNG6j9y8O0FBZi2GzEn34aKTd+Ccd4NRgXkQ7BoJ/C9Z+muXlrr+OTJ/0IqzWWxqaN1NS8SXt7Tb9zxsTk43ROp6mpiLa2sl5joqOzWbL4bSyWYydtmmaQ2tp3qap6DX/Ag8s5ncysz+CISj25hxMRERGR46YtdQNccPr6jlL+cbgeUOPwk+V2u/nLX/5CfX19r+M2m40vf/nLpKVp+8TH4a+uZs+nzsZsa+s31oiKImbOHFrWroWP/DlgiYsj75GHiZk5s49Xi8ho095ex46dd1FTc6zReFRUKpMm/hcZGZd03qusepWtW782YO87c8ZDpKWdBUAg0MaWLV+ltu69bjFWaywzZ/yR5ORlA/a+IiIiItKTCk4DXHD6c3k13z/SOPxP08ZykRqHn5Qnn3ySXbt29Tk+b948LrroojBmNDI1v/UWB2+/A7O9vdv96DlziJk5g8bnnifodvc7j6OggHEvvqAVfSLSTWvrAZqbd2CzxZOYuACLJarbeDDoZeWHp/a5ysnhyCY+fjLNzVtpbw990AF0NBpPSJiNM34qHk9xj2LTUVZrPMuWvovd3v3vaK+3msqqf9HeXkNs7DjGpJ+P1aoDLEREREROxokUnGz9h8gs57F/mBY1t6jgdJIOHjwYcry0tDRMmYxszrPOYvyrr9Lw9NO07diBJT4e13nn4TzrTAybjbTbbqPhH89Q8+CDBJua+pzHu2sX3h07iJ46NYzZi8hQFxOTR0xMXp/jFouD6dPuY3PRl3qccOdwZDJv7t86X19d/R+Ktnwl5PsFg63U16+ivn5VyLhAwE3F4RfIy/1C570DZY+wZ89/Y5r+znvFxb9gxvTfazWUiIiIyCBTwek4TDvSODwIFDW3RjqdYctiCd2jvr9xOX5ROdmkf+uOXsesTicpN36RoNdLzX33hZyn/eBBFZxE5IQlJS1i0cLXKD/4OA0N6zAMG6kpZ5KdfVW3FUipqWcRE5NHa+uBXucxsBIbO46W1v2YZqDf992373c0Nm7E5ZxGIOBlf8nvesT4/Y1sLvoyixe9TkxMTrcx0zRpatpIa2s5Dkc6iYkLMQz93SQiIiJyMlRwOg6xVguT4qLZ5WmjqLkF0zS1zegkTJ48mVBbHZOSksKYjTjG5vcbc/iHP6K9pISkK6/E6nIRaGig/h/P4H7vXfAHiF24kKRrrsaekRGGjEVkOImJyWHSxO+FjDEMC1MKfsamzTdimj1PLJ0y5edkZX2GQMBLQ+M6Nm36fMj5AoFmqqpeoarqlZBxwWArBw8+wcSJ3+2819y8g23b78Dj2X3sGaLzOOWU/yYpaVHI+URERESkJ/VwOk5f21HKM0cah69dfAp5ahx+wurq6njwwQfxer19xnzqU59i6dKlKuiFQdDrZc8ZZxKoq+s31hIXh/O8c3G/+x6B6u49Vyzx8eQ+9BCxc+cMVqoiMsI1NRWxv+QP1Na+i2kGSExcwNj8L5OSclq3uA0brqW+YXWf89jtSfh8vR9O8VE2m4vs7GtxOqfhcKSzadNNBAKNPeIslmgWzH+B+PjJ3e6bZpD6+lW43buw2VykpX2yR/8oERERkZFGTcMHoeDUtXH4n6eN5UL1cTophw4d4qWXXqKysrLzntPppLm5ufN6wYIFnHfeedpiFwaeDz+k7JZbe5xqZ01OJm7JEprffBMzRIHwKFtGBhP/828Mu32wUhWRUaDj3yRmn9vYmpq3sn79VQSDPbe3Z2V+llNO+SVebxXNzdso2vLVXldNnYyMjMuYNvXXndcezz62bL0Fj6e4857F4mDC+DvJy/vigLyniIiIyFCkgtMgFJzWNri5eOMeAL6el85/Tcga0PlHE9M0qaiooLm5mYSEBDIyMtiwYQP//Oc/OfrfY0FBAZ/+9KeJiorqZzb5uNpLS6n72xO0FBZi2GzEn34aSVdfjS05GX9tLfVPPEHdXx/v92S7nD/8H86zzgpT1iIyWjU372DfvnuoqV0BmDgcmeTm3kBe7he7Faq2brudysqXB+Q9LZYo8vO/SnzcZKKjc9hc9GXa2w/3Gjtt6r1kZFzca96NjRuwWKJISTkNhyN9QHITERERCScVnAah4OQJBJj03haCwOlJTp6aPWFA5xcoLi7mmWeeob29HYDs7GwuvPBCdu3aRU1NDbGxscyaNYvs7OwIZzr6NPzrX1Tc+e2QMam33Ubabbf2uO+rqCDY0oI9JweLQ1tRRWRgBAItBAIt2O3Jva6IcnuKKSy8vMdJeQCxsROYO+cJWtsO0Ny8jX37fovf33M73cmIi5vMooWvdm4N9/nq2brtdurq3u+MMQwbubk3MHHCd9WUXERERIYVFZwGoeAEcOqanexuaSPZbmXbsunqMzQIKioqeOKJJ3CHWE2zYMECzj//fH3/w6hl40ZKr74mdJDNRsLFF5N09dXEzJhOS2EhVf/7a1o3bwbA4nKRdNVVpN12K4ZWrolIGDQ1FbFr149oai46cscgNfUsphT8DIcjrTNud/HPKSt7eMDeNyvzShISZhMXN4XiPT+nsbH3f5OMG/dNxo/7Wq9519a9D2aQpOSlJLjm6u88ERERGRJUcBqkgtNt20t5trKjGem6JVPJjdaH5sHQ0NDAY489Rn19341fL7roIubNmxfGrEY3Mxhk3/kX0F5SclzxUePH015aCoGex5g7zzmH7N/eqw9PIhI2Hs8+2ttriInNI9rR81TN1tYy1qy9gEDA02PMMOzMmvUwFsOK27ObsrJHaG0tHZC8bDYny5d9iNUaC4Df72Hrtm9QW7uiW1xS4mJmzLgfuz1hQN5XRERE5GSdSMFJ67hPwCxnbOfXRc09l+jLwEhMTGT8+PEhY9asWROmbATAsFjI+MmPMfrYEuc89xxsmZmd1+379vVabAJofuMNWjduHJQ8RUR6Exc3nqSkhb0WmwBiYnKZPfsRoqO7b9m221OYOeMBUpKXkpS0iNyc6xg37uv9vNvxF9P9/ma2bP0aBw78hdra99m2/Vs9ik0A9Q2r2bb99h73A4EWysoeY/2Ga1hXeDm7dt2Nx7P3uN9fREREZDDZIp3AcDLTGdP5dVFzKxek6aS6wVJTUxNyvKqqimAwqJPswihu4ULGPv0UtQ89hPuddzEDAWIXLiTlxhuJW7wI0+/H/d571D36GC1r14acq/mNN4idOzdMmYuI9C8xYR5LFr9NXf0HtLWW43Ckk5JyGhZL90L7mPTzKC19EI9ndy+zGMyc8RDRMVm43Ts5ePDvNDauD/m+tbXvUFv7Tr/51da+i9u9i/j4AgDa22vZsPHabiflNTVt5uChp5gx/T7S0s7ucy7TNLXKVERERAadCk4nYHp8DAZgohVOgy06OjrkuM1m0z+WIyB6yhSy77mn1zHDZsN55plEjRvHvvPODzmP+/33cV1wAdEzZmAYBsG2NhqefpqGl14iUFNL1LhxJF11Jc5zz9Wvs4iEjcViIzXl9H5iHMyZ8zg7tn+b2rr3Ou87osYwafIPSEs7EwBn/BRiY8dRWHj5gOW3afONJCbMIzZ2PHX1H3QrNh1lmj62bruD5cvex25P6rzf3l5DSckDHK58CZ+vntjYCeRkX0tOzucwDOuA5SgiIiJylHo4naBPrNlBcYtXjcMHWVFREc8//3yf4xaLhYsvvphZs2bp12CICba3s+fU0wg0NPQb65g0EddFF9P8n//QtmVLj/Gka65mzA9+oF9jERmSWlr243bvxmZzkpi4AIvF3iNm0+Yb+1zBNHHC90hPPxePZzeHK/9FZeVLA5bb+HF3MHbsLRiGgbe9hvXrP0Nr64EecWPGXMy0qff0+udsa2sZbW2HiI7OJCYmb8ByExERkeFLTcMHseDUtXF44ZKp5Khx+KAIBAL89a9/pbQ0dGPWGTNmcOGFF+Loo7eQREb1ffdRc/8DvQ8aBpzAnzu5f/4z8cuXDVBmIiLh5fd72Lnr+1RW/gsIAmC1xjE2/xby87/cWejx+5t5/4MlBIOtfc5ls7nw+5uO+71ttkTi4ibg8zXS0rKnz7jZsx4mJeW0zuuWlv3s3PUD6utXdd5LTFzElIKfERcXuseiiIiIjGwqOA1iwemhsip+uOcQAA9PH8v56uM0aNrb21mxYgUbNmzA6/ViGAYFBQW4XC7WdukRlJyczBVXXEFGRgb79+/n0KFDREVFMWXKFBISdKJPJJg+H4fu+h5Nr7zS7b41OZmcP/wfgYYGGp9/nuYV74DfH3Iu1/nn9bmNT0RkuGhrO0Rj40YsFgdJSYuw2Zw9Yvbv/z/27b+319fn5X2JiRPuwuero75hHVu33jpguSUkzGNKwU+JiRmL39/E2nUX095e1SMuKiqVBQte6tF8PRj0U1v3Lh53MXZ7Iunp53TbziciIiIjhwpOg1hwWt3g5tKNHT8l/Gb+GO4an9nPK+Tj8vv9uN1uYmJiOlcy7d+/n+eeew632w10bLGLjY3tvAYwDIOlS5fyyU9+UluyIsA0TdqKimh6/Q2CHg/R06aRcOEFWOLiOmN8NTXsWf6JkPPYMjLIe+RhHOPGdczb3k7dE3+n4Zln8JWVYRszhoTLLyPlhhuwxMaGnEtEZCgzTZMDZX+mpOSP+P0d25JtNie5uV9k3NjbMIxjB2Vs3Hg9dfUr+5jJIDXlDNq8FbS07CMY9B5nBhZstviQq6jy8r7EpInf67xubt5G0ZZbaGsrPzaLEcWEid8hL/cLx/m+IiIiMlyo4DSIBSePP8DE97dgAmckO3ly1oRBeR/pn8fj4cUXX6S4uGfT1K7OPvtsli5dGqas5ETtOfMsfIcO9RsXPX06rvPPo/m992hdvabn+KyZ5D/6KJaYmF5eLSIyfAQCbTQ3b8U0g7hc07FaexbTPZ49FK6/srMw1dWECd9hbP6XgY7VRys/XE57e/WA5Ga1xjN+/DeJi51AVFQaGzZeh99f32vsjOl/ID393G73Ghs3cKDsEZoaN2GxxpCWdja5uTfgiEodkPxERERkcKngNIgFJzjWODzFbmPrsmlaPRNBpmnywgsvUFRU1GdMfHw8t99+O1arTuEZiqrvv5+a+34/IHOl3XEHqTffNCBziYgMda2tBygp/SNVVW8QDLbgcs4iL+9G0tI+1S2upOR+9u77TZ/zjM2/lUCwBY9nD/V1H2ISGJD8nM7pLFxwrBF6RcVzbN/xXTrO+z3G4RjDvLlP9dqYvLX1IPUNqzAwSEpaQnR01oDkJiIiIifnRApOtsFOZiSa6YyluMVLrc/PIa+PbDUOjxjDMPptGO52u6mrqyMtLS1MWcmJSPniF2n5cBUtvRSJE6+8kpjZs2l65RU8q1ZBIPSHoMYXX1TBSURGjZiYPE6Z8gtOmfKLkHF5eV+ioXEDtbUreoxNnvQDcnNv6Lzetu1bHK58cUDya27eyrrCK4iLHUdUVBqlB/7MR4tNAF5vJbt2/5jZs/7SeS8Y9LJz1w+pqHieo83WwUJW5hUUFPwYi0X/9hIRERnqVHA6CTOdMTx35KS6ouYWFZwizGKx9Bvj76cxtUSOJTqa3If/QsM/nqHx5ZcJ1NQQNXYsiVddifNTn8IwDBIvuxR/TQ37Lr2MQE1Nn3O1l5XhWbuW2PnzMSwWTNOk6V//ou7xv+HdvRur04nrggtI+dKN2FK1fUNERgeLJYpZMx+kqvrfHD78Ij5fPXGxE8jOuRaXc3q32NzcGzhc+U/odZWT8f/Zu88AqaqzgeP/e6fPTtnZ3ndZYJfeEQQVwdjQEBsqKvYaS9TkNWpi1MTEEmPX2FERK3aiKHYB6bDUZSnbe5tt0+fe98PCwDAzC+pSPb9v99xnzj2XZXdmnnvOcxgy+ElkjR5X11aqql/H4+l5SXR7+2ra21fvdYzNzd9S3zAfu204BkPqjmTT3D2iFGpq30GSNAwYcF/YGbe7kvKK52ls/BJF8WC3jyQn+woSEsQup4IgCIJwsIgldT/Dj85OztxROPyW3FT+LAqHH1Tbtm1j9uzZPcZYLBZOOOEEhg8fHkpQqaqK2+1GkiRMou7PYaHyut/T+U3kE/o9adPTsZ9+Ov6GBto/+ijivC4zk9w33kCXmrI/hikIgnBYq6+fx6ZNdxBUXKE2WTYxcMC/SEubFmqrqXmHTcV3ROsCAI3GjKoqKIrnJ11fkvSoqq+HCA0TJ34f2i2vs7OElatmRK1nNaDwPjIzZ0S0B4MeWlp+wO9vw2IpwGodKkokCIIgCMI+EDWc9nPCqTMQpP+OwuFTEqy8IQqHH1SKovDaa69RVla219j09HROOeUU2tvb+eGHH2ho6N72OSsri0mTJtG/f//9PFrhl+j87jsqr7m2V/qyn3EGGQ/c3yt9CYIgHGn8/nbqG+bh8dRgNKSTmno6Op09LCYY9LBs+e9wubZGvF6StIwaOQe7fRReXwNlZc9SXd3zw6GfwmTMwWobismUTUPDZ7jd5VHjJEnHMRMXot+tKHlt7QeUbLkvLEFls41gyODHMJmye22MgiAIgnAkEgmn/ZxwAjhm6Sa2urwk6bSsE4XDDzqv18u8efNYv349O/9PWywWJk2aRH19PStXrmRf/q+fc845DBkyZK9xwsGhqioNDzxIy6uvRpyznnYa8WeeSfu8eXR88QWKyxWlh10kvZ6C5cuQ91IDTBAEQYjN621gU/HtNDd/F2ozGXMoLLyHxMRJoTafr5mFi46JOXPJYhlISvIpuD2VtLWtwuXa3mtjTE+fTk72ZZhMObS2LqFo7ZVR40zGHMaN+1/YroDBoJvKylnU1M7F663DaMwiI+NcsrMuFnWkBEEQhF8lkXA6AAmn328s5/0ddZxWHT2IDFHH6ZDQ0dFBbW0ter2e7Ozs0M509fX1fP7552zf3vMHWLGj3aFPVVW6Fi/G+e5c/JWVaFNTsZ95BtYTTkDasVxScbspv/gSPOvW9dhXxr8fwnbqqUja7nJ2XcuW0fLyLNyrVyPp9VimTCHxyivQZ4sn3oIgCD1xucpxubah08Vjs41AkiLrK9bWvr9jlzolrN1gSNuxS13331qPp4ZFiydFxO1Oo4kjGOz6yeOUJB2q6o95fvcleMGgh9VrLqGtLfIzZULCsQwf9gKyrAtrDwQ6qa+fh8u1HZ0ugbS0aWJnPUEQBOGIIhJOByDh9GxFA/ds6y6U+cqQPpySbN/LK4SDTVVV5s+fz9KlS3uMmzlzJn37imWSh7uGhx+m+cWX9hqnSUzEdtpU5DgLzf/9b8R52WYjd/ZrGAsL98cwBUEQflXa2tZQUfky7e1FyLKRlOSTyM6+JGzJG8Cm4jupqXk7ah9ZmTMpKLibQKCNtrbVMWcs/RwajQVH/FEYTZm43dU0N38dM3bggPvJyDg3dNzcspD1628kEGjfLUqmb/6t5OVdF7WPYNCL212ORhOHyZTZW7chCIIgCPvNT0k4iV3qfqZh1l3TrYs6XCLhdBiQJAmHw7HXOI/npxU3FQ5N9rPPpvnlWaDEfkIOEGxupvW12HVFlPZ26u6+h7y33ow4jNfAUQAAIABJREFUpyoKvvJyUFX0OTmhmVKCIAhCdHb7CIban9hrXGHBPUjIVNe8w84d8yRJS0bG+fTv/xckSUKniycpaTKJiZNpbo6+oYQsG8jPvxWvtx5X1zaaW76LGrdTMNhJUw9Jpt2VlT2DTufAaMpGQsPatdeiKO49ohS2bX8YkzmX1JSpu1oVP6Wlj1NVPSeUoLJah9Cv3+0kOI7ep+sLgiAIwqFOzHD6mTp2FA4HOCHBxpzh+fv1ekLvKC0t5dUo9X92V1hYyOmnn47Vaj1AoxL2l9a33qLu3r/DHn/njMOHk3LzH2j/4gs6Pv2MYFvbXvvK//RTDPl9QsfODz6k6emn8VdVAaBNSyPx6qtwzJgharoJgiD0Eo+3DmfrMgAcjvEYDJG7i3o8NaxcNQOPpyqsXZJ0DB3yJMnJJ4balq84i/b2opjX02qtBAIdvTT6XazWIRw1tnvXVFVV2bDhZuob5kXESZKWEcNnkZAwIay9rW0N5RXP09q6BEnSkJh4HLk512CxFPT6WAVBEAShJ2JJ3QFIOAVVlVGLN1DvC6CV4OwUB5dlJTPCZt77i4WDRlEUnn322dDudLHo9XqOPfZYjj76aGRZZvv27WzevJlgMEhOTg6DBw9Gp9P12IdwaHCvW0/rG2/g3bwZ2WbDNvVU7Gecgazvrrum+nxU33YbHfM/77EfXW4u1uOPxzzuKLzbt9P48H+ixiXffDNJ117T6/chCIIgxOb3t1Fd/SaNTV+gKF7s9lFkZV2MJS5899mmpq8pWntV1D6MxmzGHfU/JEmL11tD0dprcLm29doYtdp4TMZMNNo4nM5lMeOs1qEcNfbD0HFD4+esX38TqhoIi5NlEyNGzMIRPzaiD7e7kqbmb1HVAPH20dhsw3rtPgRBEIRfN5Fw2s8JJ6+icNm6Ur5uiXwC9vd+GVydHfn0TTh0NDU1MXv2bNr2mNVis9lQVZWOjl0/1/j4ePR6fUSCKj4+nosuuoikpPCaE8Lhyfnee9T+5a+90pdkMND/u2/RxMf3Sn+CIAhC76qtfY+SLfeF1VqyWocydMgTmEw5obaa2rls2vTnmP3k5V6P3pCMx11JXf1H+HxNvTbG5OSTiTPno9MnsW3bIyhK9ALpJlMeR49fECrSrigBSrbcS3X1m8Cuz/gOx9EMHfIkOl1kaQFVDdLWtppAoAOLpVAUORcEQRB6JBJO+znh9FhZHQ+U1sU8/+WYAoZYxUynQ5nX62XdunWUlpYiSRL9+/dn0KBBqKrK4sWLWbhwIYFAoMc+EhMTuf7665HlyJ14hMNLsLOLrZMno3REX0Yh2+2oXi/qPtb3Sn/gfuLPOCN0HGhupuW12XR88QWKx4Np6FASLrkY8+jRvTJ+QRAE4acJBt00N39PINBGXFwBNtvwiOXQqqqwcdP/UVf3YcTrs7JmUtD/7tBrqqvfonjzX2Jez2BIQ69PwuOpwe9v6dV7yc//I0mJkzGZsikre5ryiuejxsXHH8WokW+E3WdD4+dsKbkPj7dmR4tEcvLJDBzwT3S66A9OVFVBUTzIskksIRcEQfgVEgmn/ZhwUlWV0T9upMYbe0vdSzISebBQbKN+OHM6nXz22Wds3ry5x7gLLriAggJRP+FI0PHNN1Tf9AdUf/jvtjYjndzXXkOXkoJ7/XpaZr9Ox2ef9djXzqV7lkmT0OfkUHHlVQRqa8ODJIm0v9+LY/r03r4VQRAEoZeoqkpT0wJqaufi9dZhNGaSkXEeiQmTwpItwaCHFSvPprOzOKIPjcbMmDHvh5b3VVXNYXPJ3w7YPexu1Mi3cDi6l+A1N3/PmqIrgMjNNWzWYYwe/S6yvGszDL+/le2lT1BX9wGBQAd6fTIZGeeRl3sdGo3xQN2CIAiCcJCJhNN+TDi5gwp9vl/bY8yxDgvvjui3X64vHDjbtm1j9uzYu5cBTJ48mUmTJh2gEQn7m3frVlpmv4579WokvR7rCVOIP/98tLvtbugtLWX7qVN76GUPshx7pzytln5ff4UuRSzDFQRBONz5fC2UlNxLQ+P8UL0lu20kBYX3YLMOCcUFgy4W/zgFn68xaj+5OdeSk3MZHm8dDQ2fU17+TK+NUZI0GAwZGAwpdHVtJRCIvWnG0CHPkJJyMtBdI2vFyulRa1rFx49j5IhXkGV9WLvTuYLq6jdxucsx6JNISz+T5KQTQ8v/BEEQhMPTT0k4iT28fyKDLGHVyHQEY2+1nqwXxaSPBHq9fq8xW7ZsYdiwYTgc4TUR2tvb8Xg8oRpQwuHB0K8f6ffe03NMnz5Yjj+ezm+/jXpek5AAikLQ6exuiJVsAggEaP/kExKvuCLUpHR10fLGG7R//AkBZyuG/L44ZszAevJJYumCIAjCIUyvT2DIkMfx+ZpxuyvQ6RIwm3Mj4jQaM8OHv0hR0ZURSaeUlNPIz78ZWdah1ydhiSukru59vN7opRxMplxysq/A7amgvW0tzrbYxcihu16Tx1OJx1O51/spLr6TuroPMBjT6eosiVlA3elcSl3dx2RknBNq2176BKWlj4fFNTYtICVlKkMGP4YkacLO+f1Oaus+oLNjIxptHCkppxFvHyPe9wRBEA5zYobTz3BHSRWzqmMXhnxzWD6TE2377frCgaEoCo8//nhEcfE9ybLMsGHDOPbYY3G5XHzxxRdUVnZ/kNPr9YwePZopU6aIXe2OIIHWVqquvQ53UfjW2oYBA8h+/jm0iYm4i9bifG8ube+932Nf2pQUHDPOJ27CBHS5uVRefgWejRsj4hIuuYTUO27v1fsQBEEQDp5g0EV9/f9o71iLRmMmJWUqNuuwiCSL07mCNUWXEwyGFw7X6RIZNfJ1LJaCHf15WbhoAoGAM+Y1rdZhBIMdeDx1KIq71+7FYEgnJ/syDMZ0/L7WHpcMFhTcQ3bWzNBxa+tS1q69hkAwvI5iSspUBg96BFkO//ykKAGam7/B6VyOJOtISpyM3T5aJKcEQRAOELGkbj8nnBp9fqat2kKp2xdxbkqClTnD8sWb3hFi48aNvPPOO1HP6XQ6/LvV+9n5M4/2O9W/f39mzJghCowfQVRFoeuHH+hcuAgUhbijx2M5/ngk7a6Jo4GmJrYcN6nnWU67kfR6VF/k35Wdcme/hnls+PbXqqriLS7GX1uLLi0Nw8CB4u+PIAjCEcbtrqKq6jVaW5eAJJOYeBxZmTMxGJLD4ioqXmbL1n9G7SMlZSpDhzwJdD9U+3HJlB5nOmk0ZoJBV+/dxA4GQxqDBz2CwZCCLBtZuvTUiGTTTn3ybiQ//+bQscdTw5qiK+jqKgmLS0ycxNAhT6HRRG7a09W1neaW71DVAI748dhsQ3v3hgRBEH5lRMJpPyecAJp8AZ4sr+e9+laa/YHQxrOnJNl4ZWj+fr22cGBt3ryZr7/+mvr6egCsVisTJkxgzJgxrF69moULF9Le3r6XXmDmzJn07dt3fw9XOMRUXn8DnV99Ff2kJHXXeAoG96kv2xlnkPnA/aFjz+bN1N75FzwbNoTaDIMGknHffRgHDfpF4xYEQRAOP6qqUlH5ImVlTxMIdCdxJElLevrZFPS/G43GEIqtrnmb4uI7o/ZjMKQxftwXSJKM11tHUdFVuNylB+QedqfRWBkz+h2MxnRkOY4VK8+go2ND1Nj0tLMZNOih0LGieNlUfGfELoMJjokMGfIkOp09oo9g0EVLy0ICQRdW6+BQoXdBEARhF5FwOgAJp90FFYXJy0socXVvmf7dUQMojBO7dRxJVFWlra2NYDBIfHw8Gs2u2gOBQIAVK1Ywf/78HvsYNWoU06ZN299DFQ4x/poayi66iEBN5C516ff9A+tJJ+FaupTOHxbifPvtnjvTaIg7ZiLmkaPQ5+VSe/c9KFGWfMo2G33efx99VmYv3okgCIJwuAgGXTidy1EUPzb7CAz6pIgYVVUpK3uK0rKnQkXOAczmPgwb+ixxcbs2wKmpncumTX+Oeb1BA/+DXp+Ax1tLefnzuN1lvXo/ALJsQFG8PURoOOqoeVji+iFJMsWb/0Z19ZyokQkJxzJyxCthbVVVc9i2/d+hRN3OuMGDHkYf5d8PumdP+XxNmMw5GA1pP/WWBEEQDksi4XSAE04A79a1cOOmCgCmpzl4cmBkkUjhyNXW1sajjz7aY0xaWhqXXnopRuOuZGQwGKSsrIyOjg4SEhLIzs4Wy6GOQIGWFlpff52OBQtQXG6Mw4aScPHFmEeODMWoqsqWY44l2NzcK9d0XHQRaX/9S1hb56JFtL7xJt6tW9DEx2M/7XTiz52ObBQJckEQhF8rr7eBxsYvCAQ6sFgHkphwbERRb1VVKd78V2pq3op4fb++t5Gbe03ouL5+Hus3/CHm9ZIST8AePxqvt57mpm9weyp672bontGl0yXsKMge+3vOmDHvY7cNB6C27kM2bvxj1DirZTBjxryPLO9aMt/evpbNm++mvWPnztUSSUknMKDwHxgM0XeedburcHsqMRrSMJv7/Kx7EwRBOBSIhNNBSDj5FZWjl26kyuNHK8GP4weRbRS7k/1aKIrCI488QmdnZ49xer2eESNGcNRRR9HW1sZHH30UthwvJSWFs846i7Q08ZTs16jhkUdpfv75mOd1mZn4q6v3qS/Z4SDnuWcxDhiApNfT+PTTND35VESccfgwcl56GY0l7mePWxAEQTjyqapKq3MJtbXv4fM2YjLnkJFxHjbrkD3igqxd93uamr6M6MMSV8jo0W+j1VoBaHUuZ9Wq82Ne02TKISlxCl5fAx0d63G7ezc5pdcnodcn43KV9VhEfejQZ0hJPhmAzq4trFhxVtT6VmZzPmPHfIhWu+s91e2upLj4r7S0Lgy12e2jGFB4HxZLYdTrBQIduN0VaLU2TKbsn3t7giAI+4VIOB2EhBPArOom7iipAuDyzCT+VZB1wMcgHDwLFy7kyy8jP1z9VGazmeuuuw6r1doLoxIOJ0pXFxWXXxGx+x1A4jXXkHLLzQSdTlyrV1Pz59tR9qF2mKTXo+/TB+/mzTFjEq+8gpQ//Smi3V9Tg3v9emSTCfOYMcgm00+7IUEQBOFXSVH8VFW9RnXNm7hc5ej1SaSnn0VuzjXodLt2clZVlfUbbqKh4dOIPmTZyOhRb2KzDQPA7a5m8Y+TiDVrSZZNpCSfhM/XhMtdhsezbw9o9oUsGzGb8zEYkunqKsXTw6ysgoK7yc66GACfr4Vly6fh9dZGxGm18Rw19sOwhFIw6GLL1georX0PReku1WGzjaB//zuJt4+Oej2vr4mO9rVIsp54+xg0GjFrWRCE/UsknA5SwskdVBj740aa/AGMssTyoweRrNft/YXCEUFRFD755BNWr14d1m4ymZg2bRq1tbWsWLECl2vvO74cd9xxTJkyZX8NVTiEKR4Pznfn0vbJJwRbWzHk5+O4YAaWSZPC4mr+8hfa3nu/V64p2+0U/LgYaccuisHOLur+9jfa588P7bAn220k33gTCRdd2CvXFARBEAToTk6VlT1NVfUc/P4WAByOCfTr+3+hZNNOJSX/oLLqlaj9DBzwABkZ0wEIBLpYuOhogsGumNeNjz+KQKATr7cWv7+1d26G7uSU1ToEvT4Rr6d2t2V3kTIzL2RA4d+B7plhq1dfTKtzSZQ+DYwe9VbYv0cw6KGk5F5q694P1eDSau3k97mJrKxLopZoCAQ6aWtfA6qCzTYiLPknCIKwr0TC6SAlnACeLK/nn9u7n2L8ITeVO/LTD8o4hIOnpqaG9evX4/V6SUtLY+jQoaG6TYFAgPXr1/PRRx/R0+9eRkYGV1999YEasnAY8pSUUHbOdFSfL/KkVkvyzX8g2NiEu6ioe8bUXv7Wm0aMwDxmNMYRI2h95VVcMf6Gpv3j7zimT49oV9xuPJs2AWAcNEjUhRIEQRB+EkXx4fXWo9HEodcnRI1RVYWy8v9SWTkrlCQyGrPJz7+Z9LQzwmLLyv7Ltu0PR+0nPX06gwY+EOpz8eIpeLyVMcdmNGajKB58viZ6qgv1U0losNtHoTckEwx6aG7+OmbsnoXO1677PY2Nn0eNLSi4h+ysmaFjVVUoLXuaiooXQkk4WTaRnTWT/Pw/htWn2ikQ6KClZRHBoBubbRhxcWKnZUEQuomE00FMOLUHgoxevIGOoIJNK7Py6MFYtZq9v1D4Vbn//vvxemPvtGI0Gjn//PPJzc0NPaHy+Xxs3LiRlpYWLBYLgwcPJi5O1N35Nev87jtq/nw7Qacz1Cbb7WTc/y+su82Qq7nzTtre/6BXrqlNS6PflwuQtN0fTlVFofn5F2ie9TJKW3toDElXXUnCFVeIIviCIAhCrwsGvbhcW5EkLXFx/SKKnEP3cr3yiucpL/9vaOc5WTaQmTGDfv1uR5Z3rUKoqXmXTcW3R71WXFwBR439GFnWoSgBVqycTkcPs5a0WhuqGuxxdtXPZbUORq9PBiSam7+JGafTJXLMxIXIcnc92W3bH6WsLLKOI0BW1iUUFvwtdKyqKhWVL1Fa+kTYPSQmHs/gQQ+j0zki+vB666mv/x9+fwvmuH6kJJ8ilvYJwhFMJJwOYsIJ4F/baniiogGAv+Snc2Nu6kEbi3Boevvtt9m0YzZIT1JTUxk/fjxxcXF88MEHuN27ClpqNBpOPfVUxozZp9914QileDx0fPUVgdpatGnpWH9zQsTsItfq1ZTPuCBmH5qkJIJtbeD379M1Ey69FMtxx2IYMICW116j+dnnosYl3XADyTdcH33cPh8EAshm8z5dUxAEQRB+jmDQhbNtFaoawG4bgU4XHzWuouIltm1/NKx4uN0+hqFDnsBg2PVZvrHxS9auuyZaF8iyifHj5mMyZREMutm46XYaGubFHJssmwA1VK+pNxn0qRiNGWh18TQ3fw8EY42CYyYuDN1jVfUbbN58V9RIu20ko0e/gyTJobay8ufYtu0/Yf3rdIkMG/oM8fGRn1Hd7kpqa9/D46nGYEwnPe1szGaxu7cgHE5EwukgJ5wafX7G/rgRj6KSrNeybPwgTBp57y8UfjWqq6t56aWXUHbUx9mdJEk9Lrfb00UXXUS/fv16c3jCEaj+wYdomTUrot1QUEDu7NeQDAY8GzbQ+PjjuJYt75VrSkYj/b/7Fo3dHmpzr99A01NP0fn996AoGPr3J+GKy7H/7ndiNpQgCIJwUAUCHTQ1fUMw2IXVOgSbbWjUuIrKWWzd+kCodhJ0FwEfOuQJEhImhto6u7awbNlvUdVoD3QkRo18g/j4sQSDXZSWPkVF5QsxxyZJWjQaC4GAM2bMz6XVWjEaM9FpHbS1r+4xATZ82AskJXXPoq6vn8f6DX+IGqfRWDh6/AIMhpRQW2XVa5SU/APY/fOvTP9+d5CTc3lEH23tRVRXzaGraws6XTypqdNITT09bHbanhTFjyRpwpJigiD0LpFwOsgJJ4A7SqqYVd0EwAMFWVyamXRQxyMcejZv3szHH39MV9eu6coOh4Ozzz4bn8/HkiVLKCkp2Ws/+fn5XHzxxWFt1dXVLF26lLq6OgwGA4MHD2bUqFHo9fpevw/h8KCqKh1fLKB1zhy827ahiY/HfvppOGbORGOxhOI6v/+eyqujP7n9ORwXXYhjxgz0eXm4i4qouOxy1CjLSZNuvIHk6yNnQyldXXT+8ANBZxuGggJMI0eIxJQgCIJw0Hm9jdQ3zMPna8ZsziM1ZSoaTeSs3camr9iw4VaCwc5QmywbKCz8Oxnp54TafL4WflxyAoFA9B1oC/rfRXb2pShKgMamr1i//vc9jE7GZMrG52sOu25v0Wqt6LQOvL7GsBlhe8rMnEl+n5vQ6ew4nStZtXpGzNiRI14LS9ZVVr5CyZZ/RMQ5HBMYPuxFNBpDWHtd3cdUVL5IR8cGJElPcvJv6JN3IxZLQdTrqaoS2snQaMwUCSpB+AlEwukQSDhVuL0cvXQTQRVyjHoWjxuIVhZfkoRwgUCArVu30tnZicPhoE+fPsjyrje85uZmXn755bCk1J4kSeLss88mNzcXq9XKypUr+eSTTyLiUlNTueSSSzCLJUxCD1RFoeyc6Xg2box6PuHqqzCPHIm3uJiOr77Cs37DPvUrmU2gguqO8cFUlun35QJ0GRmhJud771F//wMonbs+LBsGDSTr0UfR54rp94IgCMLhIRDopKHhU9zuSgyGNFJTp0atheR0rmDtumsjds3LyrqEgv53hR64qKrKipXTaW9fHdEHQF7udfTt+ycAurpKWbL0RHoqdm425xMMdOHzN6GqsZbe/RIykqSJMdOrm802nIL+f0Wnc+D1NrFq9fkxY/vk3Uh+/s2h49LSJ9le+lhEnEZjZtTINyJmqtXUzqWs7Gnc7goATKY8+vS5MaLw/E7BoAdn2wqUoAebbVjYrC1B+DUSCadDIOEEcMPGcubWd79hPD0wh7PTou+4IQg9mTVrFuXl5fsUa7fbaWtri3l+1KhRTJs2rbeGJhyhAo2NVP3hZtyrVu1q1GpJvPQSkm+9FWlHUtS7fTvbp57Wa9e1nnwSCRddhC43F/fatVTfcGPUOF1GBvmffIy8R9F8X3k5bR99hL+uHl1mBvFnnIEuM7PXxicIgiAI+1sg0EV9wzw6O4vRam2kpp6OJa5/RJzP18S6dTfgbNt9GbxEZuaFFBb8LayQ+oaNf6KuLvrmIUlJv2H4sO5ajMGgn8WLj8Xnb4w5Ppt1OJIk4/c7cblLf95N/kKybKJv3z+i08ajqAGKi+8gVkLNbh/FmNHvho4rKmexZct9UWP33N0PupcBbt/+GIHAzs/XGtLTfkdh4b0RM9oUJUBD42fU18/D73disRSQmXkhVsuAn32vgnAoEgmnQyThVNzl5vhlmwEYEGfk67GFyGIpiPATLVq0iAULFvRKX1qtlttuuy1iaZ3f76epqQlZlklOTg6bZSX8OqmqimftWtxFRUhGI5bjj0eXEvlEr/K639P5TfSdcszjx2ObeiqeDRtxLV+Ob/v2fR+ALEOUGmc7pd39Nxwzdk3Nb37pZRoefhh2f0/TaEi76y4c55+379cVBEEQhMOEqqq0t6/G2bYKWTaQlDgZkykrIq67gPltNDR8GtaemHg8QwY/hlZrDbVVVs2mpOSeqNezWYcxZsx7oeVnRWuvoanpy5jjS3Acg96QhN/fSmvr0v1SHH1fJCediNGUjUZjprz8eVTVFzVOo7FwzMTFaLXdD7Sqq9+kePNfo8YmJU5h+PBdNbeCQS9r115NS+vCPSJlBgy4j8yM8M8iqqrS1PQlNbVz8XprMRqzyMw4j4SE42KWDlDVIH6/E63WgiwbosYIwoEgEk6HSMIJ4NJ125nf1L0W+7WhfTgpyb6XVwhCOI/Hw3PPPUdra2vEOZ1OxxlnnEFbWxsVFRVs2bKFYLDnqdBTp05lxIgR6PV6FEVh4cKF/Pjjj6Ed8OLj45k8eTLDhw/fL/cjHFmC7e1U3fQHXEuWhLXHTZxI5uOPhepDBZqb2XLscT0mkX4KTWIi9tNPQ5/fF6Wzk4Z//zt6oCSR+8YczCNHhjX76+pwvv8+vtIyNI547NN+h2nI4F4ZmyAIgiAcijo7N9PSshAVhQTHBKzWyPc9VVUpL/8vpWVPoSi7ai46HBMYMvhR9PpddWk7OjawYuW5URNJlrhCxox5H42me+fc0tKn2F76aMyxJSYcj90+Ar/fSVPT17g9Fb/kVn82kykPszkXrdZOY+OCHmtUjRjxKo74cciyjm3b/kNZ+TMxImWOHv8FZnMfoLt+1MZNt0WddZaVNZOC/neHJZ2CQS9lZU9RXfMWfn8LkqQjNeU0+vb9I0ZjRkQfAH5/G51dJWg1ZiyWgaJGldCrRMLpEEo4rWrrYuqqLQCMsJp4pDCbNKOeBJ32II9MOJy0tbXxySefsHXr1lBbeno6p59+Opm7LRn64osvWLx48V7702q19O/fH7/fH9bn7qZNm8aoUaN++eCFI56qqrhXr6Hrx8UgSVgmTMA4fHjEE7rqP/6J9v/9L2ofst1O6p13EqipxrNlKx2ffho17uewTZ1K5iP/CR23f/opNX++HdUfXkvCcdFFpP7lzqhPFoMdHXi3bkU2x2Eo6C8KlwuCIAhHNL+/jebm7wgqbmy24TGXhTmdK9hcci+dnTtrP8qkJJ9MYeG96PWJu/XXzoqVZ+FyRS7DM5lyGDvm/VBdq5bWH1m9+qKYY0tOPonMjBmh5FR9Q2Tt0gNJknQ7diyM/b06IWES2Vkz0esTcTqXsWXr/TFjhw19luTkE4HuZXpFRVdEmTkFen0qY8e8h9GYHmoLBt1s2fovamvmouyYyWUy5dC/3x0kJ58U0Ucw6KGh4X+0ti4BSSYxcRLJSSf2uBOgIIiE0yGUcAL47coSlre7Qsca4JRkO3/vl0mmUewaJuw7p9NJS0sLFouFlCjLmxoaGnjmmVhPV36auLg4brnlFrTa8OSooig0NTWhKApJSUkR5wUhlqDTScUVV+LZEF5oXLZayX7uWcy7JTjLzjsfd1FR7M4kKXz5XE80GuKOPhpDYQGaeAeNjz4ac6ZV2j/+jmP69NCx4vXS8NC/cb73Hqqn+wmuPi+PlD/fhnXy5H27viAIgiAcwVRVpatrC35/K2Zzn5hFtb2+JrZuuZ/6hv+hqn4kSUtKylT697sj7DWqqlJcfCc1te9E9GEy5TB69LsYdsy0CgQ6WbT42Ji7+6WmTKOg4C78/lba2laxqfj2Hu9Fq7ESCHbSU/JofzMZc8jKugit1kZXVwkVlS/HjM3IOJ+BA/4JdP+7Fa29kubmb6NESgwf9jxJSVNCLW53BavXXBIqnr6T1TKYESNeDpvNBuDx1FJZOYum5q9RlADx9tHk5FwedaZc93iCtLWvIeBvx2IpjDkbSzj8iITTIZRwavUHmLS0mAZ/IOJcllHH/NGFJOnFF3ah93z77bd8++23Ee0Y3clAAAAgAElEQVQ2m43TTz+dyspKNm7cSHNz8177mjlzJn379g0dr1mzhm+//Ran0wmA2WxmwoQJTJw4Ucz4EPaJ4vPRMX8+HV9+heL1YB45kvhzzkGbFP6hpvP776m85tqoSSVdbg55b79NoL4e37Zt1N5zL0p79A+aP5U+L4/8Tz5G0ulQVZWq318fvUaVLJP932ewTJoUcUpVVQINDUiyjCYpSfxuCIIgCMJuAoEOfL4m9PqksPpRu1NVhZrad6mqmk1X11Z0unjSUqeRm3tN2MwpgJaWRRStvSZi+ZvFMohRI2ej08WH2laumoHTuSzqNR2OCYwaORtVDeL1NrBk6ckEg7F3inY4JqDRmAkGOml1LuXgJKlkkpKmoNM5CAbdNDTMixkZZy5g3LhPkSQJVVVZtnzabjPTwiUmTmLE8F2Jrs6uLaxadQF+f0tYnCRpGTL4CVJSTg5rb2z8gpKSf+Dx1oTGmZx8EgMH/DPs5xHqv3MztbXv4fU1YjLlkJE+PWo9MuHQIBJOh1DC6eHSOh4uq4t5/pbcVP6cnx7zvCD8HJs2bWLp0qXU1tZiMBgYMmQIEyZMwLKjno6qqsyfP5+lS5f22I9GoyEvL4++ffvi8Xj4/vvvo8ZNnDiRE088sdfvQ/h1a/tkHvX/+hfB3eqXmUaOJPPhf4ftPtfwn0dofuGFaF0AoHE4CDqd+z4jSpbRpqWisdnxFhfHDDMUFtLnww/CEkrODz6k+bnn8JWVdccMHEjyDddjPeGEfbu2IAiCIAg/mcdTQ3X1m7S1r0EjG0lOPpnU1N+i0YQX1/Z661mz5jI6uzaHtVssgxgxYlZo5hTA9tInKC19POr19iyg3tNOgAB98m5Cp3fg8zVTVTV7t13vDixZNqDVWpEkLV5v7O+oAIUFf8diKUSrtbFx0210dKyLGtddbH0RWm3394zm5u9ZU3QFEDmb3GYdxujR7yLL3RMuVFVl+/ZHotS/0jBwwH1kZJwb1qooAaqrX6e65i1crjL0+iTS088iN+eqmMlLl6t8x66PFuLjxyLLYoXRLyUSTodQwun4ZcUUd8XekaG/2cAP4wYewBEJQrfS0lJeffXVXulLkiRuueUWbDZbqM3lcrFkyRI2btyI1+slIyODcePGkZ+f3yvXFH4dFK+XrsWLUdrbMfTvj3HQoIiYQEsLZedMx19TE3FOn59P3ttvIWm1eLduo/auu3pMIv1UqXf9Fcsxx6DLzqZl1isxi5dnPPQg9mnTwsfd2EjL63Po/OYb1EAA06iRJFx8McaCgl4bnyAIgiAI4RTFT2PTl7S2LAIgIeFYkpJOCCVBdlJVhS1b/kll1Wvsnjyx20czdMjTGAzJoTaPp5YVK6fj9dZGXC8j/VwGDPhX6AFVecWLbO2hhlNe3g044scRCLRTVTWbVueSmLEgcTCX/+1kt4/Bah2MVmuhtvb9qP8OOw0Z/CSpqVMBqG/4jPXrb4gRKTF2zAfYbEOB7p/HuvU30Nj4eUSkxTKI0aPeCEs6+XwtbCq+naamr0JtOl0i/fvfSXraGRF9eDw1VFW/gdO5DEnSkpQ0hYz0c9HpbBGxO/n9bahqAJ0u4Vc1o10knA6hhNNRP26kwhN9602ADIOOVRPEzkjCgaeqKs8//zy1tdHfEJKTk5Flmfr6+n3qb+zYsUyePBmz2Ux7ezsvv/xyaOnd7k455RTGjx//i8YuCHvy19VR/+CDdCz4EgIBJJ0O29SppNz2f2gTd029b/vkE2r+77aY/ej79EGXmYm/uhpfefk+76onmc2obnfMWVSaxET6f/M1kr77qZq3tJTymRcTbGoK70enI/OJxyPqQ6mBAB1ffkX7Z5+hdLRjGDAQx3nnos/N3afxCYIgCILw83g8NTQ1fY2ieLHbR2KzjYyaXPB66ykte5r6+o8JBDqIi+tPVuZMMjNnhO0SFwx6WL3mYtraVkb0kZBwLMOHvRAq2t3ZWcLSZacRbbYQdM9CysiYjt/vpKb2PbZvfzjmfUiSDrt9JMFgF15vPT5fU8zY/U2WTWi1VgKB9qi7HO5kt48hN+dKtForbe1FbNv2UMzYvLwb6Jt/C9A9E2rFyrPp6FgfNXbo0GdISd61DNDpXMGaoisIBjvD4ozGLEaNnBOxvK+5+Xu2lz5Be/tqAMzmfuTlXRc1kaUoAZqavqS+4VOCQRdW62AyM84PK/a+J1VVUdVgRBL0UCESTodQwunK9aXMa4w9ZfLUJDuzhvY5gCMShF3a2tqYM2cODQ0NYe39+vXj3HPPRa/X09nZycKFC1mypKenK7vEx8ejKArtMWrqSJLETTfdhMPhCGtXFIXq6mq6urpITEwkOTk56usFoSfB9nYCzc1ok5PR7FhCujvV76f8sstwr4j8kKdJTCTv7bfRZ3Uv13N+8AG1d9zZa2OzTJ6MaeRIdOlpNL34Ir7NJVHjZJuN/t9+g2w2A6B4PFRedx2uH/f4HdRqyXz439hOOSWij2BHBx1ffEGgsRFddjbW3/wG2WCIiBMEQRAEofepqhKWZNpTMOimsnIWNbXv4vXWYzRmkpFxHtlZF0cs+aqpncumTXcCwbD27plT/wxdJxj0smTpiXg81VGvmZ9/K33yrgfA729l4aKJKIo3aqxGY6Fv/q0Egy5c7jJqa+fu660fNLJkICn5BLRaKz5fM01NX8aMNZv7MnrUu2i1cYDCosWT8PkaosY6HEczauTroeOGhvmsW38D0WaW9et3O7k5V4WOg0EXRUVXRcxSk2UjQ4c+TVLi8WHtPl8LZeXPUFv7AYGAE5Mxh8zMGWRnXxZ150BVDdLSshC3uwqDIZXExOMOyJJBkXA6hBJOy5ydTFsdfdt5gNeH9uE3SfYDOCJBCBcMBtmyZQvl5eVoNBoKCgrIzs4Oe3LT1tbGY489Rm/9vTjuuOOYMmXXLhmlpaXMmzcvrJB5bm4uZ5xxRkRiShB+KcXlovHxJ3C+9x5KZydoNFinTCHl//6EPidnV5zXy7ZTTyVQE30WoP3sszEOGoi3uJjOHxYSqOu5FsJPYT/rLGwnn4Q2JQXn3Lm0znkjapyk09H3ywXoUlNDbW0ff0zt3fd0z7jaQZOQQOYj/yEuyuxCNRCgc+FC/BUVaJOSsEyejGwy9dq9CIIgCILwy7hc5dTUvI3LtR2dPoG0tDOJt4+JmGnV1bWNorVX4XaXh7VnZl5IYcHdSJIm1FZe8QJbtz4Q9XqDBv0nbLbOihXn0LZjNs+eZDmOcUd9AqgEAp0Urb0any/2CgmLZQBarZ1AoIPOzs3smUg70CRJh6r6e4zJyb4CkykXjSaOki33EQi0Ro2TJT3HHLMYna77+8vmkr9TVRW9hIlGY2bihO9DsX5/KytWTsflKo2ITUr6DcOGPhP282trW8X6Dbfg8VSF2vT6JAYOeICkpMidlFtaFlNV9RqdnZvR6mykpp5OZsYFO5JukdzuCtra1iDLehyOCWFLC0XC6RBKOAHMrmnijpIqAlH+qc9JdfDkwJxf1ZpP4fA0d+5c1q+PPi01MTGRYcOGUVtbS2VlJV1dsXf0ANBqtfTv35/s7GzMZjMff/wxSpSlS3a7nWuvvRbTHl9+29vbWbNmDU6nE6vVyvDhw0lISPj5Nyf8Kik+H8HGRmS7PepsKADv1q1UXnsd/qqqsPb46dNJu+duJE33G79r+XLKZ16838ccjf3ss0i67vfoUlNwrV5NxSWXRl3aJxmN5H/8UVhSzV1URPUtt4bVv5JtNtLvvQfbqaceiOELgiAIgtCLFCVAc/PXtLevRaOJIzn5ZOLiotdQramdS1nZf3G7ywCwWAaS3+cPJCeHbwbUvUvdDPz+8ERLtF3qqmveprg4+gxxgyGN8eO+CCU5Srb8k8rKl6PGAqSnn4vdNoxAoIPa2vfocsWeyHGo0Gji0GntSLJhR+IvdnmG5OSTSUk+Ba3WSl3dh9T3sMvgkCFPkZrS/dnM7a5i6bKpUXdRlCQdY8bMxWYdEmorL3+OrVGWI1osAxk1cg463a4JMIFAJ5uK76Ch4dNQmyybyMu9lry865EkSSScDrWEE0C1x8fculaqvD7sWpk3a1tp9gcAeGxANuenJ+6lB0E4uLxeL2+99RalpeFZ99TUVC688MJQwXCPx8ODDz7Ya7OhTjrpJCZMmBA6XrNmTdQE1YknnsjEiRN75ZqCsDvV76fjq6/xrF+HZDZjO+kkDP36hceoKqVnnY1306aofZiPmUj63/6Gv7YOd9EaGh95tPcHKklIOh2qL3bdwPjzziXtnnuQJAl/XR3bfzsNpaMjMlCWyZ39GubRo8Oa2xcsoPW12XiKi5Hj4rCdcgqJV1yOViyBFQRBEITDkqqqeL11SJIGvT455kQIj6eWyspZNDV/jaL4ibePITvnsrDExs7+SsueoqzsKVQ1EGo3m/swbOizxMXt+gzl9TayfMWZUYuMW62DGT3qbTSa7gfPra1LWLX6wpj30b/fXWRkTCcQ7KCu9gO29VDPSqOxkJQ0pXvJoKsUl2tbzNhDhVYbj90+Aq3GQmdXCV1d0UszACQ4jqWw8B40Wgsedw0rVp4ZMzYrcyaFhfcA3T+7oqLLaW6JvjP5ziWDIuF0CCac9rSwtYPpa7ahAiZZZv6YAgrjjAd7WILQI1VVKS8vZ8uWLQSDQfLy8igoKECWw9eo9zQbCrpnLrW17dt2sGazmTFjxpCYmIiqqnz44YcxYy+44AIK9tjhS1EUtmzZQkNDA0ajkYEDB2KJMZtFEH4JX2UlFZdfgb+yMqzdMHAgOS++EFa8vPTc8/CsXRu1H8lgIO3ee1E6OvBVV9P66qsxi5H/LDod2sRE1GCQYGNjzDDL8ceT/ex/Q8eNTz1N01NPRcRp09PJm/M6uoyMsPauJUtofuFFXCtXImm1WCZNIvGaq8UufIIgCILwK+D1NtDY+AWBQAcW60ASE44NWxK2k8dTw9atD9LQOB9VDaCRzaSln0Hf/P+L2CFu2/ZHKCt7OqKP5OSTGTL4iVCR7WDQy9Jlp+B2V0Qd28AB95ORcS4Afn87CxdNQFHcUWONxmwGDXyIYLCTrq7tbN0We4dBAJOpD7KsJRh0xayndSiRJA2pqdPQai0E/B3U1cf+rqXTOZg4YRFarVEknA71hBPAQ6W1PFLWvb51QJyRT0cXYNbELi4nCIeLjo4OZs2aRUtLS8S50047jbFjx9LR0UFFRQXvvvtur103Pz+fiy/etaypvr6et99+O2wcsiwzefJkjj322F67riDspHg8tH82H9eSJaDRYDnuWKwnnICkCy/06Nm8mfKZF6PsWVxfoyHjoQexn3ZaqKnqxhu7d9+LIf7c6YCEv66WrkWLIdh7tRBMY0ajz8pGjjPHrCMFYJt6KpmPPBI6dr7/AbV/+UtEokwyGsl5+SXMo0aFtauqiqeoiI7vvoNAEPNRRxE3cQKSHPs9MdDSguJyoUtNjfj3FQRBEATh8BIIdOL3O9Hrk9BoYk/EaG1dQnXNW7hcZRj0yaSln0VK8skRRdrd7grWrruezs6NoTZZ0tOnzx/Izb0mbCZXTc07bCq+I+JasmxkxIhXcMSPDbWtKbqC5uZvo47NbO7H+HGfhcayevUltLQujHkvBf3/hk4Xjz/QTmnpE/j9kd+ddpIkbdiMsYNl9Ki3cTjGioTT4ZBwCqoq56zZyo/O7rWXF6Yn8J8BOXt5lSAcHtxuN8uWLWPjxo14vV4yMjIYN24cuXts4/7qq69GLNP7uSRJYvDgwaSlpZGYmMi8efNi1pM644wzGDFiRER7IBCgpaUFrVaLw+EQ9dWE/cZXVU3LrFl0fPM1qt+PefQYEi+7FNPw4eFxZWWUzbiAYGtkgcr4c88l/e/3ho5r7riTtg8+iHlNw4ABaBzxBJua8G4v7b3klCSRcOUV6NLSkM1mau++B2Is7TP070efjz8O/W4pLhdVt9xC13fh07eNgweT/ex/I5bruYuKaPjPI7iWLQO6C6I7LryApGuuQdJG3z5Y8XgItrejdThEckoQBEEQfiVUVcXpXEZH50a0GgtJSSeg10ev+9rc/APlFc/jdC5HkrQkJU0mL/f3WK0Dw+K8vibWrLmUzs7wMgoGQzojR7xKXFzfUFtHZzErV55HMNgZcb3dl7IBVFa+QsmWf8S8lzFj3sdmHUow6GLr1oeorpkTMzYurgCHYzzBoAunc3lEEflfQiScOHwSTgC1Xh8nLN9Mi7/7Q/8zg3I5K1XsyiX8emzbto3Zs2dHPWcwGLj88svx+Xw0NTXx1Vdf0dkZ+Qf750hKSuL666/f9aVXUVi0aBE//vgjLpcLgOTkZE444QQGDBgQtQ9FUWhpaUFVVRISEtBoIqcJC0Jv8FVW0vTU07TPn4/q9aLv0wfHRRfimDEjbBaQd/t2yqafixIl0apNS6PPB++j3bHzY9Ozz9L42OMxrylbLKh+P6o3+pbJv0TSDTcQN34cuqwsGh55lPaPP44aZxo5ktw35oR+T91r1lB+yaVRx2T77W/JeOjBsCSxv6aGhkcfo2P+fFS/H9luI/6cc0i+4QaxE58gCIIgCD+LovhoaJhPc/N3KKofh2M8aam/i7rjW2fXFkq3P05j0wJUNYDJlEt29mVkZV4U9plFVYOs33ALDQ3/i+ijX787yM25MnTs8dSydNnpBALOiFhZNjJ2zPtYLIUAdHRsYtny02PeS1ra2fTvd9uO5NQKNm76v5ixWq2NYyYuRqs1H3oJJ0mSTgEeBzTAi6qqPrDH+UeBnfv3mYEUVVXjd5y7BPjrjnP3qaoafW/BHQ6nhBPA183tXLB2OwBxGpkFYwrJNxsO8qgE4cBZvXo1n376KX7/ri1JbTYb06dPJzs7O9S2aNEiFixYELMfnU4X1sfeHHPMMeTl5ZGRkcE333zD8uXLo8ade+65DBo0KGLM3333HU6nMzTeY445hrFjx0adFaWqKk1NTfj9fpKSktDr9fs8TkHYSQ0GUQMBJL0+5uw799q11N71N7ybN4fazGPHkv7P+8J2qAu0tlI67XcEotRxkvR68t56E8OAAQQam2h87LEeZ07tT0k33IBxQCGSwUj9w//Gtzl2kcy8t98KzRDz19dTdu55BOojt2Y2jx1Lzssvhc12Un0+Wua8gfOdd/BXVaFNScF+1pkkXnYZstkc9XqK242/pgbZakWXkvIL71QQBEEQhCOVovhQFC8ajSXmZzhVVWlq/pq6ug/x+Zowm/LIzJyBzTYsIrajYyMbNt5KV9eWUFt3vakHcDjGh8VuL32C0tLIh4xmc19Gj3orNOtLVVVWr76IVueSqOPLz7+VPt071R1aCSepuzpYCXAiUAUsB2aoqroxRvyNwEhVVS+XJCkBWAGMAVRgJTBaVdXItQU7HG4JJ4D7ttXwVEUDAEMsJuaO6EuzP4BdqyVJH32JgCAcSTweD8XFxXR2dpKYmEhBQUHEjCGPx8OLL75IU1NTxOutVitXX301ALW1tXz11VfUR/mi+XPEx8dz0003hYqjL1u2jE8//TRq7JQpUzjuuOPC2kpKSliwYAGNO77Y6/V6xowZw5QpU9DGWAIkCL+Eqqp4N20i0NSELisbQ36fqHHe7aXU/PnPeNatC7XpsrJIv+8fxI3f9WHFX13N1pNOjrkEz3baaSRceimB5iY6v/sO55tv9e4N7SNdZibmcePQJiXhWrEC96pVMWMzHnwA++9+B4AaCFB5/fURy/oAjMOHkTtrVljSSfF4aHz0MZxz54Zmk5nHjiXlttswDR0S0Qd073boq6xC0uvRZWaI5bqCIAiCIPwiqqrS1r4Kj7sKvSEFR/xRUQuzAzQ1fU1l1Wt0dhaj1dpITf0t2VmXRBRm9/vb2LDxjzQ3fxNqkyQdOTlX0Df/j0iSfEgmnI4G7lFV9eQdx3cAqKoatcS7JEmLgbtVVV0gSdIM4HhVVa/Zce454FtVVd+Mdb3DMeHkV1TOXL2FFe3dS3m0EgR2/GiOd1i5u18GAy1i+r8gdHV18fnnn7NhwwaCwSCSJFFYWMjJJ5+Mw7FrOeq6det47733eu26GRkZJCQkYDKZWLVqFcEYX7y1Wi233nor5h1fTktKSnjzzTeJ9rd28ODBTJ8+PWo/bW1tuFwuHA4HRqPYwVLYf3Ymp3zl5WiTkjCNHh21WHfru+9S97e7IwqBGwYMIPeVWWji4wFQfD62nnACwcbIxDB079qXeNml+KqqcK9cRdeiRb1/U/tAk5yMfepUtCnJ+MorcL7zTszY5FtvJenqqwBQFYXKq66OOm7JaCTvjTkYd5sRqSoKzS+9RMurrxHckSw3FBSQfMvNWCdPjuhDVVVcy5bTtXAhqArm8UcTN+HoHguoC4IgCIIg9KbOzhLa2lYhywYSE49Fr08KnTsUE07nAKeoqnrljuOZwDhVVW+IEpsLLAGyVFUNSpL0J8Coqup9O87fBbhVVX14j9ddDVwNkJOTM7q8vPcKYx0oZS4vxyzbFEo07c6mkZk3uoCCOPHFUxCge7ZTe3s7FosllNzZXSAQ4MUXX6Suri7inCRJTJ8+Hb1eT01NDUVFRTQ3N/fa2BITE0lJSSEuLo6NGzeGakJFc9VVV5GZmRk6rq2tZf78+ez8G6bRaBg2bBgnn3xy1MST0+mkqKgIp9OJ3W5n+PDhYYk3QehN7nXraX39dTzFxcgWC7ZTTiH+7LMilpy5li+n4pprUff4v6/NSCf31VfR71gqq3g8bJl0PEpbW/QLyjKpd9yOZDSidHbR+PhjqJ7erym1N3JcHLZpv0VjtxNoaqZt7tyYsXGTjiPnuedCx3X3/ZPW11+PDJQkMp94HNuJJ4aagu3tVN1wY6gg+k7GYcO6C6gnhBc6Dba10frmW3QsWIDi8WAaMgTHxTMxDR4cc3yB1laCTmd3gXdRx0oQBEEQhJ/ocE84/ZnuZNONO473KeG0u8NxhhPAvAYnV24oi3l+Wko8zw/OO2DjEYTDXVdXF5988gnFxcWhtvj4eE499VQKCwtDbcXFxbz1Vs9LgGRZRlGUXh9j3759Oe6440hNTaW9vZ0XX3wRX5TdvTIzM7nsssvCluAtX76cTz/9NGz2lCRJnHLKKYwbNy6ij87OTtasWUNjYyMmk4lhw4aRkZHR6/ckCNC9DK/1zTdxLV8BOi3W448nfvp0NHZ7WFzLnDnU/+O+qH0kXHIxqXfs2qa44T+P0PzCC1FjZbudvvM/g0CAQFMTVTfehL+qqvdu6CeImzgBbUoqkk7X48wpXXY2fT+fH5q9VHXjjXQs+DJqrHn8eHJfmRU69tc3UD5zJv6KivBAjYaMB+7H/tvfhjV7t26l/sGH6PrhBwBksxn7mWeScustyHGRRU7VQICuH3/EX12DLj3t/9k77/go6vz/P2e2JZtk03tIAknoTQhYUFRUEEQUBEEQiSAWsJ3end+788565+mdnqcHFmxYQBSQYqGjiFTpEHpCek82yW62zszvj90M2exu9L5f736WfT0eeSQzee/MZz7zmc/OvOb1fr2JGDEiaBVAAMlixVVRgSYmGl1yctC4EEIIIYQQQgjhp40fI+H0vVPqBEE4AMxXFGWHd/kXkVIHcOexc6yp9Xeab4dWgOKRg9CJId+HEEL4d2A2m6mtrSUsLIyMjAzVi6kdkiSxcOHCoCqnIUOGcP3112O32ykvL+eDD4KXIQVPWp3b7f632/ldpucTJ05kkNcQuaSkhLfffjto7KxZs+je/bxvz8mTJ1m+fLnf9vPz8xk3bpxfn7S0tLBv3z7Ky8vR6XT07t2b/v37d+k55XK5EAQh5EsVwr+Npo8+on7BQtXgW4w2ETdrFgl33+2TSqY4nVQ8/LAfKSOaTHR7ZSHGoUPVdebly6l69I+BdygIZH3wvkexVFdHzbPP4Th+PHDsfxiahAS0sTEIOj32woDWlipS//w0Yf0HoDFFUf3U01i2bAkYJ+j15G7dgjY+HgBHcTHnpk5Dbmnxiw3PH0rW22/7GKi37d1LxSOP4K6sUtdpU1JI++tfibjIl8yWLFZq//Y3mlevRrHbAY+fVfLv/scntbAj3PX12I8fRzAYMA4ejBAqohBCCCGEEEIIPxn8GAknLR7T8KuACjym4dMVRTnWKa43sA7orngb5jUN3wcM8Ybtx2Ma3hhsfz9VwmnGoSI2N/rfDHbE2csGEKENlV4PIYQfGvX19bz//vtq1bl25ObmcvPNN/tUlVuyZAmnTgWulJWZmcns2bNxOp2YzWYWLVr0b1XO6wqiKBIREYHBYMBisWD3PtwFQrdu3bjxxhuJiIjAZrOxYMGCoCTYuHHjGD58uLpcXFzM0qVL/ZRWqamp3HbbbYR3SsM5c+YM27Zto9SrtMjKymLkyJHk5OQE3F9dXR0nT57E7XaTkZFBjx49/AivEH55UNxu7CdPgiRh6NkTMYh3maIotO3dS+u69chWK2H9+hF9wwQ/5ZSiKNQ89TRNS5b4bkCjIfXJJ4m5aZK6yvLVV5TddXfQtiX/8VEiL78CqdlM05KlNHflDyeKHp+r/1IV4GAIHzaMyEsuRowy0fzpp9gPHgwam/b834m+7jrAQ04VT7oJxWbzixPCwui+YjkG77WtuFyUzCoIaM4uGI1kf7iUsJ491XWyzUb100/TvHoNeOcjTUICSb96kJibbvLbhrupCfOyj7Du3AmCQMQllxAzZTLaIGnD7oYGLF9/jeJwEj54MGG9egaMCyGEEEIIIYQQ/vf40RFOAIIgjANeBDTAW4qi/FkQhCeBbxVFWeONeRxP+tz/dPrsbOD33sU/K4oS/LU+P13C6dmiKv5REryqllaADwfmcGlc1H+xVSGE8MuB2+2msLCQsrIyNBoNvXv3Jisry6+aVFtbG0uWLKG8U7pOcnIyt956K1FR56/RTZs2sX379oD7i4yMZPTo0dedCrYAACAASURBVNTX11NdXR2UxPq/QhCEgKblHdsxbdo0jEYjWq2WBQsW4HAE9skZOHAgkyadf1A/fPgwK1euDBg7efJk+vc/X7FLkiQ+/fRTDhw44BOXnJzM9OnTie5EGICnr0+cOIHNZiMpKYmcnJyg5FRDQwMHDx6kpaWFmJgYBg8eHPKzCgHbwYM0r1mDu74BfXY2MZNvQp+Z6ROjKAq1zz5H4zvv+H3edP31pD37V1Vp5W5o4OzoMWp1us5IeuQR4mbeitTURPPnn1P7zF+DN04Q0Hfvjmy3ITWZA5I8/w2IRiOGvDzEaBPOkhJcJaVBY40jLiH+9tkIWi1t3+6l/l8LgsZGXXMNGS+/BHj6uPzue7B89VXA2I5VAwHsp05RevtspE7KU01CAlmL31FJr/Zt1/3znzS++RZKB4I/YsQI0v7+Nz+Cyt3YSMOiN2j59FOklhYMeXnEzphB9I03BKweKJnNtHzxBa7qGnTpaZjGjkUTFboXCyGEEEII4ZeJHyXh9N/ET5VwqrA7GbH7OHa563MyMy2eP+akYQopnUII4f8bZFnm7NmznDlzBkVR6NGjB3l5eWg0vtelJEmsWrWKIx3KzgNER0czY8YMkpKS1HULFy6ktrY26D5NJhPh4eE4HA6am5u7JJH+UxAEgauvvpqoqCi0Wi2rVq0K6DkFYDQaeeihh9QUu67It+TkZO666y4fMmnXrl1s2rTJR5kVFxfH1KlTSe7kEfPNN9+wceNGv7aOGzeOYcOGBdxnW1sbZrMZo9FIjLfCWjDIsozFYkGn0/kpvEL4eUBRFNp27qTpo49xlZejTUoiZtJEIkeN8qsQZ929h/J770VubfVZH3PzzaQ8/pgar7jdFF03HmeQQiYxUyaT+tRTgCc17fTIkX5G6ypEkfi77gK3C1dNLS1r1vwfj/i/g/DBg9HEx4MsYdn6ZdA4bUoKPT5dq/pJFV1/Pc4zZwPGGnr3pvsnK1VyqH7RIuqefyHw/ocOJev999RYd10d56bPwFVW5hcbO306KX/yTcNsXr2aqsceV9MFwWMin/rXZ3wM38FTkbB51WqaPvwQZ3ExmthYoidMIK5gFprISL/9yU4nrRs2Yj9yBDHCSNTo0YT17h2siwCQWlsRRDGg71YIIYQQQggh/DcQIpx+ooQTwKaGFu48eo62TubEWWF6SuznH+pS9Dqe7ZVBpEbktbI69rW0Ea4RGJcQw93dEkkLC/khhBDCjwmVlZUcP34cl8tFeno6ffr08fM6OnjwIKtWrQr4eZ1Ox/z581Vi5Msvv+TLL78Mur/s7GySkpKwWq0UFxd3WSnvP4m4uDiioqLQ6XQUFRV1abw+efJk+vXrhyAIHD16lOVBKoFFRkZy7733qlX7Tp8+3aWv1uzZs8nsoGhpa2tj3bp1HD16VG1PVlYWY8eOJSUlxeezsiyzc+dOdu/eTYvX/6ZHjx5cddVVPtUF29Hc3MzevXspLi5GFEVyc3PJz88noouHQ4vFgiRJREVFhVILf0KQWlpoXr0Gx6lTiKYoTOPGBawO5ygqpmzuXFwVFT7rI0aMIOPll3wq/DW89Ta1zz0XcH8J8+4h8f771eXSO+/Euu3rwI0TRTLffgtBr0dubaXy0T8idUFmi1FRIAgeAu3/932hICAYDD4ETyBEXX01uvQ00Gppev8DlCCqTIDkP/2JiGH5COHh1L74Iq2ffhY0NmvJEoxDLgCgbf9+Sm6dCYHmLa2W7iuWE+YtQKEoClV//CPNy/3TLQ29e5P13rs+qij7yVOU3X037qoqn9joiRNJfepJP4P2lnXrqX/9NRyFHq+x8KFDSZh3D5EjRvjtrz0V0bJ1K4rLRfjQocTdOgN9VlbAY1YUBcfp08gWC/ru3YOmLHaMddfVoe/WzU8tGEIIIYQQws8fIcLpJ0w4ATQ43XxU3chJq51YnYZJybEMiDLyaa2Z350up87ZtRlxok7LqiG55BgD+1+EEEIIP04oisLmzZv9VEAGg4EpU6aQm5urrmtra2PRokU0NTX5bScmJoa5c+eqJEdhYSEfdVEpKyUlhd69e2O1WikpKelSZfWfhkajITIyEqvV2qXxem5uLunp6ciyzJEjR/y8tzqiV69eTJs2DUEQcLlcvPnmm1RXV/vFGQwG7rjjDhITE9V1a9euZd++fX6xWq2WWbNm0a1bN3VdeXk57733nl86YmRkJAUFBSQkJPisP3PmDFu2bKGyshLwnLcRI0aQn58fMK2nvr6ewsJC7HY7KSkp9OnTB10Ho+eOMJvNHDlyhNbWVuLi4hgwYECXpJfT6aS1tRWj0RhScP0HIDsctK5bR9v+AwgGPVGjrsJ44XC/86woCuYPP6T+lVdxe69DTXw88XPmEHd7gU+8s7SUc9NnINXX++0v+Y+PEjdjhrrc9NFHVP/psYBtE/R6enz+GfqMDBRJouqxx2kOQvYChA8ZgunaMSguN5Zt22jbvTv4gWu1iAZD0PTDHyM08fGE9eqJEG7EfuwY7gBzRTuMl1xC4r3zESMisBcep6pDRcXOiL9jDkm//jUAst3O2WvHBt12wvz5JN53vpBz45Il1Dz5lH+gKJL+j39gGjNaXeUsL6dk5m1+RJYQFkbGgn/5EVTWHTuoeeYZHKfPeOJ0OkwTrif5d79HE+k7Z9iOHqP6scewHztvwRpxycWkPPkU+gx/Al62WmlZtw5nSSnahARM48ai7TQPtqPdm81x/DhiZCSRo0Z1SXyBpx8RRcTvYTov2+0gSSFlWAghhBDCD4AQ4fQTJ5y6QpPLzRNnKvmwOqhnOgCXx0axbHBgw94QQgjhx436+nqOHDlCW1sbiYmJDBgwICAJ0NLSwoYNGygsLESWZURRpE+fPowePdrHD0mWZd577z2Ki4v9thEWFsacOXNUkqWpqYl//vOfQduWmJjINddcg91up6qqip07d3Z5LOHh4bjd7h/MOP1/C51OR0xMDIIgdEmo9ezZk7FjxyIIAnV1dV0qp7p168acOXMAT+rkSy+9RHNzc8DYtLQ05s6dqxIGJ06cYNmyZQHTIi+77DKuuuoqdVlRFNavX8+uXbt84qKiorjllltIS0vzWb9jxw42btzos22tVsvEiRPp10mBY7PZ2LhxI4cPH8btdiMIAr169WL06NHExcX5ta2kpIQ9e/ZQVVWFwWCgX79+5Ofnq2qzjpAkiVOnTlFaWoooivTs2ZPMzMyAZFoIvlBcLhxnzqDIMmF5eUGruLlqaml8+21aN2xAttkIGziA+IICIi6+2Hd7ikLNM8/Q9O57PuuFsDDSX3ieqFGj1HXO0lKKJ04KSBIJRqPHNNxbAdNVU8PZa8cG9Z5KeeIJYqfejOx00vLpZ1T9/vcB4wDQ6Yi64gpkhx1XRSXOs4HT6X6y0GgwDh2KEGZAamj0IW06QwgPJ/Xpp9GYolBkmfL7H4AgCi5tYiK5WzarVQZLCm6nrdNcoTYhJobcrVsQvd8nbXv3UlJwO0iSX6wxP5/Mxe8geNPEHcXFnJtyM7LF4t+GtFR6rFyJpkNqsuXr7VQ8/LBPZURBpyPpd/9D3PTpPp93lpVRfv8DPpUiBb2ehHvvJX7uHX5zRsv6DTQsWoT96FGPmfyIESTMm6cq0zrCumcP9QtfUfskrF8/4u+804ek6wjZ6cT27bfIbW0YevcJSKS1Q1EUnGfPevzhsjLRpaYGjQWQ29pw19ejiYsLmGIZQgghhPBTQYhw+hkTTu349YlS3q/qmnTad3Ff0kOpdSGE8LOHzWbDYrEQGRkZVJ3idDrZsmULBw4cUBU4ubm5jB492sdHCmDr1q18FcDYV6fTUVBQoKaSKYrCa6+9FlAtBL4ki8vl4qWXXqK1k+dNR+Tm5iJJEq2trdQHUG38GKHX69FqtSiKgu07DJ/z8/NJSEjAYDCwcePGoGmOgiDw4IMPqqThrl27WLduXcBYo9HI/fffrxI+p06dYknnqmxeiKLIXXfdpfpfuVwu3nrrLao6KSHAo8qaO3euD3G5Z88ePv/8c7/YxMRECgoKfBRUZrOZDz74gLq6Op/Y3NxcpkyZgsFg8Fnf1tbGvn37OH36NLIsk52dzbBhwwIaySuKQlFREYWFhTidTlJTUxk8eDDGDqlp7ZAkicOHD3P48GEsFgvx8fHk5+f7qAU7w2az0draSkRERJeqMIDq6mpqamoIDw+ne/fuQRVn/0koikJdXR0ul0sdX8FgP3GC5rVrkZrMGHJ6EH3jjWjj4/3ibAcPUvHIIz7m4brMTNL++gzGIUN8Yi3ffEPFAw/6ERFxBQUkPfJblSxQJIniiZNwBCmOkHD/fSTOmwd4FGFnrrgSKYCCE0CMj6P70qUgy0gWK6Vz5iAHIXsBom+ahDY2FqnNhvnjj6ELAlzQ60FRfMzHf+wQjEYPiSQIARVvHRFx2aWEDxiIGBFB07JluEqDG8Qn/vY3RAwZgqIo1L/2GtYvAxu+AyQ+9BAJd84FPKRl0fUTgqY5dlv0OpGXXQZ4znXR9ROCtiP16aeImTxZXW589z1q/vIX/0Ctlm6vvkrkpecVXK1bt1J+730BCbXkRx8l7tYZPuvMq1ZR+7e/nzeqFwSirr6K1Kee8iHTAGyHD1P9xJPniUNBIPKKK0h5/HF0yb7fqe6mJmqff56WtZ96+kSrxTR6NEm//Q26TmncAPbjx2lashT7qZNoTNGYxo0j+rpxAYlnRZax7tyJ7cBBBL2eqFFXYuhiflMUBamxEWQZTULCD/oCQPFeN4JOF3qxEEIIP3OECKdfAOH02OkKXiuv6zLmzf5ZXJfoK0c+ZrGxrbEVAbg8Loo+kaHUiRBC+CXB5XLR0tJCeHh4wAd08Nw0Hj58mB07dlBTU4MoivTq1YvLL7/cz+Oovr6ed999V/U3akd0dDSzZs3yUcl05VHVt29fbr75ZnX57bffpiSI0TLAyJEj6d27N6Iosnv3br/Kdx2RkpKCXq+nqampS8Lrx4Tw8HBMJhN6vZ6qqqou0wt79OihqpyOHDkSVGUFHq+q/Px8dDodxcXF7O4iHSo/P5/x48cD0NjYyMsvvxzUqH7w4MHceOONgGf8vP766wGJLIBBgwYxceJEdbmhoYHFixf7jSGDwcCMGTN8/Lfcbjcff/wxJ0+e9Iu95ZZbyM7O9oldunQpZwMoZS699FKuvvpqn3UWi4X169dz7NgxZFlW1V5jxozxq3bY3NzMypUrfcao0WhkzJgxDBo0yG9/jY2N7Nmzh7KyMrRaLb169WLIkCEBlWEAra2tnDt3DkVRyMrKCki8AZw8eZKNGzeqBK1er2fo0KFcddVVfh5xiqJQUlLC4cOHsdlsJCQkMGTIkICVHCVJYv/+/ezbvp0WqxWTMYIhl45g6NChfoURANzNzRR+9BG1FRWEG40MvP56ovr08YtzVVVRPv9ebIWF2MPC0EgSereb2OnTSf7971Q1DXhULGUPPURVSjLVyZ55J6W6mtSaGjJf+idRHVSAje++S81fngnYRxGXXEy3N99UH4Brn3+ehkVvBIxFEOjx6VoMOTkobjfVTz2NedmywLFAxGWXYRw6FNlqpXXTJpwBVKQdty0YDB7C4Wd4740goEmIR2OKRm5pwV0X/B5Vl5lJ7NSpIAjYjh2j9bPgnlqaxETS//43xPBwZIeT0tmzgxKG2m7dyFm7BkEUUSSZM+PGIlVVIwMtJhOKIGBqbUUjywhhYeR99SUa77XVsm49FQ8+iC0sjIr0dNw6LbGNTSTV1mIcNIisJR+cV3udPk3x1GkBDf712dlkL1+upiPKVivnbpkekGjVpqXS/aOPfNIMzcuXU/Wnx/x8w4z5+XR7/TUfzzdXdTXl8+ZjLyz0iY2+4QZSn35KVb21o2XjRuoXLMRx4oSnrbk5JNx1N9HXj/drm7uujoZ33qF14yYUm42wAQOImzWLiAuH+8W6amupX7CQlrVrkdva0KWnEzt9OnGzbvPzIlMUBev27ZiXr8BVWYkuJYWYyTcRMXJkQJLKXV9P86pVOE6fQRMTg2n8eMIH9PeLa4f95ElsBw8hhocRcdll3+lH5q6rQxCE70W+SWYzstOJNiHBr5BEZzjLy5HMzeizMkMVLUP4WSJEOP0CCKc3yut49HTFd8aNjI3k1rQELouJ4IETZWxo8L2hH5cQzct9M4kIcAMZQgghhOByuRBFMeBDZjvsdjsHDx6kqKgIgJycHAYNGhTwYfrAgQNs2bJFJX60Wi1Dhgzhmmuu8VGHFBcX8+677wYkOBITE7nzzjvVeKvVyqJFiwL6OCUkJHDHHXeobVm/fn2XaYBGo5H+/ft7UpBqaijt4u2/IAhkZGSoFey6Inp+qhAEQT33XZFegiCQk5ODTqfDbrcHTN/sGDtt2jTi4+MJDw9n6dKllJeXB4w1mUw88MADahs2btzIN998EzA2LCyMBx54QFX5bd++nU2bNgVtR0FBgUpQ2e123njjjYDKuqioKO68806ivA8NLpeLV199lYZ2FUQn3HLLLfTyGkkDnD17lqVLl/r1X2xsLAUFBT5kkiRJrF+/nm+//VY1tBcEgcGDB3Pdddf5kEgnT55k6dKlAdvQt29fpkyZoj5AybLMmjVrOHjwoE+cKIrceOONDBw40KcNH330kR+pB5CXl8e0adN85oO6ujo+/vhjn1RVg8HAtddeywUX+KY4KYrC7t272fHlV7TYPYrArNRUrho71odYBA/p9t6iRdR2IiKTo6OZOXcukR1SkhRFofq119m3bh0VSYlIGg3xDQ0MSksj76mnfB74ZJuNkrlzKaqopDQrE6dOT3RLMzlnz5Lz4K98VC+uykqKb5qM2e2iOLs7NmM4EVYr2cXniImMpPvKFapCrG3/AYpnzOBsTg5nc3NojYpC73SSfe4cvY+fIOOeu0m45x6PV9GePZTOKqAyJYVz3bNpjYrC4HCQWlVNVkkJkQkJJMy7B8Vmx3HyBOaPl9MWFkZzTAwunQ5RljC22YhqbUXndhPWry9otMitrTiLi1EAWRSRRRFRlhFlmZDexAOXVosiCOhcLgw9emDIzUU0mWjdtIlKg56K9AwcYQbc3jijxUr/Y8fodvddGAcPBkGk/pXzKXpt4WE4dXrCbTYMXiIs7o47iLlhAmg0mFetovH1RQAogCSKHsLL256O3l7O0lLOjh0HkkRrVCTm6Bj0LieJtXWIikLc7Nkk//Y3nm3JMsWTJ6sm8p0RV1BA8v88oi6bP1kV1GMs+Q9/IG7mreqys7yckhm3YrZaKE/PQNJqiGto9BC+jz9G7LRp5/uztpaSabfgqqz0HJ9Gg0aSEICoMWNI/8cL56t2Kgo1Tz1F05KltERF0RZhxGhtw9TaSsyUKaQ8+YQP6dO6ZSsVDz2EbLerY1kAYqZNJeVPf/IhfdyNjZT+5jccr6yiMT4OjSSRUV1D74kTSbzvXv+0zM8/p27hQrUSpiEvj4T58zFdO8avf6q2bWPbyk84p9chiyIJVisXDhjIgADpnraDB6l+5hksR4/h1moxALETJpD0yCN+nmgAksVC6/oNuGqq0aenE3XNNT6kYmfYT57CUXQWbVw8xvyhPiR9R8gOBy3r1tF65Chao5GYMaMDFrVQ++PgIcp2fIMgasgedSXGnj2DxoJHtac4XWiTEr+TqJNtNuS2NjSxsd9J1P2n4HK5MJvN6PX6oC9w/tOQJImKigqcTifJycnqPcVPFSHC6RdAODW63AzZcQy7/P3On14QcAY515OSY1nYN3DlkhBCCCGEHxqSJFFZWYnb7SY5OTmo0urEiRN8/vnnPsqX3NxcbrjhBr8v6paWFtavX8/x48eRZRmNRkP//v0ZPXq0T1pUU1MTCxYsCEqe3HTTTQwYMADwkBD//Oc/g6bKXXTRRVx77bWAJ2XxhRdewB6kspbJZGLy5Mk4nU7MZjOffvppkN45H68oCg6HA6fT2WXszx2pqamYTCYANeUuGJKSkoiOjkaWZUpKSrokyeLj4xkwYAAGg4GysjIKO6kEOmLAgAEMHz4cWZY5efIkO3bs6LINt912GwaDAVmWefHFF4OOoby8PGZ0MPf+4osvgqrOOqvIFi5c6Jey2BF33HEHGRkZQNdpmaIoMm/ePNXUfv/+/axZsybodsePH09+vuce0263s2DBgqDKwenTp9Ozw4PLunXr/LzIwFMsYObMmT4KtXfeeYdz584F3G5OTg4zZ85Ul61WK4sXL/bzZ9PpdEyfPp3uXs8p8Mw/Hy9bxolOihNREJg4aZJ6/bfjq9Wr2bp/P3R4qBIUhdGXXMLFY84/nCqKwpJnnuF0gOs12unkzt/+lghvWpaiKHz2yCN8G2DuM9jtzLjySjK921Zkma2zZrGtRw+fNgCIksToujouevVVdbunJ09ma1w8VekdvN0UhdjGRi7du5e+ixaBICK1tnDy93/gSEYGGllG53KhkSQQQEHA1NJM3+49MA4eBIKAedVq2srLqUpJwRwbAwiE2e3ENTYQ19iEPjkZTWwscnMzrpqagGlsIQSGGBGhqt/cbW20GY04DAYkjQZZFAEFU0sLRrcb07XXIhg8PmCWrVtxabU0xsVhjYhAEQSiWlqIa2xEq9GQ9pc/o4mJBY1Ixf0P4GxroyotlZrkZA9xUl9PZkkpWr2evG1fofXOs6V338O2lhbO9MzzaafJ3MxlO75h8Nq16Lyp2VV/eoyqNWs42r8/pVmZuHU6jFYruafP0OvkSTIXLiDqyisBj2rx+B8fZe+wYdR3KM4RX1/PsD176fPYn4i+7jrAQ/aeHncdp7IyOZ2XhzUyEq3LRWZpKf2OHqP7ww+r5LAiyxwsuJ0NKcnYOl1TaRUV3HDxxSTPnq2ua1q6lOonnvR81ruu/cpKeepJYqdMUWNLN2zg/a1bcQZIVb5Cr+eKDr50tmPHOHrnXRzu1ZPyjAwUUSTMZiP3zBmGhIXTffE7Poov8+efs+u11yhKS6PNaCTCaiW3sooLf/UgJm+ftcNeUsKWZ5/luCGMVpOHoO5RX8+o6dNJ7ODBB2A7cYKtTzzJ8dQUWk0mBFkmraKSixMT6ff0Uz5tcDc38/ljj3M0PEw9xjCbjaEIjHricTSdXhxad+zg+GuvUWy1IosiKQj0nzSJuKk3+xFPTUePsuXddzmrKLh0OmKtbQzt0YPh9873I8raGhrY9PrrHG9pwabXE2W3MzAllSvvvgttJ6sIRVFo3raNw6tW02RpJcJgYOCoUSROmOCnqHO73Wz69FP2HzqkPgenmkyMvekmMgNU7WyprWPnR8soq65GFAR65uQwbMoUDEHS6x1FRVi//hrFLWEcPozwTt8f7Th69CgbNmxQ72cFQaB///5cd911AV/ONjQ0cHDXLixNTSRkZDB42LCAKf6yLHPo0CG+3b0bs9lMVFQUF+TnM3ToUD+VM3heEO3Zs4fa6mrCjEYGDBhAnz59Ar5UdrlcHDt2jNLSUlWZ3aNHD/U8hwinXwDhBLCqpon5hSV0/kpPN+iYk5HAp3XN7G/57lLoArD7oj5khp+fTCVFYWN9C1/UN+OQZYaaIrg5JZZonf/gDSGEEEL4T0GWZUpLS7Hb7SQmJhIfwG+mI+x2O1arlcjIyKBeNqdPn2b58uV+1eSuuOIKrrjiCp91JSUlLF261I9Iys3NZerUqT6qrGPHjrF8+XI/VZZWq2XGjBk+D73Lly/n6NGjAdsXFxfH/Pnz1RuAN998k7KysqDHPHbsWLKyshAEgW3btnGsCzPiwYMH06NHD1wuFwcOHAiqLAKPYig9PR1JkmhoaPjJpCP+mCAIQtA0xHYMGDCAyMhIVf3TVXxOTg4ul4vW1taAFSo77zssLAy9Xo/FYkHqggAwmUzExsYiSRI1NTVdmvwbDAZyc3PR6/U0NDR0qQJMSEhQlVmtra1dVsuMi4tj7NixuFwuamtr+fLLL7s8vokTJ5Keno7RaGTt2rUcPx5Y6REeHs6vfvUr9F7/m6+++oqtW7cGjBVFkfvuu09NMzx9+nSXhQNmz56tKrO6UpwBXHzxxYzxkkiNjY289NJLQWM7EpFOp5Pn//53HEGI51iTifsefBDRqxxY+c5iDp8r9qTtdXoATAgLY95vf6vGvvXsc5TaAtwnej87c8oUcryqiLpt23hnzRqsAcyu4xsbmXP//Ri93kHVb77FsoMHsEZGonc60blcCF51isFu53JBJGPuHSArtOzZzYa939JmNCIonhhRktFKbjSSRG5dPT1vugnF4cBx8iQnjh1T0zE1koTQ4XqJam0lbcAARGM4bouVs+fO4dZokDQa3DodCqD1fk6UJbSKgqIoqhpMEjXq34KiYLS1Ed5mw9jWht7p/PmrxLRaBI0Gt8uFW6tFFkUUQUARBARFQVAURFkmTBQ9flKShOQlHtrPg9ChPwH0oogmLg5Bq8XV0ECzTouk0aox7dtHUUh2OonKH4oganCcPUNFcwuWyEhPjCggCyKKKCBKMt0bG0mafguiToe9tIxdJ05gDwvzxKnnU0TWaEitb+DKhx9CNEaguN2U3XUXDWFhnM7LpTYpCUUQSKyto+epUyS4XPRYvRoxPAwFWPzYY9R2GvNqX0gSsydOJDo7G0VROPnoH9kSFYlbp/MwWQK4dDqcej2plVXcPGUK0V4FlfXAAZYuWEC596VAR2SfK2Ha739HmJesd7e08N7//I6SpES/2KjWVgpuvpn4Cy8EPGqij+69lxMdKuiqp9flYlJ8PH0fekhdt+LXv+ZIEAP7i2WZMU8+qS43bdrMig+X+rU5usnMjd2z6f7gg+q6+kOHePuDD7AGINUvcLqY8OenVeKiraGRRc89S1MAD9IMu53bn3wSjXf+VhSFg399lvXmJuwd4jUuFyOsVq587jnV60xRFN5fsICzHZXL3rlNVBRm3XYbWTnnC2ydVrRVEgAAIABJREFU272bJavX4NT7pqKa7A5unz+P2A59KjudnH30UQ4UF1OZmoYiesbQoMgI+v7tb2qqLnhenn744Ye+B+ZtR2a3bhTcfrs6JwNs+uQTth865BOuVRQmTphAv6FDz7dBllm+dCmFp0/79Vt2aiq3zpnjQzodPHiQ1atW0fkOIzstjRm33+5zP1tXV8d7b75JS6d73+y0NKYXFKDX60OE0y+FcAI40trGG+X17G+xEi6KjE2MpiA9gVgvMXTMYuOx0xVsN/tXFemIgrR4fpWdQrJBR6tb4tbDRexu9q1QE6/TsmRQDwZF+U8esqJwoKUNs1uiZ0QY3UJm5SGEEMKPGHa7ncOHD9PQ0IDR+5YnUFU28BhaHzx4kIqKCvR6PX379iUnJ8fnBqEdJSUlfPPNNxQXFyOKIrm5uVx66aWkdqpeZLPZWLJkiR+RZDKZuPXWW32M3EtKSli8eHFAZU9WVhazZs1S22I2m3n99dcDGpJ3Ti+sqanh1VdfDUpwTJ48mf79PV4Z9fX1/Otf/woYBx7SZOLEiciyTENDA6+88krQWL1ez8UXX4zD4aC+vp4zZ84Ejf3fQhTFLpVQIfyykJycrKZRnD17tkvyLTExkdTUVARBoKioqEuiNSIigoSEBNxuN/X19X4kdme0k+But7vLNrTHCoKAJEnfWeUzMTGRyMhIRFEM6FnWEUlJSYiiiMPh+E7S0mQy0aNHDyIjI6moqAicKut9cLrmmmsYNGgQiqKwf/dutm7fHnS7PbtlMuLqq5AkiTOnT7OjqzRnrZZrvKoXc1MTX23bFpBMAw8R0CM3F4fDQXNz8w9KkouyjEmvJ1KjwVFVhSzLaF0udN4fQVYQFRlBVsgQBboNHoygKFR+s4MaUVD/L0qySlgIKEQ5XcRkZuJ2u7FUV2MTzlt8CYDW7ULv9OxD63arSjTxB35+UwC3VovDYMCp1+M06HHoDTgNeiTRkyandbs9RKDb83egdaIsowjnSR9ZFM+TQILou95LZLWnfao/HfpJTQlVFA9xqNXi1mlxaXX+v7VaXDrPb73LSWSrhajWViItFvT/ZhEABbAZw2mOjsYcHUNzdDTN0dG49Dr0TicGhwO904ne4fnb4HSof3f8f3vbZVFE1GoJS09HYzLRVFlJTViYh5Ay6HHpPOl6CqCIIr0QyJt4I2i0FG/dwv5WC4rg+Z8idBhPskyGzUb+xEkoLhd1e/awv6YGUZYQvTGKcJ6sC3M4GHjllShaLW119Rw4exZZFFBEERkvuYji3YfMhAkTCE9LA52ODS+8QHFyMpIoInlTThXvdRhtNjP7wQfRe30+lz3xBMXee432mHZiURFFbhs5kqyrrgJB4PPnnmNvIBWw9zq/NjubiwoKAKjcvJm3tmzxkHoB4m9MTmHwvHsAOHPoEO9/8knQ+SJVELjrsccAcDud/OOxx7AGeVGZ4XRyR4diBYWPP85qux1HJ3WSxu1mdGMTFy5c4G2SwoLnn6c+QHXP9nZ1VAIf/OorVm3dGrDNoixzz913k+j17Dy8dy8rP/vMP9a7fPUll3DpaE81zsbGRl7+5z/Vc9E59uKBAxkzaRLgUQG//OyzmDu/5PDGDuzRg0m33RYinH5JhNP3wfLqRu49HvwNZEfkGg0oCpy1Bb5pSjXo2HVRHwwdHrQ2N7Twh9PlnLN5BqYAjE4w8fde3UjU//cr9oQQQggh/BQgyzKnTp3i5MmTSJJEVlYW/fv3D6jMKioqYt26dWrKkEajYdCgQYwZM8Yvvr6+ni+++EJ98BRFkT59+jB27Fgf3xuAw4cPs3r1ar+H35EjR3LllVf6SOS//vprNm/e7Ne2mJgYZs+eraa9QfDUKYAJEyYwxFvpTJZlFixYENQPSa/X89BDD6HX65FlmcWLF3ep9pozZw4ZGRkIgsDmzZv5+uuvg8aOHz+elJQUHA4H27Zt69KkPjY2lgsuuABRFKmsrOwy/S48PJxevXrhcDioq6v7wSsu6nQ6jEYjLS0tXaqhkpKS0Ov1OJ1Ov1SzzhAEAZ1OhyiKOJ3OEFkXQgg/cmgEAdmr8FJ/ZMWjElMUREUhMT4erSDislpobmlR/y8qCrIgIGm0KoEjeRVNP1cY7HYPAWVp9SGiolpbkUWR5pholVQ6Ty79cC/PBVn2I9d8CDVZ8aSzesm6djKnqx/Ah0BQfuDzJ8iyqlDUOV3oXU50XvJT73IiSjKSRoOk9agIO/64tVqf5fZj8mtzp3Xtv7VuNxq3G62X1NQLIjpZBpsNQZYQJQmN5Ok7t1aDW6vDpdMia7SI0SacoohNlnF52xKIcEJR0AI6RUFwu1G8hGn7j0aSPG3wkqs5ySlER5swSDKn9uzGoTdAB+WdJIo49XpkQaBn//7YoqJoaGujpLoat0aD0oUXapheT5heT5vZ7MlcUklpb9qnt/1ROh3xGRm4JYnqigo1TbBjTDs0QEJyMoIg0Go2Y7Hb/fvBuywqCr379UMURVqamiitCO4TLcoyDz/yCJGRkSHCKUQ4nUetw8WQncdw/0Cn+pm8dGalJyAKAjvNFqYcPBNw230jwvgiv6cPOeWUZZZUNbK0qoEqh4tuYXpmpMUzNSUOTRDTuXqnm1K7g3idlqzw4OWeQwghhBB+zlAUhbq6OhwOB/Hx8UG9r9rR0tKCxWIhOjo6YO5/x7iDBw/S2NhIREQEgwYN8lFYdcTp06fZtWsX1dXVGAwG+vXrx0UXXeS3fVmW2b59Ozt37lT9i2JiYhg1apSPQTXAuXPneP/99/38lgRBYOLEiT7x5eXlvPPOOwG9mTpXv3M4HLzzzjsBq+UNGjSIG2+8USXUKisrWbRoUVACZ8aMGeTlefxMnE4nCxcuDGhSDzBlyhT6edOQrFYrL7zwQlA1S3JyMrfccgtutxuz2cwHH3zQJYk0b9489dx0RajFxMRw7733qnL6VatW+RmGd8TNN99M3759ATh06BCffPJJ0NgJEybQr18/nE4n+/bt6zL1LTY2lvz8fNxuNxUVFZwKUKmrHaIoMmrUKHVcf/bZZ0H7TaPRMGLECJxOJ01NTQENzv9b0Ov1aLVaXC5Xl0okjUZDeno64Lnmgo2fdqSnpyOKIna7vUuvLvCoob6Paqq9HeHh4ej1esxm88+SXAwLC8NgMHxnIYeLLrqIhIQERFFk69atgRVR3rf6SUlJapXX79PPIfz8oPEqzNxabXASI4RfFHReNZsiCLQZjT846fdjQlRzM61epfD00WPoNeKSEOEUIpx88diZCl4rC3zDMi0llj6R4ewwW9jeZMEqfffNh04QSA/T0eSSaHYH/+L9V59MJqd40lRcssJtR4rY2uj/hX59Ygyv9svyIZ0anG4ePV3O2jqzSmgNM0Xw557pDAyQ1ldmd/JuRT2HWtswakTGJ8YwISkGfZCLv9nlZn9LGxpBYGi0sctKfYqiUGJ34pIVssMN6MTQl0wIIYQQwnfB7XZTV1eHRqNRH+wCobq6mu3bt3Pq1ClkWSY7O5sRI0b4+F61o6Kigs2bN6tVESMjIxk+fDiXXnqp3/YdDge7du3i8OHDWCwW4uPjyc/PZ/DgwX6xBw8eZO3atT4Pk4IgcPXVVzNixAif2MbGRj7++GMfMkuv13PNNdcwbNgwn9i9e/fyWYCy73q9noKCAtLSzps7f/755+zZsydgHw0cOJBJXsk7eGTvq1ev5vDhwz5xMTExzJgxg8QOprxms5lFixZhtfqmyoPHj2z69Olqf8iyzMqVKwN6jPXt25fJkyersQ6Hg1deeSUgeSIIAjNnzqRHjx6Ax4D0xRdfDNgGgCFDhjBhwgR1+ZtvvmHjxo0BY6+99louuugidfn111+nsrIyYKwgCDz44IOqEf+KFSu69DmbOnUq2V5Plq+++iqoiTvA1VdfzaWXXgp4xuWiRYu+V6zFYuHFF18MamrfkTyVJImXXnopKHmSlpbG3LlzEQQBt9vNiy++iCVQ+oYX999/v5o+3JWflV6v57777lPbu2LFii7VerGxsWRnZyMIAqWlpV3Gmkwmhg8fjkajoba2lgMHDnQZ2+591dzczIYNG4LG6nQ6HnroIbVS5YoVKzhy5EjA2IyMDObMmaOSzoWFhUE9xrp168btXp8VRVFYtGhR0PEGkJKSQnp6OrIsU1RU1CXxFRERQf/+/dW+6CrFOCwsjFxvymB9ff13pkT+O0hKSiIxMZHw8HCOHjmCPVB6qJd8u+iii4iKisLpdLJr586g/mLgSffMyspCFEWKioq6HBfxcXHk5OaqRR+6Ilr1Wi0jr7gCg8GAzWZjy5YtQVOndIJAXFISjY2N35meCh7yOykpieTkZM4dOEBzkO8tjdvN7LlzUTQarFYrK5ctw9EFgRsdFUV0bCyyLFNdVobbq2BRfci8y+3Km6joaERBwNrUhMvrgSYoilfOcv5YRSA5PR0EAUuTmWZbm0cF1b5jb+oiXuVbXs+e6DQabM3NnCsv9/HfalfUtKuooqKjcSkKdrcbhyx/L0JFoyggSX6pkuePD7QC6A1hCCjY29o8llcKnnQ+QBY1qnpK9qqU/NLBgqA99VTndPopsxDArdGqqiy3Vuu3LHWlivpfQOtyEWGxEmm1EGG1YnA4cGl1uPQ6nDq9z+92zy/5O6rHi6oX3Xk1lijLKHj8zjoq4QIp5lTF2b9xjCazmRZv4YuZF19C7rVjQoRTiHDyhaQoPFdczaKyOtq8k2GERuTOjER+0z0F0Tvgjra2cfW3wd9A/rtI0Gm5Kt5EtzA952x2ltcEf6O3oE8mN3nJKZskM37/KY5Z/Cs+RWpEPhvak14R5/NmNzW0MOdoMY5OVfuGmox8OCiHKO35C9ctK/y5qJK3K+rVKn9RGpF7M5O5PyvJr8rC53Vmnimq4nSb58s3Ua/l7m5J3NMtUe23dljcEu9U1LOm1kyzW6J/VDiz0xMYEetf+lJRFHaYLXxU3USt00VWuIFbU+PoH4BMAw9BtrLWzLk2B4l6LZOSY0kL4pUlKwrfNFk4YrERpRW5NiG6y/TGU1Y7u5staAWBK+KiSDUElxG3uiV2mS1ICgyNNna5XYcss9tsxSJJ9I8M9zGm7wxFUThhtVPndNPdaPhOH7AGp5tKh5NkvY4kQ9epm1ZJoszuJFqr6fLYwDM+yh1O9IJAqkHXZblXRVGod7lRFM+4+K7SsFa3hF1WiNNpvjPWLsm0ShJxOm1Q9V87JEXBKslEaMTvjAXPedELwne2ATz9IQr4jfVAkL3fJ98nVlE8NxXfJ7Y9/vu0N4RfDmw2G06nk6ioqKBE1r+LlpYWDh06RFNTEyaTiYEDBwb19lIUhbKyMmpqaggLC6Nnz55dGtXv2LGDsrIyNBoNvXv35tJLL/UhhcBDLnzxxRfs27fPR+k0aNAgxo8f72Pq2Y7q6moKCwtxuVykp6fTu3fvgNVpmpqa2Lx5M4WFhciyjNFoZOjQoVx++eV+8bIsc/ToUQ4cOIDZbCY6OpoLLriAAQMG+PV1U1MTK1eu9El1jIiIYNy4carSqx3l5eV88MEHflX7srKymD59ul//7du3j23btqkP7DExMVx++eVccMEFfttdvHhxwIfJjkQPeMjCN998MyDx1a9fPyZPnqzONVarlUWLFgUk1Dp7ogFs376dTZs2+cXm5eUxdepUn34+evQoK1eu9FMYJSUlUVBQ4KNgLCsr4/333/fziIqMjGTWrFk+46grhdqwYcO4zuuHBJ7xtnLlSj8CTq/XM23aNJUsBDhy5AgrVqwIuF3wrYr4Xf5wHVWAkiTx+uuvU1NTEzC2o5ccwLJly4IaxHcu/OBwOFi2bJlKTrcjOTmZGTNm+KQBgyfFeMOGDSphJwgCffr04frrr1dJLIDjx4+zbNmygG3QarXMmzdPnTeKiop49913A8YCTJo0SVVxOhwOFixY4FOZtSM6VlG1WCy88MILQRVqKSkp3HnnncB5sjdYtUy9Xs/DDz+sXn/Hjh3j448/Dhibl5fH9OnT1WukK2JYEATuueceVZVZVVXFokWLArZZFEXmzJmjqgDr6upYuHCh/xjykkqjRo1i5MiR6upVy5dzsCNJ7o0TgOlehaqiKBQWFgY9tvbP3H777WR5K5g1VFTw1sKFWDvNvYIsc8OIEQzuUKly27ZtQYkvjSBw3wMPEON9YP9m/Xo27twZ1H9nwtVXM8Q7b5UWFvJWIDLUGzsyN49R3op9bW1t/OOvz+Lq/GLcGzsoI4OJd9yhrn798cfxo069sXkGAzN+9zvAc53+7YknsAf6rvXG3zV3LsmpqYiiyMfPPMOxYJ52isK8mTNJ8hYZ2Pbqq2yprg7aFzfn59N3/Hjcbjcb33mH3V0UO8nT65nhrRzYtHcvr6xYgUuvV9NO27crAINaWxn3l2cAhdbycha+8855Qq0jCadAmNPBiGuvxWa3U1dTy6niItVvq50Ia0+/07lc9ExMJDcri0hZ5sy771GZkODjq9XOBuqdTgakphDeqxcoULhlC80GT1qeJGpQRMHDLXrTL9MFyB42HEEQqPzmG0o0ompQr7ZX9vjDJdsdZAwfDpJE49GjVLic3rTa8x5yipfkC3M4SO7ZC0VRsNfWUuM1C1e815AiwJEBA2iJiSGytZX7HvwVYZndQoRTiHAKjBa3xL5mK4IAQ00RPkQMeG6gr9h7kpPWwKW9AcYnRtPkkii1Oyiz/3smfF0hSa/lhqQYTFoNp60O1tQFJ6duSIzmtf6eN9+NLjfDdhYGVWbNTIvnb73OVxZ45GQZiysD+4X8tnsKD2WnqMurapq4uzCwr8ddGYk8kZeuLje63Ew8cCZg3z2Rm8Zd3c6nqCiKwiOnynk3QDv+mJPG/EzfdJYN9c3cU1jic4wab+zdnWKL2xzcfrSYEx3aoRMEHspO5sGsZJ+H9la3xH3HS1hXf/7mRgPcnpHAE7npPuSFoii8cK6GBaW1KmmpEwSmp8bxZF66T+okwLKqRp46W0m9y/MGVwDGJkTzfO9uqql9O/Y1W/ntqTIfgnFUXBTP9+7mRxDVOFw8erqCz+s8ec4CMCrOxNN56XQ3+j6s2CSZvxRVsqSqUe27i6IjeCw3nQtMvsSerCi8XlbHq2V1VDs943pAZDi/65HKqHjfm1KAL+rMPH+uhqMWzw1cT2MYv8pOZmJyrF/swZY2ni2u4svGVhQgM0zPXd0SmZ2e4EeilNgc/LWoik/rmnEpCvE6LbelxfNgdrJfH7e4JZ4/V82yqkbMbolorYapKXE8nJ3sV1FSUhTeKK/j7Yp6ztmcRGhEJibF8nD35IAk3KqaJhaW1XK41YZeELg2MZqHspPpHeFfRWRHk4WXSmr42uxRL46MjeKBrGQuivGvfHLCauMf52pYX9+MQ1YYYjIyLzOJ6xJj/GKrHS5eLKnhk5ommt0SvSLCmJ2ewMy0eD+iyipJLCyt5cOqRp903TszEgnT+PabpCgsrqhncWUDZ9vsxOu0TE6J497MJL+xCbCm1syisjoOtrYRoRG5LjGa+7OSA6b47jZb+FdpLd+YLYjAqHgT92cmBSSST1rtvFxSw4aGZlyywvDoSOZnJjEyzp+grnW4eLm0htW1ZlrcEv0iw7kjI5Ebk2L8xpBVknitrI6PqhupcbjJDtdza1o8s9IS0Ha6CZUUhfcqG3i3op5im5MkvZabU+K4u1siEVr/N2yf1pp5o7zOQ2ZrNExIimF+ZhLJAUjfXWYLC0tr2eUls6+Jj+bezCTyIvxLAJ+02vlXaQ2bG1pwKwqXxEQyPzOZYdH+qYA1DhcLS2tZW2fGIkkMjDQyt1siYxKi/WJb3RKvltWyvLqJOpebnHADt6XHMyPVfwy5ZYV3K+t5v7KBUruTFIOOqSlx3JGRSHinMaQoCqtrzbxVUc9xiw2TVsONybHM65ZEvN5/DO1osvBKWS3fNlvRiwJjEqKZn5kUcAwdt9h45UQxZ4qKAZms7Gzu7pMTsGBHpd3JwrJaPqtrxibJXGAycle3RK6I85+zWtwSr5XV8kllHa12O5nRJgoykpicHOs3hpyyzOKKBpZUNVDhcJJm0HNLahwF6Ql+85CiKHxSa+bDY6cwN9SjM4RxWZ9e3JOdErCy7ebKOlbs3I29phq0WlJz87jroqFkGv3HxdHWNhaUVLO/ogYBGJqRyvysZPpG+s9DB0vLWLFpC5SVoFFk7LEJDLv4Ym4aPtQv9lxdHe99vh7XuSJERcYZbiR70AXMuWaUX3noerOZN9Z8RlvxGY/3jSgSlduTO68fT3SU7xynKAqLDxXy7bd7EVqakQ1hZPbpxz2XXogpwMuZtaeK2LRzF0p9HbJWS3xOHndcPoJuUf7jfmdFDcu/3o6zsgIEAWO3LGZcfikDE/0J0S379vPl1q2IFs+cLOn19B6Szy2jr/YjDOsdLl7Zf4SzhYXgcmKIT+C6C4cxIdO30IGiKHy+fj17O3uzCQJjx47lwuHDfVYfPnyYT1avRumUfnbJyJGM7lTGvbW1lTc++pjmsg5+o3oDV159FZd32q7T6WTxyk8oP3GCdg2HImoYOPxCJgY4vtNWG6/vO0J50VmQZeIzunH78AsYGhN4nl1YUs3O02dwO5ykpKRS0DOLawLMLV9u386Xmzefd/gGRL2eaVOm0NObftuOrV9+xVdf+ivJhuQP4/rrxvlcf/X19byxZCn2xg73hxoNI0ddxagRl/h8fteuXaxbt85vu1qdjoJZs1QCEDznY+XKlZ7+8q5r3+t1113np8z8at9+tmzejNDmIWZlUSS9Tz9uv+F6teIjeIiIDz/+mNMnTvi1Y/z48eTn+z6PHiksZOUnq1Bc51VRgk7HjTfcwKAOxCJ4COe1n37q08cAPfJ6MmPaVJ9rVZZl3t+wiTPf7kV0e+7hlOhYxl07hgv79PaJW7hwYVClVXJyMnfffbfPOTlV38iqJUtxVnuoGTE8gqHXT2DMYN/2SpLEh8tXcPq4r8efIIpMvukmHwLe7Xbz3qJFlAQgWntmZjKtoMBnLH+1ciVbO6lZAbJNJmY+8IBPX5z2VkXrnH+SEh3N7fPm+RD7luZm3vrHizR2ql2WqtMx6+GHfQj1fTt2sDaIwnBQTg4TZ85Ul20tLSx69lkaA7wkuSovj8u8VTgBJJeLDx5/nKIAsUMNBq73kl4AbVYrC559FqsonieovL81sszc2bNJyc5W4w88+ihfSBLOTi8zssvKuPnXv8HYYWxsfv4Fvm71J3tFSWLiwIEMmDIF8MyFL//5zzQGUajqFYVf/+EP6nVS98orbNm6leN9+vgQaqbmZi4/VsgFqz5B9L5gKFmxgg++/dbT3k4EXGyTmTn3zifSO7/Yjh3jo+eeoziAEjytooKpt84k+nIPKesoK2fZH/5AUQ//2DCbjSnJyeR4qxdKFivL5s/nVPfz/dixr69qaOCyf/0rZBoeIpz+b/i6sZXph4twBRgb87ol8adcj/xfURSu2HOCk23Bq7JowG/C+6GgEwSMGo+8uaWLNEANcGe3RGK0WuyKzIvnavxKQrYjTBR4f2APIjUaUBRuO1JMrSvwhAKwY3hvuhs9lWR+faKM96sCE1kCsP3C3uR4b6iXVTXywIngRu6rL8jlQu/D+pk2O6P2nFSN4Trj3QHdGe29GbJJMpfvOUGpPbC8+W+9MpiZlqAuTz90li0BUhwB7s1M4tGc86keL5yr5rni6oCxU1JieblPlrq8ptbMncfOBYwdajKyZkieSmYdt9gYt+8UNtn/+LqH69mY34tI74Nvi1vi2m9PURTA1D5Jr2V9fk+VPJEVhVsOFfFVk//xhYsCq4fk+aRmPna6gtfK/eXbIvD2gO4+D7MfVTdyfxAj/j/npTMn4/zb5m+brdx08Iyf+g5gbkYCT+WdvyEsszu5bt8pap3+Y25UXBTvDeyh9ptVkph44AyHW/3fWPaLDGP1BXlqvymKwrzCEj75f+2deZhdRZn/P3W33vctnc7S2TcSIIR9U9xxARFURBSB0XFGccaFGXVGcV9HHZ1RRnZcQEFkEZFFRUAggSQEspB966ST9L5v997z++Oce/vce6rq3jg9cvPz/TxPP+nb/U11nXpPvW/Ve+pUHQkmcacXRXnwpAUZSafv7jnENzS2LguHuOfE+RkT3weO9PKhTXvI7oFh4MbjWnmTL5G0vn+Yi1/YoU0Of27edP7BlzxtHxvnzWu3c3AsmNS+rLmWby+aOXmkbiLJJS/sYG1/8GS2s2vK+dmKuelXax3H4R+37OOew8FXERaUFnHfygXU+ibJpvu+Nhrm/pULmO+bJN93pIcPb9obaIuikOJnK+Zylm+l47r+IS55Yae2Lb6zaCbvmV6X/nxgdJy3rdvOAU1b/OOsRv7d10+HEgkuXr+T9QPBtji/voobjmtN30NJx+HvN+/lfs19saKihHtOmJ++hwC+ubud7+wJDo6nF0W5b+WCjFWJvzrUzUe37Au0RVk4xF3Hz2OlL5G0pneQd2/YlU5kpwgDP1w2mwsaJ5O4+0bGuGDdDtrHg23xmbnNXDO7Kf15IO72kVRS2M/FTTX8YMms9D2UcByu3riHhzqDr76cWlXGncfPy0g6fWnnQf57X3Aj7tnFMR5YuSBj1eUd7V18/OX9gbhTFQnz6xPnZyRP/twzwGUv7kqvvk0RVYqbj2vNmPjuGh7jgnXb6dDEqC8vaOFqnx/qnYhz4fodGQ8iUrx/eh1fXzgj3RYTSYf3v7RLGxvOrangJyvmZLyq/m/b27ixLTh50/WnWw908q/bgk+n66IR7ls5P6M//bGrn/e/tDsQ+2LK7U9n+xKzW4dGuXDddnriifTT69RT528vmsl7ff2pczzOheu3s2N4jFAySSSZYDzsvj7x4ZkNfH7+5IOksWSSyzbs4qneQaLxCUomxhmOFREPR3hDfSUMhMpPAAAgAElEQVQ3LZuTTuI6jsMnt+7nZ+3dges7rryEe06cT6WvP12/7wjX7Qy+ljUtFuG+lQsykpEPd/Zx1cbdgf0yS0Mh7jx+Lqf4kvsvDQzzjvU76I8nqBkeIJRM0ltWgRMK833fNgfgJm/fum67dszw8dYmrp0zmXQaTiR51ws72dPWxqLD+ygdH6W/uIwtzbN5VetMfrR0djqJm3QcPrplHw/ta2fh4X1UjQwxEitia9NM5k1r4q4T5mVsY5CKOTVD/dQP9jEejtBW00hzWTH3n7ggYzX3vYd7+IfNeykdHWZaXxfJUIgD1Q3Eiou564T5nOB7mLS2b4h3bgj62YiC/1nWmvGgY//oOBes08ecz85t5qM+3zIYT/COF3awo7Ob+UfaKBkfo6+0nB0NLVw0o4nvLZ6Z4Vs+uGkPa3btYXH7XipHhxiKlbB12ixmz57NncfPy3go8rVd7fznnkNM7+2kbqiP8UiU3XXNzKis4L6V8zNWlf+ivZvvPrWa4/dvp6m/m2QoxJ66ZrbPXcKtZ5/EEp9vebpnkE/98c+s2P0yTQNu/Ossr2LD7EV8/ryzeK3vwdru4THetm47nePjNPb3EknG6SqvYjRapPUtb1+3naH9e1lweD8lE+P0lFWwubmVCxbO5WsLWjJ8yxUv7eaJI13M7TxI+egIg0Ul7GqYzpkNtdye5Vv+fXsbd2/dxdL2PdQMDTAajbGjcQaxma3ce1Kmb7ntQCf/sq3NPU1wdIh4KMxgUQl1sWjAt/xm206evOuXlExkjiWHo0Wc965LeeP81vTPtg6N8vb12+meCM5m/mPRTC7L9i3rtjFy8ADzj+ynKD5Bd2klLzfP5vIFswO+5b3rt9O9ZRMr9m+ndHyMoaISNsxawOxlx3Hj8nkZvuVTW9t49OVtnL1lPVWjQ4yFozw3bynRBYu5Z+WCgG/51uadLN+/gxndRxiLRNk0Yy4j02dx76pFQd/y0i5a2/ezrG0nSUK80LqQrsbp3HnCvAzfsnFgmE89+Bgnbt1AzHHbIwlsmj6XS99yPhf75heHxya4cPUmlqx5kjmd7ShgNBzhiSUrecuZpwZ8y6XrtlHz+KMsbt9LSEEcxbrWRdSd99qAb/mnZ1+k9OH7KfbNMBNJ2HnqudzwxnMyfcuug+y66WZO276RZCxCOB6nq7iCR9/yDq5/y3mZvuVQN4/ecCNzug8xWFWJSiYp6x/gxbnL+PgV78nwLY9t2caTd/48+Eaa4zDv9W/i8jMnX/neNzTCAx//F0577inaWmYQj0ao6e4hNDbOnq98kyvOm3xlf2Aizv98/suUdR/isHfiXyiRoGV/G31nvYZ/vuqyDN/ynR/ezNIH7mZf62yGysooGRlh1t69bDn9VVzz6U9k+JbbbvoJ1ffexc758+ivqiIyMcGsffsIx+F1N1xPY/nkuOzBex5g+Ke3snPBfEa91Z3VPT3M3LOPpd/+NksWzJOEkySc/vc82zvIN3a380yv+2RjVnGMv5/ZwAeyVmT86lA3/2iYeNdHIzx5yiLGHDeQ/+PmPeybwhVRhYICYiGlTSj4aYq5m56HFbw0MMKgJUk2qzjGmTXlRJTi+b4htlhWnM0sjnFZs7vp+ksDI9aVYbWRMNd6r1AeGBvnP/eaTy6KKsU3Fs6gLBJiPOHwyW37rdf47UUzmVYUBcfh2m37OThmTtRd2zqNVVVlhJQ7oX+6V7+nB8Dfz2zg7U01hIA727u56YD5/f93NtVwTWuTm+DrGeRfNBObFKdXlfG9JbMAd0J/0Qvmo6RnFEe5/0Q3STaWSPK657fSZ7BfiVI8ceri9OrBd27YqU0KpXjopAUs8AZC125t454j5j0Zfrh0Fm+or0KhuHF/B1/dHdwMOcWn50zjI94A+fGufi57SXOctcelzbV8d7HbFvtGxjjt2S2BREGKkyvLeOAk9+nKaCLJiU9vcid5GhpiEdaevjQ9gHzj89t4QZMIATe58PwZS9OJr2u27OWXh8xt8esT53O6NxD6r72H+fIuc1v4J5yPdPbxPktb+Fcu7hoe44zV+lc3AF5dW8Edx88D3AHTiU9vMu5pN7s4xjOnLSGkFI7j8Nrnt2pfGQY38b3+jGXp1VYf3rRHmyxM8diqhekVVKZkYYrrl87mQm8V3m+O9HK1ITEM8InWJj7lDQpfHhrhVWvMGzO/rbGaHy9rBdxEzwlPbzKuOl1SVswfTl6E8trinDUvp19ZzqYyHGL9mcvSA8grX9rNbzVJIXB98Z9PXcJcb6XjV3ce5PuapFAKf7I+12mu/pWnGwaGeYPl1fN3T6tN+5buiTgnPr3J6DtXVZbym5Pc45ATjsPpz24xPjCoj0ZYe8bS9AqjSzfs1O6JCG5/WnP6Ulq8wbQpoZ7i7hPmpROiPz3YxSe3mk8C/NrCGXygxZ1YrO4d5IL15j1nrmyp56sLvdesxiZY9cxm7cMsgHNqyvnlCe4rFhNJh1XPbOKwJvkO0FIUZfVpS9MTsgvXbefZPn0ciSnF2jOWpifq127dr11dnOK3Jy1gZaU78L5hfwf/vsN8Ws9/Lp7Fu5rdBM7j3f28e8Muo9b/EGfvyBinW/zsG+oruW25+zrbSCLJSoufnVdSxJOnLk77ljeu3cYGQ8wpDYV44cxl6cnpRzbv5W5N8j3FH09elE5afH/vYb5q8bM3LGvlrY1uAuehjl4+sHGPUetfTb5taJRz1gRXx6S4sLGa6z3fMuj5FtP46bjyEh5dtTDtW85ds5Vtw3o/WxUJs/6MZZR6E7KrNu7mwQ6zb3n61CXpVdTf2NXOd/fqXwEE+OmKuekEzj1egszE5+dN58Oeb3lxYJjXW3zLe5pr+Y4Xq3s835JOTqf6lTdG98fqhONwxrNb2Ov5llh8HOXAWNT1D9m+xfYwMgw8d/rS9ETdtkcswK9OmJfeVuJnB7v4hMW3fH3hDK7wfMua3kHeZvEtV7XU85W/0Lec/MxmegcHWdq+hxk9R3BQtNU2sqV5NnXl5aw5fWn6wUwu37LujGXUe6taC8G35BrD+X1LrjGc37cAvOH5rWwYGEE5SeoG+wk5SbrLKomHI6+Ib6keHqDSS2Z3lFeDUhm+ZfvQKGenfIvjUDE8xGgsxoR337+9sZof+XzLiU9vYiDh7jVV19fDRCRCf7nbj7N9y6ue28qhjg5O2rOV1q52lONwsLqeta1LGKut1/qW1gP7OWf9akrHRtk+cw5PnHgK8WhU61tmHG7ndc89ReXwIO21DTx86jn0VVRqfUtNfy+vW/0UzZ1H6Kyu4dFTz+ZIbb3Wt7QcaeetT/6euW17GSwt44+rzuDPx6/i3S31Ad/ScLCNt//xYRYc3Ec8FOL5xSu4/5zXsri5iQdOWiAJJ0k4TR29E3HGkg4NsYhxr5X/3neEr+9qz3D0M4tj3Lp8Dst8T1dSTx9MfGvhDM6sqaAvnuBH+45YEyeVkTAnVJQwnEiyZ2Q8/dqWIAh/Gf7enSsqhFP/wcm9gjGsIORtA5nrpMwwpAd5phV9fm005L5bPpp0rHUOQfopz1gimbPOZZ52POkYB7B+rQLijhNYlZJNaThEREHCIefhDMUhRcybTPfH7doipSjx6tEXTxgHmuAmkisi7vUNxJPW6wsB1VF38DicSOa8vppIGKVgLOnkvL6qSJiwgrjjrly0UR4OpdtC95TZT0lIpVci9UwkrPdFTCnKIyEUiv54wtoWYUgnAAcTiZxtUe9pR5NJ68MFcFfLhZViIunQm6MtKiMhikIhkg505Yh7ZeFQ+l7uGI9b26I4pNIThZ6JOBMWcUSRXlnQH7e3hcJNPIN7D+Vqi7pohLB3D9kOJAGojoSJhRQJx6Erx31RHg6lB/+6FaR+Snxt0TURt/qtqFLUen2kL0dbhJhsi8FEMmcfaYhGCCnXv+Vqi9pImGhIEc+jLSrDIUrCIRxyt0VpKJT2F53jcavvjClFjdcWvfGE9QFVGNKT9IFEkuEcbdEYi6CAkaST01/URcNElGLCcXL6i6pImOKQIonbR2yUhUOU+/qTrcZFIUV1uj8lrPEsrCb9xUA8GVjpmU1TLIJCMZxIWFf3g9ufIsqNZaakQorqSJiikCLpoF016afc51uO5OFbqr29y7om4lY/G1WKOq8t+uJx7cr3FAr3vgA3nubyLfU+35LLz9akfQs55xcV4RBl4TAOTs62KAkpqry26JyYeMV8S2MsQgjFaDKZsy1qo2FiKkTc27PURmUkTGkohINjfFiQojQcotJ7kNQxPmH1LUVKpeNvTzye07ekHi4MJBI52yLVn0aSyZx+ti4aJqpCTDjJnH62KhKmJBQi6d0XNsrCISq8tjgyPpHTt9R491D3RNzqWyIK6r3XFPvjiSn1LX8+dTHzy0ok4SQJp78uHeMTPNjRR99EgsXlxbymtjKwV0g86XD1pt0Z+wWluLiphu8vmZVOau0fHeec1VuMweZ/fK9Z5Ho6tqqylG8smslIIsmWwRE+ZUl6hYAPzWygNBxiIJ7ghrZOa/BYVVlKZSTMSNLh2d5Bq7YopCgKKeIOOQdXgiAIgiAIgiAIglBI3H3CPM6urcw74RTczVEQ/gIaYtH0MlgTkZDixmVzuOtwN79o7+agt7Hve6fXcUFjdcYKKneF1Fz+buPujCxrCHcptn9Pj4VlxfzjrEbtfhqV4RDfXDQzvUfGqqoyftfZz++79SeAvLu5NuMd6954wvhaT2p/itSKjI9t2ccvDgX3bkjxyKpF6ZP1cr3q8aUF07mwsYa44/DTA138h2Xp9uvrKrlmdhNJx+GZ3kG+ZnmdZnZxjC/NbwEF+0bG+TfLct4ipfjawhaioRD98QSf334AW47+Y7MbaS6KkXQcvrm7nV7LqoxLmmpYXlFC0oGbD3QaXyEBOKWqjLNrykk67rvmm62vF0Y539uTYX3/MGsMy6DBfaLwLm8vi70jY/zesHwc3PvuoqYaikMheuJx47L7FK+uraAuGmHCcXjgSK/1acWJFaW0lrjLex/p6rc+jZlXUsSyihIcB1b3DVqfmtRFI5xcVYoDvDw4ml5Kr6MopDiruhwHODg2od3nJYUCzqwuJxpS9E4ktHsF+VlZ4SZlx5xk+hVdEwtLi2guiuHg8EzvkPVJaEtRlDnePgQvDgxbn8ZUR8JpH7BjeNTabkVKsbLKfT3t4OiEtd0UsKqyjLByV4XY9rMDd0l2WTjEaDJpfOUlxbySIupiEZKOw7r+Yes91FwUpcXbM2jT4Ij1qXB1JMw8b+n2ruEx61Pv4pBKr1I9ODZBu2ZvkxQKOL6ilJDXFrtHzO0G7mt1xaEQI8mk9X4Ddy+3qojbFi8NjlgT+81FUabFJtvC9vSvNhpmdrHbFjtHRq0ryUpCikVlqbYYt95DIWB5havtnojnPFxjcVkxsZBiOJFkR457aE5JjIpImHjSsfpCcPfWqo9F3JNvhkasT9Nro2FmeK/I7Bwes/qhsnAofQ8dGJ2wrrQKK1hWVgLKXSGj2x/Hz5KyYqJKMZBI5LyH5pUUURYJMZF0rK+dg+sv6ry22DQ4Yn2aXh8NM917tXf78Ki1P5WHQ8z1/ND+0XFrf4oqWOLdQ0fGJziU46n30rJiIkrRF09Y/RDA/NIiSkMhxpIOWw2vkKWYWRylJhJx917J0Z8aohH3VXlg2/CodWVBZTiU3htm7+i4dSVSkVLp8dCh8Qlrf1LA0vJiwih64wnreAHcsVnKt+TqT7OKY1RHwiRw2Dw4am2LpliEJs+3bBkatcan6kiYWV5/2j0y5r6mY6A4pFjovVZ/cGzCuookhNsWCkX3RFy7r5+fRWXFFCnFUCLJTs3el35aS2JUhsPEndy+ZVosSkMs4o4vcviWmsikb9k1YvctpSGfbxkbt65QCwNLyktQuKuQbPEJXD8bVYrBPHzL3JIiysIhxh3HeogSuH62Luq2xebBEWusrvP5lh15+JY5Pt9iW4kUVaQPdDkyPpFzddHSsmLCyl3Zm8u3zCspojQcYiyZZFuO/jSjKEpNNEIyj/70f+lbFnq+5XCeviXk+Zb9U+xbqiJhEo4bn3L5lkbPt7ycw7dU+XzLnjx8S2rLjvYp9i25ThLPRlY4CQXNYDzBr4/0sGN4jPpohAubarQ3ueM43Hmomx/v72DL0CgxpTi/oYpPtE4LnIjUNxHnQ5v28njWZtIXNlbzvcWzMjZYG4on+MDG3TzRM5ihbS2JcceKeRmnonWMT3DBuh3aDa0/PaeZj7VObjjZMxHnreu2ax3WGdXl3Hn85CbHg/EEb1q7TbvHSVUkzMOrFtLqOeJ40uGt67YbkwC3ZW1+bduz4FOt0/jEnMkT+z67rc24f9Jraiv52fGTxyebNoYFN3j98ZRF6et7tLOPyw176pSFQ/zplMXpwcpGb+8UU6i58/i56dOZOsfjnP7sZqMz9p8cGE86nLl6izHw+vdkAbh4/Q6e6h3UaldUlPDwSQvTe53Z3umviYR5/vSl6dPAbmzr4N+265OAEQV/OmVy4/nfd/Vz2Yvmd/r9tt4+NMq5a142DoT+dc40/sl7530wnuCkZzYblxZf1FTDD5e6G8TnOtXS/8472O+3mkiY589Ymt6r5+u72vmeIdEaAp70bcJ/l7dBtQn/Xj3r+4d501rzHhn+zWE7xic4+ZnNxqXs75xWw/e9zfLjSYfTVm+mzZBcOKGilIfcd97d//vCjoBfSVEXjfD86UvTr4Z9ZlsbNxv6Xhh4+rQl6cHYzW0dfMZwDwHcsWIur/be/3+qZ4CLLXuXfXH+dD7o9ZH9o+Oc/uxm48TCfxroaCLJqmc2Gwc3p1aVcd/KyVOczl+7jXWaDd/BHYg959sHzLa3V1Qp1py+JL0PWK69ve49cX76JMWHO/t4v2Vvr28unMH7vIcqO4ZHOXv1y8YBpP8wgMF4gpVPbzImRM+tqeAXJ7j7gDmOw6uf22pMwM0odvctSj3kuHrjbn5j6E9FIcXa0yf3Fsm158xDJy1Mn9z568M9fNiy58z3l8zinV6y/qWBYV5n2XPmmlmNfMbbW6RnIs7KpzcZJ1lvrK/kVm9vkYTj+uQ9honhgtIi/nTK5N4il23YZXyQVB4Ose6Myb1Fcu05499bJNeeM/69RXLtOePfW+TwmOtbTAlR/94iE0mHU57dbJxQZ/vZt6/fbkzuV0fCrPPtLWKLTyHcPdFS4xxbfILMfYv+1D3AuzaYfYt/b5F93n5Wprh+WXMt/+HtLTKSSHLSM5uMiQj/vkWO4/CmtduN+wY2xFw/m9q36KNb9nKXwbdEFKw+bXJPtB/sPcxXLL7Fv2/R7zr6uGKj2bf49y3a5sVqk2/x71uUseeMBv++RVPtW54/fXJPtFy+xb9vUS7f4t+3KJdv+eisRj77F/oW/35W2cwvLeIJn2+x7WdVFg6xXnwLUJi+Ze/IGGdYfIt/TzTxLUfvW45mD6fwddddl4/umOLHP/7xdR/84Adf6WoIU0AsFOL4ilJeXVvJqdXlVGmO6QZQSrG8opQrWur56OxGPtE6jbc21miPpy4Oh7h4Wi2vq69kTkkR59VW8IUFLVzREjwyPBYKcXFTDadXl1MdibC8wj1y+msLZ1CfdcRxWTjMRU01lIRCdIxPEFWKU6vK+PKCGVzqO8UCoCQc4oLGGkaSSXYOjzLuONRHI3xoZgPfXDQzI+kVC4V4a2M1baMT7ByezJKfUV3ODctaMxJqIS/Rtnt4LCOZ1RSL8O1FM3lLY+bR86+rq+Lg2Dgv+55EFIcUH53VxCfmTMvYIP7smnIOj08ETnx6bV0lP1o2O+Oo7OMrSgjhbnjud93Ly0u4fcUcan1tN6+0mLpYhKe6BzK0ddEItxw3h2UVk/uANRZFmVtaxKOd/RnaMPClBS1c1DR5+k5pOMSqyjJ+19kXeHLyvul1/Muc5vT1hZTinNoKHunsCzjYs6rL+cGSWRknp5xTW8Hj3QOBCXVrSYzbl8+l2ndyyhnV5TzXPxRIRFSGQ9y+Yi5zfCenHF9RStvoeGAz6YhyB2Jn+k45m1taRNJbCZTNNbMa+YDvFJm6WITmoiiPdvUHgs3r6yr58oIZ6YFmLBRiRUUpD3b0BpILy8qLuX5ZazoRopTi9OoyHjzSF3g3vCkW4fblc6nz2fqM6nJ+3zUQWBFRElLcfNwc5vvu5VWVZazrHwoMChXuJuDn+o59X1pWTPvYBC9pTiP70IwGrpoxedhBc1GUopDiSU2y53V1lXxxQUu6LcrCYeaXFvNQZ18gWbesvJgfLZ1si5BSrKos44EjvYxlDfSaYhFuWzEnoy1Oqy7nd519gSd1paEQNy9vTSfTwF3l91TPIIeyTmZLtYX/tK7l5aVsHR7VJqj/fmYDV/gOfphVUsRYMqldCfjG+ko+N2+yLaoiYaYVRXm0M3gPLS8v4QdLJpP1kZBiRUUJDxwJ3kPNsSg3L5+T3o8hdX0PdvQGnnyXhUPcvHxOOqHuasv5Q3d/oO+FgO8snpVOIAGcUFnKiwMj7NY8BPjn2U28u3nSL88rKaJ7IqEdQL6loYrPzJuenoDURiNUR8PaSciJFaV8b8mstD+MhUIsKivmNx3Be6ilKMqNx7Wm/YVSipOr3HsoO8lZEQ5x6/K56eQ7wKlV5TzS2R9YVRMGfrBkdsZJgCdVlrGmb1CbEL12zjQuappcNbyorJi20Qk2afrTO5pq+GTrZGxosvSnU6rK+NbimWnfWRIOMbe0mIc6g6s+ZxfHuGFZa/qwhZBSrKws1fanqkiYW5bPyThZ89TqMh7S9KeoUvxo6ez0pvrgThye6h3UTrI+N296epUswLLyEnYMj2mT6pc11/KRWY3ptmgpjpFwHO0Gw2dVl/O1hTPSY4xyb9XHw519gf60oLSI65e1ppPvYaU4vqKU33T0Bp5810Uj3Lp8Dg2+UxFPrSrntx19gT1tikKKHy9rTa8sAji5qozHuwe0qwC+sqCF83wnly0vL2XT4Ih2pcxVLfVc7fOzs4tjDMaT2pNDz6utyPCzVdEIjUURHusKJgyXlBXzX0tnp/1sNKQ4rtxti2zf0hiLcMvyOen9xVL96TdHegPxqSSkuOm4OczN8LPlPNbVH4hPIeBbi2dmxN8TKktZ3z+sTVp8ZFYj7/Wd1jWvtIiO8bh2VeubG6r47NxJ31IXi1AZCWkPA1hRUcL3l8zO8C2LLb7lpuPmZPiWVV5/yvYt5eEQt2h8y8OdfYFVNWHg+0tmc5LPt6ysLGN17yBtmv70qdZpvMN3KuKismL2jY6zWXNgxkVNNXxqTqZviSqlfbh3cmUZ3/5f+JaTKku5/0hvIClTFQlzy3FzaPa3RXU5v+3sDayAjSrFDzW+5UlNrIaj8y3vaa7lo3n6ljOry/l6nr5lfmkR/6PxLQ8cCfqW2mhY61se7OgN+JaYUvz4uMLzLdXRCA0xs2/57yzfsqy8hAc6ekn8lXyLAr59DPuWL3zhC+3XXXfdjwMFapAVToLwCpNwHEYTSUrDoYwEj46O8Qn2j47TEIvmXM64b2SMLUOjlIdDnFLlvgJl4sDoOM/1DRENKc6sLs9ImGSzd2SMP3YPEHcczqguzzjSW1ffRzr7GUwkWFFRymlVZcZr7ByP80BHL53jE8wrLeZN9VUZx5Bnl3v3oR72jY7TXBTlHU016acD2fTHE9x9qJuXh0apjIS5sLE6Y4DgZziR5N4jPTzfN0QsFOKN9ZWcU1Oh3TB/LJnkwY4+/tjdj+PAWTXlXNBYo61zwnF4tLOf33T0MpJMckJFKZc216VXH/hxvEHFrw710D0RZ0FZMe9prs04xtbP+v5h7mjvom10nJbiGJdOq82YaPp5eWiE2w90sW1olJpohIuaqnlDfZX2+tpGx7n9QCfr+ocpDoc4v6GKixprMpKhKbrG4/y8vYsnvFWD59ZUcNn0uoykQorhRJK7DnXzu84+RpNJVlWW8f6W+ozBbop40uGBjl5+fbiHvniCRWXFXNFSr73nHMfh990D3NHexaGxCWYUx3hvcx1n1ZRr77nn+4a47WAnO4bGqI9FeEdTDW9trE4PVPxsHxrllgOdvDAwTFk4xFsaqrlkWm36SZ6fg6Pj3Hqgkz/3DhJWilfXVnD59Hqtrfsm4vysvZtHOvsYdxxOrSrjipZ6ra1HE0nuOtzNvYd7GUgkWFZewgda6lmhuZcT3qucvzjUTcd4nNklMS6fXse5NRXatniie4DbD3aye2SMxliUd06r5W2GtnhxYJib2zrZODhCeTjEBU01vMvQFntGxri5rZNnegeJKMXr6yt53/R67YOAjvEJbj/QxWNd/e5pbTXlXGloi6F4gp+3d3P/kd60b7lqhr4t4kmHXx/p4e5DPXSMTzC3tIj3T6/PSNKlcByHx7r6+Wl7F/tGxplWFOVdXlvo+sjaviFuPtDJ5sERqiJhLvTaQucDtg2NcmNbB8/1DVEUCvGG+kre31Kfcax3ivaxcW5p6+SP3QM4uL7lypZ6Zmnaoj+e4PYDnfy2s889KbGylKta6rU+bjyZ5K5DPdx9uJvuiQQLSou4oqU+fTpddlv8trOPnx3s4sDYBNOLorynuY43N+j9xdM9g9xyoJOtQ6PURsO8vamGdzfXZjyISLF5cIQb2zpc3xIK8eaGKi6fXqeNO/tHx7m5rYMnegZQKM6treDKlnqtv++ZiHP7gS4e8nzLyVVlXNlSn3E0fIqRRJI72ru490gvvd7ekx9oqU+ffOkn6Tjcd6SXO9u7OTg2nt4K4E31Vcb+dOuBTnYMj1EXC3NJUy0XT6vJeGiR4sWBYW5s6+CF/hHXtzRW877pdenJsZ89I2Pc1NbBUz2ubzmvtoIrZzSkX0vx0zke59YDnTzS1eeuYqgq4+oZDYEV3wBDiQR3tHdz7+Ee+uNJjqso4cqWelZp4kiqP/2ivS6Ij1IAABzcSURBVJvD4xO0lhRx+fQ6XldXGWiLlE/+ycFOdg1P+pZ3NNUEHu4BrOsb4qYDnbw0MEJFJMQFjdVc1lyXXgHsZ+fwKDe2dfJs7yBRpXhtfSUfaKlPr7rxc3hsglsOdPL7rn4mvHHL1TMa0qdZ+hmMJ/jpwS7u7+hlMJ5kRUUJV81oSK/+8zORdLjrcHfat8wrLeJ90+szJtL+tni40/Ute0fGaPZ8y4VNNVo/u7p3MOBb3tNcp/WzWz3fsqZviKKQ4g11VVzRovezB0fHuflAJ493D5B0HM6uqeDKGXo/2zcR5/aDXTzY4fqWlZWun12u8S1jySS/PNTNrw710DURZ2FZMVcY/GzScXiwo4+fe+OW6UUx3jO9lrc26P3sUz0D3HKgk21Do9RGI65vmVarHYtsGhzhprYO1vYPUxxSvLnB7U8m33JTWwdPdA+gFJxbU8mVM/Rjke6JOLcd6OQh7wHmqspSrp7RYPUtvz7cS288zuKyEq6cofctCcfh/iO93NHeRXtq3DK9jvMNvuVP3QPcdqCT7cOj1EUjXDKtlksMvmXDwDA3ZfmWy6fXpVdj+dkzMsaNbR38uWdy3HKVwbd0jE9w64FOHu3sZ9yZ9C0Lp8C33OP5liPjE8wuKeJ9Ft/yWFc/PznYxe6RMRpiUd45rYaLm2qNvuXGA51s9HzL2xqrea/Bt+zI9i117n0xFb7lJwe7eOAofMtdh7rpHI+nxy023/KTg13sGx1jWlGUd/+VfIucUicJJ0EQBEEQBEEQBEEQhCnlaBJO+uUDgiAIgiAIgiAIgiAIgvAXIgknQRAEQRAEQRAEQRAEYUqRhJMgCIIgCIIgCIIgCIIwpUjCSRAEQRAEQRAEQRAEQZhSJOEkCIIgCIIgCIIgCIIgTCmScBIEQRAEQRAEQRAEQRCmFEk4CYIgCIIgCIIgCIIgCFOKJJwEQRAEQRAEQRAEQRCEKUUSToIgCIIgCIIgCIIgCMKUIgknQRAEQRAEQRAEQRAEYUqRhJMgCIIgCIIgCIIgCIIwpUjCSRAEQRAEQRAEQRAEQZhSJOEkCIIgCIIgCIIgCIIgTCmScBIEQRAEQRAEQRAEQRCmFEk4CYIgCIIgCIIgCIIgCFOKJJwEQRAEQRAEQRAEQRCEKUUSToIgCIIgCIIgCIIgCMKUIgknQRAEQRAEQRAEQRAEYUqRhJMgCIIgCIIgCIIgCIIwpUjCSRAEQRAEQRAEQRAEQZhSJOEkCIIgCIIgCIIgCIIgTCmScBIEQRAEQRAEQRAEQRCmFEk4CYIgCIIgCIIgCIIgCFOKJJwEQRAEQRAEQRAEQRCEKUUSToIgCIIgCIIgCIIgCMKUIgknQRAEQRAEQRAEQRAEYUpRjuO80nWYcpRSHcBeza/qgc48i/m/0hZKPY41baHUoxC0hVKPQtAWSj0KQVso9TjWtIVSj0LQFko9CkFbKPUoBG2h1ONY0xZKPQpBWyj1KARtodSjELSFUo9jTVso9SgEbaHUoxC0f+16zHYcpyGv/+04zt/MF/D8K60tlHoca9pCqUchaAulHoWgLZR6FIK2UOpxrGkLpR6FoC2UehSCtlDqUQjaQqnHsaYtlHoUgrZQ6lEI2kKpRyFoC6Uex5q2UOpRCNpCqUchaAupHtlf8kqdIAiCIAiCIAiCIAiCMKVIwkkQBEEQBEEQBEEQBEGYUv7WEk4/LgBtodTjWNMWSj0KQVso9SgEbaHUoxC0hVKPY01bKPUoBG2h1KMQtIVSj0LQFko9jjVtodSjELSFUo9C0BZKPQpBWyj1ONa0hVKPQtAWSj0KQVtI9cjg/8tNwwVBEARBEARBEARBEIRXjr+1FU6CIAiCIAiCIAiCIAjC/zGScBIEQRAEQRAEQRAEQRCmlv/NEXfH0hfwRmArsAP4V4vuZuAIsDGPMmcCfwQ2A5uAj1m0xcAaYIOn/UIe5YeB9cBvcuj2AC8BL5DHsYVANXA38DKwBTjdoFvklZn66gf+yVLuP3vXthG4Ayi2aD/m6TZll6mzAVALPAps9/6tyaG/xCs7CazKof2W1xYvAr8Gqi3aL3m6F4BHgOm57hvgE4AD1FvKvQ444Gvr823lAh/16rwJ+GaO6/uFr9w9wAsW7QnAs6l7CTjFoj0eeMa79x4AKm39QmdDizZgP4s2YD+LNmA/k1ZnP0u5JvsZy862oaXsgP0s2oD9LNqA/TD4KWAOsBrXf/4CiFm0H/F06Xve5gOBn+H65o2491nUor3J+9mLuD6s3KT1/d3vA4M56nArsNvXzidYtAr4CrAN139eY9E+6SvzIHBvjnq8Bljn6Z8C5lu053najcBtQMQUO3T2s2i19jNoA7azaAO2yxXr/LazlBuwXQ59wH4WrdZ+Bm3AdhatzXZ7yIrnGOKfQWuKfTqtKfbptKbYF9BaYp+u3OvQ+05tuZhjn65sU+zTaU2xT6c1xb7A2MpiO53WZDudVms7i95kP+N4UGM/Xbkm+2nL1dnPUK7JdjqtyXY6rcl22vGuzn4WrW7cYtLqxi0mrW7cYh2fkzluMZVrsp2x7Gz7WcrWjVtMWt24xaQ12S8w/8AQ9wxa07hFp7XFPZ1eG/t0WlPsM5R7K5rYZ9Bq455Baxq36LS2uKfTa2MfmjkhZt+p05p8p05rins6rclv2uaw2X5TV+51aPqerWz0vlNXtsl36rQm36nTavtevl95C4/lL9zB3k5gLu5EaQOw1KA9B1hJfgmnZmCl930Fbkc2lauYdDJRXCd4Wo7yPw78nPwSTvW56uvT3wZc7X0fwzdQydGGh4DZht+34Dq+Eu/zL4ErDNrjvBu5FIgAj5HppAI2wA1s/+p9/6/AN3Lol+AGrMfJdD467euZdHrfSJVt0Fb6vr8GuN523+BO9h8G9jLpfHTlXgd8Mp/7EXi112ZF3ufGfO9f4D+Az1nKfgR4k/f9+cDjFu1zwLne91cCX7L1C50NLdqA/SzagP0s2oD9TFqd/Szlmuxn0gdsaKtHtv0s5QbsZ9EG7IfBT+H253d7P78e+LBFeyLQSpZfsujP936ncAcltrL99vsO7n1k9K3AKuAnTCacTOXeClyc1dYm7QeA24GQz3Y5/TvwK+B9OcreBizxfv4PXr102jOA/cBC7+dfBK7y/a2M2KGzn0WrtZ9BG7CdRRuwnUmrs52l3IDtcugD9rPVQ2c/Q7kB2+m0uCvLbbbTtbs2/hm0ptin05pin05rin0Brc53Wsq9Dr3v1GltsU9bj2zfaSnbFPt0WlPsC4ytLLbTaU2202m1trPoTfbTjgcN9tOVa7KfTqu1n6kOBtvpyjXZTqfV2i7r76XHuyb7GbRa+xm0RvtptFrb6bQm2xnK1drOojf2P109dPYzlKu1n0GrG7do5x/oxy0mbSDuWbTauGfR68YtxjkTwXGLqdxbCY5bTFrduCXnvA0v7lnK1cY9g/5KNLEPw5wQ/ZzBpNXNGUxa3ZzBpNXNGYxzWIJzBlO516H3mya9bs5gnUv7+56lXN2cwaTN6TttX38rr9SdAuxwHGeX4zjjwJ3ABTqh4zhPAN35FOo4TrvjOOu87wdws8YtBq3jOM6g9zHqfTmmspVSM4A3AzfmU5d8UUpV4SYPbvLqNe44Tm8e//U1wE7HcfZaNBGgRCkVwb1RDxp0S4DVjuMMO44TB/4EXJT6pcEGF+AOHvD+vdCmdxxni+M4W7P/sEH7iFcPcDO9Myzaft/HMjwbWu6b7wLX4rP1Ud5jOu2Hga87jjPmaY7kU7ZSSgHvxA2QJq2Du9IFoArPhgbtQuAJ7/tHgXd4WlO/CNjQpNXZz6IN2M+iDdgvRz/OsN/R9Pkc+oANc5Xtt59FG7CfRRuwn8VPnYf7ZA4mbafVOo6z3nGcPZq2MOl/6/3OwV3JM8Oi7fe1RclksUGtUiqM+yTr2lx1yK5rDu2HgS86jpP0dEdylauUqvTa8N4cZevsp9MmgHHHcbZ5P0/3v+zY4bVVwH46rVc3rf0M2oDtLNqA7Uxane1MWhsGfcB+ucrOtp9Bq/WdGm0dBttZMMa/bHS+06LVxj6DVhv7LARi3xRgjH02smOfAa39DAR8p2VsFbCdSauznUWrtZ1FH7BfjvFghv2OZuxo0Qbsl6tcv+0s2oDtLFrtuCUL/3g3V99La/Poe35trr7n1+bqe9njc1vfy2csb9Ln6n+Bsi19z6/N1ff8WpP9sucf7RjinkZ70BT3DFpt3LPotbFPpzXFPp1WU1ebVhv3bOVmxz2D1ma7bP0Q+thnmhPq+p5Wa+h7Jq2u75m0ur5nm8Nm9z3rfFeDSa/re9ays/qeSauzn0mbj+808reScGrBzaqmaMMySfxLUEq14mbIV1s0YaXUC7ivJj3qOI5RC3wP96ZN5vHnHeARpdRapdQHc2jnAB3ALUqp9UqpG5VSZXn8jXdjGaw5jnMA+DawD9fR9zmO84hBvhE4WylVp5Qqxc2qzszx95scx2n3vj8ENOVR57+EK4GHbAKl1FeUUvuBy3AzxybdBcABx3E25Pm3P6KUelEpdbNSqsaiW4jbfquVUn9SSp2cZ/lnA4cdx9lu0fwT8C3v+r4NfNqi3cRk4vYSNDbM6hdWG+bTh/LQBuyXrbXZz6/NZT9NHaz2y9JbbWi4Pq39srRW+2VptfbL9lO4q0N7fQE67T+P0qdZ9UqpKHA58DubVil1C+79sxj4gUX7EeB+3z2Xqw5f8ez3XaVUkUU7D3iXUup5pdRDSqkFebTFhcDv/YMXg/5q4LdKqTavLb5usMkaIKKUWuUVdzGT/S87dtRhsJ9Ga8OozbadSauznUGrtZ2lDgHbWfRa+9muj6D9dFqt7TTaTsy2A308N/nOo4n9ubR+36nVGnxnQGvxnaY66HynTmvzm7bry/adOq3Jd+q0Ot9pGlvpbHc047B8tH7bGfUa+2m1BvvZ6pFtP5NWZ79c1+e3nUmrs51Jm3PcQuZ4N9fY0zo2zlOrG3dmaA19L6DNNW7R1CHXuNOvzzX21F2fadzp1+Yad/q1Afvp5h/AWjRx72jmKrm02XHPps+OfRZtIPblqEdG7LNoA3Evj7ZIxz2LVhv3DDb5JfrYZ5oT6vre0cwf89Gm+p5Rq+l7Wq2h79nqoOt7Jr2u7+W6Pn/fM2l1fc+kzcd3mnGOYjnUsfqFe1Pf6Pt8OfBfFn0rebxS59OX4zq3i/LUV+PuqXKc4fdvAX7off8qcr9S1+JMLrHbAJxj0a4C4sCp3uf/JMeyONylyJ24nd+kqQH+ADTgPoG/F3ivRX+V12ZPAD8CvmezAW7g8P++Jx+boVnabNF+Fvd9XpXPvYDbMb+g0+Jm81cDVd7nPWS+XpR9fU24y4ZDuO9Z32zRbsSdrCnc1Xu786mz186fyNHO38dd6QJuZvwxi3Yx7nLMtcDngS5bv7DZMFubw34mrc5+xr6psV9am4f9sq/NaD+D3mhDy/Xp7Jddrs1+2dpc9kv5qbNwV4imfj4z+/5C49Oy2ywP/Q1k+QGLNgz8EPiAQXsO7n4CqWXTg7ZycV87VEAR7pO07OX/fu1gyg7evfJkHvV9KGWXHPW4h0nf/Cl8cUujPR13r4U1wJdx38EPxA7cZd0B++m0WX8rbb88tGnb5aFN285Q3+k625nKNdnOog/YL486p+1nKTdgO4s2YDvf3wrEcwy+U6f1aR4n87UsmzbDd9q02b7TUF+t7zRotb7ToLX5Tdv1ZfhOQ9la32nQBnwnhrGVznYmrc52eWizbZdzjJeyn0H7LZ39LNcXsJ9Fq7NfrutL285SbsB2Fm2uuJcx3tXZz6Q19b0cWt24xTjmJjhuSWvJPW7JvrZc45Zsva3/ma5PN27JLtc2bsnW6vqedv6BPu5Z5ypk+qtc2owxSx56f+zTad+HPvaZri8Q+yxaXdzLVV9/3DOVqx2zWPTa2IdmTog57hnnjwTjnk2b7TtzzUv9cS9b+2PMcU93bbY5n06v7Xs5ri877unKNcU9ndbqO3N95S08lr+8G/zhrJvm0xZ9K3kmnLyO9DDw8aOs0+cwvDsNfA03G78HN6s7DPw0z3KvM5Xr/X4asMf3+WzgwRxlXgA8kkNzCXCT7/P78AbaedT5q8A/2GyAu0Ffs/d9M7A1H5uRZ8IJ913kZ4DSfO8FYFZWHdNaYDnuaoQ93lccN9M/LY9ys689+/PvgFf7Pu8EGnJcXwQ4jPu6ku1v9THpfBXQn2dbLATW2PqFyYY6rcl+Jq3OfrZys+2XrbXZL49ys9tU1xZaG1quL2A/Q7la++VR5wz7+X7+OdxBRCeTg6AMf5ql/aTv8x7se6qk9bjB6168/QVyle397Bz0e+18zivvkM9+SXyDzxzlvspS7idxN22c42vjvhzXVo87KLYdopBq551Z9+fmPOv8etynh7rY8TOd/Qzan/rKTNvPps22Xa5y/bYzaHt0tsuz3LTtTHqd/XJcX4b9DNoHdbbLs86vB35puC+uw73nrPHPr/V9fhzNPjLZWgyxz1Su7/p08fY64N+xxL4c5bZayv0kOWKf4fq0sU9TtjH25ajzQtzJk3ZspbOdSauznU2rs12usv32M2h/b7Df8jzKbbWU+6DBfsss15dhO0u5Advl2Q6BuEfWeFdnP5PW1vd0Wp39bOXq+p5fS+5xp63cVoLjxuy2MPY/w/WZxp3Z5drGnbY6p/qebv7xI/RxzzpXITPuGbVoxiy5yvZ+lop9Ou1u9LEvn3JfZSn3h+jjnu36suOeqY21Y5Y866yNfXhzQvKLexnzR+xxL60ld9zTzUtNce+ruJts5xP3cs53DW2RM/ZlXV+uuJcqN5+4p6uzds5g+/pbeaXuOWCBUmqOUiqGuzzz/v9toUophft++BbHcb6TQ9uglKr2vi8BXofrAAI4jvNpx3FmOI7T6tX1D47jvNdQbplSqiL1PW4H3miqh+M4h4D9SqlF3o9egzswtnEpuZcM7wNOU0qVeu3yGty9YrQopRq9f2fhZtt/nqP8+4H3e9+/H7gvhz5vlFJvxH3l4W2O4wzn0C7wfbwAsw1fchyn0XGcVs+ObbgbNx8ylNvs+/h2LDbEDXKv9v7fQiafANl4LfCy4zhtOXQHgXO978/DPR1Ci8+GIeDfcDfTs/WLgA2Psg9ptTr7WbQB++m0JvvhOnBduVr7Wa7PZENTW2TYz1JuwH6WtgjYz+CntuCuqrnY+68p2+Xt0zyNVq+Uuhp4A3Cp4+0vYNBuVUrN913/27z/r9OudRxnms9+w47jzLfUodlX7oXARsv1pW3ntfW2HG1xMW4SZDRHW2wBqrz7gdTPLHVO2a8I+BfcjWR1seMynf2OJs6YtDrb6bTA5TrbGcqt0dnOUoeA7Wx11tkvR1tk2M9wfRfobGepc8B23mdTPNf5zrxjv0lr8J0mrc536rTPGXzngKHcgO+0XJvWb+Zoi2zfadLqfKepLQK+0zK2CtjuaMZhJq1p3GLRB+xn0K4zjF1eMpQbsJ/l+nT222xpiwzbWcoN2M7SDtpxi4/s8a5t7JnP2FirNdnPoLWNO9PaPMad2eXmGndmX59t7KlrC9O4M1trG3dm11lnP938YzOauGfQmuYqWq0u7uXQB2KfQfsdXeyzlKuLfabrC8S9HG2RPW4xtXEg7uVoC1Ps080JtX3PoNWi05r6nkGr7Xsa7W2mvmco19j3DNdnin2mtgj0PYNW2/cMdc7lO+0cTXbqWP7CfQdxG25W8LMW3R2475tOeDfMVRbtWbjv96eOTMw42jBLuwL3WOQXvRvrc3nW+1VYXqnDPXlvA5NHZhuvzfd/TsA9/vBF7yausWjLcLPcVXmU+wXczrgR94SFIov2SVxntQF4TS4b4O5D8nuvMzwG1ObQv937fgw3SfCwRbsDd4+vlA2vt2h/5V3fi7jHQraYtFnXtIfJpya6cn+Ce9Tki7hOttmijeE+qd+Ie7zoebnuX9zTLP4+j3Y+C3e55Abc5aEnWbQfw+1T23Df205lybX9QmdDizZgP4s2YD+LNmA/k1ZnP0u5JvuZ9AEb2uqRbT9LuQH7WbQB+2HwU7h+Zo3X1nfhLuE2aa/xbBfHDWapJdYmfRzXL6fq9jmdFnfp8Z+9dt6Iu3Kn0lRulv0Gc9ThD75yf4r7+qFJW437VP0l3Cdkx9vqgPvE7Y35xAPc+/4lz36Pe+1u0n4Ld3C3laxjebNjh85+Fq3WfgZtwHY6rcl2+cQ69K9D+usQsF0OfcB+tnro7GcoN2A7i1ZrOwzxHL3vNGl1vtOk1flOk1bnO3OOP5j0naZyA77TotXGPls9CPpOU9k632nSmmJfYGyls51Faxq36LTacYtFbxq7WMeDZI5ddOWaYp9Oa7Kftg7ZtrOUaxq36LRa23n6wHjXYj+d1mQ/ndY07tRpTbazjs+zbKcrV2s7i95kP209DPbTlWuyn05r6nuB+QeGuGfQmsYtOq0x7hn02tin05pin6FcbewzaLVxz1QH9OMWXbnGuGfQm2JfYE6Iue/ptKa+p9Oa+p5Oa+p7xjmspu/pyrX1PZ3e1Pe09UDf93TlmvqeTmv0nfl8pTqqIAiCIAiCIAiCIAiCIEwJfyuv1AmCIAiCIAiCIAiCIAh/JSThJAiCIAiCIAiCIAiCIEwpknASBEEQBEEQBEEQBEEQphRJOAmCIAiCIAiCIAiCIAhTiiScBEEQBEEQBEEQBEEQhClFEk6CIAiCIAgFjFLKUUrNf6XrIQiCIAiCcDRIwkkQBEEQBOEoUErtUUqNKKUGfV//9UrXSxAEQRAEoZCIvNIVEARBEARBOAZ5q+M4j73SlRAEQRAEQShUZIWTIAiCIAjCFKCUukIp9Wel1H8ppfqUUi8rpV7j+/10pdT9SqlupdQOpdTf+X4XVkp9Rim1Uyk1oJRaq5Sa6Sv+tUqp7UqpXqXUfyullPf/5iul/uT9vU6l1C/+ipcsCIIgCIJgRFY4CYIgCIIgTB2nAncD9cBFwD1KqTmO43QDdwIbgenAYuBRpdROx3H+AHwcuBQ4H9gGrACGfeW+BTgZqATWAg8AvwO+BDwCvBqIAav+ry9QEARBEAQhH5TjOK90HQRBEARBEI4ZlFJ7cBNKcd+PPwVMAF8FWhxvgKWUWgP8AHgc2ANUO44z4P3ua0Cz4zhXKKW2Atc6jnOf5u85wNmO4zzlff4lsM5xnK8rpW4HRoEvOo7T9n9wuYIgCIIgCH8R8kqdIAiCIAjC0XOh4zjVvq8bvJ8fcDKf5u3FXdE0HehOJZt8v2vxvp8J7LT8vUO+74eBcu/7awEFrFFKbVJKXfkXXo8gCIIgCMKUIgknQRAEQRCEqaMltb+SxyzgoPdVq5SqyPrdAe/7/cC8o/1jjuMcchzn7xzHmQ58CPihUmr+X1Z1QRAEQRCEqUMSToIgCIIgCFNHI3CNUiqqlLoEWAL81nGc/cDTwNeUUsVKqRXAVcBPvf93I/AlpdQC5bJCKVWX648ppS5RSs3wPvYADpCc6osSBEEQBEE4WmTTcEEQBEEQhKPnAaVUwvf5UeA+YDWwAOgEDgMXO47T5WkuBa7HXe3UA3zecZzHvN99ByjC3QC8HngZeHse9TgZ+J5Sqsr7ex9zHGfX/+bCBEEQBEEQpgLZNFwQBEEQBGEKUEpdAVztOM5Zr3RdBEEQBEEQXmnklTpBEARBEARBEARBEARhSpGEkyAIgiAIgiAIgiAIgjClyCt1giAIgiAIgiAIgiAIwpQiK5wEQRAEQRAEQRAEQRCEKUUSToIgCIIgCIIgCIIgCMKUIgknQRAEQRAEQRAEQRAEYUqRhJMgCIIgCIIgCIIgCIIwpUjCSRAEQRAEQRAEQRAEQZhS/h8eZnJxoULcXgAAAABJRU5ErkJggg==\n",
"text/plain": [
""
]
@@ -345,7 +326,7 @@
"sns.pointplot(x3, dataplot3['sgd_mse_lr_0.001'], color=color[8])\n",
"sns.pointplot(x3, dataplot3['sgd_mse_lr_0.0025'], color=color[3])\n",
"sns.pointplot(x3, dataplot3['sgd_mse_lr_0.005'], color=color[7])\n",
- "color_patch1 = mpatches.Patch(color=color[9], label=\"svrg_mse_0.025\")\n",
+ "color_patch1 = mpatches.Patch(color=color[9], label=\"svrg_mse_lr_0.025\")\n",
"color_patch2 = mpatches.Patch(color=color[8], label=\"sgd_mse_lr_0.001\")\n",
"color_patch3 = mpatches.Patch(color=color[3], label=\"sgd_mse_lr_0.0025\")\n",
"color_patch4 = mpatches.Patch(color=color[7], label=\"sgd_mse_lr_0.005\")\n",
@@ -357,21 +338,21 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Python 2",
+ "display_name": "Python 3",
"language": "python",
- "name": "python2"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
- "version": 2
+ "version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.15"
+ "pygments_lexer": "ipython3",
+ "version": "3.6.4"
}
},
"nbformat": 4,
diff --git a/example/svrg_module/linear_regression/data_reader.py b/example/svrg_module/linear_regression/data_reader.py
index d56ae03a5f4f..23847d531946 100644
--- a/example/svrg_module/linear_regression/data_reader.py
+++ b/example/svrg_module/linear_regression/data_reader.py
@@ -15,21 +15,35 @@
# specific language governing permissions and limitations
# under the License.
+import bz2
+import os
+import shutil
+import mxnet as mx
import numpy as np
from sklearn.datasets import load_svmlight_file
+
# Download data file
-# from subprocess import call
# YearPredictionMSD dataset: https://archive.ics.uci.edu/ml/datasets/yearpredictionmsd
-# call(['wget', 'https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/YearPredictionMSD.bz2'])
-# call(['bzip2', '-d', 'YearPredictionMSD.bz2'])
-def read_year_prediction_data(fileName):
+def get_year_prediction_data(dirname=None):
feature_dim = 90
+ if dirname is None:
+ dirname = os.path.join(os.path.dirname(__file__), 'data')
+ filename = 'YearPredictionMSD'
+ download_filename = os.path.join(dirname, "%s.bz2" % filename)
+ extracted_filename = os.path.join(dirname, filename)
+ if not os.path.isfile(download_filename):
+ print("Downloading data...")
+ mx.test_utils.download('https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/%s.bz2' % filename, dirname=dirname)
+ if not os.path.isfile(extracted_filename):
+ print("Extracting data...")
+ with bz2.BZ2File(download_filename) as fr, open(extracted_filename,"wb") as fw:
+ shutil.copyfileobj(fr,fw)
print("Reading data from disk...")
- train_features, train_labels = load_svmlight_file(fileName, n_features=feature_dim, dtype=np.float32)
+ train_features, train_labels = load_svmlight_file(extracted_filename, n_features=feature_dim, dtype=np.float32)
train_features = train_features.todense()
# normalize the data: subtract means and divide by standard deviations
diff --git a/example/svrg_module/linear_regression/train.py b/example/svrg_module/linear_regression/train.py
index b3d942973f19..6b6574c96189 100644
--- a/example/svrg_module/linear_regression/train.py
+++ b/example/svrg_module/linear_regression/train.py
@@ -19,7 +19,7 @@
import argparse
import mxnet as mx
from common import create_lin_reg_network, create_logger
-from data_reader import read_year_prediction_data
+from data_reader import get_year_prediction_data
parser = argparse.ArgumentParser()
parser.add_argument('-e', dest='epochs', help='number of epochs for training phase', type=int, default=100)
@@ -37,7 +37,7 @@
logger = create_logger()
kv = mx.kvstore.create(args.kv_store)
-feature_dim, train_features, train_labels = read_year_prediction_data('YearPredictionMSD')
+feature_dim, train_features, train_labels = get_year_prediction_data()
train_iter, mod = create_lin_reg_network(train_features, train_labels, feature_dim, args.batch_size, args.updateFreq,
ctx, logger)
diff --git a/example/mxnet_adversarial_vae/README.md b/example/vae-gan/README.md
similarity index 100%
rename from example/mxnet_adversarial_vae/README.md
rename to example/vae-gan/README.md
diff --git a/example/mxnet_adversarial_vae/convert_data.py b/example/vae-gan/convert_data.py
similarity index 100%
rename from example/mxnet_adversarial_vae/convert_data.py
rename to example/vae-gan/convert_data.py
diff --git a/example/mxnet_adversarial_vae/vaegan_mxnet.py b/example/vae-gan/vaegan_mxnet.py
similarity index 100%
rename from example/mxnet_adversarial_vae/vaegan_mxnet.py
rename to example/vae-gan/vaegan_mxnet.py
diff --git a/example/vae/VAE_example.ipynb b/example/vae/VAE_example.ipynb
deleted file mode 100755
index e7ec03afdb7e..000000000000
--- a/example/vae/VAE_example.ipynb
+++ /dev/null
@@ -1,1204 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "import mxnet as mx\n",
- "import numpy as np\n",
- "import os\n",
- "import logging\n",
- "import matplotlib.pyplot as plt\n",
- "import matplotlib.cm as cm"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Building a Variational Autoencoder in MXNet\n",
- "\n",
- "#### Xiaoyu Lu, July 5th, 2017\n",
- "\n",
- "This tutorial guides you through the process of building a variational encoder in MXNet. In this notebook we'll focus on an example using the MNIST handwritten digit recognition dataset. Refer to [Auto-Encoding Variational Bayes](https://arxiv.org/abs/1312.6114/) for more details on the model description.\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Prerequisites\n",
- "\n",
- "To complete this tutorial, we need following python packages:\n",
- "\n",
- "- numpy, matplotlib "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 1. Loading the Data\n",
- "\n",
- "We first load the MNIST dataset, which contains 60000 training and 10000 test examples. The following code imports required modules and loads the data. These images are stored in a 4-D matrix with shape (`batch_size, num_channels, width, height`). For the MNIST dataset, there is only one color channel, and both width and height are 28, so we reshape each image as a 28x28 array. See below for a visualization:\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "mnist = mx.test_utils.get_mnist()\n",
- "image = np.reshape(mnist['train_data'],(60000,28*28))\n",
- "label = image\n",
- "image_test = np.reshape(mnist['test_data'],(10000,28*28))\n",
- "label_test = image_test\n",
- "[N,features] = np.shape(image) #number of examples and features"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "data": {
- "image/png": "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\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "f, (ax1, ax2, ax3, ax4) = plt.subplots(1,4, sharex='col', sharey='row',figsize=(12,3))\n",
- "ax1.imshow(np.reshape(image[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax2.imshow(np.reshape(image[1,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax3.imshow(np.reshape(image[2,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax4.imshow(np.reshape(image[3,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can optionally save the parameters in the directory variable 'model_prefix'. We first create data iterators for MXNet, with each batch of data containing 100 images."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "model_prefix = None\n",
- "\n",
- "batch_size = 100\n",
- "nd_iter = mx.io.NDArrayIter(data={'data':image},label={'loss_label':label},\n",
- " batch_size = batch_size)\n",
- "nd_iter_test = mx.io.NDArrayIter(data={'data':image_test},label={'loss_label':label_test},\n",
- " batch_size = batch_size)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 2. Building the Network Architecture\n",
- "\n",
- "### 2.1 Gaussian MLP as encoder\n",
- "Next we constuct the neural network, as in the [paper](https://arxiv.org/abs/1312.6114/), we use *Multilayer Perceptron (MLP)* for both the encoder and decoder. For encoder, a Gaussian MLP is used as follows:\n",
- "\n",
- "\\begin{align}\n",
- "\\log q_{\\phi}(z|x) &= \\log \\mathcal{N}(z:\\mu,\\sigma^2I) \\\\\n",
- "\\textit{ where } \\mu &= W_2h+b_2, \\log \\sigma^2 = W_3h+b_3\\\\\n",
- "h &= \\tanh(W_1x+b_1)\n",
- "\\end{align}\n",
- "\n",
- "where $\\{W_1,W_2,W_3,b_1,b_2,b_3\\}$ are the weights and biases of the MLP.\n",
- "Note below that `encoder_mu`(`mu`) and `encoder_logvar`(`logvar`) are symbols. So, we can use `get_internals()` to get the values of them, after which we can sample the latent variable $z$.\n",
- "\n",
- "\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "## define data and loss labels as symbols \n",
- "data = mx.sym.var('data')\n",
- "loss_label = mx.sym.var('loss_label')\n",
- "\n",
- "## define fully connected and activation layers for the encoder, where we used tanh activation function.\n",
- "encoder_h = mx.sym.FullyConnected(data=data, name=\"encoder_h\",num_hidden=400)\n",
- "act_h = mx.sym.Activation(data=encoder_h, act_type=\"tanh\",name=\"activation_h\")\n",
- "\n",
- "## define mu and log variance which are the fully connected layers of the previous activation layer\n",
- "mu = mx.sym.FullyConnected(data=act_h, name=\"mu\",num_hidden = 5)\n",
- "logvar = mx.sym.FullyConnected(data=act_h, name=\"logvar\",num_hidden = 5)\n",
- "\n",
- "## sample the latent variables z according to Normal(mu,var)\n",
- "z = mu + np.multiply(mx.symbol.exp(0.5 * logvar), \n",
- " mx.symbol.random_normal(loc=0, scale=1, shape=np.shape(logvar.get_internals()[\"logvar_output\"])))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### 2.2 Bernoulli MLP as decoder\n",
- "\n",
- "In this case let $p_\\theta(x|z)$ be a multivariate Bernoulli whose probabilities are computed from $z$ with a feed forward neural network with a single hidden layer:\n",
- "\n",
- "\\begin{align}\n",
- "\\log p(x|z) &= \\sum_{i=1}^D x_i\\log y_i + (1-x_i)\\log (1-y_i) \\\\\n",
- "\\textit{ where } y &= f_\\sigma(W_5\\tanh (W_4z+b_4)+b_5)\n",
- "\\end{align}\n",
- "\n",
- "where $f_\\sigma(\\dot)$ is the elementwise sigmoid activation function, $\\{W_4,W_5,b_4,b_5\\}$ are the weights and biases of the decoder MLP. A Bernouilli likelihood is suitable for this type of data but you can easily extend it to other likelihood types by parsing into the argument `likelihood` in the `VAE` class, see section 4 for details."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "# define fully connected and tanh activation layers for the decoder\n",
- "decoder_z = mx.sym.FullyConnected(data=z, name=\"decoder_z\",num_hidden=400)\n",
- "act_z = mx.sym.Activation(data=decoder_z, act_type=\"tanh\",name=\"activation_z\")\n",
- "\n",
- "# define the output layer with sigmoid activation function, where the dimension is equal to the input dimension\n",
- "decoder_x = mx.sym.FullyConnected(data=act_z, name=\"decoder_x\",num_hidden=features)\n",
- "y = mx.sym.Activation(data=decoder_x, act_type=\"sigmoid\",name='activation_x')"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### 2.3 Joint Loss Function for the Encoder and the Decoder\n",
- "\n",
- "The variational lower bound also called evidence lower bound (ELBO) can be estimated as:\n",
- "\n",
- "\\begin{align}\n",
- "\\mathcal{L}(\\theta,\\phi;x_{(i)}) \\approx \\frac{1}{2}\\left(1+\\log ((\\sigma_j^{(i)})^2)-(\\mu_j^{(i)})^2-(\\sigma_j^{(i)})^2\\right) + \\log p_\\theta(x^{(i)}|z^{(i)})\n",
- "\\end{align}\n",
- "\n",
- "where the first term is the KL divergence of the approximate posterior from the prior, and the second term is an expected negative reconstruction error. We would like to maximize this lower bound, so we can define the loss to be $-\\mathcal{L}$(minus ELBO) for MXNet to minimize."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "# define the objective loss function that needs to be minimized\n",
- "KL = 0.5*mx.symbol.sum(1+logvar-pow( mu,2)-mx.symbol.exp(logvar),axis=1)\n",
- "loss = -mx.symbol.sum(mx.symbol.broadcast_mul(loss_label,mx.symbol.log(y)) \n",
- " + mx.symbol.broadcast_mul(1-loss_label,mx.symbol.log(1-y)),axis=1)-KL\n",
- "output = mx.symbol.MakeLoss(sum(loss),name='loss')"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 3. Training the model\n",
- "\n",
- "Now, we can define the model and train it. First we will initilize the weights and the biases to be Gaussian(0,0.01), and then use stochastic gradient descent for optimization. To warm start the training, one may also initilize with pre-trainined parameters `arg_params` using `init=mx.initializer.Load(arg_params)`. \n",
- "\n",
- "To save intermediate results, we can optionally use `epoch_end_callback = mx.callback.do_checkpoint(model_prefix, 1)` which saves the parameters to the path given by model_prefix, and with period every $1$ epoch. To assess the performance, we output $-\\mathcal{L}$(minus ELBO) after each epoch, with the command `eval_metric = 'Loss'` which is defined above. We will also plot the training loss for mini batches by accessing the log and saving it to a list, and then parsing it to the argument `batch_end_callback`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "# set up the log\n",
- "nd_iter.reset()\n",
- "logging.getLogger().setLevel(logging.DEBUG) \n",
- "\n",
- "# define function to trave back training loss\n",
- "def log_to_list(period, lst):\n",
- " def _callback(param):\n",
- " \"\"\"The checkpoint function.\"\"\"\n",
- " if param.nbatch % period == 0:\n",
- " name, value = param.eval_metric.get()\n",
- " lst.append(value)\n",
- " return _callback\n",
- "\n",
- "# define the model\n",
- "model = mx.mod.Module(\n",
- " symbol = output ,\n",
- " data_names=['data'],\n",
- " label_names = ['loss_label'])"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "INFO:root:Epoch[0] Train-loss=375.023381\n",
- "INFO:root:Epoch[0] Time cost=6.127\n",
- "INFO:root:Epoch[1] Train-loss=212.780315\n",
- "INFO:root:Epoch[1] Time cost=6.409\n",
- "INFO:root:Epoch[2] Train-loss=208.209400\n",
- "INFO:root:Epoch[2] Time cost=6.619\n",
- "INFO:root:Epoch[3] Train-loss=206.146854\n",
- "INFO:root:Epoch[3] Time cost=6.648\n",
- "INFO:root:Epoch[4] Train-loss=204.530598\n",
- "INFO:root:Epoch[4] Time cost=7.000\n",
- "INFO:root:Epoch[5] Train-loss=202.799992\n",
- "INFO:root:Epoch[5] Time cost=6.778\n",
- "INFO:root:Epoch[6] Train-loss=200.333474\n",
- "INFO:root:Epoch[6] Time cost=7.187\n",
- "INFO:root:Epoch[7] Train-loss=197.506393\n",
- "INFO:root:Epoch[7] Time cost=6.712\n",
- "INFO:root:Epoch[8] Train-loss=195.969775\n",
- "INFO:root:Epoch[8] Time cost=6.896\n",
- "INFO:root:Epoch[9] Train-loss=195.418288\n",
- "INFO:root:Epoch[9] Time cost=6.887\n",
- "INFO:root:Epoch[10] Train-loss=194.739763\n",
- "INFO:root:Epoch[10] Time cost=6.745\n",
- "INFO:root:Epoch[11] Train-loss=194.380536\n",
- "INFO:root:Epoch[11] Time cost=6.706\n",
- "INFO:root:Epoch[12] Train-loss=193.955462\n",
- "INFO:root:Epoch[12] Time cost=6.592\n",
- "INFO:root:Epoch[13] Train-loss=193.493671\n",
- "INFO:root:Epoch[13] Time cost=6.775\n",
- "INFO:root:Epoch[14] Train-loss=192.958739\n",
- "INFO:root:Epoch[14] Time cost=6.600\n",
- "INFO:root:Epoch[15] Train-loss=191.928542\n",
- "INFO:root:Epoch[15] Time cost=6.586\n",
- "INFO:root:Epoch[16] Train-loss=189.797939\n",
- "INFO:root:Epoch[16] Time cost=6.700\n",
- "INFO:root:Epoch[17] Train-loss=186.672446\n",
- "INFO:root:Epoch[17] Time cost=6.869\n",
- "INFO:root:Epoch[18] Train-loss=184.616599\n",
- "INFO:root:Epoch[18] Time cost=7.144\n",
- "INFO:root:Epoch[19] Train-loss=183.305978\n",
- "INFO:root:Epoch[19] Time cost=6.997\n",
- "INFO:root:Epoch[20] Train-loss=181.944634\n",
- "INFO:root:Epoch[20] Time cost=6.481\n",
- "INFO:root:Epoch[21] Train-loss=181.005329\n",
- "INFO:root:Epoch[21] Time cost=6.754\n",
- "INFO:root:Epoch[22] Train-loss=178.363118\n",
- "INFO:root:Epoch[22] Time cost=7.000\n",
- "INFO:root:Epoch[23] Train-loss=176.363421\n",
- "INFO:root:Epoch[23] Time cost=6.923\n",
- "INFO:root:Epoch[24] Train-loss=174.573954\n",
- "INFO:root:Epoch[24] Time cost=6.510\n",
- "INFO:root:Epoch[25] Train-loss=173.245940\n",
- "INFO:root:Epoch[25] Time cost=6.926\n",
- "INFO:root:Epoch[26] Train-loss=172.082522\n",
- "INFO:root:Epoch[26] Time cost=6.733\n",
- "INFO:root:Epoch[27] Train-loss=171.123084\n",
- "INFO:root:Epoch[27] Time cost=6.616\n",
- "INFO:root:Epoch[28] Train-loss=170.239300\n",
- "INFO:root:Epoch[28] Time cost=7.004\n",
- "INFO:root:Epoch[29] Train-loss=169.538416\n",
- "INFO:root:Epoch[29] Time cost=6.341\n",
- "INFO:root:Epoch[30] Train-loss=168.952901\n",
- "INFO:root:Epoch[30] Time cost=6.736\n",
- "INFO:root:Epoch[31] Train-loss=168.169076\n",
- "INFO:root:Epoch[31] Time cost=6.616\n",
- "INFO:root:Epoch[32] Train-loss=167.208973\n",
- "INFO:root:Epoch[32] Time cost=6.446\n",
- "INFO:root:Epoch[33] Train-loss=165.732213\n",
- "INFO:root:Epoch[33] Time cost=6.405\n",
- "INFO:root:Epoch[34] Train-loss=163.606801\n",
- "INFO:root:Epoch[34] Time cost=6.139\n",
- "INFO:root:Epoch[35] Train-loss=161.985880\n",
- "INFO:root:Epoch[35] Time cost=6.678\n",
- "INFO:root:Epoch[36] Train-loss=160.763072\n",
- "INFO:root:Epoch[36] Time cost=8.749\n",
- "INFO:root:Epoch[37] Train-loss=160.025193\n",
- "INFO:root:Epoch[37] Time cost=6.519\n",
- "INFO:root:Epoch[38] Train-loss=159.319723\n",
- "INFO:root:Epoch[38] Time cost=7.584\n",
- "INFO:root:Epoch[39] Train-loss=158.670701\n",
- "INFO:root:Epoch[39] Time cost=6.874\n",
- "INFO:root:Epoch[40] Train-loss=158.225733\n",
- "INFO:root:Epoch[40] Time cost=6.402\n",
- "INFO:root:Epoch[41] Train-loss=157.741337\n",
- "INFO:root:Epoch[41] Time cost=8.617\n",
- "INFO:root:Epoch[42] Train-loss=157.301411\n",
- "INFO:root:Epoch[42] Time cost=6.515\n",
- "INFO:root:Epoch[43] Train-loss=156.765170\n",
- "INFO:root:Epoch[43] Time cost=6.447\n",
- "INFO:root:Epoch[44] Train-loss=156.389668\n",
- "INFO:root:Epoch[44] Time cost=6.130\n",
- "INFO:root:Epoch[45] Train-loss=155.815434\n",
- "INFO:root:Epoch[45] Time cost=6.155\n",
- "INFO:root:Epoch[46] Train-loss=155.432254\n",
- "INFO:root:Epoch[46] Time cost=6.158\n",
- "INFO:root:Epoch[47] Train-loss=155.114027\n",
- "INFO:root:Epoch[47] Time cost=6.749\n",
- "INFO:root:Epoch[48] Train-loss=154.612441\n",
- "INFO:root:Epoch[48] Time cost=6.255\n",
- "INFO:root:Epoch[49] Train-loss=154.137659\n",
- "INFO:root:Epoch[49] Time cost=7.813\n",
- "INFO:root:Epoch[50] Train-loss=153.634072\n",
- "INFO:root:Epoch[50] Time cost=7.408\n",
- "INFO:root:Epoch[51] Train-loss=153.417397\n",
- "INFO:root:Epoch[51] Time cost=7.747\n",
- "INFO:root:Epoch[52] Train-loss=152.851887\n",
- "INFO:root:Epoch[52] Time cost=8.587\n",
- "INFO:root:Epoch[53] Train-loss=152.575068\n",
- "INFO:root:Epoch[53] Time cost=7.554\n",
- "INFO:root:Epoch[54] Train-loss=152.084419\n",
- "INFO:root:Epoch[54] Time cost=6.628\n",
- "INFO:root:Epoch[55] Train-loss=151.724836\n",
- "INFO:root:Epoch[55] Time cost=6.535\n",
- "INFO:root:Epoch[56] Train-loss=151.302525\n",
- "INFO:root:Epoch[56] Time cost=7.148\n",
- "INFO:root:Epoch[57] Train-loss=150.960916\n",
- "INFO:root:Epoch[57] Time cost=7.195\n",
- "INFO:root:Epoch[58] Train-loss=150.603895\n",
- "INFO:root:Epoch[58] Time cost=6.649\n",
- "INFO:root:Epoch[59] Train-loss=150.237795\n",
- "INFO:root:Epoch[59] Time cost=6.222\n",
- "INFO:root:Epoch[60] Train-loss=149.936080\n",
- "INFO:root:Epoch[60] Time cost=8.450\n",
- "INFO:root:Epoch[61] Train-loss=149.514617\n",
- "INFO:root:Epoch[61] Time cost=6.113\n",
- "INFO:root:Epoch[62] Train-loss=149.229345\n",
- "INFO:root:Epoch[62] Time cost=6.088\n",
- "INFO:root:Epoch[63] Train-loss=148.893769\n",
- "INFO:root:Epoch[63] Time cost=6.558\n",
- "INFO:root:Epoch[64] Train-loss=148.526837\n",
- "INFO:root:Epoch[64] Time cost=7.590\n",
- "INFO:root:Epoch[65] Train-loss=148.249951\n",
- "INFO:root:Epoch[65] Time cost=6.180\n",
- "INFO:root:Epoch[66] Train-loss=147.940414\n",
- "INFO:root:Epoch[66] Time cost=6.242\n",
- "INFO:root:Epoch[67] Train-loss=147.621304\n",
- "INFO:root:Epoch[67] Time cost=8.501\n",
- "INFO:root:Epoch[68] Train-loss=147.294314\n",
- "INFO:root:Epoch[68] Time cost=7.645\n",
- "INFO:root:Epoch[69] Train-loss=147.074479\n",
- "INFO:root:Epoch[69] Time cost=7.092\n",
- "INFO:root:Epoch[70] Train-loss=146.796387\n",
- "INFO:root:Epoch[70] Time cost=6.914\n",
- "INFO:root:Epoch[71] Train-loss=146.508842\n",
- "INFO:root:Epoch[71] Time cost=6.606\n",
- "INFO:root:Epoch[72] Train-loss=146.230444\n",
- "INFO:root:Epoch[72] Time cost=7.755\n",
- "INFO:root:Epoch[73] Train-loss=145.970296\n",
- "INFO:root:Epoch[73] Time cost=6.409\n",
- "INFO:root:Epoch[74] Train-loss=145.711610\n",
- "INFO:root:Epoch[74] Time cost=6.334\n",
- "INFO:root:Epoch[75] Train-loss=145.460053\n",
- "INFO:root:Epoch[75] Time cost=7.269\n",
- "INFO:root:Epoch[76] Train-loss=145.156451\n",
- "INFO:root:Epoch[76] Time cost=6.744\n",
- "INFO:root:Epoch[77] Train-loss=144.957674\n",
- "INFO:root:Epoch[77] Time cost=7.100\n",
- "INFO:root:Epoch[78] Train-loss=144.729749\n",
- "INFO:root:Epoch[78] Time cost=6.242\n",
- "INFO:root:Epoch[79] Train-loss=144.481728\n",
- "INFO:root:Epoch[79] Time cost=6.865\n",
- "INFO:root:Epoch[80] Train-loss=144.236061\n",
- "INFO:root:Epoch[80] Time cost=6.632\n",
- "INFO:root:Epoch[81] Train-loss=144.030473\n",
- "INFO:root:Epoch[81] Time cost=6.764\n",
- "INFO:root:Epoch[82] Train-loss=143.776374\n",
- "INFO:root:Epoch[82] Time cost=6.564\n",
- "INFO:root:Epoch[83] Train-loss=143.538847\n",
- "INFO:root:Epoch[83] Time cost=6.181\n",
- "INFO:root:Epoch[84] Train-loss=143.326444\n",
- "INFO:root:Epoch[84] Time cost=6.220\n",
- "INFO:root:Epoch[85] Train-loss=143.078987\n",
- "INFO:root:Epoch[85] Time cost=6.823\n",
- "INFO:root:Epoch[86] Train-loss=142.877117\n",
- "INFO:root:Epoch[86] Time cost=7.755\n",
- "INFO:root:Epoch[87] Train-loss=142.667316\n",
- "INFO:root:Epoch[87] Time cost=6.068\n",
- "INFO:root:Epoch[88] Train-loss=142.461755\n",
- "INFO:root:Epoch[88] Time cost=6.111\n",
- "INFO:root:Epoch[89] Train-loss=142.270438\n",
- "INFO:root:Epoch[89] Time cost=6.221\n",
- "INFO:root:Epoch[90] Train-loss=142.047086\n",
- "INFO:root:Epoch[90] Time cost=8.061\n",
- "INFO:root:Epoch[91] Train-loss=141.855774\n",
- "INFO:root:Epoch[91] Time cost=6.433\n",
- "INFO:root:Epoch[92] Train-loss=141.688955\n",
- "INFO:root:Epoch[92] Time cost=7.153\n",
- "INFO:root:Epoch[93] Train-loss=141.442910\n",
- "INFO:root:Epoch[93] Time cost=7.113\n",
- "INFO:root:Epoch[94] Train-loss=141.279274\n",
- "INFO:root:Epoch[94] Time cost=7.152\n",
- "INFO:root:Epoch[95] Train-loss=141.086522\n",
- "INFO:root:Epoch[95] Time cost=6.472\n",
- "INFO:root:Epoch[96] Train-loss=140.901925\n",
- "INFO:root:Epoch[96] Time cost=6.767\n",
- "INFO:root:Epoch[97] Train-loss=140.722496\n",
- "INFO:root:Epoch[97] Time cost=7.044\n",
- "INFO:root:Epoch[98] Train-loss=140.579295\n",
- "INFO:root:Epoch[98] Time cost=7.040\n",
- "INFO:root:Epoch[99] Train-loss=140.386067\n",
- "INFO:root:Epoch[99] Time cost=6.669\n"
- ]
- }
- ],
- "source": [
- "# training the model, save training loss as a list.\n",
- "training_loss=list()\n",
- "\n",
- "# initilize the parameters for training using Normal.\n",
- "init = mx.init.Normal(0.01)\n",
- "model.fit(nd_iter, # train data\n",
- " initializer=init,\n",
- " # if eval_data is supplied, test loss will also be reported\n",
- " # eval_data = nd_iter_test,\n",
- " optimizer='sgd', # use SGD to train\n",
- " optimizer_params={'learning_rate':1e-3,'wd':1e-2}, \n",
- " # save parameters for each epoch if model_prefix is supplied\n",
- " epoch_end_callback = None if model_prefix==None else mx.callback.do_checkpoint(model_prefix, 1),\n",
- " batch_end_callback = log_to_list(N/batch_size,training_loss), \n",
- " num_epoch=100,\n",
- " eval_metric = 'Loss')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 23,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "data": {
- "image/png": "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\nDiJmZlaz/w/UuBmpwDVyRQAAAABJRU5ErkJggg==\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "ELBO = [-training_loss[i] for i in range(len(training_loss))]\n",
- "plt.plot(ELBO)\n",
- "plt.ylabel('ELBO');plt.xlabel('epoch');plt.title(\"training curve for mini batches\")\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As expected, the ELBO is monotonically increasing over epoch, and we reproduced the results given in the paper [Auto-Encoding Variational Bayes](https://arxiv.org/abs/1312.6114/). Now we can extract/load the parameters and then feed the network forward to calculate $y$ which is the reconstructed image, and we can also calculate the ELBO for the test set. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 80,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "arg_params = model.get_params()[0]\n",
- "\n",
- "# if saved the parameters, can load them using `load_checkpoint` method at e.g. 100th epoch\n",
- "# sym, arg_params, aux_params = mx.model.load_checkpoint(model_prefix, 100)\n",
- "# assert sym.tojson() == output.tojson()\n",
- "\n",
- "e = y.bind(mx.cpu(), {'data': nd_iter_test.data[0][1],\n",
- " 'encoder_h_weight': arg_params['encoder_h_weight'],\n",
- " 'encoder_h_bias': arg_params['encoder_h_bias'],\n",
- " 'mu_weight': arg_params['mu_weight'],\n",
- " 'mu_bias': arg_params['mu_bias'],\n",
- " 'logvar_weight':arg_params['logvar_weight'],\n",
- " 'logvar_bias':arg_params['logvar_bias'],\n",
- " 'decoder_z_weight':arg_params['decoder_z_weight'],\n",
- " 'decoder_z_bias':arg_params['decoder_z_bias'],\n",
- " 'decoder_x_weight':arg_params['decoder_x_weight'],\n",
- " 'decoder_x_bias':arg_params['decoder_x_bias'], \n",
- " 'loss_label':label})\n",
- "\n",
- "x_fit = e.forward()\n",
- "x_construction = x_fit[0].asnumpy()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 78,
- "metadata": {
- "collapsed": false,
- "scrolled": true
- },
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAsMAAADSCAYAAACvmc1VAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3XuUXGWZ7/HfkyvkSkKHJARICDAkxMkBaUm4CZrAwQku\nRAdnogzxNjhneRhdOjOiZ80ZdIYj5yxvzNHlDB4QFEcFuSsjIANyRxIMQhJCQi6E0Em6SUI693R4\nzx+1w5Sp56F7d1d1d9X+ftbKSvev3q7au/dT1W9X7+fdllISAAAAUEQD+noDAAAAgL7CZBgAAACF\nxWQYAAAAhcVkGAAAAIXFZBgAAACFxWQYAAAAhcVkuB8ws/9nZl/u6+0AesLMpphZMrNBwe1LzOzc\nXt4soFPULuoRdVs9TIYlmdn2sn9vmtmuss8/WuvHTyl9KqX0v2r9OKgfZrbGzOb29XZUU0ppRkrp\n4b7eDtQWtYt6RN0Wm/vbRNGklEYc+NjM1kj6VErp19F4MxuUUurojW0DaoU6Rr2idlGPqNv+i3eG\nu8DM/snMfmZmPzGzdkmXmtnNZnZV2Zi52UT6wOdHmdkdZtZqZqvN7DNvc/9v3deB+zGzL2Vf+5qZ\nvd/MLjSzFWa22cz+ruxrTzezp8xsq5m1mNk/m9ngstvfZ2YvmdkbZvZ/zexxM/tY2e2fMrMXzWyL\nmf27mR1dpW8baiSrhcXZMX/CzGaW3Xalmb1sZu1mttTMLi677WPZ8f+Wmb0u6aose8zMvp7VwGoz\ne1/Z14w2s+uz2lqfPRcGZrcNzL6uzcxWSZrXyXa/9c6LmV1lZrdmtd9uZs+b2R9ldb/JzNaZ2fll\nX/txM1uWjV1lZp8+6L7/LtvG17KaTmZ2fHbb0Gw7XzGzjWb2L2Z2aM+OArqD2qV26xF12/h1y2S4\n6y6W9G+SRkv62dsNNLMBkn4h6RlJkySdJ+lvzWxOFx/rKJWOzZGS/lHS9ZL+XNIpks6V9FUzOyYb\n2yHps5KaJJ0p6QJJn8624whJt0j62+z21ZJOK9vOD2W3XSRpnKSns31EP2Vmp0i6QaVjfLikf5V0\nt5kNzYa8LOlsler0K5JuNrOJZXcxS9IqSeMlXV2WLVepRv6PpOvNzLLbblSpxo5Xqf7Ol/Sp7La/\nlHRhljdL+tOcu/N+ST+SNEbS7yTdp1LdT5L01WzfDtiUPdYoSR+X9C0ze2f2PblA0uclzc2289yD\n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\nMgwAAIDCYjIMAACAwmIyDAAAgMLq0WoSZrZGUruk/ZI6UkrN1dgoAMVV61UCelsRViWoRof92+Xe\nJW2jy99GvMu8vl0eHbe8216Nx8ybo+eq9b3Ncz/t7e1u/tprr7l5W1ubm0erRkybNs3NZ82aVZGd\neuqp7thx48a5ebTKRFTrefRGnVdjabX3pJT8IwIAAAD0Y5wmAQAAgMLq6WQ4Sfq1mS0ys8ursUEA\nAABAb+npZPislNLJkt4n6TNm9u6DB5jZ5Wa20MwWtra29vDhgN5D7aIeUbeoV9Qu+kqPJsMppfXZ\n/5sk3SHpNGfMdSml5pRSc3TiNdAfUbuoR9Qt6hW1i77S7QY6MxsuaUBKqT37+HxJX63algENIk+X\ned5O9aijPk+nfX/rVM/zuNXa9lrua72ujlGNuo3qsKOjw82jbvrNmzdXZNu3b3fH7tu3z82jYxx1\n3kf5oEH+j00vP/TQQ92xUR515Odd2YJVJmon77Hw7Nq1y81feuklN3/llVfcPKrRGTNmuPmcOXPc\nvLm5ciGwsWPHumOHDx/u5tHzIu/KKX2lJ6tJjJd0R7ZDgyT9W0rpV1XZKgAAAKAXdHsynFJaJem/\nVHFbAAAAgF7F0moAAAAoLCbDAAAAKCwmwwAAACisalyOud956qmn3Pzaa69180mTJrl51PG7YMGC\niizqvIxy1K9qrPiwd+9ed+y2bdvcPFpzc+vWrW4eddR79u/f7+YDBw5087wrB+S9f68refDgwe7Y\nqLN5woQJbj5mzBg3j+4/6oRuJHnqOe+x37Nnj5u3tLS4+SOPPOLmzz//fEX2+uuvu2Oj59bQoUPd\nfPz48W4eLe01atQoN/dq68QTT3THHnPMMW5e7x35qOStnLJ27Vp37GOPPebmmzZtcvPode7MM890\n89mzZ7t5U1NTRZb3NTHvyj79rXYb/5UeAAAACDAZBgAAQGExGQYAAEBhMRkGAABAYTEZBgAAQGE1\n5GoS3moPkrRixYqq3P/VV19dkY0ePdodG3Vv1oMpU6a4+Ze+9CU3jzqkG03UUe91DUt+R/2rr77q\njn344Yfd/Omnn3bzDRs2uPmWLVvcfOfOnRVZtBJA1DUcrQ7h3ffb3f+QIUPc3OvWj1YC8LqgJens\ns89280svvdTNo5UDvI7n/tYFXStenUfHMqr9aMWHX/ziF25+//33u/mqVasqssMOO8wdG71uRcc4\n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\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "# learning images on the test set\n",
- "f, ((ax1, ax2, ax3, ax4)) = plt.subplots(1,4, sharex='col', sharey='row',figsize=(12,3))\n",
- "ax1.imshow(np.reshape(image_test[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax1.set_title('True image')\n",
- "ax2.imshow(np.reshape(x_construction[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax2.set_title('Learned image')\n",
- "ax3.imshow(np.reshape(x_construction[999,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax3.set_title('Learned image')\n",
- "ax4.imshow(np.reshape(x_construction[9999,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax4.set_title('Learned image')\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 37,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[('loss', 139.73684648437501)]"
- ]
- },
- "execution_count": 37,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# calculate the ELBO which is minus the loss for test set\n",
- "metric = mx.metric.Loss()\n",
- "model.score(nd_iter_test, metric)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 4. All together: MXNet-based class VAE"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "from VAE import VAE"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "One can directly call the class `VAE` to do the training:\n",
- "\n",
- "```VAE(n_latent=5,num_hidden_ecoder=400,num_hidden_decoder=400,x_train=None,x_valid=None,\n",
- "batch_size=100,learning_rate=0.001,weight_decay=0.01,num_epoch=100,optimizer='sgd',model_prefix=None,\n",
- "initializer = mx.init.Normal(0.01),likelihood=Bernoulli)```\n",
- "\n",
- "The outputs are the learned model and training loss."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "INFO:root:Epoch[0] Train-loss=377.146422\n",
- "INFO:root:Epoch[0] Time cost=5.989\n",
- "INFO:root:Epoch[1] Train-loss=211.998043\n",
- "INFO:root:Epoch[1] Time cost=6.303\n",
- "INFO:root:Epoch[2] Train-loss=207.103096\n",
- "INFO:root:Epoch[2] Time cost=7.368\n",
- "INFO:root:Epoch[3] Train-loss=204.958183\n",
- "INFO:root:Epoch[3] Time cost=7.530\n",
- "INFO:root:Epoch[4] Train-loss=203.342700\n",
- "INFO:root:Epoch[4] Time cost=8.887\n",
- "INFO:root:Epoch[5] Train-loss=201.649251\n",
- "INFO:root:Epoch[5] Time cost=9.147\n",
- "INFO:root:Epoch[6] Train-loss=199.782661\n",
- "INFO:root:Epoch[6] Time cost=8.924\n",
- "INFO:root:Epoch[7] Train-loss=198.044015\n",
- "INFO:root:Epoch[7] Time cost=8.920\n",
- "INFO:root:Epoch[8] Train-loss=195.732077\n",
- "INFO:root:Epoch[8] Time cost=8.857\n",
- "INFO:root:Epoch[9] Train-loss=194.070547\n",
- "INFO:root:Epoch[9] Time cost=9.216\n",
- "INFO:root:Epoch[10] Train-loss=193.186871\n",
- "INFO:root:Epoch[10] Time cost=8.966\n",
- "INFO:root:Epoch[11] Train-loss=192.700208\n",
- "INFO:root:Epoch[11] Time cost=8.843\n",
- "INFO:root:Epoch[12] Train-loss=192.191504\n",
- "INFO:root:Epoch[12] Time cost=8.152\n",
- "INFO:root:Epoch[13] Train-loss=191.842837\n",
- "INFO:root:Epoch[13] Time cost=6.180\n",
- "INFO:root:Epoch[14] Train-loss=191.310450\n",
- "INFO:root:Epoch[14] Time cost=6.067\n",
- "INFO:root:Epoch[15] Train-loss=190.520681\n",
- "INFO:root:Epoch[15] Time cost=6.058\n",
- "INFO:root:Epoch[16] Train-loss=189.784146\n",
- "INFO:root:Epoch[16] Time cost=6.046\n",
- "INFO:root:Epoch[17] Train-loss=188.515020\n",
- "INFO:root:Epoch[17] Time cost=6.062\n",
- "INFO:root:Epoch[18] Train-loss=187.530712\n",
- "INFO:root:Epoch[18] Time cost=6.088\n",
- "INFO:root:Epoch[19] Train-loss=186.194826\n",
- "INFO:root:Epoch[19] Time cost=6.491\n",
- "INFO:root:Epoch[20] Train-loss=185.492288\n",
- "INFO:root:Epoch[20] Time cost=6.182\n",
- "INFO:root:Epoch[21] Train-loss=184.922654\n",
- "INFO:root:Epoch[21] Time cost=6.058\n",
- "INFO:root:Epoch[22] Train-loss=184.677911\n",
- "INFO:root:Epoch[22] Time cost=6.042\n",
- "INFO:root:Epoch[23] Train-loss=183.921396\n",
- "INFO:root:Epoch[23] Time cost=5.994\n",
- "INFO:root:Epoch[24] Train-loss=183.600690\n",
- "INFO:root:Epoch[24] Time cost=6.038\n",
- "INFO:root:Epoch[25] Train-loss=183.388476\n",
- "INFO:root:Epoch[25] Time cost=6.025\n",
- "INFO:root:Epoch[26] Train-loss=182.972208\n",
- "INFO:root:Epoch[26] Time cost=6.014\n",
- "INFO:root:Epoch[27] Train-loss=182.561678\n",
- "INFO:root:Epoch[27] Time cost=6.064\n",
- "INFO:root:Epoch[28] Train-loss=182.475261\n",
- "INFO:root:Epoch[28] Time cost=5.983\n",
- "INFO:root:Epoch[29] Train-loss=182.308808\n",
- "INFO:root:Epoch[29] Time cost=6.371\n",
- "INFO:root:Epoch[30] Train-loss=182.135900\n",
- "INFO:root:Epoch[30] Time cost=6.038\n",
- "INFO:root:Epoch[31] Train-loss=181.978367\n",
- "INFO:root:Epoch[31] Time cost=6.924\n",
- "INFO:root:Epoch[32] Train-loss=181.677153\n",
- "INFO:root:Epoch[32] Time cost=8.205\n",
- "INFO:root:Epoch[33] Train-loss=181.677775\n",
- "INFO:root:Epoch[33] Time cost=6.017\n",
- "INFO:root:Epoch[34] Train-loss=181.257998\n",
- "INFO:root:Epoch[34] Time cost=6.056\n",
- "INFO:root:Epoch[35] Train-loss=181.125288\n",
- "INFO:root:Epoch[35] Time cost=6.020\n",
- "INFO:root:Epoch[36] Train-loss=181.018858\n",
- "INFO:root:Epoch[36] Time cost=6.035\n",
- "INFO:root:Epoch[37] Train-loss=180.785110\n",
- "INFO:root:Epoch[37] Time cost=6.049\n",
- "INFO:root:Epoch[38] Train-loss=180.452598\n",
- "INFO:root:Epoch[38] Time cost=6.083\n",
- "INFO:root:Epoch[39] Train-loss=180.362733\n",
- "INFO:root:Epoch[39] Time cost=6.198\n",
- "INFO:root:Epoch[40] Train-loss=180.060788\n",
- "INFO:root:Epoch[40] Time cost=6.049\n",
- "INFO:root:Epoch[41] Train-loss=180.022728\n",
- "INFO:root:Epoch[41] Time cost=6.135\n",
- "INFO:root:Epoch[42] Train-loss=179.648499\n",
- "INFO:root:Epoch[42] Time cost=6.055\n",
- "INFO:root:Epoch[43] Train-loss=179.507952\n",
- "INFO:root:Epoch[43] Time cost=6.108\n",
- "INFO:root:Epoch[44] Train-loss=179.303132\n",
- "INFO:root:Epoch[44] Time cost=6.020\n",
- "INFO:root:Epoch[45] Train-loss=178.945211\n",
- "INFO:root:Epoch[45] Time cost=6.004\n",
- "INFO:root:Epoch[46] Train-loss=178.808598\n",
- "INFO:root:Epoch[46] Time cost=6.016\n",
- "INFO:root:Epoch[47] Train-loss=178.550906\n",
- "INFO:root:Epoch[47] Time cost=6.050\n",
- "INFO:root:Epoch[48] Train-loss=178.403674\n",
- "INFO:root:Epoch[48] Time cost=6.115\n",
- "INFO:root:Epoch[49] Train-loss=178.237544\n",
- "INFO:root:Epoch[49] Time cost=6.004\n",
- "INFO:root:Epoch[50] Train-loss=178.033747\n",
- "INFO:root:Epoch[50] Time cost=6.051\n",
- "INFO:root:Epoch[51] Train-loss=177.802884\n",
- "INFO:root:Epoch[51] Time cost=6.028\n",
- "INFO:root:Epoch[52] Train-loss=177.533980\n",
- "INFO:root:Epoch[52] Time cost=6.052\n",
- "INFO:root:Epoch[53] Train-loss=177.490143\n",
- "INFO:root:Epoch[53] Time cost=6.019\n",
- "INFO:root:Epoch[54] Train-loss=177.136637\n",
- "INFO:root:Epoch[54] Time cost=6.014\n",
- "INFO:root:Epoch[55] Train-loss=177.062524\n",
- "INFO:root:Epoch[55] Time cost=6.024\n",
- "INFO:root:Epoch[56] Train-loss=176.869033\n",
- "INFO:root:Epoch[56] Time cost=6.065\n",
- "INFO:root:Epoch[57] Train-loss=176.704606\n",
- "INFO:root:Epoch[57] Time cost=6.037\n",
- "INFO:root:Epoch[58] Train-loss=176.470091\n",
- "INFO:root:Epoch[58] Time cost=6.012\n",
- "INFO:root:Epoch[59] Train-loss=176.261440\n",
- "INFO:root:Epoch[59] Time cost=6.215\n",
- "INFO:root:Epoch[60] Train-loss=176.133904\n",
- "INFO:root:Epoch[60] Time cost=6.042\n",
- "INFO:root:Epoch[61] Train-loss=175.941920\n",
- "INFO:root:Epoch[61] Time cost=6.000\n",
- "INFO:root:Epoch[62] Train-loss=175.731296\n",
- "INFO:root:Epoch[62] Time cost=6.025\n",
- "INFO:root:Epoch[63] Train-loss=175.613303\n",
- "INFO:root:Epoch[63] Time cost=6.002\n",
- "INFO:root:Epoch[64] Train-loss=175.438844\n",
- "INFO:root:Epoch[64] Time cost=5.982\n",
- "INFO:root:Epoch[65] Train-loss=175.254716\n",
- "INFO:root:Epoch[65] Time cost=6.016\n",
- "INFO:root:Epoch[66] Train-loss=175.090210\n",
- "INFO:root:Epoch[66] Time cost=6.008\n",
- "INFO:root:Epoch[67] Train-loss=174.895443\n",
- "INFO:root:Epoch[67] Time cost=6.008\n",
- "INFO:root:Epoch[68] Train-loss=174.701321\n",
- "INFO:root:Epoch[68] Time cost=6.418\n",
- "INFO:root:Epoch[69] Train-loss=174.553292\n",
- "INFO:root:Epoch[69] Time cost=6.072\n",
- "INFO:root:Epoch[70] Train-loss=174.349379\n",
- "INFO:root:Epoch[70] Time cost=6.048\n",
- "INFO:root:Epoch[71] Train-loss=174.174641\n",
- "INFO:root:Epoch[71] Time cost=6.036\n",
- "INFO:root:Epoch[72] Train-loss=173.966333\n",
- "INFO:root:Epoch[72] Time cost=6.017\n",
- "INFO:root:Epoch[73] Train-loss=173.798454\n",
- "INFO:root:Epoch[73] Time cost=6.018\n",
- "INFO:root:Epoch[74] Train-loss=173.635657\n",
- "INFO:root:Epoch[74] Time cost=5.985\n",
- "INFO:root:Epoch[75] Train-loss=173.423795\n",
- "INFO:root:Epoch[75] Time cost=6.016\n",
- "INFO:root:Epoch[76] Train-loss=173.273981\n",
- "INFO:root:Epoch[76] Time cost=6.018\n",
- "INFO:root:Epoch[77] Train-loss=173.073401\n",
- "INFO:root:Epoch[77] Time cost=5.996\n",
- "INFO:root:Epoch[78] Train-loss=172.888044\n",
- "INFO:root:Epoch[78] Time cost=6.035\n",
- "INFO:root:Epoch[79] Train-loss=172.694943\n",
- "INFO:root:Epoch[79] Time cost=8.492\n",
- "INFO:root:Epoch[80] Train-loss=172.504260\n",
- "INFO:root:Epoch[80] Time cost=7.380\n",
- "INFO:root:Epoch[81] Train-loss=172.323245\n",
- "INFO:root:Epoch[81] Time cost=6.063\n",
- "INFO:root:Epoch[82] Train-loss=172.131274\n",
- "INFO:root:Epoch[82] Time cost=6.209\n",
- "INFO:root:Epoch[83] Train-loss=171.932986\n",
- "INFO:root:Epoch[83] Time cost=6.060\n",
- "INFO:root:Epoch[84] Train-loss=171.755262\n",
- "INFO:root:Epoch[84] Time cost=6.068\n",
- "INFO:root:Epoch[85] Train-loss=171.556803\n",
- "INFO:root:Epoch[85] Time cost=6.004\n",
- "INFO:root:Epoch[86] Train-loss=171.384773\n",
- "INFO:root:Epoch[86] Time cost=6.059\n",
- "INFO:root:Epoch[87] Train-loss=171.185034\n",
- "INFO:root:Epoch[87] Time cost=6.001\n",
- "INFO:root:Epoch[88] Train-loss=170.995980\n",
- "INFO:root:Epoch[88] Time cost=6.143\n",
- "INFO:root:Epoch[89] Train-loss=170.818701\n",
- "INFO:root:Epoch[89] Time cost=6.690\n",
- "INFO:root:Epoch[90] Train-loss=170.629929\n",
- "INFO:root:Epoch[90] Time cost=6.869\n",
- "INFO:root:Epoch[91] Train-loss=170.450824\n",
- "INFO:root:Epoch[91] Time cost=7.156\n",
- "INFO:root:Epoch[92] Train-loss=170.261806\n",
- "INFO:root:Epoch[92] Time cost=6.972\n",
- "INFO:root:Epoch[93] Train-loss=170.070318\n",
- "INFO:root:Epoch[93] Time cost=6.595\n",
- "INFO:root:Epoch[94] Train-loss=169.906993\n",
- "INFO:root:Epoch[94] Time cost=6.561\n",
- "INFO:root:Epoch[95] Train-loss=169.734455\n",
- "INFO:root:Epoch[95] Time cost=6.744\n",
- "INFO:root:Epoch[96] Train-loss=169.564318\n",
- "INFO:root:Epoch[96] Time cost=6.601\n",
- "INFO:root:Epoch[97] Train-loss=169.373926\n",
- "INFO:root:Epoch[97] Time cost=6.725\n",
- "INFO:root:Epoch[98] Train-loss=169.215408\n",
- "INFO:root:Epoch[98] Time cost=6.391\n",
- "INFO:root:Epoch[99] Train-loss=169.039854\n",
- "INFO:root:Epoch[99] Time cost=6.677\n",
- "INFO:root:Epoch[100] Train-loss=168.869222\n",
- "INFO:root:Epoch[100] Time cost=6.370\n",
- "INFO:root:Epoch[101] Train-loss=168.703175\n",
- "INFO:root:Epoch[101] Time cost=6.607\n",
- "INFO:root:Epoch[102] Train-loss=168.523054\n",
- "INFO:root:Epoch[102] Time cost=6.368\n",
- "INFO:root:Epoch[103] Train-loss=168.365964\n",
- "INFO:root:Epoch[103] Time cost=10.267\n",
- "INFO:root:Epoch[104] Train-loss=168.181174\n",
- "INFO:root:Epoch[104] Time cost=11.132\n",
- "INFO:root:Epoch[105] Train-loss=168.021498\n",
- "INFO:root:Epoch[105] Time cost=10.187\n",
- "INFO:root:Epoch[106] Train-loss=167.858251\n",
- "INFO:root:Epoch[106] Time cost=10.676\n",
- "INFO:root:Epoch[107] Train-loss=167.690670\n",
- "INFO:root:Epoch[107] Time cost=10.973\n",
- "INFO:root:Epoch[108] Train-loss=167.535069\n",
- "INFO:root:Epoch[108] Time cost=10.108\n",
- "INFO:root:Epoch[109] Train-loss=167.373971\n",
- "INFO:root:Epoch[109] Time cost=11.013\n",
- "INFO:root:Epoch[110] Train-loss=167.207507\n",
- "INFO:root:Epoch[110] Time cost=11.427\n",
- "INFO:root:Epoch[111] Train-loss=167.043077\n",
- "INFO:root:Epoch[111] Time cost=10.349\n",
- "INFO:root:Epoch[112] Train-loss=166.884060\n",
- "INFO:root:Epoch[112] Time cost=13.129\n",
- "INFO:root:Epoch[113] Train-loss=166.746976\n",
- "INFO:root:Epoch[113] Time cost=11.255\n",
- "INFO:root:Epoch[114] Train-loss=166.572499\n",
- "INFO:root:Epoch[114] Time cost=10.037\n",
- "INFO:root:Epoch[115] Train-loss=166.445170\n",
- "INFO:root:Epoch[115] Time cost=10.406\n",
- "INFO:root:Epoch[116] Train-loss=166.284912\n",
- "INFO:root:Epoch[116] Time cost=10.170\n",
- "INFO:root:Epoch[117] Train-loss=166.171475\n",
- "INFO:root:Epoch[117] Time cost=10.034\n",
- "INFO:root:Epoch[118] Train-loss=166.015457\n",
- "INFO:root:Epoch[118] Time cost=10.047\n",
- "INFO:root:Epoch[119] Train-loss=165.882208\n",
- "INFO:root:Epoch[119] Time cost=10.008\n",
- "INFO:root:Epoch[120] Train-loss=165.753836\n",
- "INFO:root:Epoch[120] Time cost=10.056\n",
- "INFO:root:Epoch[121] Train-loss=165.626045\n",
- "INFO:root:Epoch[121] Time cost=10.704\n",
- "INFO:root:Epoch[122] Train-loss=165.492859\n",
- "INFO:root:Epoch[122] Time cost=10.609\n",
- "INFO:root:Epoch[123] Train-loss=165.361132\n",
- "INFO:root:Epoch[123] Time cost=10.027\n",
- "INFO:root:Epoch[124] Train-loss=165.256487\n",
- "INFO:root:Epoch[124] Time cost=11.225\n",
- "INFO:root:Epoch[125] Train-loss=165.119995\n",
- "INFO:root:Epoch[125] Time cost=11.266\n",
- "INFO:root:Epoch[126] Train-loss=165.012773\n",
- "INFO:root:Epoch[126] Time cost=10.547\n",
- "INFO:root:Epoch[127] Train-loss=164.898748\n",
- "INFO:root:Epoch[127] Time cost=10.339\n",
- "INFO:root:Epoch[128] Train-loss=164.775702\n",
- "INFO:root:Epoch[128] Time cost=10.875\n",
- "INFO:root:Epoch[129] Train-loss=164.692449\n",
- "INFO:root:Epoch[129] Time cost=8.412\n",
- "INFO:root:Epoch[130] Train-loss=164.564323\n",
- "INFO:root:Epoch[130] Time cost=7.239\n",
- "INFO:root:Epoch[131] Train-loss=164.468273\n",
- "INFO:root:Epoch[131] Time cost=10.096\n",
- "INFO:root:Epoch[132] Train-loss=164.328320\n",
- "INFO:root:Epoch[132] Time cost=9.680\n",
- "INFO:root:Epoch[133] Train-loss=164.256156\n",
- "INFO:root:Epoch[133] Time cost=10.707\n",
- "INFO:root:Epoch[134] Train-loss=164.151625\n",
- "INFO:root:Epoch[134] Time cost=13.835\n",
- "INFO:root:Epoch[135] Train-loss=164.046402\n",
- "INFO:root:Epoch[135] Time cost=10.049\n",
- "INFO:root:Epoch[136] Train-loss=163.960676\n",
- "INFO:root:Epoch[136] Time cost=9.625\n",
- "INFO:root:Epoch[137] Train-loss=163.873193\n",
- "INFO:root:Epoch[137] Time cost=9.845\n",
- "INFO:root:Epoch[138] Train-loss=163.783837\n",
- "INFO:root:Epoch[138] Time cost=9.618\n",
- "INFO:root:Epoch[139] Train-loss=163.658903\n",
- "INFO:root:Epoch[139] Time cost=10.411\n",
- "INFO:root:Epoch[140] Train-loss=163.588920\n",
- "INFO:root:Epoch[140] Time cost=9.633\n",
- "INFO:root:Epoch[141] Train-loss=163.493254\n",
- "INFO:root:Epoch[141] Time cost=10.668\n",
- "INFO:root:Epoch[142] Train-loss=163.401188\n",
- "INFO:root:Epoch[142] Time cost=10.644\n",
- "INFO:root:Epoch[143] Train-loss=163.334470\n",
- "INFO:root:Epoch[143] Time cost=9.665\n",
- "INFO:root:Epoch[144] Train-loss=163.235133\n",
- "INFO:root:Epoch[144] Time cost=9.612\n",
- "INFO:root:Epoch[145] Train-loss=163.168029\n",
- "INFO:root:Epoch[145] Time cost=9.578\n",
- "INFO:root:Epoch[146] Train-loss=163.092392\n",
- "INFO:root:Epoch[146] Time cost=10.215\n",
- "INFO:root:Epoch[147] Train-loss=163.014362\n",
- "INFO:root:Epoch[147] Time cost=12.296\n",
- "INFO:root:Epoch[148] Train-loss=162.891574\n",
- "INFO:root:Epoch[148] Time cost=9.578\n",
- "INFO:root:Epoch[149] Train-loss=162.831664\n",
- "INFO:root:Epoch[149] Time cost=9.536\n",
- "INFO:root:Epoch[150] Train-loss=162.768784\n",
- "INFO:root:Epoch[150] Time cost=9.607\n",
- "INFO:root:Epoch[151] Train-loss=162.695416\n",
- "INFO:root:Epoch[151] Time cost=9.681\n",
- "INFO:root:Epoch[152] Train-loss=162.620814\n",
- "INFO:root:Epoch[152] Time cost=9.464\n",
- "INFO:root:Epoch[153] Train-loss=162.527031\n",
- "INFO:root:Epoch[153] Time cost=9.518\n",
- "INFO:root:Epoch[154] Train-loss=162.466575\n",
- "INFO:root:Epoch[154] Time cost=9.562\n",
- "INFO:root:Epoch[155] Train-loss=162.409388\n",
- "INFO:root:Epoch[155] Time cost=9.483\n",
- "INFO:root:Epoch[156] Train-loss=162.308957\n",
- "INFO:root:Epoch[156] Time cost=9.545\n",
- "INFO:root:Epoch[157] Train-loss=162.211725\n",
- "INFO:root:Epoch[157] Time cost=9.542\n",
- "INFO:root:Epoch[158] Train-loss=162.141098\n",
- "INFO:root:Epoch[158] Time cost=9.768\n",
- "INFO:root:Epoch[159] Train-loss=162.124311\n",
- "INFO:root:Epoch[159] Time cost=7.155\n",
- "INFO:root:Epoch[160] Train-loss=162.013039\n",
- "INFO:root:Epoch[160] Time cost=6.147\n",
- "INFO:root:Epoch[161] Train-loss=161.954485\n",
- "INFO:root:Epoch[161] Time cost=9.121\n",
- "INFO:root:Epoch[162] Train-loss=161.913859\n",
- "INFO:root:Epoch[162] Time cost=9.936\n",
- "INFO:root:Epoch[163] Train-loss=161.830799\n",
- "INFO:root:Epoch[163] Time cost=8.612\n",
- "INFO:root:Epoch[164] Train-loss=161.768672\n",
- "INFO:root:Epoch[164] Time cost=9.722\n",
- "INFO:root:Epoch[165] Train-loss=161.689120\n",
- "INFO:root:Epoch[165] Time cost=9.478\n",
- "INFO:root:Epoch[166] Train-loss=161.598279\n",
- "INFO:root:Epoch[166] Time cost=9.466\n",
- "INFO:root:Epoch[167] Train-loss=161.551172\n",
- "INFO:root:Epoch[167] Time cost=9.419\n",
- "INFO:root:Epoch[168] Train-loss=161.488880\n",
- "INFO:root:Epoch[168] Time cost=9.457\n",
- "INFO:root:Epoch[169] Train-loss=161.410458\n",
- "INFO:root:Epoch[169] Time cost=9.504\n",
- "INFO:root:Epoch[170] Train-loss=161.340681\n",
- "INFO:root:Epoch[170] Time cost=9.866\n",
- "INFO:root:Epoch[171] Train-loss=161.281700\n",
- "INFO:root:Epoch[171] Time cost=9.526\n",
- "INFO:root:Epoch[172] Train-loss=161.215523\n",
- "INFO:root:Epoch[172] Time cost=9.511\n",
- "INFO:root:Epoch[173] Train-loss=161.152452\n",
- "INFO:root:Epoch[173] Time cost=9.498\n",
- "INFO:root:Epoch[174] Train-loss=161.058544\n",
- "INFO:root:Epoch[174] Time cost=9.561\n",
- "INFO:root:Epoch[175] Train-loss=161.036475\n",
- "INFO:root:Epoch[175] Time cost=9.463\n",
- "INFO:root:Epoch[176] Train-loss=161.009996\n",
- "INFO:root:Epoch[176] Time cost=9.629\n",
- "INFO:root:Epoch[177] Train-loss=160.853546\n",
- "INFO:root:Epoch[177] Time cost=9.518\n",
- "INFO:root:Epoch[178] Train-loss=160.860520\n",
- "INFO:root:Epoch[178] Time cost=9.395\n",
- "INFO:root:Epoch[179] Train-loss=160.810621\n",
- "INFO:root:Epoch[179] Time cost=9.452\n",
- "INFO:root:Epoch[180] Train-loss=160.683071\n",
- "INFO:root:Epoch[180] Time cost=9.411\n",
- "INFO:root:Epoch[181] Train-loss=160.674101\n",
- "INFO:root:Epoch[181] Time cost=8.784\n",
- "INFO:root:Epoch[182] Train-loss=160.554823\n",
- "INFO:root:Epoch[182] Time cost=7.265\n",
- "INFO:root:Epoch[183] Train-loss=160.536528\n",
- "INFO:root:Epoch[183] Time cost=6.108\n",
- "INFO:root:Epoch[184] Train-loss=160.525913\n",
- "INFO:root:Epoch[184] Time cost=6.349\n",
- "INFO:root:Epoch[185] Train-loss=160.399412\n",
- "INFO:root:Epoch[185] Time cost=7.364\n",
- "INFO:root:Epoch[186] Train-loss=160.380027\n",
- "INFO:root:Epoch[186] Time cost=7.651\n",
- "INFO:root:Epoch[187] Train-loss=160.272921\n",
- "INFO:root:Epoch[187] Time cost=7.309\n",
- "INFO:root:Epoch[188] Train-loss=160.243907\n",
- "INFO:root:Epoch[188] Time cost=7.162\n",
- "INFO:root:Epoch[189] Train-loss=160.194351\n",
- "INFO:root:Epoch[189] Time cost=8.941\n",
- "INFO:root:Epoch[190] Train-loss=160.130400\n",
- "INFO:root:Epoch[190] Time cost=10.242\n",
- "INFO:root:Epoch[191] Train-loss=160.073841\n",
- "INFO:root:Epoch[191] Time cost=10.528\n",
- "INFO:root:Epoch[192] Train-loss=160.021623\n",
- "INFO:root:Epoch[192] Time cost=9.482\n",
- "INFO:root:Epoch[193] Train-loss=159.938673\n",
- "INFO:root:Epoch[193] Time cost=9.465\n",
- "INFO:root:Epoch[194] Train-loss=159.885823\n",
- "INFO:root:Epoch[194] Time cost=9.523\n",
- "INFO:root:Epoch[195] Train-loss=159.886516\n",
- "INFO:root:Epoch[195] Time cost=9.599\n",
- "INFO:root:Epoch[196] Train-loss=159.797400\n",
- "INFO:root:Epoch[196] Time cost=8.675\n",
- "INFO:root:Epoch[197] Train-loss=159.705562\n",
- "INFO:root:Epoch[197] Time cost=9.551\n",
- "INFO:root:Epoch[198] Train-loss=159.738354\n",
- "INFO:root:Epoch[198] Time cost=9.919\n",
- "INFO:root:Epoch[199] Train-loss=159.619932\n",
- "INFO:root:Epoch[199] Time cost=10.121\n"
- ]
- }
- ],
- "source": [
- "# can initilize weights and biases with the learned parameters as follows: \n",
- "# init = mx.initializer.Load(params)\n",
- "\n",
- "# call the VAE, output model contains the learned model and training loss\n",
- "out = VAE(n_latent=2, x_train=image, x_valid=None, num_epoch=200) "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "# encode test images to obtain mu and logvar which are used for sampling\n",
- "[mu,logvar] = VAE.encoder(out,image_test)\n",
- "# sample in the latent space\n",
- "z = VAE.sampler(mu,logvar)\n",
- "# decode from the latent space to obtain reconstructed images\n",
- "x_construction = VAE.decoder(out,z)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "data": {
- "image/png": "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\nRU5ErkJggg==\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "f, ((ax1, ax2, ax3, ax4)) = plt.subplots(1,4, sharex='col', sharey='row',figsize=(12,3))\n",
- "ax1.imshow(np.reshape(image_test[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax1.set_title('True image')\n",
- "ax2.imshow(np.reshape(x_construction[0,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax2.set_title('Learned image')\n",
- "ax3.imshow(np.reshape(x_construction[999,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax3.set_title('Learned image')\n",
- "ax4.imshow(np.reshape(x_construction[9999,:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- "ax4.set_title('Learned image')\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 78,
- "metadata": {
- "collapsed": false
- },
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXYAAAEICAYAAABLdt/UAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztnXuUZXV157/7vmjurQbjacQRqFuK4izGqIlE42saLZNg\ngxLHLJUU0s7MsuTmMcSBPLBWBieZMq9ZQSfIYI3iOH1vjMwYHzE4JATGrPggNAhMRHTQrioUH3QB\nAl1IN1V7/rj3FKdOncfvnHve9/tZa6/uuvc8fufcc75nn/3bv/0TVQUhhJDqUMu7AYQQQpKFwk4I\nIRWDwk4IIRWDwk4IIRWDwk4IIRWDwk4IIRWDwk5yRUSWReR1ebeDkCpBYSelQURURJ6b0LbOFpHv\nJLEtQooGhZ0QQioGhZ0UBhF5qYh8WUQeFpHvichVItIafff3o8XuFJHHROSto8/PE5E7Rut8SURe\n6NjesohcJiJ3iciPROQTIrJLRDoAPg/gWaNtPSYiz/Jozz4RuVtEHhWR74rIZaPPzxaR74jIe0Tk\n8Gg/c471zhWRr4rIIyJyn4i817XdV43a+vDo+3eMPj9ORP6ziKyKyA9E5BoROT7Rk0wmAgo7KRIb\nAN4NYA+AlwOYBfArAKCq/3K0zItUdUpVPyEiPwXgWgDvAmAB+BCAz4rIcY5tvgXAOQCeDeCFAN6h\nqkcAvB7A/aNtTanq/R7t+QiAd6nqbgAvAHCT47tnjtp5CoD9AJZE5Pmj744AuAjA0wCcC6AnIr8I\nACLSxfCh8mcATgLwYgB3jNb7QwBnjD577mjb/8Hw3BGyBYWdFAZVvU1Vv6KqT6rqMoZCvTdglXkA\nH1LVW1R1Q1U/BuAJAD/rWOa/qOr9qvoggL/CUDRNOQbgTBE5QVUfUtXbXd//rqo+oapfAPDXGD5E\noKr/R1X/r6puqupdAD7uOI5fBnCjqn5cVY+p6pqq3iEiMjqed6vqg6r6KID3AXhbhPYSAoDCTgqE\niJwhIp8Tke+LyCMYCtuegFW6AC4dhTQeFpGHAZwGwBlW+b7j/+sApiI06c0A9gFYEZEviMjLHd89\nNPL8bVbs/YrIy0TkZhF5QER+BOBix3GcBuBbHvs6CUAbwG2OY/nfo88JiQSFnRSJ/wrgHgDPU9UT\nALwHgAQsfx+ARVV9msPaqvpxg32FljVV1VtV9XwAzwDwaQDXOb7+iVGs3mYagB3O+XMAnwVwmqqe\nCOAax3HcB+B0j90dBvA4gH/hOJYTVTXKg4gQABR2Uix2A3gEwGMi8s8B9Fzf/wDAcxx//zcAF488\nZBGRzqjjcrfBvn4AwBKRE72+FJGWiMyJyImqemzUrk3XYv9xtNyrAZwH4H86juNBVf2xiLwUw/CL\nzQDA60TkLSLSEBFLRF6sqpuj47lSRJ4xasMpIvILBsdCyDYo7KRIXIahCD6Koch9wvX9ewF8bBSq\neIuqHgTwTgBXAXgIwL0A3mGyI1W9B8PY97dH29uRFQPg7QCWR2GhiwHMOb77/mif92Mo1hePtgkM\nO3x/T0QexbDzc8vTV9VVDMM7lwJ4EMOO0xeNvv7t0TF8ZbTPGwHYHbKEGCOcaIOQaIjI2QD6qnpq\n3m0hxAt67IQQUjEo7IQQUjEYiiGEkIpBj50QQipGI4+d7tmzR2dmZvLYNSGElJbbbrvtsKqGDlrL\nRdhnZmZw8ODBPHZNCCGlRURWTJZjKIYQQioGhZ0QQioGhZ0QQioGhZ0QQioGhZ0QQioGhZ0QF4PB\nADMzM6jVapiZmcFgMMi7SYREIpd0R0KKymAwwPz8PNbX1wEAKysrmJ+fBwDMzc0FrUpIYaDHToiD\nhYWFLVG3WV9fx8LCQk4tIiQ6FHZCHKyurkb6nJAiQmEnxMH09HSkz6sK+xnKDYWdEAeLi4tot9vb\nPmu321hcXMypRdlj9zOsrKxAVbf6GSju5YHCToiDubk5LC0todvtQkTQ7XaxtLQ0UR2n7GcoPxR2\nQlzMzc1heXkZm5ubWF5eLrSopxEyWVnxrjPl9zkpHkx3JKSkpJWaWa/XsbGx4fk5KQf02EnpKXtH\nX9z2RwmZRNmHl6gHfU4KiKpmbi95yUuUkCTo9/vabrcVwJa1223t9/uxt9ftdlVEtNvtxt6O6fZ7\nvV7s9ovItvVsE5Ed+3Tvo9VqqWVZnsfZ7XY9t9vtdhM9FyQ6AA6qgcZS2EmpSVKEkn5ImGzfT5xN\n2m967H7L+R1n2ueBxIfCTiYCU6/VhLQ9VROBjdJ+UwH2O0dBx5n2mwuJB4WdlB4TcUlSjJN8SETZ\n/jjtDzpH9ndJPkxIvlDYSakx9UaTDBuEPSScQlmv17e+M92X3/bdgp9E2MPrvCT1MCH5QWEnpcLt\neVqWFSpCtvj1er0toa3X69rr9Yz2EeUhESSUpkLst/1er7ejQ3XcMEiQp25ZljabTd/jZAimuFDY\nSWmI4106RTFJz77f7297qFiWZRTS6HQ6RsfZ6XS21qnVajseQmHtNBXesLCS13bYaVp8KOykNESJ\nA7vN9tS9vFKncDkF1b2cEz9xM2mL35uCvV23lwwM0w6dHrrf8djiayq8cfoekuqvoNefHhR2Uhqi\ndCqmYVNTU1seq5+w+n3uXsaPoIeX6fHXarVA4XUKqmVZ2mq1PB8CfsKbROcxvf50obCTwuEnKH6i\nNzU1ZSSmSYh7o9Hw9KjdAhW2HT8PNe2Hl5egNpvNHYOQvN4c6vV6YJ9GFI897ZTRSYfCTgqF3+Cc\nXq/nOzIyTGiBYWw7K48/rEPX2XZnTNx0vXEs7E3D/jfOuQoKMblJO2V00jEVdhYBI5ngVddEVXHN\nNdcAAI4//vit72u1Go4ePWq03SNHjiTb0ADW1taMljt69CguuugifPGLX8RHPvIR42MZh7D6Lva/\nQ22IxvXXX2+87PT0tGcVyEmbqCRvWASMZILf1HK2uDtFc3NzM6tmpcbm5iauueaaQFG3LAu9Xg+t\nVivDlkUnyrSAnKikGCQm7CJSF5GvisjnktomqQaDwQC1mv+lFseLLANBxyUiOHz4MK6++mrs3r07\nw1ZFJ4q3zYlKikGSoZhLAHwdwAkJbpOUHLtmOEu+bkdVISJ5NyOUVqsV2duem5ujkOdMIh67iJwK\n4FwAH05ie6Q6eMXW42BP8hDk+ZPk2b17N0W6hCR1l7wfwG8B8A2Oisi8iBwUkYMPPPBAQrslRSco\nPtvtdo238/znPx8iUon4e5l48MEH824CicHYwi4i5wH4oareFrScqi6p6lmqetZJJ5007m5JSQiL\nz1qWZbSdu+++u7Kx+LRIYio7ZrOUkyQ89lcCeKOILAP4CwCvFZF+AtslFcArS8JmZWUFjzzySOGz\nQsqIiBj3a9gPAHfMn9ks5WVsYVfVy1X1VFWdAfA2ADep6oVjt4yUnsFgsBVj9/Mejx07ht27d29l\nUZBkiPJ2Mz8/D1XFgQMHtmWz7N+/H5dccglEBCKCPXv2lG4+2YnFZBSTqQE4G8DnwpbjyNP8SLpA\nk9/2olZstOn1epmMIqU9ZV7D/YOKlrHuS36AJQWImzQmfvbbXtSZe5xtSKr+C83c3AT9fqz7kh8U\ndrIDv5olzhs1ikcfVPApak0Suw302LO3Wq224zcP+v1Y9yU/KOwThIkY9/v90Bs16iQPQduLWmPd\nbgO99fyt3W4nVu2RJAso7NXFXXfbb5ozJyav1kEeeL/f31Hf288sy/IUhiDBsCfGyFvUaE/9Hoyx\nFw9Q2MuN3xRtpp2Sbq8q6NXaLssaVHLVtPRss9n0fABYlqW9Xs9zO37r0PIzu9/D6xok+QEKe3np\n9/vaaDQ8BdBUYN1x0HE9dpN91ut138kxvGb0sduZt4hNkrVarW2Tb5j0u5DiAENhZ+GNAnLJJZfg\nySef3PH5sWPHjGuCP/3pT9/2d9BAk5WVFczMzGDfvn2eJVf37dtntM+NjQ089thjnt+tra15lrAd\nXqvED9Pcfnu5sNGm1157LQ4fPozNzU0sLy/jAx/4AMvsVhET9U/a6LEHgwQ8s2azuW1+S5N12u32\ntomVu92u9nq9SPnotOTNfqvxe7up1Wqh85ba5gUnny4PYCgmO5K+MZIUhKixa/crODs0i2F2fNuv\n/yJs/liv35aUDzAUkw12vfGVlRWoKlZWVjA/Pz/W0GvTwlhhqGrkadnc05p5TXOWBZ1OJ5f9FhU7\nBHfttdfuuD7W1ta2rrnFxUU0m03PbZiG1EgFMFH/pK1KHnsas7L7DefOyuwQjF9HaBb7z2IC6LKZ\nSSe3ff10Op0d348zypgUAzAUkw1RZ2V3xrztwTi2kLo/n0SLkvkzaWZfUybXnJ/41+t1xtJLDCjs\n2RDFY49aGGvSzC8lkja0MI/djrVH6SynuJcLUNizIUphLXZExhMs2lNZTvY15xWqazQakUN47FAt\nF6CwZ4c7K8adMmiailZVs9Mow4aoT+r5MbF6vb5tYFFS/R/ukCFTH4sNKOz5EOTBFzl2nFaIyCkO\nvV5vm3hPTU1tewhWsW/Br+ZKUczpsSdd1pkkDyjs+eAXTiiyN2qLb9LbdXrjXqLRarW0Vqvlfvxp\nn9u82+BnbtFOI8OLJAso7PlQZAEPu7nTEjbV8ePntlef9/lK27I6RhHZKv4Wdu2y/npxAAco5UOZ\nZnW3LAtLS0sAgJmZmVT2sbq6uu3fOIgIFhcXS3Vu49BsNnHxxRcbD84aZ45YVcX111+/7TO/85vm\neR8MBpiZmUGtVsPMzAznVE0KE/VP2qrssff7/dLEikVEZ2dnY8fXTbxLO2963HMSpWRx2c3rvLqr\nfdod0uOcD6+O0yxj7IzpRwcMxeRD1aZ2ExE988wzdwhz1gXC7Bu+6J3QaZk9EYnXpOHOz6OcG7+x\nFlllxTCmHx1Q2POhLN66qXU6nR2DhsYR2XE6S22hKXKHZFpmEuf2+z28JjJJ2jOO80CIEtNnGuYQ\nUNjzIW8ByMosy8olLFLmUIxX/RZTC/Ni/cJUzpm3kp4NyfmQdYu0+8HhJcymHjtDNk8BCns+VD19\njxbf4oaQTETMpDBYkuJo0t8Rtm+vUJ7XA8HvLdgvPFVlQGFPD7/XwjJ1nNLKYU7POigcERbWiOId\nm4ilSTjMZN9B+4vaWT4JXjwo7MkR9MrpvKCyjv3mldfdbrcr04FpdwLn3Q4/c86EFeTdhgl30LXi\nvM7d+7DXM32QRN130AMkzv1kWVZ6QlAAQGFPBlOvIesBNPV6XWdnZzMXmnq9rv1+v9BiaGp5PZCT\nNNNQS9CIaHuZsIe1U+SjLGvSt+DlbY9zP1U1NAMKezJEuemzEog8PXWTKdho2ZkzgySogzSsZMQ4\nHbtJmTssNO41Zl+vvV5vK0Rar9efGnF7++2qtZpqibx8ZCXsAE4DcDOAuwF8DcAlYeuUSdijiGin\n0yl0wacwC+ofiPMqTkvfnGIY5LWnUQsoDXM+iJII9/k9sHoXX6z6ileonnwyhd1zA8A/A/DTo//v\nBvBNAGcGrVMmYY/qNRSxsFWYCDvjnL7ezZjnhZaOtVqtbbNveZmdPZJ3W02s0Whk8vZwkYjq6aer\nXn45hd1og8BnAPxc0DJFFXavHvqqDGP388btmHmcc1WF80KrlnWBoax52N7RMlOAfhfQd550kt75\npjdR2EM3BswAWAVwgsd38wAOAjg4PT2d/hmIgN9rnzNGV4XQg58Qx00Tm9Th/WW0SUnDbQH6spHt\nPe44fbmIXgfoOqDPGS3zx4DeOPr/f2o09PGpqRRUJR2QtbADmAJwG4B/FbZskTz2MM+z2+2W5jU2\nyOw3EL8b3KQ+R1D+vv25ZVkU+4Jald6wOp1OqLPV6/X0g69/vW4AetHoszMAPQLoC0Z/XwHoWq2W\nssokB7IUdgBNADcA+PcmyxdJ2MNEW0Qq4a2bTD8XNDAlSo6zyXmlZWtFz9e37Qp4h1F+Ica2ngfo\nQ4B++pRTthyazwP6Qdf+HgAy05txQYadpwLgfwB4v+k6RRL2MNGumscelM/s/Nt0dh2/5csgIpNk\ns7OzRtdx3iGbKzAU45e57ISI25kC9GuA/gOgxzca2u/3df8znqGPAHo6oCeO7A8AfbBWU33oIdUf\n/zgPCYoEMhT2V41O5l0A7hjZvqB1iiTsQRe7M1WsKq+xXtUa/R5uzvCMyVuLPeqvSueLlq1dgaEH\nHXW9CwA9BOjG6N9bAL0f0GeOvu92u3rrhRd6vg1s2e//fk4qZA44QMmMsKp4zuW8vJmXAPpRQO8Z\nXVQfLcDNEWbNZnPbjPd+yzkHv5i+tbBDlRZkXfgL617EE/YLAH3Mta1NQN/rWk7vu0//5j3v0bee\nfLKeDegbTzhB//y44/RhQN968sn6qfe/Pw8JigQo7OaYFD7yE6x/B+j/A/QAhilUHy3AzWN0gzm8\n8bA6I6rmoRWKOi3InFkrtjmzVq4A9BiG4n4U0NsBfVPINg/B+0Fxv2Mfr6jVVH/0o233c7vd3vYg\nKUMRMVDYkyMotCCO/9+K+ML+CkC/AujjgH4b0F9P+QZzeuN+ou0cnFSFfgZa8excYFvWyhyg7wb0\nbEDfAOjnMBTpIHHfgP9bwDa7+eYd1/MV2P6GYJIdliegsCfDp6680vdC2eu6wOIK++mAPgroxwF9\nDaC/jaHX8m9TvKGcxaNMUiCrkBlEK5bZWStXhSz3JQw9d7/vD8H7/jzkWMZdesFvWyYzVeUJKOzj\n0+/39WnHHx/46ui8KOIK+zWAfgPQuuOzDwK6mtINFaVT2MbEY3d3ytJofubMWmmELHsZhl55zed7\nrxj7Y6PP7WXst8+wa96efL2o1SFBYY+OycTA7ldHp/kJe9d10TltL6ArGKZdOdd59ej7F3hsbxwT\nEe31ekaTgtTr9W3nxuuGsAeKmJRypdFs+yS2Z60E2aUIFnZgZ1bMBa7v45RoLmLMHRR2M6L82GGv\njn7CHtRhdCaGAr7ftc6e0ee/lMJNFWW+Uq9z5Rxhaos689ZpfuYW3esAfQLQVxqu/yVAb0ugHfa1\nG3WdIgEKezhR8q1NXh1NQzFOr/9ZGAr4+a5l6qPP35njDel3UVeldg4tffNLRbwJO52d3YDeDOiv\nADoL6C8C+tcY3itvSKg9UQdgFS3mDgp7OH6eutdrncmro4mwu73+Igu710z2/X6fok4ztkPwD0O6\nbS+gHwb0Wxi+zT4G6N8Dek5CbYlz3dJjL5GwB4VfvDyMJzDMUgl7dQwTdi+vvz3ahztmn1YoJup8\npfacmzZMe6RFMb9UxI0CtC3MosTYTScBHxdQ2L0JC78c8rkQH4H3q+MeQN88snsxfMV8M6BXwtzr\nXwH0fa7PXjXabxKdp/brZ9wa851OJ1Z8kkY7BO/76VAB2ua2Vqu1rd8oiqgHzTebJKCwexPmcRoP\ndsDw1XGv4bJBXv81gH4d23v9/wxDwU/qovUSd/tcULBpaZlJKmIW5jerWRLpjSYjt5MCFHZvgkSs\nXq8n4mH4bcPP67cHKA0wHHH3mxgOp05rgJK73G5Z5sOkldPCUhGzsE6n4/uW6tWXlISmpNHxCgq7\nN35PV3uKuMUzzxzbw4jq9QNDT/4WDEsKHEL6JQVss18ZmYNOm2Rz9yUloSn02DMkKFWv2Wxqr9cb\n28M4hPG9/iyNg4totKHFCckwxp6TsGcdTy5KXDGKMc5OK5o1Gg1tNpuZ79dLlMOyXpgVk7Gw5zXp\nQxHiilHMz2PftWtX7m2jldPGdRbsN+g8Umydnaq9Xi8zjzwMUNiHMO96PPPLJqDRgszus+p0OmNt\nx7KsWPdwkp6+yQxjWQFDYW+gwgwGA6ysrOTdjFKzubmZdxNICdnY2MCFF1449nbW1tawtrYWeb1j\nx46NvW+boZ7uZHV1NbF9JE0t7wakxWAwwPz8fN7NqCyWZaFer+fdDEJyY3p6Ou8m+FJZYV9YWMD6\n+nrezagk3W4Xhw8fxq5du/JuCiG50G63sbi4mHczfKmssBf5NaksNBrekbp9+/YBAI4cOZJlcwhJ\njShvn5ZlYWlpCXNzcym2aDxKI+yDwQAzMzOo1WqYmZnBYDAIXL7Ir0lZUavF/3kty8KJJ57o+d11\n110Xev4JKSoisuOzjY0Nz8+9ePzxx7f9HVWbMsGkhzVpi5oVE2cAQF5pjkWzqFktlmXp7Oxs5LrV\nNFqZbNxsL+ecwVmmQqJK6Y4mQ3a9Bgi4Z/zJ+2Iqg3HeUlrVLYnBeHYdmCzLCahWTNjDiuyEPTXp\nvdNotCTNFu4sC4CpVkzYw56KYYW9OEiJRptMc761x/XU3es5nUZ67GMIe5hHHvSD0VOn0SbT3OIa\ndxtBdWD8JnHv9XqRNM4UZCnsAM4B8A0A9wL4nbDl45QUCDq5YR45h8XTaNWzoP4grzK8Ubdv0gla\nWY8dQB3AtwA8B0ALwJ0AzgxaJ+laMf1+P5cqcDQaLT+zLCswKcI9gYZJAoUdsnE7j36OZWVj7ABe\nDuAGx9+XA7g8aJ0khZ0xdBptsi0sFOtMojDJ+vLSGL9QcJU99l8C8GHH328HcFXQOkkJO7NdaDSa\nqdkleIMcwVqttkNngsQ7Sh57EjXbUTRhBzAP4CCAg9PT05EPyAt66skac9hpVTdbdMOWixJuMRHs\npAYyYRJCMZz1J3njOaVV3UxHVScZbkkqZANDYU+iVsytAJ4nIs8WkRaAtwH4bALbDYX1YJJneO0Q\nUl02NjaMlltfX8fCwgL27du3o45M1OqOfkUJUytWaKL+YQZgH4BvYpgdsxC2fJIxdhTAA6DRJsXK\n0KeV9FunO+NORCLnqZfRY4eqXq+qZ6jq6apa3CLFhJCx2L9/f2AVxLwnXxERHDhwAO12O7Ftumdj\nUlVcf/31kbaxuLi4o02p1nQ3Uf+kLSmPnZ2nNFp21ul0Au85u+NwdnY213baBQDT3EecPPVSZcXE\nsSw7T8vw6kijld2cQuc3zD4rM818GcfymMhaNeNQTF6EdZ52u13s378/o9YQMrmo6tYkE0tLS7m2\nxe70tCwrcLl6vY7Z2Vl0u91I2y/6tHgAUGqPPWiAUliqEo1Gq66JiJHXbuLdt1ottSxrrBBKUmAS\nQjGq20sK2PmpJoMLaDRaeaxWq0W6ly3LUlWz+jDdbtd3uVqtlquQu8GkCHsY9NhptGpYlDdwu7qj\nSdkR27t3j7xutVqFEnVVCvsWXj9s3KHznF6PRsvP/GqzBC2vOuzMDRptam/XeX+7K0MWBVDYn8Jv\nPlT3D9npdHx/fLvXP++LO+qNoJp/lgKNloQ5a7OYOllhy7Xbbe31eplOSD0OoLCbY/KKFzYNn22m\nk3o4Lzg7dmhXnwu7GE3ije4LkyGp9OxdgP4doD8E9GFA/wHQnytAu6pm9j2YVFVX5+xIQfsrEqCw\nm2FykZhMjN3pdELnQbTNZDhy0Pr2G4dfSElEtt4+7FfQoLcR2ni2AuiHAD0f0NcB+t8B3QD0DQVo\nW1XM9qyTdFBsgpykonntoLCb4Xeh1Ot13/SmsBFkfuIfpcaEiRfBEEsxzPL47IuA3lSAtpXZnFlu\nXuGSsX+3kBmYgOKFZEBhNyOtqa3GHT7s9XBwPxgYXsnGusDwVvGwvT7rXAXoPQVoe9mt2WzmnrRQ\npJAMKOxmFDm+1uv1djx4nB4Ec/SzsRagL3PZdYCuA/ocn3VuA/SzBWh7auek1drRH+TMJPG6dr2s\nVqsVfoIXd7kE+02iXq9HrvI4LqCwm5HUzCZpEPbQoceej52LYQz9Ip/v/zWG3vzZBWhrUtZoNHxH\nX7rfTp2xcGc4xa+fJywjLW+zw7JTU1Oe32cp7qCwm5NE1bU0MJmSi0XOsrXnAfoQhqEWr+9/GtDH\nAL2yAG1N0vxGYJpcg/ZAnyAPvihvn3HaUa/XM9MEUNjLj59HblnW1oPI2QFke0dFuUmqZlOAfg3D\ndMaGx/fPBvR7gH4G0FoB2pu02cP0Ta5Rt3U6Hd9BQqZT1aVtduZNnJh+VmASqjtWHa/i/K1WC488\n8ghWVlagqlhbW8Pjjz+Ofr+PJ598EqqKAwcOoNFo5NTq6nABgEMANkb//h2An8Bw9vYnXcueBOAG\nACsYzg25mV0zM2NtbW3HZ6ZTux05csR3SjrTqeqSwLIsdLtdiAgsy4JlWRARdLtdLC0t4eqrr8bU\n1FSkbeY9uYgnJuqftNFjN8cdJvLzJpydvVFG5tG87QIMQyrqsE1A3+uxbAfQWwH9JqB7CtD2NM1N\nmfp5gvrOnPdZnG1nFcIFQzHVJOjCq9VqOjs7y7h7AnYI20XdtvuxPTtmN6A3APoEhg8Dd/ZM3seR\npDlDMXmWxHaP1Lbb4b436vX61kjwoAyWKH1VjUbD9x7MIukCFPbq4PQmihKPrLptwFvY3bY35Ht7\nez0MvfoHAT0C6F2jz/I+TlNzVjrMu9PeT0Cd94llWTsmofZbz/QBVavVth4OeaVJg8JeDfK+iSbV\nDsFbqA/F3N57RnYeoK8FdBHDh8elBTjWMJuamopUd8jZue83anTcDJkwAY0ivFHCL/bDIa2BjWGA\nwl58TEoT0EPPx7xi7I+NPk9qH31A7yzAsYaZPeLZNAbtJW5eue5ujxoYvhn45YuH7cNJ0LpuooaU\nut0uPXYvo7CHD4yip56/XYChh74x+jdI1LvwD8fs9VnnA4B+vQDHmbSZiptfDfRxPfagqe68cs6j\n3mv2xBx5DGwEhb3YBD3x6amXz0zLDtQxzKI5B8OBTr9WgLYnbe5JKqIOAAzzoG1hjbu+F+74fFBW\nmf1QyWNgIyjsxSbIK/F6RaWVy7zKDpyM7Z787xWgnWmaZVm+8XVnhopXmMbPgzapkBp0b0UJlRSx\n3Ago7MXGz6vgqNHym1/ZgTqgL8EwNHMFoI8D+psFaG+aFtTJ6Dcnqbv2utck9W5MMsfCPP2w7Rah\n3Ago7MXG74LO+0akjWdhZQec9rsYhmqOL0C78zA7e8bruzDP2h06CasQKSI6OztbKJGOAyjsxcOv\nCp7zQsv7ZqONZ5/EcBDTMw2WPRfDkMzpBWh30Swo68W0s9M5WY7fvKZe92CRQRbCDuBPANwD4C4A\nnwLwNJN/GYuVAAAQmklEQVT1JlHYTeN1LAVQHnNnzVyH4QjUVxqu/z4MwzGT6rEHWZDHbpqe6Hw4\nmIY+846hh4GMhP3nATRG//8jAH9kst4kCrvpKye99nKYXy2Zm7AzO2Y3oP8I6K9iOCfq6wH9U0CP\nAvoHBTiWollYB6lpP5Tz3oqy/7Rz0ccBWYdiALwJwMBk2UkU9rCeeuerIOcyLb4dwnZRD7K9gC4B\n+g0Mywk8AOiXAJ0rwHEU1bzEPUp9GveYkChJCWmPHh0H5CDsfwXgwoDv5wEcBHBweno69ROQBVF6\nzKNekEWeUYbmX0tmowBtq4o5s1jC4urOuVHdk2BHHRMyER47gBsB/JOHne9YZgHDGLuY7LQKHnvU\nHNd+v79Vac7kgs77pqIF2yF4C/uhArStSmZXlAxyjGynKs5o7aLlqYeBrDx2AO8A8GUAbdN1qiDs\nUdO0GDuvlmVRS4Y2tKCEApMOUj+zHwjMitkp6ucAuBvASVHWq4KwR63uFvWioxXfotSSmUQzfUMd\nx5yOVJwqjUEUUfSRkbDfC+A+AHeM7BqT9aog7FE9doZXaEW2tK7PNNN33eIcxXnq9XqBwl3EcgKq\nGYZi4lgVhN3kh+cUdbSimt3Z6BzAk8Z+ugHTObotyr3i5UFHyX5pNps7Rqs679+4I2LTBhT29Al7\n4rOYF62I5iWKaYYKe71e6JB/u00mnZ9B4jruA8redl4TaYQBCnu+MKZOK7o5y+uaerpx4+adTmfL\nIw8a7el0lrxSfk3CIb1eL3ZoyRZueuwUdk8YU6eVwZrNpvHAn3a7rbOzs7H3ZYuySaeklziblOy1\niTtPsC3cjLFT2D2hx04ri9nC5xbSVqu1FYe3LMtoyrowM/F4g2LlcTxmL5EOi7Hb601kVkxcmwRh\nZ4ydVkazBdUpZFEG/oSFakxi1EFOUdwYt5dIF1G4wwCFPX+YFUPL26KGIoCdXnGUa9iyrMCHgInH\nHRTGdJbiLYMQJw0o7MWCk1PT8jD72ov69ui8bqOsZ9d38XoY+HWSukU6TqGvSQEU9uIRpTodjZaE\nOa89U8+7Xq9vrRdnmL77eneLd1jHZBQnKO8slawBhb24MGOGlpXFzVePc62aetAmqYTuh4LfPvPO\nK88aUNiLC+PutKzMK9MjzBt2CqzpgyBKzDvO4J+i5pVnDSjsxYXCTsvS7NK3NmEhQefApbAYe5w4\ndxyRLmpeedaAwl5cyh6KyaJqHy1Z86qrEua52wLvN+mLcyKMKHjt274nLMvaVsMmSl55GdMXowIK\ne3EpcweqPfqPOfrlsnq9rr1eb0v4TB/O7Xbbd2CS+00gCs63hiBHx9QrnxSPHhT24tLv90OLIjlt\ndna2MA+DXq/HUBJNgWQ6Lk2ua5M4+qTE4GEo7DWQzJmbm8Pu3buNl//yl7+M5z73uSm2KJxOp4N+\nv4+rr74aa2trubaFFIPp6emxt7G6uprqMibrVhEKe8YMBgPMzMxEEsf19XXcdNNNKbYqnCNHjuDC\nCy9Eo9HItR2kGLTbbSwuLgJ46pqu1WqYmZnBYDAw3o7Jw2GcZZJ4+JQSE7c+aZvUUAxHn5bf7Jht\n0DKNRiP3dqZpUQYbjXtPMMa+HTDGXjzC4ontdjvx+PXs7OxWrRBms4xvNkH1VyzL8u1cnpqaKnwf\nRVD73DHrJGLbzmyWoKyYKNthVgyFPTOCev+7o+nJkr7p3Z20ZU+1zNtsxtnGrl27cj+OIPMTa6/0\nxqLONFRVkQeFvXgEeTdlDdOIyEQ9LMJ+yyKa14QVfsva3rLf926xLGI2SpXDMqCwF4+gCy5roWBY\nJro5xcpvbs1Go1HIUItXLXL321yr1Yp0Lbbbbe31eoUT0SI+bJICFPZi4veKOElebxnNLVZ+4mGP\n1owyTiFtswXNfe05ByyFdYgGbbtoYY+ihoeSABT2clGmV/tJM/vtxunt+i1rx6HzGJnbaDR27Nc5\nz2iYZ93r9bZ1tPuVEnAfb9Ggx66gsBeEuDH2Xbt2RZ4hp8yWtyfcarUCRbvb7eb2kG42mzu8cPvv\noPaq+oeWwqyIYskYu4LCXiA4EUe5zRaPPNvgFFoTZ8H2uOM4B0UWy6KFh5ICFPbywnj75JplWWO9\nldhC3e/3jcTafhDEaWdVxLJMgLViyovfMGgRybgl43Pccceh3W4nuk3LshLfZt602230+30cPnwY\n1157LSzLirWd6elpDAYDzM/PY2NjI3SfdlmAer1uvA/LsnD48GHMzc3FaiPJABP1DzMAl2L4JN9j\nsjw99mD8YoRx46B5W6/XM+qIM7FWqxUpnbBIw/ud4YFOp7PVKWuX1HUTNW3SvkZMPXV3x6nJ+Sty\n+GUSQFahGACnAbgBwAoo7InhFyMsYwy+2+0m1sEbNdtkdnY29+O3z4HztzXp3PNarlarbT0Q7MwV\nZ0dpWEw9SJidDwX7YVPVWHVZyVLY/xeAFwFYprCnTxlHqCbVZxDn4ZDkQKywUZl+1mw2t9U/8fPE\nvTJM3HVU3PF3p1CHPfTr9TqFueRkIuwAzgfwgdH/A4UdwDyAgwAOTk9PZ3AKqkvYzV40S8JjzzKk\n4jeaMs6I0k6nY/z7hOWEh+VnJzETESk2iQk7gBsB/JOHnQ/gFgAnqoGwO40eezBRX3/dQl+kaets\nQQnqH7AsS2dnZ32FaWpqKrNjsgcYef0GUb31qOmrYTnhYSMq/fZFT706JCbsvisCPwnghyNBXwbw\nJIBVAM8MW5fC7k8SgytMU93SMHsaP6+HklcM1/7cS7Q6nU5udXS84t1RzqktpqYPA5PfOMxjr/LA\nHDIkdWHfsSF67ImQ1HDoOB6m2+yJq3u9nnGsul6vh9YiidLOvPoTnIIYt1/DJHzjdW783thMhJud\nndWGwl5S4hQw8ruZ/TzhKGYLdBJxfDsdz9nWtCshhpUACDt21fEykYI6XP06S/1SXe122G8OFO7J\nI3Nhj2IUdn9MPPYomRLOUEbcDJEkR8KmPaq2Xq/r1NTUNmGNm0dvP0zHabP91uPehl+IxO/3N12f\nVBsKe0kJe902DQv4hW6KWCs8bWs2m7HeOJLw2O3QlD3lm/2ZvX3TGYmi/MakulDYS0xQnNRUZPxC\nN3mLbFnM/TBN4k3DKyxkWuc9ym9MqoupsLNWTAGZm5vD8vIyNjc3sby8vK0mx+rqqtE2/OrNhJFH\nPRrLsmLXRhmXer2OXq+Hfr+PbrcLEUG328XS0tK2855EbZqjR4/i2LFj2z5bX1/H/v37MRgMAACL\ni4s79uX3m8T9jckEYKL+SRs99viYeHRB8dewGei9BuckGRcP6w9Iaj9xzF2xMEo20LjmPg/O32lq\nampHO5rN5o5+FGbCVB8wFFNNvGLsziHrdizX7yYPmuvSuYxTKExqkEQRTz8RCppSrl6vez4Ukiou\n5hbMKAXXknrwBeWju83+zZi7PllQ2CvMOHnOQesHkUTqpC2CfoS9jXg9FNKondM1LIHgbEdSndIm\n58HZzqBl6b1XDwr7BJLU4KYo247rlXoR9uDweygk9dBx7ifqcSQ1gXWUYxGR0GXpvVcLCvsEkubs\n7EkIZ9jbQ9g+/HL5owz1N91P2Da96qcnVcrBmYcf1k6TBy7TIqsDhX0CKaLHHhTyiRpGsTs30yxd\nbNeJCYux+53TpNoW5v3bD8ko85qS8kNhn0DS7EiLI1hhD5Q4DwsRMfZo45hlWVvtCxJ30xIPcT14\nd39CUN2dsEJp9NirA4V9Qkkz9W3cTls3RZy02y3YYemhJiWV46SPxvGymSFTfSjsJFPiPFCS6pC1\nzV1Z0inKziH9Qeb2bsPW8RNOdz0frxTUoDeCuF42c9qrDYWdpIapeIQtFxbeierRuwcYebUnaH9e\nIh03Q8bLS/fqcI1SIIwQCjtJhSi58lFz6t2ebVDqn58HHiaKJp60E5O3CnfYxG8de3amoDbRyyZB\nUNhJKphm3iSVoRPk0aaZBWRj0mns3l+Ql8+OTDIOFHaSCqa58knm1Pt5tEnn7Qd1DtsPEZOwSZCX\nz9RDMg4UdpIKWXvsSbTFhCTLMQSV+aXHTsaBwk5SIekYexZtMSHpBxE7RUkaUNhJaiSVFZNlW8JI\noxwDO0VJ0pgKuwyXzZazzjpLDx48mPl+STUYDAZYWFjA6uoqpqensbi4uG1SjDjMzMxgZWVlx+fd\nbhfLy8tjbZuQpBCR21T1rLDlOIMSKRWDwQDz8/NYWVmBqmJlZQXz8/NbMxDFxWvmona7jcXFxbG2\nS0geUNhJqVhYWMD6+vq2z9bX17GwsABgKPwzMzOo1WqYmZkxFvy5uTksLS0FTo9HSFlgKIaUilqt\nBq9rVkRw4MABzM/PbxP+drtNgSaVgaEYUkn8JnCenp4O9eYJmRQo7KRUBMXCV1dXPdfx+5yQqkJh\nJ6UiKBYe5M0TMklQ2EnpmJubw/LyMjY3N7G8vLwVP2dmCyFDxhZ2Efl1EblHRL4mIn+cRKMIiQMz\nWwgZMlZWjIi8BsACgHNV9QkReYaq/jBsPWbFEEJIdLLKiukB+ENVfQIATESdEEJIuowr7GcAeLWI\n3CIiXxCRn/FbUETmReSgiBx84IEHxtwtIYQQPxphC4jIjQCe6fHVwmj9pwP4WQA/A+A6EXmOesR3\nVHUJwBIwDMWM02hCCCH+hAq7qr7O7zsR6QH4y5GQ/6OIbALYA4AuOSGE5MS4oZhPA3gNAIjIGQBa\nAA6P2yhCCCHxGTcrpgXgWgAvBnAUwGWqepPBeg8A2FkjNX32YHIfPDz2yYTHXi26qnpS2EK5FAHL\nCxE5aJIqVEV47Dz2SWOSj50jTwkhpGJQ2AkhpGJMmrAv5d2AHOGxTyY89glkomLshBAyCUyax04I\nIZWHwk4IIRVjYoVdRC4VERWRPXm3JStE5E9GJZbvEpFPicjT8m5T2ojIOSLyDRG5V0R+J+/2ZIWI\nnCYiN4vI3aOS2pfk3aasEZG6iHxVRD6Xd1uyZiKFXUROA/DzACZtzrS/BfACVX0hgG8CuDzn9qSK\niNQBfBDA6wGcCeACETkz31ZlxpMALlXVMzGs5fSrE3TsNpcA+HrejciDiRR2AFcC+C0AE9VzrKp/\no6pPjv78CoBT82xPBrwUwL2q+m1VPQrgLwCcn3ObMkFVv6eqt4/+/yiGAndKvq3KDhE5FcC5AD6c\nd1vyYOKEXUTOB/BdVb0z77bkzL8B8Pm8G5EypwC4z/H3dzBB4mYjIjMAfgrALfm2JFPej6Hztpl3\nQ/IgtLpjGQkpNfweDMMwlSTo2FX1M6NlFjB8VR9k2TaSPSIyBeCTAH5DVR/Juz1ZICLnAfihqt4m\nImfn3Z48qKSw+5UaFpGfBPBsAHeKCDAMRdwuIi9V1e9n2MTUCCqzDAAi8g4A5wGY9aqbXzG+C+A0\nx9+njj6bCESkiaGoD1T1L/NuT4a8EsAbRWQfgF0AThCRvqpemHO7MmOiByiJyDKAs1S1ahXgPBGR\ncwD8KYC9qlr5mvki0sCwk3gWQ0G/FcAvq+rXcm1YBsjQc/kYgAdV9Tfybk9ejDz2y1T1vLzbkiUT\nF2OfcK4CsBvA34rIHSJyTd4NSpNRR/GvAbgBw87D6yZB1Ee8EsDbAbx29FvfMfJgyQQw0R47IYRU\nEXrshBBSMSjshBBSMSjshBBSMSjshBBSMSjshBBSMSjshBBSMSjshBBSMf4/1HOo5p+uW1YAAAAA\nSUVORK5CYII=\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "image/png": "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\ncK/JFby5YjzXwfDGG2/MYsuWLctiJbr5NIkFku4cS+IRkr6eUvq/XTweAAAAMKU6ngynlJ6RlH/+\nBQAAAFAT/F4eAAAAjcVkGAAAAI3Viw50A9Hvji2u+MQtQnfFcq4r3amnnjrhtuSLOaZz0VWv9LqI\nqOTxXVHG448/nsVckcHixYvbthctWpTt4wo8uil46bfWY1RaLOc6Q/3oRz/KYtu2bWvbdsfZffyQ\nK55ynafe/e53F32vK4yq7ucqv+fPn5/F3LXqTHUuW+9b7hx3eXT3NZfH7du3t2274qfXXnsti7lj\nNWfOnCw2e/bsou8tKTR2RZXuOd35VNpJsN/3rV4p+fnmzgGX33/4h3/IYiMjI1nMFUC5c8rda6rF\nlS4f7t5TPT8l6aGHHspis2bNymLVe3o/dXNPKOku5wpb165dm8XuvPPOLOaK5aodASV/b3H312pn\nP/d9roPhli1bsti73vWuLLZgwYIs5jraleCdYQAAADQWk2EAAAA0FpNhAAAANBaTYQAAADRWLQro\n+l0s57gCOtdh5cknn8xirlPKe97znrZtV3TlFqC7wo06F9UNoujEPafL0WOPPZbF9u7dm8VWrFjR\ntn3yySdn+5R2zpouuWw9RqUFpa6IxR2vp556qm3bFZi689wVsy1dujSLuQI6V5jjCniqxSbuGncF\nN6Xn8VTmPKXUlid3DFyxiuvS5e5r69ata9t2XQNdkYu717kOdO7+516Dy1E1j66A1Z3Dbhwu5gz6\n2u3mZ1m1YM51kbv99tuz2AMPPJDFSq8ZVzReLbBy3+uKPqv3FMkX0D3//PNZzN3nXSfZfum0s5xU\nlss1a9Zk+9x2221ZzOXSFd+5ItPTTz89i7n7dbW7nOtQ6c49lzeX8+o9SZIuvfTSLFaCd4YBAADQ\nWEyGAQAA0FhMhgEAANBYTIYBAADQWLUooOs3t1DdLdp3HXncwnu3yP2ss85q2z7hhBOyfVynHRya\nkg49Ut4VTfKL8V3HnGoRl+ucVdIlazppLQYqLfpyx8YVY1WLIdy1ddppp2Wxiy++OIudeOKJWcwV\nXrlCDZeTasHfm2++me1z0kknZTGntNh1smPdjckKId3rc4Vw7lp4+umn27ZdkZorrKkWnErS3Llz\ns5jLoyvocbndvXt327Z7na64z3UX7KY4sl/cmEoLrFzRZLVzqiuwuvvuu7OYKzx2hVPnnXdeFnPX\n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\nAAAAGuv/A7waJ9gxyNuaAAAAAElFTkSuQmCC\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "z1 = z[:,0]\n",
- "z2 = z[:,1]\n",
- "\n",
- "fig = plt.figure()\n",
- "ax = fig.add_subplot(111)\n",
- "ax.plot(z1,z2,'ko')\n",
- "plt.title(\"latent space\")\n",
- "\n",
- "#np.where((z1>3) & (z2<2) & (z2>0))\n",
- "#select the points from the latent space\n",
- "a_vec = [2,5,7,789,25,9993]\n",
- "for i in range(len(a_vec)):\n",
- " ax.plot(z1[a_vec[i]],z2[a_vec[i]],'ro') \n",
- " ax.annotate('z%d' %i, xy=(z1[a_vec[i]],z2[a_vec[i]]), \n",
- " xytext=(z1[a_vec[i]],z2[a_vec[i]]),color = 'r',fontsize=15)\n",
- "\n",
- "\n",
- "f, ((ax0, ax1, ax2, ax3, ax4,ax5)) = plt.subplots(1,6, sharex='col', sharey='row',figsize=(12,2.5))\n",
- "for i in range(len(a_vec)):\n",
- " eval('ax%d' %(i)).imshow(np.reshape(x_construction[a_vec[i],:],(28,28)), interpolation='nearest', cmap=cm.Greys)\n",
- " eval('ax%d' %(i)).set_title('z%d'%i)\n",
- "\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Above is a plot of points in the 2D latent space and their corresponding decoded images, it can be seen that points that are close in the latent space get mapped to the same digit from the decoder, and we can see how it evolves from left to right."
- ]
- }
- ],
- "metadata": {
- "anaconda-cloud": {},
- "kernelspec": {
- "display_name": "Python [Root]",
- "language": "python",
- "name": "Python [Root]"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 2
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.12"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/include/mxnet/base.h b/include/mxnet/base.h
index dfe18996aec5..5f16eb441868 100644
--- a/include/mxnet/base.h
+++ b/include/mxnet/base.h
@@ -153,10 +153,10 @@ struct Context {
return dev_type;
}
/*!
- * \brief Returns dev_id for kGPU, 0 otherwise
+ * \brief Returns dev_id for kGPU and kCPUPinned, 0 otherwise
*/
inline int real_dev_id() const {
- if (dev_type == kGPU) return dev_id;
+ if (dev_type == kCPUPinned || dev_type == kGPU) return dev_id;
return 0;
}
/*!
@@ -225,11 +225,11 @@ struct Context {
/*!
* \brief get the free and total available memory on a GPU
* \param dev the GPU number to query
- * \param free_mem pointer to the integer holding free GPU memory
- * \param total_mem pointer to the integer holding total GPU memory
+ * \param free_mem pointer to the uint64_t holding free GPU memory
+ * \param total_mem pointer to the uint64_t holding total GPU memory
* \return No return value
*/
- inline static void GetGPUMemoryInformation(int dev, int *free, int *total);
+ inline static void GetGPUMemoryInformation(int dev, uint64_t *free, uint64_t *total);
/*!
* Create a pinned CPU context.
* \param dev_id the device id for corresponding GPU.
@@ -334,8 +334,8 @@ inline int32_t Context::GetGPUCount() {
#endif
}
-inline void Context::GetGPUMemoryInformation(int dev, int *free_mem,
- int *total_mem) {
+inline void Context::GetGPUMemoryInformation(int dev, uint64_t *free_mem,
+ uint64_t *total_mem) {
#if MXNET_USE_CUDA
size_t memF, memT;
@@ -354,8 +354,8 @@ inline void Context::GetGPUMemoryInformation(int dev, int *free_mem,
e = cudaSetDevice(curDevice);
CHECK_EQ(e, cudaSuccess) << " CUDA: " << cudaGetErrorString(e);
- *free_mem = static_cast(memF);
- *total_mem = static_cast(memT);
+ *free_mem = static_cast(memF);
+ *total_mem = static_cast(memT);
#else
LOG(FATAL)
diff --git a/include/mxnet/c_api.h b/include/mxnet/c_api.h
index a01cc6a77940..e9f1e2d6cccc 100644
--- a/include/mxnet/c_api.h
+++ b/include/mxnet/c_api.h
@@ -441,6 +441,7 @@ MXNET_DLL int MXGetGPUCount(int* out);
/*!
* \brief get the free and total available memory on a GPU
+ * Note: Deprecated, use MXGetGPUMemoryInformation64 instead.
* \param dev the GPU number to query
* \param free_mem pointer to the integer holding free GPU memory
* \param total_mem pointer to the integer holding total GPU memory
@@ -448,6 +449,15 @@ MXNET_DLL int MXGetGPUCount(int* out);
*/
MXNET_DLL int MXGetGPUMemoryInformation(int dev, int *free_mem, int *total_mem);
+/*!
+ * \brief get the free and total available memory on a GPU
+ * \param dev the GPU number to query
+ * \param free_mem pointer to the uint64_t holding free GPU memory
+ * \param total_mem pointer to the uint64_t holding total GPU memory
+ * \return 0 when success, -1 when failure happens
+ */
+MXNET_DLL int MXGetGPUMemoryInformation64(int dev, uint64_t *free_mem, uint64_t *total_mem);
+
/*!
* \brief get the MXNet library version as an integer
* \param pointer to the integer holding the version number
@@ -1542,18 +1552,17 @@ MXNET_DLL int MXSymbolInferType(SymbolHandle sym,
* \param sym_handle symbol to be converted
* \param ret_sym_handle quantized symbol result
* \param num_excluded_symbols number of layers excluded from being quantized in the input symbol
- * \param excluded_symbols array of symbols to be excluded from being quantized
+ * \param excluded_symbols op names to be excluded from being quantized
* \param num_offline number of parameters that are quantized offline
* \param offline_params array of c strings representing the names of params quantized offline
* \param quantized_dtype the quantized destination type for input data.
+ * \param calib_quantize whether calibrate quantize op with offline calibration data.
*/
-MXNET_DLL int MXQuantizeSymbol(SymbolHandle sym_handle,
- SymbolHandle *ret_sym_handle,
+MXNET_DLL int MXQuantizeSymbol(SymbolHandle sym_handle, SymbolHandle *ret_sym_handle,
const mx_uint num_excluded_symbols,
- const SymbolHandle *excluded_symbols,
- const mx_uint num_offline,
- const char **offline_params,
- const char *quantized_dtype);
+ const char **excluded_symbols,
+ const mx_uint num_offline, const char **offline_params,
+ const char *quantized_dtype, const bool calib_quantize);
/*!
* \brief Set calibration table to node attributes in the sym
@@ -1571,6 +1580,15 @@ MXNET_DLL int MXSetCalibTableToQuantizedSymbol(SymbolHandle qsym_handle,
const float* high_quantiles,
SymbolHandle* ret_sym_handle);
+/*!
+ * \brief Run subgraph pass based on the backend provided
+ * \param sym_handle symbol to be converted
+ * \param backend backend names for subgraph pass
+ * \param ret_sym_handle returned symbol
+ */
+MXNET_DLL int MXGenBackendSubgraph(SymbolHandle sym_handle, const char *backend,
+ SymbolHandle *ret_sym_handle);
+
//--------------------------------------------
// Part 4: Executor interface
//--------------------------------------------
diff --git a/include/mxnet/engine.h b/include/mxnet/engine.h
index 11e64edfcd54..e02b995d6857 100644
--- a/include/mxnet/engine.h
+++ b/include/mxnet/engine.h
@@ -25,7 +25,6 @@
#ifndef MXNET_ENGINE_H_
#define MXNET_ENGINE_H_
-#include
#if DMLC_USE_CXX11
#include
#include
diff --git a/include/mxnet/ndarray.h b/include/mxnet/ndarray.h
index afae5dcfcffe..e877d35dbb5b 100644
--- a/include/mxnet/ndarray.h
+++ b/include/mxnet/ndarray.h
@@ -667,6 +667,12 @@ class NDArray {
}
#if MXNET_USE_MKLDNN == 1
+ /*
+ * Create NDArray from mkldnn memory.
+ * mkldnn_mem The mkldnn memory to be managed.
+ * static_data If true, mkldnn memory won't be freed on destruction.
+ */
+ explicit NDArray(const mkldnn::memory *mkldnn_mem, bool static_data = true);
/*
* Test if the data is stored in one of special MKLDNN format.
*/
@@ -742,6 +748,11 @@ class NDArray {
* It's used by FullyConnected right now.
*/
NDArray MKLDNNDataReshape(const TShape &shape) const;
+
+ /*!
+ * \ Fix mkldnn memory descriptor mismatch from NDArray.
+ */
+ void UpdateMKLDNNMemDesc();
#endif
/*!
diff --git a/include/mxnet/op_attr_types.h b/include/mxnet/op_attr_types.h
index aa5d4e6de784..dd818457f827 100644
--- a/include/mxnet/op_attr_types.h
+++ b/include/mxnet/op_attr_types.h
@@ -300,6 +300,14 @@ using FQuantizedOp = std::function;
*/
using FNeedRequantize = std::function;
+/*!
+ * \brief Register a function to determine if the input of a quantized operator
+ * needs to be quantized. This is usually used for the quantized operators
+ * which can handle fp32 inputs directly.
+ */
+using FAvoidQuantizeInput = std::function;
+
} // namespace mxnet
#endif // MXNET_OP_ATTR_TYPES_H_
diff --git a/julia/.gitattributes b/julia/.gitattributes
new file mode 100644
index 000000000000..4b76ca8606cb
--- /dev/null
+++ b/julia/.gitattributes
@@ -0,0 +1 @@
+NEWS.md merge=union
diff --git a/julia/.gitignore b/julia/.gitignore
new file mode 100644
index 000000000000..d6791c8491bf
--- /dev/null
+++ b/julia/.gitignore
@@ -0,0 +1,13 @@
+*.jl.cov
+*.jl.*.cov
+*.jl.mem
+*.pyc
+.ipynb_checkpoints
+data
+deps/src
+deps/usr
+deps/deps.jl
+docs/_build
+docs/build/
+docs/site/
+.vscode
diff --git a/julia/.travis.yml b/julia/.travis.yml
new file mode 100644
index 000000000000..680df7af481e
--- /dev/null
+++ b/julia/.travis.yml
@@ -0,0 +1,59 @@
+# Documentation: http://docs.travis-ci.com/user/languages/julia/
+sudo: false
+
+language: julia
+
+os:
+ - linux
+ - osx
+osx_image: xcode8
+
+julia:
+ - 0.6
+# - nightly 0.6 supports depends on #170
+
+branches:
+ only:
+ - master
+ - stable
+ - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ # for tagging
+
+cache:
+ directories:
+ - $TRAVIS_BUILD_DIR/deps/src
+
+# dependent apt packages
+addons:
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ packages:
+ - doxygen
+ - wget
+ - git
+ - libcurl4-openssl-dev
+ - unzip
+ - libatlas-base-dev
+ - libatlas-dev
+ - libopencv-dev
+ - gcc-4.8
+ - g++-4.8
+
+before_install:
+ - export TRAVIS_DIR=test/travis
+ - source ${TRAVIS_DIR}/setup_env.sh
+
+notifications:
+ email: false
+
+script:
+ - ${TRAVIS_DIR}/run_test.sh
+
+after_success:
+ # See https://github.com/dmlc/MXNet.jl/pull/303#issuecomment-341171774
+ - julia -e 'using MXNet; mx._sig_checker()'
+
+ - source ${TRAVIS_DIR}/run_coverage.sh
+ - echo $TRAVIS_JULIA_VERSION
+ - julia -e 'Pkg.add("Documenter")'
+ - julia -e 'cd(Pkg.dir("MXNet")); include(joinpath("docs", "make.jl"))'
diff --git a/julia/LICENSE.md b/julia/LICENSE.md
new file mode 100644
index 000000000000..5ecf95ac60bc
--- /dev/null
+++ b/julia/LICENSE.md
@@ -0,0 +1,179 @@
+The MXNet.jl package is licensed under version 2.0 of the Apache License:
+
+> Copyright (c) 2015-2018:
+> * Chiyuan Zhang
+>
+> Apache License
+> Version 2.0, January 2004
+> http://www.apache.org/licenses/
+>
+> TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+>
+> 1. Definitions.
+>
+> "License" shall mean the terms and conditions for use, reproduction,
+> and distribution as defined by Sections 1 through 9 of this document.
+>
+> "Licensor" shall mean the copyright owner or entity authorized by
+> the copyright owner that is granting the License.
+>
+> "Legal Entity" shall mean the union of the acting entity and all
+> other entities that control, are controlled by, or are under common
+> control with that entity. For the purposes of this definition,
+> "control" means (i) the power, direct or indirect, to cause the
+> direction or management of such entity, whether by contract or
+> otherwise, or (ii) ownership of fifty percent (50%) or more of the
+> outstanding shares, or (iii) beneficial ownership of such entity.
+>
+> "You" (or "Your") shall mean an individual or Legal Entity
+> exercising permissions granted by this License.
+>
+> "Source" form shall mean the preferred form for making modifications,
+> including but not limited to software source code, documentation
+> source, and configuration files.
+>
+> "Object" form shall mean any form resulting from mechanical
+> transformation or translation of a Source form, including but
+> not limited to compiled object code, generated documentation,
+> and conversions to other media types.
+>
+> "Work" shall mean the work of authorship, whether in Source or
+> Object form, made available under the License, as indicated by a
+> copyright notice that is included in or attached to the work
+> (an example is provided in the Appendix below).
+>
+> "Derivative Works" shall mean any work, whether in Source or Object
+> form, that is based on (or derived from) the Work and for which the
+> editorial revisions, annotations, elaborations, or other modifications
+> represent, as a whole, an original work of authorship. For the purposes
+> of this License, Derivative Works shall not include works that remain
+> separable from, or merely link (or bind by name) to the interfaces of,
+> the Work and Derivative Works thereof.
+>
+> "Contribution" shall mean any work of authorship, including
+> the original version of the Work and any modifications or additions
+> to that Work or Derivative Works thereof, that is intentionally
+> submitted to Licensor for inclusion in the Work by the copyright owner
+> or by an individual or Legal Entity authorized to submit on behalf of
+> the copyright owner. For the purposes of this definition, "submitted"
+> means any form of electronic, verbal, or written communication sent
+> to the Licensor or its representatives, including but not limited to
+> communication on electronic mailing lists, source code control systems,
+> and issue tracking systems that are managed by, or on behalf of, the
+> Licensor for the purpose of discussing and improving the Work, but
+> excluding communication that is conspicuously marked or otherwise
+> designated in writing by the copyright owner as "Not a Contribution."
+>
+> "Contributor" shall mean Licensor and any individual or Legal Entity
+> on behalf of whom a Contribution has been received by Licensor and
+> subsequently incorporated within the Work.
+>
+> 2. Grant of Copyright License. Subject to the terms and conditions of
+> this License, each Contributor hereby grants to You a perpetual,
+> worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+> copyright license to reproduce, prepare Derivative Works of,
+> publicly display, publicly perform, sublicense, and distribute the
+> Work and such Derivative Works in Source or Object form.
+>
+> 3. Grant of Patent License. Subject to the terms and conditions of
+> this License, each Contributor hereby grants to You a perpetual,
+> worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+> (except as stated in this section) patent license to make, have made,
+> use, offer to sell, sell, import, and otherwise transfer the Work,
+> where such license applies only to those patent claims licensable
+> by such Contributor that are necessarily infringed by their
+> Contribution(s) alone or by combination of their Contribution(s)
+> with the Work to which such Contribution(s) was submitted. If You
+> institute patent litigation against any entity (including a
+> cross-claim or counterclaim in a lawsuit) alleging that the Work
+> or a Contribution incorporated within the Work constitutes direct
+> or contributory patent infringement, then any patent licenses
+> granted to You under this License for that Work shall terminate
+> as of the date such litigation is filed.
+>
+> 4. Redistribution. You may reproduce and distribute copies of the
+> Work or Derivative Works thereof in any medium, with or without
+> modifications, and in Source or Object form, provided that You
+> meet the following conditions:
+>
+> (a) You must give any other recipients of the Work or
+> Derivative Works a copy of this License; and
+>
+> (b) You must cause any modified files to carry prominent notices
+> stating that You changed the files; and
+>
+> (c) You must retain, in the Source form of any Derivative Works
+> that You distribute, all copyright, patent, trademark, and
+> attribution notices from the Source form of the Work,
+> excluding those notices that do not pertain to any part of
+> the Derivative Works; and
+>
+> (d) If the Work includes a "NOTICE" text file as part of its
+> distribution, then any Derivative Works that You distribute must
+> include a readable copy of the attribution notices contained
+> within such NOTICE file, excluding those notices that do not
+> pertain to any part of the Derivative Works, in at least one
+> of the following places: within a NOTICE text file distributed
+> as part of the Derivative Works; within the Source form or
+> documentation, if provided along with the Derivative Works; or,
+> within a display generated by the Derivative Works, if and
+> wherever such third-party notices normally appear. The contents
+> of the NOTICE file are for informational purposes only and
+> do not modify the License. You may add Your own attribution
+> notices within Derivative Works that You distribute, alongside
+> or as an addendum to the NOTICE text from the Work, provided
+> that such additional attribution notices cannot be construed
+> as modifying the License.
+>
+> You may add Your own copyright statement to Your modifications and
+> may provide additional or different license terms and conditions
+> for use, reproduction, or distribution of Your modifications, or
+> for any such Derivative Works as a whole, provided Your use,
+> reproduction, and distribution of the Work otherwise complies with
+> the conditions stated in this License.
+>
+> 5. Submission of Contributions. Unless You explicitly state otherwise,
+> any Contribution intentionally submitted for inclusion in the Work
+> by You to the Licensor shall be under the terms and conditions of
+> this License, without any additional terms or conditions.
+> Notwithstanding the above, nothing herein shall supersede or modify
+> the terms of any separate license agreement you may have executed
+> with Licensor regarding such Contributions.
+>
+> 6. Trademarks. This License does not grant permission to use the trade
+> names, trademarks, service marks, or product names of the Licensor,
+> except as required for reasonable and customary use in describing the
+> origin of the Work and reproducing the content of the NOTICE file.
+>
+> 7. Disclaimer of Warranty. Unless required by applicable law or
+> agreed to in writing, Licensor provides the Work (and each
+> Contributor provides its Contributions) on an "AS IS" BASIS,
+> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+> implied, including, without limitation, any warranties or conditions
+> of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+> PARTICULAR PURPOSE. You are solely responsible for determining the
+> appropriateness of using or redistributing the Work and assume any
+> risks associated with Your exercise of permissions under this License.
+>
+> 8. Limitation of Liability. In no event and under no legal theory,
+> whether in tort (including negligence), contract, or otherwise,
+> unless required by applicable law (such as deliberate and grossly
+> negligent acts) or agreed to in writing, shall any Contributor be
+> liable to You for damages, including any direct, indirect, special,
+> incidental, or consequential damages of any character arising as a
+> result of this License or out of the use or inability to use the
+> Work (including but not limited to damages for loss of goodwill,
+> work stoppage, computer failure or malfunction, or any and all
+> other commercial damages or losses), even if such Contributor
+> has been advised of the possibility of such damages.
+>
+> 9. Accepting Warranty or Additional Liability. While redistributing
+> the Work or Derivative Works thereof, You may choose to offer,
+> and charge a fee for, acceptance of support, warranty, indemnity,
+> or other liability obligations and/or rights consistent with this
+> License. However, in accepting such obligations, You may act only
+> on Your own behalf and on Your sole responsibility, not on behalf
+> of any other Contributor, and only if You agree to indemnify,
+> defend, and hold each Contributor harmless for any liability
+> incurred by, or claims asserted against, such Contributor by reason
+> of your accepting any such warranty or additional liability.
diff --git a/julia/NEWS.md b/julia/NEWS.md
new file mode 100644
index 000000000000..71ee86ff7da4
--- /dev/null
+++ b/julia/NEWS.md
@@ -0,0 +1,692 @@
+# v0.4.0 (#TBD)
+
+* Following material from `mx` module got exported (#TBD):
+ * `NDArray`
+ * `clip()`
+ * `clip!()`
+ * `context()`
+ * `empty()`
+ * `expand_dims()`
+ * `@inplace`
+ * `σ()`
+ * `sigmoid()`
+ * `relu()`
+ * `softmax()`
+ * `log_softmax()`
+ * `broadcast_to()`
+ * `broadcast_axis()`
+ * `broadcast_axes()`
+
+ * `SymbolicNode`
+ * `Variable`
+ * `@var`
+
+ * `Context`
+ * `cpu()`
+ * `gpu()`
+
+ * `AbstractModel`
+ * `FeedForward`
+ * `predict()`
+
+ * `MLP`
+
+ * `Executor`
+ * `bind()`
+ * `simple_bind()`
+ * `forward()`
+ * `backward()`
+
+ * `AbstractEvalMetric`
+ * `ACE`
+ * `Accuracy`
+ * `MSE`
+ * `MultiACE`
+ * `MultiMetric`
+ * `NMSE`
+ * `SeqMetric`
+
+ * `KVStore`
+ * `init!()`
+ * `push!()`
+ * `pull!()`
+ * `barrier()`
+ * `set_updater()`
+ * `set_optimizer()`
+
+ * `AbstractInitializer`
+ * `UniformInitializer`
+ * `NormalInitializer`
+ * `XavierInitializer`
+
+ * `AbstractOptimizer`
+ * `AdaDelta`
+ * `AdaGrad`
+ * `ADAM`
+ * `AdaMax`
+ * `Nadam`
+ * `RMSProp`
+ * `SGD`
+ * `getupdater()`
+ * `normgrad!()`
+ * `update!()`
+
+ * `AbstractDataProvider`
+ * `AbstractDataBatch`
+ * `ArrayDataProvider`
+ * `ArrayDataBatch`
+
+ * `to_graphviz()`
+
+## New APIs
+
+### `SymbolicNode`
+
+* `mx.get_children` for exploring the graph programmatically. (#TBD)
+
+* A handy macro `@mx.var` for creating `mx.Variable`. (#TBD)
+
+ ```julia
+ julia> x = @mx.var x
+ MXNet.mx.SymbolicNode x
+
+ julia> x, y, z = @mx.var x y z
+ (MXNet.mx.SymbolicNode x, MXNet.mx.SymbolicNode y, MXNet.mx.SymbolicNode z)
+ ```
+
+### `NDArray`
+
+* A handy constructor: `NDArray(Type, AbstractArray)` is added. (#TBD)
+
+ E.g.
+ ```julia
+ julia> NDArray([1, 2, 3])
+ 3-element mx.NDArray{Int64,1} @ CPU0:
+ 1
+ 2
+ 3
+
+ julia> NDArray(Float32, [1, 2, 3])
+ 3-element mx.NDArray{Float32,1} @ CPU0:
+ 1.0
+ 2.0
+ 3.0
+ ```
+
+* A port of Python's `autograd` for `NDArray` (#274)
+
+* `size(x, dims...)` is supported now. (#TBD)
+
+ ```julia
+ julia> x = mx.NDArray([1 2; 3 4; 5 6])
+ 3×2 mx.NDArray{Int64,2} @ CPU0:
+ 1 2
+ 3 4
+ 5 6
+
+ julia> size(x, 1, 2, 3, 4)
+ (3, 2, 1, 1)
+
+ ```
+
+* `copy(AbstractArray, context)` is implemented now. (#TBD)
+
+ ```julia
+ julia> copy(1:4, mx.cpu())
+ 4 mx.NDArray{Int64,1} @ CPU0:
+ 1
+ 2
+ 3
+ 4
+
+ julia> copy(1.:4, mx.cpu())
+ 4 mx.NDArray{Float64,1} @ CPU0:
+ 1.0
+ 2.0
+ 3.0
+ 4.0
+ ```
+
+* `copy!(NDArray, AbstractArray)` is implemented now. (#TBD)
+
+ ```julia
+ julia> x = mx.zeros(3)
+ 3-element mx.NDArray{Float32,1} @ CPU0:
+ 0.0
+ 0.0
+ 0.0
+
+ julia> copy!(x, 3:5)
+ 3-element mx.NDArray{Float32,1} @ CPU0:
+ 3.0
+ 4.0
+ 5.0
+ ```
+
+* `Base.ones(x::NDArray)` for creating an one-ed `NDArray`. (#TBD)
+
+* `Base.zeros(x::NDArray)` for creating a zero-ed `NDArray`. (#TBD)
+
+* Modulo operator. (#TBD)
+
+ ```julia
+ x = NDArray(...)
+ y = NDArray(...)
+
+ x .% y
+ x .% 2
+ 2 .% x
+ ```
+
+* Inplace modulo operator, `mod_from!` and `rmod_from!`. (#TBD)
+
+ ```julia
+ mod_from!(x, y)
+ mod_from!(x, 2)
+ rmod_from!(2, x)
+ ```
+
+* `cat`, `vcat`, `hcat` is implemented. (#TBD)
+
+ E.g. `hcat`
+ ```julia
+ julia> x
+ 4 mx.NDArray{Float64,1} @ CPU0:
+ 1.0
+ 2.0
+ 3.0
+ 4.0
+
+ julia> y
+ 4 mx.NDArray{Float64,1} @ CPU0:
+ 2.0
+ 4.0
+ 6.0
+ 8.0
+
+ julia> [x y]
+ 4×2 mx.NDArray{Float64,2} @ CPU0:
+ 1.0 2.0
+ 2.0 4.0
+ 3.0 6.0
+ 4.0 8.0
+ ```
+
+* Transposing a column `NDArray` to a row `NDArray` is supported now. (#TBD)
+
+ ```julia
+ julia> x = NDArray(Float32[1, 2, 3, 4])
+ 4 mx.NDArray{Float32,1} @ CPU0:
+ 1.0
+ 2.0
+ 3.0
+ 4.0
+
+ julia> x'
+ 1×4 mx.NDArray{Float32,2} @ CPU0:
+ 1.0 2.0 3.0 4.0
+ ```
+
+* Matrix/tensor multiplication is supported now. (#TBD)
+
+ ```julia
+ julia> x
+ 2×3 mx.NDArray{Float32,2} @ CPU0:
+ 1.0 2.0 3.0
+ 4.0 5.0 6.0
+
+ julia> y
+ 3 mx.NDArray{Float32,1} @ CPU0:
+ -1.0
+ -2.0
+ -3.0
+
+ julia> x * y
+ 2 mx.NDArray{Float32,1} @ CPU0:
+ -14.0
+ -32.0
+ ```
+
+## API Changes
+
+### `NDArray`
+
+* Broadcasting along dimension supported on following operators,
+ and the original `mx.broadcast_*` APIs are deprecated
+ (#401) (#402) (#403):
+
+ * `+`
+ * `-`
+ * `*`
+ * `/`
+ * `%`
+ * `^`
+ * `==`
+ * `!=`
+ * `>`
+ * `>=`
+ * `<`
+ * `<=`
+ * `max`
+ * `min`
+
+ ```julia
+ julia> x = NDArray([1 2 3;
+ 4 5 6])
+ 2×3 mx.NDArray{Int64,2} @ CPU0:
+ 1 2 3
+ 4 5 6
+
+ julia> y = NDArray([1;
+ 10])
+ 2-element mx.NDArray{Int64,1} @ CPU0:
+ 1
+ 10
+
+ julia> x .+ y
+ 2×3 mx.NDArray{Int64,2} @ CPU0:
+ 2 3 4
+ 14 15 16
+ ```
+
+* Please use dot-call on following trigonometric functions.
+ Also, the `arc*` has been renamed to keep consistent with `Base`.
+ (#TBD)
+
+ * `sin.(x)`
+ * `cos.(x)`
+ * `tan.(x)`
+ * `arcsin(x)` -> `asin.(x)`
+ * `arccos(x)` -> `acos.(x)`
+ * `arctan(x)` -> `atan.(x)`
+
+* Please use dot-call on following hyperbolic functions.
+ Also, the `arc*` has been renamed to keep consistent with `Base`.
+ (#TBD)
+
+ * `sinh.(x)`
+ * `cosh.(x)`
+ * `tanh.(x)`
+ * `arcsinh(x)` -> `asinh.(x)`
+ * `arccosh(x)` -> `acosh.(x)`
+ * `arctanh(x)` -> `atanh.(x)`
+
+* Please use dot-call on following activation functions.
+ And the `dim` of `softmax` and `log_softmax` has been fixed
+ as Julia column-based style.
+ (#TBD)
+
+ * `σ.(x)`
+ * `relu.(x)`
+ * `softmax.(x, [dim = ndims(x)])`
+ * `log_softmax.(x, [dim = ndims(x)])`
+
+* `rand`, `rand!`, `randn`, `randn!` is more Base-like now (#TBD).
+
+ ```julia
+ julia> mx.rand(2, 3)
+ 2×3 mx.NDArray{Float32,2} @ CPU0:
+ 0.631961 0.324175 0.0762663
+ 0.285366 0.395292 0.074995
+
+ julia> mx.rand(2, 3; low = 1, high = 10)
+ 2×3 mx.NDArray{Float32,2} @ CPU0:
+ 7.83884 7.85793 7.64791
+ 7.68646 8.56082 8.42189
+ ```
+
+ ```julia
+ julia> mx.randn(2, 3)
+ 2×3 mx.NDArray{Float32,2} @ CPU0:
+ 0.962853 0.424535 -0.320123
+ 0.478113 1.72886 1.72287
+
+ julia> mx.randn(2, 3, μ = 100)
+ 2×3 mx.NDArray{Float32,2} @ CPU0:
+ 99.5635 100.483 99.888
+ 99.9889 100.533 100.072
+ ```
+
+* Signature of `clip` changed, it doesn't require any keyword argument now.
+ (#TBD)
+
+ Before: `clip(x, a_min = -4, a_max = 4)`
+ After: `clip(x, -4, 4)`
+
+### Optimizer
+
+We overhauled the optimizer APIs, introducing breaking changes.
+There are tons of renaming, and we try to increase the flexibility.
+Making it decouples from some high-level, so user can use it without
+understand some detail implementations of `fit!`.
+
+See #396.
+
+* All the keyword argument of optimizers have been renamed.
+ Now we have more elegant keyword arguments than Python's,
+ thanks to well Unicode support on Julia's REPL and editor plugin.
+ *These are breaking changes, no deprecation warning.*
+
+ | old | new | comment |
+ |---------------------------|-----------|--------------------------------|
+ | `opts.lr` | `η` | type `\eta` in REPL |
+ | `opts.momentum` | `μ` | type `\mu` in REPL |
+ | `opts.grad_clip` | `clip` | type `\nablac` in REPL |
+ | `opts.weight_decay` | `λ` | type `\lambda` in REPL |
+ | `opts.lr_schedular` | `η_sched` | type `\eta_sched` in REPL |
+ | `opts.momentum_schedular` | `μ_sched` | type `\mu_sched` in REPL |
+
+ For instance, one accessed the learning via `SGD().opts.lr`,
+ but now, it's `SGD().η`.
+
+* New keyword argument `scale` for gradient rescaling.
+
+ Docstring:
+ ```
+ If != 0, multiply the gradient with `∇r` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+ ```
+
+* Keyword arguments of `NadamScheduler` has been renamed.
+ *This is a breaking change, no deprecation warning.*
+
+ * Before
+
+ ```julia
+ NadamScheduler(; mu0 = 0.99, delta = 0.004, gamma = 0.5, alpha = 0.96)
+ ```
+
+ * After
+
+ ```julia
+ NadamScheduler(; μ = 0.99, δ = 0.004, γ = 0.5, α = 0.96)
+ ```
+
+* The attribute `optimizer.state` is removed.
+ `OptimizationState` is only used by high-level abstraction, like `fit!`.
+
+* `LearningRate` scheduler API changes:
+
+ * `get_learning_rate` is removed.
+ Please use `Base.get` to get learning rate.
+
+ ```julia
+ julia> sched = mx.LearningRate.Exp(.1)
+ MXNet.mx.LearningRate.Exp(0.1, 0.9, 0)
+
+ julia> get(sched)
+ 0.1
+
+ julia> update!(sched);
+
+ julia> get(sched)
+ 0.09000000000000001
+ ```
+
+ * `update!` to bump counter of `Scheduler.t`
+ ```julia
+ julia> sched.t
+ 1
+
+ julia> update!(sched);
+
+ julia> sched.t
+ 2
+
+ julia> update!(sched);
+
+ julia> sched.t
+ 3
+ ```
+
+* `Momentum` module API changes:
+
+ * `get_momentum_scheduler` is removed. Please use `Base.get` instead.
+
+ ```julia
+ julia> get(mx.Momentum.Fixed(.9))
+ 0.9
+ ```
+
+----
+
+# v0.3.0 (2017.11.16)
+
+* Update `libmxnet` to
+ * On Windows: v0.12.0.
+ (See https://github.com/apache/incubator-mxnet/releases/tag/0.12.0)
+
+ * On Linux/macOS: v0.12.1.
+ (See https://github.com/apache/incubator-mxnet/releases/tag/0.12.1)
+
+* Drop 0.5 support. ([#300][300])
+
+## New API
+
+### `SymbolicNode`
+
+* Debugging print support. ([#276][276])
+
+### `NDArray`
+
+* `deepcopy` for `NDArray` ([#273][273])
+
+* `scalar ./ NDArray` is available now. ([#292][292])
+
+* `fill` and `fill!` for `NDArray`. ([#297][297], [#311][311])
+
+ An API correspond to Python's `mx.nd.full()`
+
+ * `fill(x, dims, ctx=cpu())`
+ * `fill(x, dims...)`
+ * `fill!(arr::NDArray, x)`
+
+* Matrix (2D `NDArray`) multiplication is available now. ([#300][300])
+
+ ```julia
+ julia> x
+ 1x2 mx.NDArray{Float64} @ CPU0:
+ 1.0 2.0
+
+ julia> x' * x
+ 2x2 mx.NDArray{Float64} @ CPU0:
+ 1.0 2.0
+ 2.0 4.0
+ ```
+
+* `NDArray` `getindex`/`setindex!` linear indexing support and `first` for
+ extracting scalar value. ([#294][294])
+
+ ```julia
+ julia> x = mx.zeros(2, 5)
+
+ julia> x[5] = 42 # do synchronization and set the value
+ ```
+
+ ```julia
+ julia> y = x[5] # actually, getindex won't do synchronization, but REPL's showing did it for you
+ 1 mx.NDArray{Float32} @ CPU0:
+ 42.0
+
+ julia> first(y) # do sync and get the value
+ 42.0f0
+
+ julia> y[] # this is available, also
+ 42.0f0
+ ```
+* Elementwise power of `NDArray`. ([#293][293])
+
+ * `x.^2`
+ * `2.^x`
+ * `x.^y`
+ * where `x` and `y` are `NDArray`s.
+
+* Elementwise power of irrational and `NDArray`. ([#310][310])
+
+ * `e.^x`
+ * `x.^e`
+ * `π.^x`
+
+## API Changes
+
+### `SymbolicNode`
+
+* `reshape` of `SymbolicNode` shares the same interface with Base
+ and additional keyword argument. ([#279][279])
+
+ * `reshape(SymbolicNode, dim; reverse=false, name)`
+ * `reshape(SymbolicNode, dim...; reverse=false, name)`
+ * `Reshape` is deprecated.
+
+* `mx.forward(x)` will return `x.outputs` now. ([#312][312])
+
+### `NDArray`
+
+* `reshape` of `NDArray` shares the same interface with Base. ([#272][272])
+
+ * `reshape(NDArray, dim; reverse=false)`
+ * `reshape(NDArray, dim...; reverse=false)`
+ * `Reshape` is deprecated.
+
+* `srand!` deprecated, please use `srand`. ([#282][282])
+
+* `mean` and `sum` of `NDArray` share the same interface with Base
+ and fix the `axis` indexing. ([#303][303])
+
+ * This is a breaking change; no deprecated warning.
+ * Before: `mean(arr, axis=0)`
+ * After: `mean(arr, 1)`
+
+* `max` and `min` of `NDArray` renamed to `maximum` and `minimum` and share the
+ same interface with Base. The `axis` indexing is fixed, also. ([#303][303])
+
+ * This is a breaking change; no deprecated warning.
+ * Before: `mx.max(arr, axis=0)` or `mx.max_axis(arr, axis=0)`
+ * After: `maximum(arr, 1)`
+
+* `mx.transpose` for high dimension `NDArray` has been renamed to `permutedims`
+ and shares the same interface with Base. ([#303][303])
+
+ * This is a breaking changes; no deprecated warning.
+ * Before: `mx.transpose(A, axis=[2, 1, 3])`
+ * After: `permutedims(A, [2, 1, 3])`
+
+* `prod` of `NDArray` shares the same interface with Base and fix the `axis`
+ indexing. ([#303][303])
+
+ * This is a breaking change; no deprecated warning.
+ * Before: `prod(arr, axis=-1)`
+ * After: `prod(arr, 1)`
+
+## Bugfix
+
+* Broadcasting operation on same variable is back. ([#300][300], [#314][314])
+ ```julia
+ x = mx.NDArray(...)
+ x .* x
+ ```
+
+ ```julia
+ y = mx.Variable(:y)
+ y .* y
+ ```
+
+[272]: https://github.com/dmlc/MXNet.jl/pull/272
+[273]: https://github.com/dmlc/MXNet.jl/pull/273
+[276]: https://github.com/dmlc/MXNet.jl/pull/276
+[279]: https://github.com/dmlc/MXNet.jl/pull/279
+[282]: https://github.com/dmlc/MXNet.jl/pull/282
+[292]: https://github.com/dmlc/MXNet.jl/pull/292
+[293]: https://github.com/dmlc/MXNet.jl/pull/293
+[294]: https://github.com/dmlc/MXNet.jl/pull/294
+[297]: https://github.com/dmlc/MXNet.jl/pull/297
+[300]: https://github.com/dmlc/MXNet.jl/pull/300
+[303]: https://github.com/dmlc/MXNet.jl/pull/303
+[310]: https://github.com/dmlc/MXNet.jl/pull/310
+[311]: https://github.com/dmlc/MXNet.jl/pull/311
+[312]: https://github.com/dmlc/MXNet.jl/pull/312
+[314]: https://github.com/dmlc/MXNet.jl/pull/314
+
+# v0.2.2 (2017.05.14)
+* Updated supported version of MXNet to 0.9.4.
+* Improved build-system with support for auto-detecting GPU support.
+* Several updates to Metrics.
+* CI for Windows.
+* Verbosity option for `predict` (@rdeits)
+
+# v0.2.1 (2017.01.29)
+* Bugfix release for Windows
+
+# v0.2.0 (2017.01.26)
+* Drop support for Julia v0.4.
+* Added support for NVVM.
+* Updated supported version of MXNet to 0.9.2
+* New optimizers (@Arkoniak).
+
+# v0.1.0 (2016.09.08)
+
+* Track specific libmxnet version for each release.
+* Migrated documentation system to `Documenter.jl` (@vchuravy)
+* Simplified building by using Julia's OpenBlas (@staticfloat)
+* Freezing parameters (@vchuravy)
+* Support `DType` for `NDArray` (@vchuravy)
+
+# v0.0.8 (2016.02.08)
+
+* Fix compatability with Julia v0.5.
+* Fix seg-faults introduced by upstream API changes.
+
+# v0.0.7 (2015.12.14)
+
+* Fix compatability with Julia v0.4.2 (@BigEpsilon)
+* Metrics in epoch callbacks (@kasiabozek)
+
+# v0.0.6 (2015.12.02)
+
+* Variants of Xaiver initializers (@vchuravy)
+* More arithmetic operators on symbolic nodes
+* Basic interface for symbolic node attributes (@vchuravy)
+
+# v0.0.5 (2015.11.14)
+
+* char-lstm example.
+* Network visualization via GraphViz.
+* NN-factory for common models.
+* Convenient `@nd_as_jl` macro to work with `NDArray` as Julia Arrays.
+* Refactoring: `Symbol` -> `SymbolicNode`.
+* More evaluation metrics (@vchuravy, @Andy-P)
+
+# v0.0.4 (2015.11.09)
+
+* ADAM optimizer (@cbecker)
+* Improved data provider API.
+* More documentation.
+* Fix a bug in array data iterator (@vchuravy)
+
+# v0.0.3 (2015.10.27)
+
+* Model prediction API.
+* Model checkpoint loading and saving.
+* IJulia Notebook example of using pre-trained imagenet model as classifier.
+* Symbol saving and loading.
+* NDArray saving and loading.
+* Optimizer gradient clipping.
+* Model training callback APIs, default checkpoint and speedometer callbacks.
+* Julia Array / NDArray data iterator.
+* Sphinx documentation system and documents for dynamically imported libmxnet APIs.
+
+# v0.0.2 (2015.10.23)
+
+* Fix a bug in build script that causes Julia REPL to exit.
+
+# v0.0.1 (2015.10.23)
+
+Initial release.
+
+* Basic libmxnet API.
+* Basic documentation, overview and MNIST tutorial.
+* Working MNIST and cifar-10 examples, with multi-GPU training.
+* Automatic building of libmxnet with BinDeps.jl.
+
diff --git a/julia/README-DEV.md b/julia/README-DEV.md
new file mode 100644
index 000000000000..a1d6fa9012fc
--- /dev/null
+++ b/julia/README-DEV.md
@@ -0,0 +1,13 @@
+# Workflow for making a release
+
+1. Update `NEWS.md` to list important changes
+2. Check out the `stable` branch, merge with `master`.
+3. Update `libmxnet_curr_ver` in `deps/build.jl` to the latest commit SHA (or any proper reference). Using `master` here is not good because future changes in libmxnet might break existing Julia packages.
+4. Run tests.
+5. Merge master into stable branch.
+6. Tag stable branch: `git tag v1.2.3`
+7. Push tag to remote: `git push origin `
+8. Edit the [releases page](https://github.com/dmlc/MXNet.jl/releases)
+ to copy the release notes from `NEWS.md` to the newly created release tag.
+9. Goto https://github.com/JuliaLang/METADATA.jl/pulls
+ and check `attobot` already make a PR for the release.
diff --git a/julia/README.md b/julia/README.md
new file mode 100644
index 000000000000..a4299575f95e
--- /dev/null
+++ b/julia/README.md
@@ -0,0 +1,60 @@
+# MXNet
+
+[![MXNet](http://pkg.julialang.org/badges/MXNet_0.6.svg)](http://pkg.julialang.org/?pkg=MXNet)
+
+
+MXNet.jl is the [dmlc/mxnet](https://github.com/apache/incubator-mxnet) [Julia](http://julialang.org/) package. MXNet.jl brings flexible and efficient GPU computing and state-of-art deep learning to Julia. Some highlight of its features include:
+
+* Efficient tensor/matrix computation across multiple devices, including multiple CPUs, GPUs and distributed server nodes.
+* Flexible symbolic manipulation to composite and construction of state-of-the-art deep learning models.
+
+Here is an example of how training a simple 3-layer MLP on MNIST:
+
+```julia
+using MXNet
+
+mlp = @mx.chain mx.Variable(:data) =>
+ mx.FullyConnected(name=:fc1, num_hidden=128) =>
+ mx.Activation(name=:relu1, act_type=:relu) =>
+ mx.FullyConnected(name=:fc2, num_hidden=64) =>
+ mx.Activation(name=:relu2, act_type=:relu) =>
+ mx.FullyConnected(name=:fc3, num_hidden=10) =>
+ mx.SoftmaxOutput(name=:softmax)
+
+# data provider
+batch_size = 100
+include(Pkg.dir("MXNet", "examples", "mnist", "mnist-data.jl"))
+train_provider, eval_provider = get_mnist_providers(batch_size)
+
+# setup model
+model = mx.FeedForward(mlp, context=mx.cpu())
+
+# optimization algorithm
+# where η is learning rate and μ is momentum
+optimizer = mx.SGD(η=0.1, μ=0.9)
+
+# fit parameters
+mx.fit(model, optimizer, train_provider, n_epoch=20, eval_data=eval_provider)
+```
+
+You can also predict using the `model` in the following way:
+
+```julia
+probs = mx.predict(model, eval_provider)
+
+# collect all labels from eval data
+labels = reduce(
+ vcat,
+ copy(mx.get(eval_provider, batch, :softmax_label)) for batch ∈ eval_provider)
+# labels are 0...9
+labels .= labels .+ 1
+
+# Now we use compute the accuracy
+pred = map(i -> indmax(probs[1:10, i]), 1:size(probs, 2))
+correct = sum(pred .== labels)
+accuracy = 100correct/length(labels)
+@printf "Accuracy on eval set: %.2f%%\n" accuracy
+```
+
+For more details, please refer to the
+[documentation](https://dmlc.github.io/MXNet.jl/latest) and [examples](examples).
diff --git a/julia/REQUIRE b/julia/REQUIRE
new file mode 100644
index 000000000000..5a76dc543b25
--- /dev/null
+++ b/julia/REQUIRE
@@ -0,0 +1,7 @@
+julia 0.6
+Formatting
+BinDeps
+JSON
+MacroTools
+TakingBroadcastSeriously
+Reexport
diff --git a/julia/appveyor.yml b/julia/appveyor.yml
new file mode 100644
index 000000000000..50e275cfa8a8
--- /dev/null
+++ b/julia/appveyor.yml
@@ -0,0 +1,56 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+environment:
+ matrix:
+ - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
+
+branches:
+ only:
+ - master
+ - stable
+
+notifications:
+ - provider: Email
+ on_build_success: false
+ on_build_failure: false
+ on_build_status_changed: false
+
+install:
+ - ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12"
+# If there's a newer build queued for the same PR, cancel this one
+ - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
+ https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
+ Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
+ throw "There are newer queued builds for this pull request, failing early." }
+
+# Download most recent Julia Windows binary
+ - ps: (new-object net.webclient).DownloadFile(
+ $env:JULIA_URL,
+ "C:\projects\julia-binary.exe")
+# Run installer silently, output to C:\projects\julia
+ - C:\projects\julia-binary.exe /S /D=C:\projects\julia
+
+build_script:
+# Need to convert from shallow to complete for Pkg.clone to work
+ - IF EXIST .git\shallow (git fetch --unshallow)
+ - C:\projects\julia\bin\julia -e "versioninfo();
+ Pkg.clone(pwd(), \"MXNet\"); Pkg.build(\"MXNet\")"
+
+test_script:
+ - C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"MXNet\")"
+
diff --git a/julia/deps/build.jl b/julia/deps/build.jl
new file mode 100644
index 000000000000..bdc33be8c79b
--- /dev/null
+++ b/julia/deps/build.jl
@@ -0,0 +1,280 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import JSON
+
+################################################################################
+# First try to detect and load existing libmxnet
+################################################################################
+libmxnet_detected = false
+libmxnet_curr_ver = get(ENV, "MXNET_COMMIT", "master")
+curr_win = "20180211" # v1.1.0
+
+if haskey(ENV, "MXNET_HOME")
+ MXNET_HOME = ENV["MXNET_HOME"]
+ info("MXNET_HOME environment detected: $MXNET_HOME")
+ info("Trying to load existing libmxnet...")
+ # In case of macOS, if user build libmxnet from source and set the MXNET_HOME,
+ # the output is still named as `libmxnet.so`.
+ lib = Libdl.find_library(["libmxnet.$(Libdl.dlext)", "libmxnet.so"],
+ [joinpath(MXNET_HOME, "lib"), MXNET_HOME])
+ if !isempty(lib)
+ info("Existing libmxnet detected at $lib, skip building...")
+ libmxnet_detected = true
+ else
+ info("Failed to load existing libmxnet, trying to build from source...")
+ end
+end
+
+# Try to find cuda
+CUDAPATHS = String[]
+if haskey(ENV, "CUDA_HOME")
+ push!(CUDAPATHS, joinpath(ENV["CUDA_HOME"], "lib64"))
+elseif is_linux()
+ append!(CUDAPATHS, ["/opt/cuda/lib64", "/usr/local/cuda/lib64"])
+end
+
+if is_unix()
+ try
+ push!(CUDAPATHS, replace(strip(readstring(`which nvcc`)), "bin/nvcc", "lib64"))
+ end
+end
+
+HAS_CUDA = false
+HAS_CUDNN = false
+let cudalib = Libdl.find_library(["libcuda", "nvcuda.dll"], CUDAPATHS)
+ HAS_CUDA = !isempty(cudalib) && Libdl.dlopen_e(cudalib) != C_NULL
+end
+
+if !HAS_CUDA && is_windows()
+ # TODO: this needs to be improved.
+ try
+ run(`nvcc --version`)
+ HAS_CUDA = true
+ end
+end
+
+if HAS_CUDA # then check cudnn
+ let cudnnlib = Libdl.find_library("libcudnn", CUDAPATHS)
+ HAS_CUDNN = !isempty(cudnnlib) && Libdl.dlopen_e(cudnnlib) != C_NULL
+ if HAS_CUDNN && !haskey(ENV, "CUDA_HOME") # inference `CUDA_HOME`
+ ENV["CUDA_HOME"] = dirname(dirname(cudnnlib))
+ end
+ end
+end
+
+if HAS_CUDA
+ info("Found a CUDA installation.")
+ if HAS_CUDNN
+ info("Found a CuDNN installation.")
+ end
+ info("CUDA_HOME -> $(get(ENV, "CUDA_HOME", nothing))")
+else
+ info("Did not find a CUDA installation, using CPU-only version of MXNet.")
+end
+
+# propagate more build flags from ENV
+const CC = get(ENV, "CC", nothing)
+const CXX = get(ENV, "CXX", nothing)
+const ADD_CFLAGS = get(ENV, "ADD_CFLAGS", nothing)
+const ADD_LDFLAGS = get(ENV, "ADD_LDFLAGS", nothing)
+const USE_JEMALLOC = get(ENV, "USE_JEMALLOC", nothing) # "0" or "1"
+
+function get_cpucore()
+ if haskey(ENV, "TRAVIS") # on travis-ci
+ 2
+ else
+ min(Sys.CPU_CORES, 32)
+ end
+end
+
+using BinDeps
+@BinDeps.setup
+if !libmxnet_detected
+ if is_windows()
+ if Sys.ARCH != :x86_64
+ info("Prebuilt windows binaries are only available on 64bit. You will have to built MXNet yourself.")
+ return
+ end
+ info("Downloading pre-built packages for Windows.")
+ base_url = "https://github.com/yajiedesign/mxnet/releases/download/weekly_binary_build_v2/prebuildbase_win10_x64_vc14_v2.7z"
+
+ if libmxnet_curr_ver == "master"
+ # download_cmd uses powershell 2, but we need powershell 3 to do this
+ run(`powershell -NoProfile -Command Invoke-WebRequest -Uri "https://api.github.com/repos/yajiedesign/mxnet/releases/latest" -OutFile "mxnet.json"`)
+ curr_win = JSON.parsefile("mxnet.json")["tag_name"]
+ info("Can't use MXNet master on Windows, using latest binaries from $curr_win.")
+ end
+ # TODO: Get url from JSON.
+ name = "mxnet_x64_vc14_$(HAS_CUDA ? "gpu" : "cpu").7z"
+ package_url = "https://github.com/yajiedesign/mxnet/releases/download/$(curr_win)/$(curr_win)_$(name)"
+
+ exe7z = joinpath(JULIA_HOME, "7z.exe")
+
+ run(download_cmd(package_url, "mxnet.7z"))
+ # this command will create the dir "usr\\lib"
+ run(`$exe7z x mxnet.7z build lib -y -ousr`)
+ run(`cmd /c copy "usr\\build\\*.dll" "usr\\lib"`)
+
+ run(download_cmd(base_url, "mxnet_base.7z"))
+ run(`$exe7z x mxnet_base.7z -y -ousr`)
+ run(`cmd /c copy "usr\\prebuildbase_win10_x64_vc14_v2\\3rdparty\\bin\\*.dll" "usr\\lib"`)
+
+ # testing
+ run(`cmd /c dir "usr\\lib"`)
+ return
+ end
+
+ ################################################################################
+ # If not found, try to build automatically using BinDeps
+ ################################################################################
+
+ blas_path = Libdl.dlpath(Libdl.dlopen(Base.libblas_name))
+
+ blas_vendor = Base.BLAS.vendor()
+
+ ilp64 = ""
+ if blas_vendor == :openblas64
+ ilp64 = "-DINTERFACE64"
+ end
+
+ FORCE_LAPACK = false
+ if blas_vendor == :unknown
+ info("Julia is built with an unkown blas library ($blas_path).")
+ info("Attempting build without reusing the blas library")
+ USE_JULIA_BLAS = false
+ elseif !(blas_vendor in (:openblas, :openblas64))
+ info("Unsure if we can build against $blas_vendor.")
+ info("Attempting build anyway.")
+ USE_JULIA_BLAS = true
+ else
+ USE_JULIA_BLAS = true
+ FORCE_LAPACK = true
+ end
+ info("USE_JULIA_BLAS -> $USE_JULIA_BLAS")
+
+ blas_name = blas_vendor == :openblas64 ? "openblas" : string(blas_vendor)
+ MSHADOW_LDFLAGS = "MSHADOW_LDFLAGS=-lm $blas_path"
+
+ #--------------------------------------------------------------------------------
+ # Build libmxnet
+ mxnet = library_dependency("mxnet", aliases=["mxnet", "libmxnet", "libmxnet.so"])
+
+ _prefix = joinpath(BinDeps.depsdir(mxnet), "usr")
+ _srcdir = joinpath(BinDeps.depsdir(mxnet), "src")
+ _mxdir = joinpath(_srcdir, "mxnet")
+ _libdir = joinpath(_prefix, "lib")
+ # We have do eagerly delete the installed libmxnet.so
+ # Otherwise we won't rebuild on an update.
+ run(`rm -f $_libdir/libmxnet.$(Libdl.dlext)`)
+ provides(BuildProcess,
+ (@build_steps begin
+ CreateDirectory(_srcdir)
+ CreateDirectory(_libdir)
+ @build_steps begin
+ BinDeps.DirectoryRule(_mxdir, @build_steps begin
+ ChangeDirectory(_srcdir)
+ `git clone https://github.com/apache/incubator-mxnet`
+ end)
+ @build_steps begin
+ ChangeDirectory(_mxdir)
+ `git fetch`
+ if libmxnet_curr_ver != "master"
+ `git checkout $libmxnet_curr_ver`
+ else
+ `git checkout origin/$libmxnet_curr_ver`
+ end
+ `git submodule update --init --recursive`
+ `git -C mshadow checkout -- make/mshadow.mk`
+
+ # copying on changed, make travis caching happy
+ `../../cpcblas.sh`
+
+ `sed -i -s "s/MSHADOW_CFLAGS = \(.*\)/MSHADOW_CFLAGS = \1 $ilp64/" mshadow/make/mshadow.mk`
+
+ # Copy config.mk, always override the file
+ if is_apple()
+ `cp make/osx.mk config.mk`
+ else
+ `cp make/config.mk config.mk`
+ end
+
+ # Configure OpenCV
+ `sed -i -s 's/USE_OPENCV = 1/USE_OPENCV = 0/' config.mk`
+
+ # Configure CUDA
+ if HAS_CUDA
+ @build_steps begin
+ `sed -i -s 's/USE_CUDA = 0/USE_CUDA = 1/' config.mk`
+ # address https://github.com/apache/incubator-mxnet/pull/7856
+ `sed -i -s "s/ADD_LDFLAGS =\(.*\)/ADD_LDFLAGS =\1 -lcublas -lcusolver -lcurand -lcudart/" config.mk`
+ if haskey(ENV, "CUDA_HOME")
+ `sed -i -s "s@USE_CUDA_PATH = NONE@USE_CUDA_PATH = $(ENV["CUDA_HOME"])@" config.mk`
+ end
+ if haskey(ENV, "CUDA_HOME")
+ # address https://github.com/apache/incubator-mxnet/pull/7838
+ flag = "-L$(ENV["CUDA_HOME"])/lib64 -L$(ENV["CUDA_HOME"])/lib"
+ `sed -i -s "s@ADD_LDFLAGS =\(.*\)@ADD_LDFLAGS =\1 $flag@" config.mk`
+ end
+ if HAS_CUDNN
+ `sed -i -s 's/USE_CUDNN = 0/USE_CUDNN = 1/' config.mk`
+ end
+ end
+ end
+
+ # Force enable LAPACK build
+ # Julia's OpenBLAS has LAPACK functionality already
+ if FORCE_LAPACK
+ if is_apple()
+ MSHADOW_LDFLAGS *= " -framework Accelerate"
+ end
+ `sed -i -s 's/ADD_CFLAGS =\(.*\)/ADD_CFLAGS =\1 -DMXNET_USE_LAPACK/' config.mk`
+ end
+
+ # propagate more build flags from ENV
+ if CC != nothing
+ `sed -i -s "s@^export CC =\(.*\)@export CC = $CC@" config.mk`
+ end
+ if CXX != nothing
+ `sed -i -s "s@^export CXX =\(.*\)@export CXX = $CXX@" config.mk`
+ end
+ if ADD_CFLAGS != nothing
+ `sed -i -s "s@ADD_CFLAGS =\(.*\)@ADD_CFLAGS =\1 $ADD_CFLAGS@" config.mk`
+ end
+ if ADD_LDFLAGS != nothing
+ `sed -i -s "s@ADD_LDFLAGS =\(.*\)@ADD_LDFLAGS =\1 $ADD_LDFLAGS@" config.mk`
+ end
+ if USE_JEMALLOC != nothing
+ `sed -i -s "s@USE_JEMALLOC =\(.*\)@USE_JEMALLOC = $USE_JEMALLOC@" config.mk`
+ end
+
+ if USE_JULIA_BLAS
+ `make -j$(get_cpucore()) USE_BLAS=$blas_name $MSHADOW_LDFLAGS`
+ else
+ `make -j$(get_cpucore())`
+ end
+ end
+ FileRule(joinpath(_libdir, "libmxnet.$(Libdl.dlext)"), @build_steps begin
+ # the output file on macos is still in `.so` suffix
+ # so we rename it
+ `cp $_mxdir/lib/libmxnet.so $_libdir/libmxnet.$(Libdl.dlext)`
+ end)
+ end
+ end), mxnet, installed_libpath=_libdir)
+
+ @BinDeps.install Dict(:mxnet => :mxnet)
+end
diff --git a/julia/deps/cblas.h b/julia/deps/cblas.h
new file mode 100644
index 000000000000..d9449dc8e21d
--- /dev/null
+++ b/julia/deps/cblas.h
@@ -0,0 +1,563 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef CBLAS_H
+#define CBLAS_H
+
+/*
+ * This file modified from the OpenBLAS repository.
+ */
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+ /* Assume C declarations for C++ */
+#endif /* __cplusplus */
+
+
+/*
+ * Since all of GotoBlas was written without const,
+ * we disable it at build time.
+ */
+#ifndef OPENBLAS_CONST
+# define OPENBLAS_CONST const
+#endif
+
+/*
+ * Add definitions for BLASLONG and blasint
+ */
+
+#if defined(OS_WINDOWS) && defined(__64BIT__)
+typedef long long BLASLONG;
+typedef unsigned long long BLASULONG;
+#else
+typedef long BLASLONG;
+typedef unsigned long BLASULONG;
+#endif
+
+#ifdef INTERFACE64
+typedef BLASLONG blasint;
+#else
+typedef int blasint;
+#endif
+
+/* copy from openblas_config_template.h */
+/* C99 supports complex floating numbers natively, which GCC also offers as an
+ extension since version 3.0. If neither are available, use a compatible
+ structure as fallback (see Clause 6.2.5.13 of the C99 standard). */
+#if ((defined(__STDC_IEC_559_COMPLEX__) || __STDC_VERSION__ >= 199901L || \
+ (__GNUC__ >= 3 && !defined(__cplusplus))) && !(defined(FORCE_OPENBLAS_COMPLEX_STRUCT)))
+#ifndef __cplusplus
+ #include
+#endif
+ typedef float _Complex openblas_complex_float;
+ typedef double _Complex openblas_complex_double;
+#else
+ typedef struct { float real, imag; } openblas_complex_float;
+ typedef struct { double real, imag; } openblas_complex_double;
+#endif
+
+#ifdef INTERFACE64
+# define cblas_sdsdot cblas_sdsdot64_
+# define cblas_dsdot cblas_dsdot64_
+# define cblas_sdot cblas_sdot64_
+# define cblas_ddot cblas_ddot64_
+# define cblas_cdotu cblas_cdotu64_
+# define cblas_cdotc cblas_cdotc64_
+# define cblas_zdotu cblas_zdotu64_
+# define cblas_zdotc cblas_zdotc64_
+# define cblas_cdotu_sub cblas_cdotu_sub64_
+# define cblas_cdotc_sub cblas_cdotc_sub64_
+# define cblas_zdotu_sub cblas_zdotu_sub64_
+# define cblas_zdotc_sub cblas_zdotc_sub64_
+# define cblas_sasum cblas_sasum64_
+# define cblas_dasum cblas_dasum64_
+# define cblas_scasum cblas_scasum64_
+# define cblas_dzasum cblas_dzasum64_
+# define cblas_snrm2 cblas_snrm264_
+# define cblas_dnrm2 cblas_dnrm264_
+# define cblas_scnrm2 cblas_scnrm264_
+# define cblas_dznrm2 cblas_dznrm264_
+# define cblas_isamax cblas_isamax64_
+# define cblas_idamax cblas_idamax64_
+# define cblas_icamax cblas_icamax64_
+# define cblas_izamax cblas_izamax64_
+# define cblas_saxpy cblas_saxpy64_
+# define cblas_daxpy cblas_daxpy64_
+# define cblas_caxpy cblas_caxpy64_
+# define cblas_zaxpy cblas_zaxpy64_
+# define cblas_scopy cblas_scopy64_
+# define cblas_dcopy cblas_dcopy64_
+# define cblas_ccopy cblas_ccopy64_
+# define cblas_zcopy cblas_zcopy64_
+# define cblas_sswap cblas_sswap64_
+# define cblas_dswap cblas_dswap64_
+# define cblas_cswap cblas_cswap64_
+# define cblas_zswap cblas_zswap64_
+# define cblas_srot cblas_srot64_
+# define cblas_drot cblas_drot64_
+# define cblas_srotg cblas_srotg64_
+# define cblas_drotg cblas_drotg64_
+# define cblas_srotm cblas_srotm64_
+# define cblas_drotm cblas_drotm64_
+# define cblas_srotmg cblas_srotmg64_
+# define cblas_drotmg cblas_drotmg64_
+# define cblas_sscal cblas_sscal64_
+# define cblas_dscal cblas_dscal64_
+# define cblas_cscal cblas_cscal64_
+# define cblas_zscal cblas_zscal64_
+# define cblas_csscal cblas_csscal64_
+# define cblas_zdscal cblas_zdscal64_
+# define cblas_sgemv cblas_sgemv64_
+# define cblas_dgemv cblas_dgemv64_
+# define cblas_cgemv cblas_cgemv64_
+# define cblas_zgemv cblas_zgemv64_
+# define cblas_sger cblas_sger64_
+# define cblas_dger cblas_dger64_
+# define cblas_cgeru cblas_cgeru64_
+# define cblas_cgerc cblas_cgerc64_
+# define cblas_zgeru cblas_zgeru64_
+# define cblas_zgerc cblas_zgerc64_
+# define cblas_strsv cblas_strsv64_
+# define cblas_dtrsv cblas_dtrsv64_
+# define cblas_ctrsv cblas_ctrsv64_
+# define cblas_ztrsv cblas_ztrsv64_
+# define cblas_strmv cblas_strmv64_
+# define cblas_dtrmv cblas_dtrmv64_
+# define cblas_ctrmv cblas_ctrmv64_
+# define cblas_ztrmv cblas_ztrmv64_
+# define cblas_ssyr cblas_ssyr64_
+# define cblas_dsyr cblas_dsyr64_
+# define cblas_cher cblas_cher64_
+# define cblas_zher cblas_zher64_
+# define cblas_ssyr2 cblas_ssyr264_
+# define cblas_dsyr2 cblas_dsyr264_
+# define cblas_cher2 cblas_cher264_
+# define cblas_zher2 cblas_zher264_
+# define cblas_sgbmv cblas_sgbmv64_
+# define cblas_dgbmv cblas_dgbmv64_
+# define cblas_cgbmv cblas_cgbmv64_
+# define cblas_zgbmv cblas_zgbmv64_
+# define cblas_ssbmv cblas_ssbmv64_
+# define cblas_dsbmv cblas_dsbmv64_
+# define cblas_stbmv cblas_stbmv64_
+# define cblas_dtbmv cblas_dtbmv64_
+# define cblas_ctbmv cblas_ctbmv64_
+# define cblas_ztbmv cblas_ztbmv64_
+# define cblas_stbsv cblas_stbsv64_
+# define cblas_dtbsv cblas_dtbsv64_
+# define cblas_ctbsv cblas_ctbsv64_
+# define cblas_ztbsv cblas_ztbsv64_
+# define cblas_stpmv cblas_stpmv64_
+# define cblas_dtpmv cblas_dtpmv64_
+# define cblas_ctpmv cblas_ctpmv64_
+# define cblas_ztpmv cblas_ztpmv64_
+# define cblas_stpsv cblas_stpsv64_
+# define cblas_dtpsv cblas_dtpsv64_
+# define cblas_ctpsv cblas_ctpsv64_
+# define cblas_ztpsv cblas_ztpsv64_
+# define cblas_ssymv cblas_ssymv64_
+# define cblas_dsymv cblas_dsymv64_
+# define cblas_chemv cblas_chemv64_
+# define cblas_zhemv cblas_zhemv64_
+# define cblas_sspmv cblas_sspmv64_
+# define cblas_dspmv cblas_dspmv64_
+# define cblas_sspr cblas_sspr64_
+# define cblas_dspr cblas_dspr64_
+# define cblas_chpr cblas_chpr64_
+# define cblas_zhpr cblas_zhpr64_
+# define cblas_sspr2 cblas_sspr264_
+# define cblas_dspr2 cblas_dspr264_
+# define cblas_chpr2 cblas_chpr264_
+# define cblas_zhpr2 cblas_zhpr264_
+# define cblas_chbmv cblas_chbmv64_
+# define cblas_zhbmv cblas_zhbmv64_
+# define cblas_chpmv cblas_chpmv64_
+# define cblas_zhpmv cblas_zhpmv64_
+# define cblas_sgemm cblas_sgemm64_
+# define cblas_dgemm cblas_dgemm64_
+# define cblas_cgemm cblas_cgemm64_
+# define cblas_cgemm3m cblas_cgemm3m64_
+# define cblas_zgemm cblas_zgemm64_
+# define cblas_zgemm3m cblas_zgemm3m64_
+# define cblas_ssymm cblas_ssymm64_
+# define cblas_dsymm cblas_dsymm64_
+# define cblas_csymm cblas_csymm64_
+# define cblas_zsymm cblas_zsymm64_
+# define cblas_ssyrk cblas_ssyrk64_
+# define cblas_dsyrk cblas_dsyrk64_
+# define cblas_csyrk cblas_csyrk64_
+# define cblas_zsyrk cblas_zsyrk64_
+# define cblas_ssyr2k cblas_ssyr2k64_
+# define cblas_dsyr2k cblas_dsyr2k64_
+# define cblas_csyr2k cblas_csyr2k64_
+# define cblas_zsyr2k cblas_zsyr2k64_
+# define cblas_strmm cblas_strmm64_
+# define cblas_dtrmm cblas_dtrmm64_
+# define cblas_ctrmm cblas_ctrmm64_
+# define cblas_ztrmm cblas_ztrmm64_
+# define cblas_strsm cblas_strsm64_
+# define cblas_dtrsm cblas_dtrsm64_
+# define cblas_ctrsm cblas_ctrsm64_
+# define cblas_ztrsm cblas_ztrsm64_
+# define cblas_chemm cblas_chemm64_
+# define cblas_zhemm cblas_zhemm64_
+# define cblas_cherk cblas_cherk64_
+# define cblas_zherk cblas_zherk64_
+# define cblas_cher2k cblas_cher2k64_
+# define cblas_zher2k cblas_zher2k64_
+# define cblas_xerbla cblas_xerbla64_
+# define cblas_saxpby cblas_saxpby64_
+# define cblas_daxpby cblas_daxpby64_
+# define cblas_caxpby cblas_caxpby64_
+# define cblas_zaxpby cblas_zaxpby64_
+# define cblas_somatcopy cblas_somatcopy64_
+# define cblas_domatcopy cblas_domatcopy64_
+# define cblas_comatcopy cblas_comatcopy64_
+# define cblas_zomatcopy cblas_zomatcopy64_
+# define cblas_simatcopy cblas_simatcopy64_
+# define cblas_dimatcopy cblas_dimatcopy64_
+# define cblas_cimatcopy cblas_cimatcopy64_
+# define cblas_zimatcopy cblas_zimatcopy64_
+# define cblas_sgeadd cblas_sgeadd64_
+# define cblas_dgeadd cblas_dgeadd64_
+# define cblas_cgeadd cblas_cgeadd64_
+# define cblas_zgeadd cblas_zgeadd64_
+#endif
+
+#define CBLAS_INDEX size_t
+
+
+typedef enum CBLAS_ORDER {CblasRowMajor=101, CblasColMajor=102} CBLAS_ORDER;
+typedef enum CBLAS_TRANSPOSE {CblasNoTrans=111, CblasTrans=112, CblasConjTrans=113, CblasConjNoTrans=114} CBLAS_TRANSPOSE;
+typedef enum CBLAS_UPLO {CblasUpper=121, CblasLower=122} CBLAS_UPLO;
+typedef enum CBLAS_DIAG {CblasNonUnit=131, CblasUnit=132} CBLAS_DIAG;
+typedef enum CBLAS_SIDE {CblasLeft=141, CblasRight=142} CBLAS_SIDE;
+
+float cblas_sdsdot(OPENBLAS_CONST blasint n, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy);
+double cblas_dsdot (OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy);
+float cblas_sdot(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy);
+double cblas_ddot(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double *y, OPENBLAS_CONST blasint incy);
+
+openblas_complex_float cblas_cdotu(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy);
+openblas_complex_float cblas_cdotc(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy);
+openblas_complex_double cblas_zdotu(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double *y, OPENBLAS_CONST blasint incy);
+openblas_complex_double cblas_zdotc(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_cdotu_sub(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy, openblas_complex_float *ret);
+void cblas_cdotc_sub(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *y, OPENBLAS_CONST blasint incy, openblas_complex_float *ret);
+void cblas_zdotu_sub(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double *y, OPENBLAS_CONST blasint incy, openblas_complex_double *ret);
+void cblas_zdotc_sub(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double *y, OPENBLAS_CONST blasint incy, openblas_complex_double *ret);
+
+float cblas_sasum (OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx);
+double cblas_dasum (OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx);
+float cblas_scasum(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx);
+double cblas_dzasum(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx);
+
+float cblas_snrm2 (OPENBLAS_CONST blasint N, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX);
+double cblas_dnrm2 (OPENBLAS_CONST blasint N, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX);
+float cblas_scnrm2(OPENBLAS_CONST blasint N, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX);
+double cblas_dznrm2(OPENBLAS_CONST blasint N, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX);
+
+CBLAS_INDEX cblas_isamax(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx);
+CBLAS_INDEX cblas_idamax(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx);
+CBLAS_INDEX cblas_icamax(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx);
+CBLAS_INDEX cblas_izamax(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx);
+
+void cblas_saxpy(OPENBLAS_CONST blasint n, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, float *y, OPENBLAS_CONST blasint incy);
+void cblas_daxpy(OPENBLAS_CONST blasint n, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, double *y, OPENBLAS_CONST blasint incy);
+void cblas_caxpy(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, float *y, OPENBLAS_CONST blasint incy);
+void cblas_zaxpy(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_scopy(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, float *y, OPENBLAS_CONST blasint incy);
+void cblas_dcopy(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, double *y, OPENBLAS_CONST blasint incy);
+void cblas_ccopy(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, float *y, OPENBLAS_CONST blasint incy);
+void cblas_zcopy(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_sswap(OPENBLAS_CONST blasint n, float *x, OPENBLAS_CONST blasint incx, float *y, OPENBLAS_CONST blasint incy);
+void cblas_dswap(OPENBLAS_CONST blasint n, double *x, OPENBLAS_CONST blasint incx, double *y, OPENBLAS_CONST blasint incy);
+void cblas_cswap(OPENBLAS_CONST blasint n, float *x, OPENBLAS_CONST blasint incx, float *y, OPENBLAS_CONST blasint incy);
+void cblas_zswap(OPENBLAS_CONST blasint n, double *x, OPENBLAS_CONST blasint incx, double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_srot(OPENBLAS_CONST blasint N, float *X, OPENBLAS_CONST blasint incX, float *Y, OPENBLAS_CONST blasint incY, OPENBLAS_CONST float c, OPENBLAS_CONST float s);
+void cblas_drot(OPENBLAS_CONST blasint N, double *X, OPENBLAS_CONST blasint incX, double *Y, OPENBLAS_CONST blasint incY, OPENBLAS_CONST double c, OPENBLAS_CONST double s);
+
+void cblas_srotg(float *a, float *b, float *c, float *s);
+void cblas_drotg(double *a, double *b, double *c, double *s);
+
+void cblas_srotm(OPENBLAS_CONST blasint N, float *X, OPENBLAS_CONST blasint incX, float *Y, OPENBLAS_CONST blasint incY, OPENBLAS_CONST float *P);
+void cblas_drotm(OPENBLAS_CONST blasint N, double *X, OPENBLAS_CONST blasint incX, double *Y, OPENBLAS_CONST blasint incY, OPENBLAS_CONST double *P);
+
+void cblas_srotmg(float *d1, float *d2, float *b1, OPENBLAS_CONST float b2, float *P);
+void cblas_drotmg(double *d1, double *d2, double *b1, OPENBLAS_CONST double b2, double *P);
+
+void cblas_sscal(OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dscal(OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, double *X, OPENBLAS_CONST blasint incX);
+void cblas_cscal(OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, float *X, OPENBLAS_CONST blasint incX);
+void cblas_zscal(OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, double *X, OPENBLAS_CONST blasint incX);
+void cblas_csscal(OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, float *X, OPENBLAS_CONST blasint incX);
+void cblas_zdscal(OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_sgemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE trans, OPENBLAS_CONST blasint m, OPENBLAS_CONST blasint n,
+ OPENBLAS_CONST float alpha, OPENBLAS_CONST float *a, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float beta, float *y, OPENBLAS_CONST blasint incy);
+void cblas_dgemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE trans, OPENBLAS_CONST blasint m, OPENBLAS_CONST blasint n,
+ OPENBLAS_CONST double alpha, OPENBLAS_CONST double *a, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double beta, double *y, OPENBLAS_CONST blasint incy);
+void cblas_cgemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE trans, OPENBLAS_CONST blasint m, OPENBLAS_CONST blasint n,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *a, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST float *beta, float *y, OPENBLAS_CONST blasint incy);
+void cblas_zgemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE trans, OPENBLAS_CONST blasint m, OPENBLAS_CONST blasint n,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *a, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx, OPENBLAS_CONST double *beta, double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_sger (OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *A, OPENBLAS_CONST blasint lda);
+void cblas_dger (OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *A, OPENBLAS_CONST blasint lda);
+void cblas_cgeru(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *A, OPENBLAS_CONST blasint lda);
+void cblas_cgerc(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *A, OPENBLAS_CONST blasint lda);
+void cblas_zgeru(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *A, OPENBLAS_CONST blasint lda);
+void cblas_zgerc(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *A, OPENBLAS_CONST blasint lda);
+
+void cblas_strsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dtrsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+void cblas_ctrsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_ztrsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_strmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dtrmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+void cblas_ctrmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_ztrmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_ssyr(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, float *A, OPENBLAS_CONST blasint lda);
+void cblas_dsyr(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, double *A, OPENBLAS_CONST blasint lda);
+void cblas_cher(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, float *A, OPENBLAS_CONST blasint lda);
+void cblas_zher(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, double *A, OPENBLAS_CONST blasint lda);
+
+void cblas_ssyr2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo,OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X,
+ OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *A, OPENBLAS_CONST blasint lda);
+void cblas_dsyr2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X,
+ OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *A, OPENBLAS_CONST blasint lda);
+void cblas_cher2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX,
+ OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *A, OPENBLAS_CONST blasint lda);
+void cblas_zher2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX,
+ OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *A, OPENBLAS_CONST blasint lda);
+
+void cblas_sgbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST blasint KL, OPENBLAS_CONST blasint KU, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_dgbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST blasint KL, OPENBLAS_CONST blasint KU, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double beta, double *Y, OPENBLAS_CONST blasint incY);
+void cblas_cgbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST blasint KL, OPENBLAS_CONST blasint KU, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_zgbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST blasint KL, OPENBLAS_CONST blasint KU, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *beta, double *Y, OPENBLAS_CONST blasint incY);
+
+void cblas_ssbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A,
+ OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_dsbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A,
+ OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double beta, double *Y, OPENBLAS_CONST blasint incY);
+
+
+void cblas_stbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dtbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+void cblas_ctbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_ztbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_stbsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dtbsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+void cblas_ctbsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *X, OPENBLAS_CONST blasint incX);
+void cblas_ztbsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_stpmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST float *Ap, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dtpmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST double *Ap, double *X, OPENBLAS_CONST blasint incX);
+void cblas_ctpmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST float *Ap, float *X, OPENBLAS_CONST blasint incX);
+void cblas_ztpmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST double *Ap, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_stpsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST float *Ap, float *X, OPENBLAS_CONST blasint incX);
+void cblas_dtpsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST double *Ap, double *X, OPENBLAS_CONST blasint incX);
+void cblas_ctpsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST float *Ap, float *X, OPENBLAS_CONST blasint incX);
+void cblas_ztpsv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_DIAG Diag,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST double *Ap, double *X, OPENBLAS_CONST blasint incX);
+
+void cblas_ssymv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A,
+ OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_dsymv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A,
+ OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double beta, double *Y, OPENBLAS_CONST blasint incY);
+void cblas_chemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A,
+ OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_zhemv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A,
+ OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *beta, double *Y, OPENBLAS_CONST blasint incY);
+
+
+void cblas_sspmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *Ap,
+ OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_dspmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *Ap,
+ OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double beta, double *Y, OPENBLAS_CONST blasint incY);
+
+void cblas_sspr(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, float *Ap);
+void cblas_dspr(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, double *Ap);
+
+void cblas_chpr(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, float *A);
+void cblas_zhpr(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X,OPENBLAS_CONST blasint incX, double *A);
+
+void cblas_sspr2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *A);
+void cblas_dspr2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *A);
+void cblas_chpr2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *Y, OPENBLAS_CONST blasint incY, float *Ap);
+void cblas_zhpr2(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *Y, OPENBLAS_CONST blasint incY, double *Ap);
+
+void cblas_chbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_zhbmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *beta, double *Y, OPENBLAS_CONST blasint incY);
+
+void cblas_chpmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *Ap, OPENBLAS_CONST float *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST float *beta, float *Y, OPENBLAS_CONST blasint incY);
+void cblas_zhpmv(OPENBLAS_CONST enum CBLAS_ORDER order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *Ap, OPENBLAS_CONST double *X, OPENBLAS_CONST blasint incX, OPENBLAS_CONST double *beta, double *Y, OPENBLAS_CONST blasint incY);
+
+void cblas_sgemm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransB, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_dgemm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransB, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double beta, double *C, OPENBLAS_CONST blasint ldc);
+void cblas_cgemm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransB, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float *beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_cgemm3m(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransB, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float *beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zgemm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransB, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double *beta, double *C, OPENBLAS_CONST blasint ldc);
+void cblas_zgemm3m(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransB, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double *beta, double *C, OPENBLAS_CONST blasint ldc);
+
+
+void cblas_ssymm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_dsymm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double beta, double *C, OPENBLAS_CONST blasint ldc);
+void cblas_csymm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float *beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zsymm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double *beta, double *C, OPENBLAS_CONST blasint ldc);
+
+void cblas_ssyrk(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_dsyrk(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double beta, double *C, OPENBLAS_CONST blasint ldc);
+void cblas_csyrk(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zsyrk(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *beta, double *C, OPENBLAS_CONST blasint ldc);
+
+void cblas_ssyr2k(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_dsyr2k(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double beta, double *C, OPENBLAS_CONST blasint ldc);
+void cblas_csyr2k(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float *beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zsyr2k(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans,
+ OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double *beta, double *C, OPENBLAS_CONST blasint ldc);
+
+void cblas_strmm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *B, OPENBLAS_CONST blasint ldb);
+void cblas_dtrmm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *B, OPENBLAS_CONST blasint ldb);
+void cblas_ctrmm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *B, OPENBLAS_CONST blasint ldb);
+void cblas_ztrmm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *B, OPENBLAS_CONST blasint ldb);
+
+void cblas_strsm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *B, OPENBLAS_CONST blasint ldb);
+void cblas_dtrsm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *B, OPENBLAS_CONST blasint ldb);
+void cblas_ctrsm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, float *B, OPENBLAS_CONST blasint ldb);
+void cblas_ztrsm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE TransA,
+ OPENBLAS_CONST enum CBLAS_DIAG Diag, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, double *B, OPENBLAS_CONST blasint ldb);
+
+void cblas_chemm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float *beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zhemm(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_SIDE Side, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST blasint M, OPENBLAS_CONST blasint N,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double *beta, double *C, OPENBLAS_CONST blasint ldc);
+
+void cblas_cherk(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST float alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zherk(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST double alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double beta, double *C, OPENBLAS_CONST blasint ldc);
+
+void cblas_cher2k(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST float *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST float beta, float *C, OPENBLAS_CONST blasint ldc);
+void cblas_zher2k(OPENBLAS_CONST enum CBLAS_ORDER Order, OPENBLAS_CONST enum CBLAS_UPLO Uplo, OPENBLAS_CONST enum CBLAS_TRANSPOSE Trans, OPENBLAS_CONST blasint N, OPENBLAS_CONST blasint K,
+ OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *A, OPENBLAS_CONST blasint lda, OPENBLAS_CONST double *B, OPENBLAS_CONST blasint ldb, OPENBLAS_CONST double beta, double *C, OPENBLAS_CONST blasint ldc);
+
+void cblas_xerbla(blasint p, char *rout, char *form, ...);
+
+/*** BLAS extensions ***/
+
+void cblas_saxpby(OPENBLAS_CONST blasint n, OPENBLAS_CONST float alpha, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx,OPENBLAS_CONST float beta, float *y, OPENBLAS_CONST blasint incy);
+
+void cblas_daxpby(OPENBLAS_CONST blasint n, OPENBLAS_CONST double alpha, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx,OPENBLAS_CONST double beta, double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_caxpby(OPENBLAS_CONST blasint n, OPENBLAS_CONST float *alpha, OPENBLAS_CONST float *x, OPENBLAS_CONST blasint incx,OPENBLAS_CONST float *beta, float *y, OPENBLAS_CONST blasint incy);
+
+void cblas_zaxpby(OPENBLAS_CONST blasint n, OPENBLAS_CONST double *alpha, OPENBLAS_CONST double *x, OPENBLAS_CONST blasint incx,OPENBLAS_CONST double *beta, double *y, OPENBLAS_CONST blasint incy);
+
+void cblas_somatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST float calpha, OPENBLAS_CONST float *a,
+ OPENBLAS_CONST blasint clda, float *b, OPENBLAS_CONST blasint cldb);
+void cblas_domatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST double calpha, OPENBLAS_CONST double *a,
+ OPENBLAS_CONST blasint clda, double *b, OPENBLAS_CONST blasint cldb);
+void cblas_comatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST float* calpha, OPENBLAS_CONST float* a,
+ OPENBLAS_CONST blasint clda, float*b, OPENBLAS_CONST blasint cldb);
+void cblas_zomatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST double* calpha, OPENBLAS_CONST double* a,
+ OPENBLAS_CONST blasint clda, double *b, OPENBLAS_CONST blasint cldb);
+
+void cblas_simatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST float calpha, float *a,
+ OPENBLAS_CONST blasint clda, OPENBLAS_CONST blasint cldb);
+void cblas_dimatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST double calpha, double *a,
+ OPENBLAS_CONST blasint clda, OPENBLAS_CONST blasint cldb);
+void cblas_cimatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST float* calpha, float* a,
+ OPENBLAS_CONST blasint clda, OPENBLAS_CONST blasint cldb);
+void cblas_zimatcopy(OPENBLAS_CONST enum CBLAS_ORDER CORDER, OPENBLAS_CONST enum CBLAS_TRANSPOSE CTRANS, OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST double* calpha, double* a,
+ OPENBLAS_CONST blasint clda, OPENBLAS_CONST blasint cldb);
+
+void cblas_sgeadd(OPENBLAS_CONST enum CBLAS_ORDER CORDER,OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST float calpha, float *a, OPENBLAS_CONST blasint clda, OPENBLAS_CONST float cbeta,
+ float *c, OPENBLAS_CONST blasint cldc);
+void cblas_dgeadd(OPENBLAS_CONST enum CBLAS_ORDER CORDER,OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST double calpha, double *a, OPENBLAS_CONST blasint clda, OPENBLAS_CONST double cbeta,
+ double *c, OPENBLAS_CONST blasint cldc);
+void cblas_cgeadd(OPENBLAS_CONST enum CBLAS_ORDER CORDER,OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST float *calpha, float *a, OPENBLAS_CONST blasint clda, OPENBLAS_CONST float *cbeta,
+ float *c, OPENBLAS_CONST blasint cldc);
+void cblas_zgeadd(OPENBLAS_CONST enum CBLAS_ORDER CORDER,OPENBLAS_CONST blasint crows, OPENBLAS_CONST blasint ccols, OPENBLAS_CONST double *calpha, double *a, OPENBLAS_CONST blasint clda, OPENBLAS_CONST double *cbeta,
+ double *c, OPENBLAS_CONST blasint cldc);
+
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif
diff --git a/julia/deps/cpcblas.sh b/julia/deps/cpcblas.sh
new file mode 100755
index 000000000000..99342897a58c
--- /dev/null
+++ b/julia/deps/cpcblas.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+# be invoked from build.jl
+
+set -e
+
+diff ../../cblas.h include/cblas.h || cp -v ../../cblas.h include/cblas.h
diff --git a/julia/docs/Makefile b/julia/docs/Makefile
new file mode 100644
index 000000000000..57c623889a83
--- /dev/null
+++ b/julia/docs/Makefile
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+all:
+ julia --color=yes ./make.jl
+ mkdocs build
diff --git a/julia/docs/make.jl b/julia/docs/make.jl
new file mode 100644
index 000000000000..6e3705a95fdc
--- /dev/null
+++ b/julia/docs/make.jl
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using Documenter, MXNet
+
+makedocs(
+ modules = MXNet,
+ doctest = false
+)
+
+deploydocs(
+ deps = Deps.pip("pygments", "mkdocs", "mkdocs-material", "python-markdown-math"),
+ repo = "github.com/dmlc/MXNet.jl.git",
+ julia = "0.6",
+)
diff --git a/julia/docs/mkdocs.yml b/julia/docs/mkdocs.yml
new file mode 100644
index 000000000000..24281730885f
--- /dev/null
+++ b/julia/docs/mkdocs.yml
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+site_name: MXNet.jl
+repo_url: https://github.com/dmlc/MXNet.jl
+
+theme: material
+
+extra:
+ palette:
+ primary: 'indigo'
+ accent: 'blue'
+
+extra_css:
+ - assets/Documenter.css
+
+extra_javascript:
+ - https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML
+ - assets/mathjaxhelper.js
+
+markdown_extensions:
+ - extra
+ - tables
+ - fenced_code
+ - mdx_math
+ - admonition
+
+docs_dir: 'build'
+
+pages:
+ - Home: index.md
+ - Tutorial:
+ - Digit Recognition on MNIST: tutorial/mnist.md
+ - Generating Random Sentence with LSTM RNN: tutorial/char-lstm.md
+ - User Guide:
+ - Installation Guide: user-guide/install.md
+ - Overview: user-guide/overview.md
+ - FAQ: user-guide/faq.md
+ - API Documentation:
+ - Context: api/context.md
+ - Models: api/model.md
+ - Initializers: api/initializer.md
+ - Optimizers: api/optimizer.md
+ - Callbacks in training: api/callback.md
+ - Evaluation Metrics: api/metric.md
+ - Data Providers: api/io.md
+ - NDArray API: api/ndarray.md
+ - Symbolic API: api/symbolic-node.md
+ - Neural Networks Factory: api/nn-factory.md
+ - Executor: api/executor.md
+ - Network Visualization: api/visualize.md
diff --git a/julia/docs/src/api.md b/julia/docs/src/api.md
new file mode 100644
index 000000000000..4984129863d0
--- /dev/null
+++ b/julia/docs/src/api.md
@@ -0,0 +1,18 @@
+# API Documentation
+
+```@contents
+Pages = [
+ "api/symbolic-node.md",
+ "api/ndarray.md",
+ "api/context.md",
+ "api/model.md",
+ "api/initializers.md",
+ "api/optimizers.md",
+ "api/callbacks.md",
+ "api/metric.md",
+ "api/io.md",
+ "api/nn-factory.md",
+ "api/executor.md",
+ "api/visualize.md",
+]
+```
diff --git a/julia/docs/src/api/callback.md b/julia/docs/src/api/callback.md
new file mode 100644
index 000000000000..f67811cc41fe
--- /dev/null
+++ b/julia/docs/src/api/callback.md
@@ -0,0 +1,6 @@
+# Callback in training
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["callback.jl"]
+```
diff --git a/julia/docs/src/api/context.md b/julia/docs/src/api/context.md
new file mode 100644
index 000000000000..93ccf83e51ba
--- /dev/null
+++ b/julia/docs/src/api/context.md
@@ -0,0 +1,6 @@
+# Context
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["context.jl"]
+```
diff --git a/julia/docs/src/api/executor.md b/julia/docs/src/api/executor.md
new file mode 100644
index 000000000000..b560c7a0864d
--- /dev/null
+++ b/julia/docs/src/api/executor.md
@@ -0,0 +1,6 @@
+# Executor
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["executor.jl"]
+```
diff --git a/julia/docs/src/api/initializer.md b/julia/docs/src/api/initializer.md
new file mode 100644
index 000000000000..d0aad2def4cd
--- /dev/null
+++ b/julia/docs/src/api/initializer.md
@@ -0,0 +1,6 @@
+# Initializer
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["initializer.jl"]
+```
diff --git a/julia/docs/src/api/io.md b/julia/docs/src/api/io.md
new file mode 100644
index 000000000000..7312259dbf3c
--- /dev/null
+++ b/julia/docs/src/api/io.md
@@ -0,0 +1,120 @@
+# Data Providers
+
+Data providers are wrappers that load external data, be it images, text, or general tensors,
+and split it into mini-batches so that the model can consume the data in a uniformed way.
+
+## AbstractDataProvider interface
+
+```@docs
+mx.AbstractDataProvider
+```
+
+The difference between *data* and *label* is that during training stage,
+both *data* and *label* will be feeded into the model, while during
+prediction stage, only *data* is loaded. Otherwise, they could be anything, with any names, and
+of any shapes. The provided data and label names here should match the input names in a target
+`SymbolicNode`.
+
+A data provider should also implement the Julia iteration interface, in order to allow iterating
+through the data set. The provider will be called in the following way:
+
+```julia
+for batch in eachbatch(provider)
+ data = get_data(provider, batch)
+end
+```
+
+which will be translated by Julia compiler into
+
+```julia
+state = Base.start(eachbatch(provider))
+while !Base.done(provider, state)
+ (batch, state) = Base.next(provider, state)
+ data = get_data(provider, batch)
+end
+```
+
+By default, `eachbatch` simply returns the provider itself, so the iterator interface
+is implemented on the provider type itself. But the extra layer of abstraction allows us to
+implement a data provider easily via a Julia `Task` coroutine. See the
+data provider defined in [the char-lstm example](tutorial/char-lstm) for an example of using coroutine to define data
+providers.
+
+The detailed interface functions for the iterator API is listed below:
+
+ Base.eltype(provider) -> AbstractDataBatch
+
+Returns the specific subtype representing a data batch. See `AbstractDataBatch`.
+* `provider::AbstractDataProvider`: the data provider.
+
+ Base.start(provider) -> AbstractDataProviderState
+
+This function is always called before iterating into the dataset. It should initialize
+the iterator, reset the index, and do data shuffling if needed.
+* `provider::AbstractDataProvider`: the data provider.
+
+ Base.done(provider, state) -> Bool
+
+True if there is no more data to iterate in this dataset.
+* `provider::AbstractDataProvider`: the data provider.
+* `state::AbstractDataProviderState`: the state returned by `Base.start` and `Base.next`.
+
+ Base.next(provider) -> (AbstractDataBatch, AbstractDataProviderState)
+
+Returns the current data batch, and the state for the next iteration.
+* `provider::AbstractDataProvider`: the data provider.
+
+Note sometimes you are wrapping an existing data iterator (e.g. the built-in libmxnet data iterator) that
+is built with a different convention. It might be difficult to adapt to the interfaces stated here. In this
+case, you can safely assume that
+
+* `Base.start` will always be called, and called only once before the iteration starts.
+* `Base.done` will always be called at the beginning of every iteration and always be called once.
+* If `Base.done` return true, the iteration will stop, until the next round, again, starting with
+ a call to `Base.start`.
+* `Base.next` will always be called only once in each iteration. It will always be called after
+ one and only one call to `Base.done`; but if `Base.done` returns true, `Base.next` will
+ not be called.
+
+With those assumptions, it will be relatively easy to adapt any existing iterator. See the implementation
+of the built-in `MXDataProvider` for example.
+
+!!! note
+ Please do not use the one data provider simultaneously in two different places, either in parallel,
+ or in a nested loop. For example, the behavior for the following code is undefined
+
+ ```julia
+ for batch in data
+ # updating the parameters
+
+ # now let's test the performance on the training set
+ for b2 in data
+ # ...
+ end
+ end
+ ```
+
+```@docs
+mx.get_batch_size
+mx.provide_data
+mx.provide_label
+```
+
+## AbstractDataBatch interface
+
+```@docs
+mx.AbstractDataProviderState
+mx.count_samples
+mx.get_data
+mx.get_label
+mx.get
+mx.load_data!
+mx.load_label!
+```
+
+## Implemented providers and other methods
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["io.jl"]
+```
diff --git a/julia/docs/src/api/kvstore.md b/julia/docs/src/api/kvstore.md
new file mode 100644
index 000000000000..34a5027f85fb
--- /dev/null
+++ b/julia/docs/src/api/kvstore.md
@@ -0,0 +1,6 @@
+# Key-Value Store
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["kvstore.jl"]
+```
diff --git a/julia/docs/src/api/metric.md b/julia/docs/src/api/metric.md
new file mode 100644
index 000000000000..63cca0cc41ba
--- /dev/null
+++ b/julia/docs/src/api/metric.md
@@ -0,0 +1,10 @@
+# Evaluation Metrics
+
+Evaluation metrics provide a way to evaluate the performance of a learned model.
+This is typically used during training to monitor performance on the validation
+set.
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["metric.jl"]
+```
diff --git a/julia/docs/src/api/model.md b/julia/docs/src/api/model.md
new file mode 100644
index 000000000000..f793c7c406c7
--- /dev/null
+++ b/julia/docs/src/api/model.md
@@ -0,0 +1,9 @@
+# Model
+
+The model API provides convenient high-level interface to do training and predicting on
+a network described using the symbolic API.
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["model.jl"]
+```
diff --git a/julia/docs/src/api/ndarray.md b/julia/docs/src/api/ndarray.md
new file mode 100644
index 000000000000..5877d8257758
--- /dev/null
+++ b/julia/docs/src/api/ndarray.md
@@ -0,0 +1,57 @@
+# NDArray API
+
+## Arithmetic Operations
+
+In the following example `y` can be a `Real` value or another `NDArray`
+
+| API | Example | |
+|-----|----------|----------------------------|
+| `+` | `x .+ y` | Elementwise summation |
+| `-` | `x .- y` | Elementwise minus |
+| `*` | `x .* y` | Elementwise multiplication |
+| `/` | `x ./ y` | Elementwise division |
+| `^` | `x .^ y` | Elementwise power |
+| `%` | `x .% y` | Elementwise modulo |
+
+
+## Trigonometric Functions
+
+| API | Example | |
+|----------------|------------|-----------------------------|
+| [`sin`](@ref) | `sin.(x)` | Elementwise sine |
+| [`cos`](@ref) | `cos.(x)` | Elementwise cosine |
+| [`tan`](@ref) | `tan.(x)` | Elementwise tangent |
+| [`asin`](@ref) | `asin.(x)` | Elementwise inverse sine |
+| [`acos`](@ref) | `acos.(x)` | Elementwise inverse cosine |
+| [`atan`](@ref) | `atan.(x)` | Elementwise inverse tangent |
+
+
+## Hyperbolic Functions
+
+| API | Example | |
+|-----------------|-------------|----------------------------------------|
+| [`sinh`](@ref) | `sinh.(x)` | Elementwise hyperbolic sine |
+| [`cosh`](@ref) | `cosh.(x)` | Elementwise hyperbolic cosine |
+| [`tanh`](@ref) | `tanh.(x)` | Elementwise hyperbolic tangent |
+| [`asinh`](@ref) | `asinh.(x)` | Elementwise inverse hyperbolic sine |
+| [`acosh`](@ref) | `acosh.(x)` | Elementwise inverse hyperbolic cosine |
+| [`atanh`](@ref) | `atanh.(x)` | Elementwise inverse hyperbolic tangent |
+
+
+## Activation Functions
+
+| API | Example | |
+|-----------------------|-------------------|-------------------------|
+| [`σ`](@ref) | `σ.(x)` | Sigmoid function |
+| [`sigmoid`](@ref) | `sigmoid.(x)` | Sigmoid function |
+| [`relu`](@ref) | `relu.(x)` | ReLU function |
+| [`softmax`](@ref) | `softmax.(x)` | Softmax function |
+| [`log_softmax`](@ref) | `log_softmax.(x)` | Softmax followed by log |
+
+
+## Reference
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["ndarray.jl"]
+```
diff --git a/julia/docs/src/api/nn-factory.md b/julia/docs/src/api/nn-factory.md
new file mode 100644
index 000000000000..833d9a3efd53
--- /dev/null
+++ b/julia/docs/src/api/nn-factory.md
@@ -0,0 +1,9 @@
+# Neural Network Factory
+
+Neural network factory provide convenient helper functions to define
+common neural networks.
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["nn-factory.jl"]
+```
diff --git a/julia/docs/src/api/optimizer.md b/julia/docs/src/api/optimizer.md
new file mode 100644
index 000000000000..28d01cc9fd89
--- /dev/null
+++ b/julia/docs/src/api/optimizer.md
@@ -0,0 +1,66 @@
+# Optimizers
+
+Says, you have the parameter `W` inited for your model and
+got its gradient stored as `∇` (perhaps from AutoGrad APIs).
+Here is minimal snippet of getting your parameter `W` baked by `SGD`.
+
+```@repl
+using MXNet
+
+opt = SGD(η = 10)
+decend! = getupdater(opt)
+
+W = NDArray(Float32[1, 2, 3, 4]);
+∇ = NDArray(Float32[.1, .2, .3, .4]);
+
+decend!(1, ∇, W)
+```
+
+```@autodocs
+Modules = [MXNet.mx, MXNet.mx.LearningRate, MXNet.mx.Momentum]
+Pages = ["optimizer.jl"]
+```
+
+## Built-in optimizers
+
+### Stochastic Gradient Descent
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/sgd.jl"]
+```
+
+### ADAM
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/adam.jl"]
+```
+
+### AdaGrad
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/adagrad.jl"]
+```
+
+### AdaDelta
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/adadelta.jl"]
+```
+
+### AdaMax
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/adamax.jl"]
+```
+
+### RMSProp
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/rmsprop.jl"]
+```
+
+### Nadam
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["optimizers/nadam.jl"]
+```
diff --git a/julia/docs/src/api/symbolic-node.md b/julia/docs/src/api/symbolic-node.md
new file mode 100644
index 000000000000..ef731d9f7d00
--- /dev/null
+++ b/julia/docs/src/api/symbolic-node.md
@@ -0,0 +1,6 @@
+# Symbolic API
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["symbolic-node.jl"]
+```
diff --git a/julia/docs/src/api/visualize.md b/julia/docs/src/api/visualize.md
new file mode 100644
index 000000000000..429a927012e4
--- /dev/null
+++ b/julia/docs/src/api/visualize.md
@@ -0,0 +1,6 @@
+# Network Visualization
+
+```@autodocs
+Modules = [MXNet.mx]
+Pages = ["visualize.jl"]
+```
diff --git a/julia/docs/src/index.md b/julia/docs/src/index.md
new file mode 100644
index 000000000000..b6a51fc162ad
--- /dev/null
+++ b/julia/docs/src/index.md
@@ -0,0 +1,55 @@
+# MXNet Documentation
+
+[MXNet.jl](https://github.com/dmlc/MXNet.jl) is the
+[Julia](http://julialang.org/) package of
+[dmlc/mxnet](https://github.com/dmlc/mxnet). MXNet.jl brings flexible and efficient GPU
+computing and state-of-art deep learning to Julia. Some highlight of features
+include:
+
+* Efficient tensor/matrix computation across multiple devices,
+ including multiple CPUs, GPUs and distributed server nodes.
+* Flexible symbolic manipulation to composite and construct
+ state-of-the-art deep learning models.
+
+For more details, see documentation below. Please also checkout the
+[examples](https://github.com/dmlc/MXNet.jl/tree/master/examples) directory.
+
+## Tutorials
+
+```@contents
+Pages = [
+ "tutorial/mnist.md",
+ "tutorial/char-lstm.md",
+]
+Depth = 2
+```
+
+## User's Guide
+
+```@contents
+Pages = [
+ "user-guide/install.md",
+ "user-guide/overview.md",
+ "user-guide/faq.md",
+]
+Depth = 2
+```
+
+## API Documentation
+
+```@contents
+Pages = [
+ "api/context.md",
+ "api/ndarray.md",
+ "api/symbolic-node.md",
+ "api/model.md",
+ "api/initializers.md",
+ "api/optimizers.md",
+ "api/callbacks.md",
+ "api/metric.md",
+ "api/io.md",
+ "api/nn-factory.md",
+ "api/executor.md",
+ "api/visualize.md",
+]
+```
diff --git a/julia/docs/src/tutorial/char-lstm.md b/julia/docs/src/tutorial/char-lstm.md
new file mode 100644
index 000000000000..369bcddd53e9
--- /dev/null
+++ b/julia/docs/src/tutorial/char-lstm.md
@@ -0,0 +1,306 @@
+Generating Random Sentence with LSTM RNN
+========================================
+
+This tutorial shows how to train a LSTM (Long short-term memory) RNN
+(recurrent neural network) to perform character-level sequence training
+and prediction. The original model, usually called `char-rnn` is
+described in [Andrej Karpathy's
+blog](http://karpathy.github.io/2015/05/21/rnn-effectiveness/), with a
+reference implementation in Torch available
+[here](https://github.com/karpathy/char-rnn).
+
+Because MXNet.jl does not have a specialized model for recurrent neural
+networks yet, the example shown here is an implementation of LSTM by
+using the default FeedForward model via explicitly unfolding over time.
+We will be using fixed-length input sequence for training. The code is
+adapted from the [char-rnn example for MXNet's Python
+binding](https://github.com/dmlc/mxnet/blob/master/example/rnn/char_lstm.ipynb),
+which demonstrates how to use low-level
+[Symbolic API](@ref) to build customized neural
+network models directly.
+
+The most important code snippets of this example is shown and explained
+here. To see and run the complete code, please refer to the
+[examples/char-lstm](https://github.com/dmlc/MXNet.jl/tree/master/examples/char-lstm)
+directory. You will need to install
+[Iterators.jl](https://github.com/JuliaLang/Iterators.jl) and
+[StatsBase.jl](https://github.com/JuliaStats/StatsBase.jl) to run this
+example.
+
+LSTM Cells
+----------
+
+Christopher Olah has a [great blog post about LSTM](http://colah.github.io/posts/2015-08-Understanding-LSTMs/) with
+beautiful and clear illustrations. So we will not repeat the definition
+and explanation of what an LSTM cell is here. Basically, an LSTM cell
+takes input `x`, as well as previous states (including `c` and `h`), and
+produce the next states. We define a helper type to bundle the two state
+variables together:
+
+Because LSTM weights are shared at every time when we do explicit
+unfolding, so we also define a helper type to hold all the weights (and
+bias) for an LSTM cell for convenience.
+
+Note all the variables are of type SymbolicNode. We will construct the
+LSTM network as a symbolic computation graph, which is then instantiated
+with NDArray for actual computation.
+
+The following figure is stolen (permission requested) from [Christopher
+Olah's blog](http://colah.github.io/posts/2015-08-Understanding-LSTMs/),
+which illustrate exactly what the code snippet above is doing.
+
+![image](images/LSTM3-chain.png)
+
+In particular, instead of defining the four gates independently, we do
+the computation together and then use SliceChannel to split them into
+four outputs. The computation of gates are all done with the symbolic
+API. The return value is a LSTM state containing the output of a LSTM
+cell.
+
+Unfolding LSTM
+--------------
+
+Using the LSTM cell defined above, we are now ready to define a function
+to unfold a LSTM network with L layers and T time steps. The first part
+of the function is just defining all the symbolic variables for the
+shared weights and states.
+
+The `embed_W` is the weights used for character embedding --- i.e.
+mapping the one-hot encoded characters into real vectors. The `pred_W`
+and `pred_b` are weights and bias for the final prediction at each time
+step.
+
+Then we define the weights for each LSTM cell. Note there is one cell
+for each layer, and it will be replicated (unrolled) over time. The
+states are, however, *not* shared over time. Instead, here we define the
+initial states here at the beginning of a sequence, and we will update
+them with the output states at each time step as we explicitly unroll
+the LSTM.
+
+Unrolling over time is a straightforward procedure of stacking the
+embedding layer, and then LSTM cells, on top of which the prediction
+layer. During unrolling, we update the states and collect all the
+outputs. Note each time step takes data and label as inputs. If the LSTM
+is named as `:ptb`, the data and label at step `t` will be named
+`:ptb_data_$t` and `:ptb_label_$t`. Late on when we prepare the data, we
+will define the data provider to match those names.
+
+Note at each time step, the prediction is connected to a SoftmaxOutput
+operator, which could back propagate when corresponding labels are
+provided. The states are then connected to the next time step, which
+allows back propagate through time. However, at the end of the sequence,
+the final states are not connected to anything. This dangling outputs is
+problematic, so we explicitly connect each of them to a BlockGrad
+operator, which simply back propagates 0-gradient and closes the
+computation graph.
+
+In the end, we just group all the prediction outputs at each time step
+as a single SymbolicNode and return. Optionally we will also group the
+final states, this is used when we use the trained LSTM to sample
+sentences.
+
+Data Provider for Text Sequences
+--------------------------------
+
+Now we need to construct a data provider that takes a text file, divide
+the text into mini-batches of fixed-length character-sequences, and
+provide them as one-hot encoded vectors.
+
+Note the is no fancy feature extraction at all. Each character is simply
+encoded as a one-hot vector: a 0-1 vector of the size given by the
+vocabulary. Here we just construct the vocabulary by collecting all the
+unique characters in the training text -- there are not too many of them
+(including punctuations and whitespace) for English text. Each input
+character is then encoded as a vector of 0s on all coordinates, and 1 on
+the coordinate corresponding to that character. The
+character-to-coordinate mapping is giving by the vocabulary.
+
+The text sequence data provider implements the [Data Providers](@ref) api. We define the `CharSeqProvider` as below:
+
+The provided data and labels follow the naming convention of inputs used
+when unrolling the LSTM. Note in the code below, apart from
+`$name_data_$t` and `$name_label_$t`, we also provides the initial `c`
+and `h` states for each layer. This is because we are using the
+high-level FeedForward API, which has no idea about time and states. So
+we will feed the initial states for each sequence from the data
+provider. Since the initial states is always zero, we just need to
+always provide constant zero blobs.
+
+Next we implement the `eachbatch` method from the [`mx.AbstractDataProvider`](@ref) interface for the
+provider. We start by defining the data and label arrays, and the
+`DataBatch` object we will provide in each iteration.
+
+The actual data providing iteration is implemented as a Julia
+**coroutine**. In this way, we can write the data loading logic as a
+simple coherent `for` loop, and do not need to implement the interface
+functions like Base.start, Base.next, etc.
+
+Basically, we partition the text into batches, each batch containing
+several contiguous text sequences. Note at each time step, the LSTM is
+trained to predict the next character, so the label is the same as the
+data, but shifted ahead by one index.
+
+Training the LSTM
+-----------------
+
+Now we have implemented all the supporting infrastructures for our
+char-lstm. To train the model, we just follow the standard high-level
+API. Firstly, we construct a LSTM symbolic architecture:
+
+Note all the parameters are defined in
+[examples/char-lstm/config.jl](https://github.com/dmlc/MXNet.jl/blob/master/examples/char-lstm/config.jl).
+Now we load the text file and define the data provider. The data
+`input.txt` we used in this example is [a tiny Shakespeare
+dataset](https://github.com/dmlc/web-data/tree/master/mxnet/tinyshakespeare).
+But you can try with other text files.
+
+The last step is to construct a model, an optimizer and fit the mode to
+the data. We are using the ADAM optimizer \[Adam\]\_ in this example.
+
+Note we are also using a customized `NLL` evaluation metric, which
+calculate the negative log-likelihood during training. Here is an output
+sample at the end of the training process.
+
+```
+...
+INFO: Speed: 357.72 samples/sec
+INFO: == Epoch 020 ==========
+INFO: ## Training summary
+INFO: NLL = 1.4672
+INFO: perplexity = 4.3373
+INFO: time = 87.2631 seconds
+INFO: ## Validation summary
+INFO: NLL = 1.6374
+INFO: perplexity = 5.1418
+INFO: Saved checkpoint to 'char-lstm/checkpoints/ptb-0020.params'
+INFO: Speed: 368.74 samples/sec
+INFO: Speed: 361.04 samples/sec
+INFO: Speed: 360.02 samples/sec
+INFO: Speed: 362.34 samples/sec
+INFO: Speed: 360.80 samples/sec
+INFO: Speed: 362.77 samples/sec
+INFO: Speed: 357.18 samples/sec
+INFO: Speed: 355.30 samples/sec
+INFO: Speed: 362.33 samples/sec
+INFO: Speed: 359.23 samples/sec
+INFO: Speed: 358.09 samples/sec
+INFO: Speed: 356.89 samples/sec
+INFO: Speed: 371.91 samples/sec
+INFO: Speed: 372.24 samples/sec
+INFO: Speed: 356.59 samples/sec
+INFO: Speed: 356.64 samples/sec
+INFO: Speed: 360.24 samples/sec
+INFO: Speed: 360.32 samples/sec
+INFO: Speed: 362.38 samples/sec
+INFO: == Epoch 021 ==========
+INFO: ## Training summary
+INFO: NLL = 1.4655
+INFO: perplexity = 4.3297
+INFO: time = 86.9243 seconds
+INFO: ## Validation summary
+INFO: NLL = 1.6366
+INFO: perplexity = 5.1378
+INFO: Saved checkpoint to 'examples/char-lstm/checkpoints/ptb-0021.params'
+```
+
+Sampling Random Sentences
+-------------------------
+
+After training the LSTM, we can now sample random sentences from the
+trained model. The sampler works in the following way:
+
+- Starting from some fixed character, take `a` for example, and feed
+ it as input to the LSTM.
+- The LSTM will produce an output distribution over the vocabulary and
+ a state in the first time step. We sample a character from the
+ output distribution, fix it as the second character.
+- In the next time step, we feed the previously sampled character as
+ input and continue running the LSTM by also taking the previous
+ states (instead of the 0 initial states).
+- Continue running until we sampled enough characters.
+
+Note we are running with mini-batches, so several sentences could be
+sampled simultaneously. Here are some sampled outputs from a network I
+trained for around half an hour on the Shakespeare dataset. Note all the
+line-breaks, punctuations and upper-lower case letters are produced by
+the sampler itself. I did not do any post-processing.
+
+```
+## Sample 1
+all have sir,
+Away will fill'd in His time, I'll keep her, do not madam, if they here? Some more ha?
+
+## Sample 2
+am.
+
+CLAUDIO:
+Hone here, let her, the remedge, and I know not slept a likely, thou some soully free?
+
+## Sample 3
+arrel which noble thing
+The exchnachsureding worns: I ne'er drunken Biancas, fairer, than the lawfu?
+
+## Sample 4
+augh assalu, you'ld tell me corn;
+Farew. First, for me of a loved. Has thereat I knock you presents?
+
+## Sample 5
+ame the first answer.
+
+MARIZARINIO:
+Door of Angelo as her lord, shrield liken Here fellow the fool ?
+
+## Sample 6
+ad well.
+
+CLAUDIO:
+Soon him a fellows here; for her fine edge in a bogms' lord's wife.
+
+LUCENTIO:
+I?
+
+## Sample 7
+adrezilian measure.
+
+LUCENTIO:
+So, help'd you hath nes have a than dream's corn, beautio, I perchas?
+
+## Sample 8
+as eatter me;
+The girlly: and no other conciolation!
+
+BISTRUMIO:
+I have be rest girl. O, that I a h?
+
+## Sample 9
+and is intend you sort:
+What held her all 'clama's for maffice. Some servant.' what I say me the cu?
+
+## Sample 10
+an thoughts will said in our pleasue,
+Not scanin on him that you live; believaries she.
+
+ISABELLLLL?
+```
+
+See [Andrej Karpathy's blog
+post](http://karpathy.github.io/2015/05/21/rnn-effectiveness/) on more
+examples and links including Linux source codes, Algebraic Geometry
+Theorems, and even cooking recipes. The code for sampling can be found
+in
+[examples/char-lstm/sampler.jl](https://github.com/dmlc/MXNet.jl/blob/master/examples/char-lstm/sampler.jl).
+
+Visualizing the LSTM
+--------------------
+
+Finally, you could visualize the LSTM by calling to\_graphviz on the
+constructed LSTM symbolic architecture. We only show an example of
+1-layer and 2-time-step LSTM below. The automatic layout produced by
+GraphViz is definitely much less clear than [Christopher Olah's
+illustrations](http://colah.github.io/posts/2015-08-Understanding-LSTMs/),
+but could otherwise be very useful for debugging. As we can see, the
+LSTM unfolded over time is just a (very) deep neural network. The
+complete code for producing this visualization can be found in
+[examples/char-lstm/visualize.jl](https://github.com/dmlc/MXNet.jl/blob/master/examples/char-lstm/visualize.jl).
+
+![image](images/char-lstm-vis.svg)
diff --git a/julia/docs/src/tutorial/images/LSTM3-chain.png b/julia/docs/src/tutorial/images/LSTM3-chain.png
new file mode 100644
index 000000000000..e962a3c72078
Binary files /dev/null and b/julia/docs/src/tutorial/images/LSTM3-chain.png differ
diff --git a/julia/docs/src/tutorial/images/char-lstm-vis.svg b/julia/docs/src/tutorial/images/char-lstm-vis.svg
new file mode 100644
index 000000000000..610abab774b7
--- /dev/null
+++ b/julia/docs/src/tutorial/images/char-lstm-vis.svg
@@ -0,0 +1,435 @@
+
+
+
+
+
+
+Network Visualization
+
+
+ptb_embed_2
+
+ptb_embed_2
+FullyConnected
+num-hidden=256
+
+
+ptb_lstm_2_i2h
+
+ptb_lstm_2_i2h
+FullyConnected
+num-hidden=1024
+
+
+ptb_lstm_2_i2h->ptb_embed_2
+
+
+
+
+ptb_embed_1
+
+ptb_embed_1
+FullyConnected
+num-hidden=256
+
+
+ptb_lstm_1_i2h
+
+ptb_lstm_1_i2h
+FullyConnected
+num-hidden=1024
+
+
+ptb_lstm_1_i2h->ptb_embed_1
+
+
+
+
+ptb_lstm_1_h2h
+
+ptb_lstm_1_h2h
+FullyConnected
+num-hidden=1024
+
+
+_plus0
+
+_plus0
+_Plus
+
+
+_plus0->ptb_lstm_1_i2h
+
+
+
+
+_plus0->ptb_lstm_1_h2h
+
+
+
+
+ptb_lstm_1_gates
+
+ptb_lstm_1_gates
+SliceChannel
+
+
+ptb_lstm_1_gates->_plus0
+
+
+
+
+activation3
+
+activation3
+Activation
+act-type=sigmoid
+
+
+activation3->ptb_lstm_1_gates
+
+
+
+
+activation2
+
+activation2
+Activation
+act-type=sigmoid
+
+
+activation2->ptb_lstm_1_gates
+
+
+
+
+_mul0
+
+_mul0
+_Mul
+
+
+_mul0->activation2
+
+
+
+
+activation0
+
+activation0
+Activation
+act-type=sigmoid
+
+
+activation0->ptb_lstm_1_gates
+
+
+
+
+activation1
+
+activation1
+Activation
+act-type=tanh
+
+
+activation1->ptb_lstm_1_gates
+
+
+
+
+_mul1
+
+_mul1
+_Mul
+
+
+_mul1->activation0
+
+
+
+
+_mul1->activation1
+
+
+
+
+_plus1
+
+_plus1
+_Plus
+
+
+_plus1->_mul0
+
+
+
+
+_plus1->_mul1
+
+
+
+
+activation4
+
+activation4
+Activation
+act-type=tanh
+
+
+activation4->_plus1
+
+
+
+
+_mul2
+
+_mul2
+_Mul
+
+
+_mul2->activation3
+
+
+
+
+_mul2->activation4
+
+
+
+
+ptb_lstm_2_h2h
+
+ptb_lstm_2_h2h
+FullyConnected
+num-hidden=1024
+
+
+ptb_lstm_2_h2h->_mul2
+
+
+
+
+_plus2
+
+_plus2
+_Plus
+
+
+_plus2->ptb_lstm_2_i2h
+
+
+
+
+_plus2->ptb_lstm_2_h2h
+
+
+
+
+ptb_lstm_2_gates
+
+ptb_lstm_2_gates
+SliceChannel
+
+
+ptb_lstm_2_gates->_plus2
+
+
+
+
+activation8
+
+activation8
+Activation
+act-type=sigmoid
+
+
+activation8->ptb_lstm_2_gates
+
+
+
+
+activation7
+
+activation7
+Activation
+act-type=sigmoid
+
+
+activation7->ptb_lstm_2_gates
+
+
+
+
+_mul3
+
+_mul3
+_Mul
+
+
+_mul3->_plus1
+
+
+
+
+_mul3->activation7
+
+
+
+
+activation5
+
+activation5
+Activation
+act-type=sigmoid
+
+
+activation5->ptb_lstm_2_gates
+
+
+
+
+activation6
+
+activation6
+Activation
+act-type=tanh
+
+
+activation6->ptb_lstm_2_gates
+
+
+
+
+_mul4
+
+_mul4
+_Mul
+
+
+_mul4->activation5
+
+
+
+
+_mul4->activation6
+
+
+
+
+_plus3
+
+_plus3
+_Plus
+
+
+_plus3->_mul3
+
+
+
+
+_plus3->_mul4
+
+
+
+
+activation9
+
+activation9
+Activation
+act-type=tanh
+
+
+activation9->_plus3
+
+
+
+
+_mul5
+
+_mul5
+_Mul
+
+
+_mul5->activation8
+
+
+
+
+_mul5->activation9
+
+
+
+
+ptb_l1_last_h
+
+ptb_l1_last_h
+BlockGrad
+
+
+ptb_l1_last_h->_mul5
+
+
+
+
+ptb_l1_last_c
+
+ptb_l1_last_c
+BlockGrad
+
+
+ptb_l1_last_c->_plus3
+
+
+
+
+ptb_pred_2
+
+ptb_pred_2
+FullyConnected
+num-hidden=128
+
+
+ptb_pred_2->_mul5
+
+
+
+
+ptb_softmax_2
+
+ptb_softmax_2
+SoftmaxOutput
+
+
+ptb_softmax_2->ptb_pred_2
+
+
+
+
+ptb_pred_1
+
+ptb_pred_1
+FullyConnected
+num-hidden=128
+
+
+ptb_pred_1->_mul2
+
+
+
+
+ptb_softmax_1
+
+ptb_softmax_1
+SoftmaxOutput
+
+
+ptb_softmax_1->ptb_pred_1
+
+
+
+
+
diff --git a/julia/docs/src/tutorial/mnist.md b/julia/docs/src/tutorial/mnist.md
new file mode 100644
index 000000000000..76430fd1b1d0
--- /dev/null
+++ b/julia/docs/src/tutorial/mnist.md
@@ -0,0 +1,264 @@
+Digit Recognition on MNIST
+==========================
+
+In this tutorial, we will work through examples of training a simple
+multi-layer perceptron and then a convolutional neural network (the
+LeNet architecture) on the [MNIST handwritten digit
+dataset](http://yann.lecun.com/exdb/mnist/). The code for this tutorial
+could be found in
+[examples/mnist](https://github.com/dmlc/MXNet.jl/tree/master/examples/mnist). There are also two Jupyter notebooks that expand a little more on the [MLP](https://github.com/ultradian/julia_notebooks/blob/master/mxnet/mnistMLP.ipynb) and the [LeNet](https://github.com/ultradian/julia_notebooks/blob/master/mxnet/mnistLenet.ipynb), using the more general `ArrayDataProvider`.
+
+Simple 3-layer MLP
+------------------
+
+This is a tiny 3-layer MLP that could be easily trained on CPU. The
+script starts with
+
+```julia
+using MXNet
+```
+
+to load the `MXNet` module. Then we are ready to define the network
+architecture via the [symbolic API](../user-guide/overview.md). We start
+with a placeholder `data` symbol,
+
+```julia
+data = mx.Variable(:data)
+```
+
+and then cascading fully-connected layers and activation functions:
+
+```julia
+fc1 = mx.FullyConnected(data, name=:fc1, num_hidden=128)
+act1 = mx.Activation(fc1, name=:relu1, act_type=:relu)
+fc2 = mx.FullyConnected(act1, name=:fc2, num_hidden=64)
+act2 = mx.Activation(fc2, name=:relu2, act_type=:relu)
+fc3 = mx.FullyConnected(act2, name=:fc3, num_hidden=10)
+```
+
+Note each composition we take the previous symbol as the first argument,
+forming a feedforward chain. The architecture looks like
+
+```
+Input --> 128 units (ReLU) --> 64 units (ReLU) --> 10 units
+```
+
+where the last 10 units correspond to the 10 output classes (digits
+0,...,9). We then add a final `SoftmaxOutput` operation to turn the
+10-dimensional prediction to proper probability values for the 10
+classes:
+
+```julia
+mlp = mx.SoftmaxOutput(fc3, name=:softmax)
+```
+
+As we can see, the MLP is just a chain of layers. For this case, we can
+also use the `mx.chain` macro. The same architecture above can be
+defined as
+
+```julia
+mlp = @mx.chain mx.Variable(:data) =>
+ mx.FullyConnected(name=:fc1, num_hidden=128) =>
+ mx.Activation(name=:relu1, act_type=:relu) =>
+ mx.FullyConnected(name=:fc2, num_hidden=64) =>
+ mx.Activation(name=:relu2, act_type=:relu) =>
+ mx.FullyConnected(name=:fc3, num_hidden=10) =>
+ mx.SoftmaxOutput(name=:softmax)
+```
+
+After defining the architecture, we are ready to load the MNIST data.
+MXNet.jl provide built-in data providers for the MNIST dataset, which
+could automatically download the dataset into
+`Pkg.dir("MXNet")/data/mnist` if necessary. We wrap the code to
+construct the data provider into `mnist-data.jl` so that it could be
+shared by both the MLP example and the LeNet ConvNets example.
+
+```julia
+batch_size = 100
+include("mnist-data.jl")
+train_provider, eval_provider = get_mnist_providers(batch_size)
+```
+
+If you need to write your own data providers for customized data format,
+please refer to [`mx.AbstractDataProvider`](@ref).
+
+Given the architecture and data, we can instantiate an *model* to do the
+actual training. `mx.FeedForward` is the built-in model that is suitable
+for most feed-forward architectures. When constructing the model, we
+also specify the *context* on which the computation should be carried
+out. Because this is a really tiny MLP, we will just run on a single CPU
+device.
+
+```julia
+model = mx.FeedForward(mlp, context=mx.cpu())
+```
+
+You can use a `mx.gpu()` or if a list of devices (e.g.
+`[mx.gpu(0), mx.gpu(1)]`) is provided, data-parallelization will be used
+automatically. But for this tiny example, using a GPU device might not
+help.
+
+The last thing we need to specify is the optimization algorithm (a.k.a.
+*optimizer*) to use. We use the basic SGD with a fixed learning rate 0.1
+, momentum 0.9 and weight decay 0.00001:
+
+```julia
+optimizer = mx.SGD(η=0.1, μ=0.9, λ=0.00001)
+```
+
+Now we can do the training. Here the `n_epoch` parameter specifies that
+we want to train for 20 epochs. We also supply a `eval_data` to monitor
+validation accuracy on the validation set.
+
+```julia
+mx.fit(model, optimizer, train_provider, n_epoch=20, eval_data=eval_provider)
+```
+
+Here is a sample output
+
+```
+INFO: Start training on [CPU0]
+INFO: Initializing parameters...
+INFO: Creating KVStore...
+INFO: == Epoch 001 ==========
+INFO: ## Training summary
+INFO: :accuracy = 0.7554
+INFO: time = 1.3165 seconds
+INFO: ## Validation summary
+INFO: :accuracy = 0.9502
+...
+INFO: == Epoch 020 ==========
+INFO: ## Training summary
+INFO: :accuracy = 0.9949
+INFO: time = 0.9287 seconds
+INFO: ## Validation summary
+INFO: :accuracy = 0.9775
+```
+
+Convolutional Neural Networks
+-----------------------------
+
+In the second example, we show a slightly more complicated architecture
+that involves convolution and pooling. This architecture for the MNIST
+is usually called the \[LeNet\]\_. The first part of the architecture is
+listed below:
+
+```julia
+# input
+data = mx.Variable(:data)
+
+# first conv
+conv1 = @mx.chain mx.Convolution(data, kernel=(5,5), num_filter=20) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.Pooling(pool_type=:max, kernel=(2,2), stride=(2,2))
+
+# second conv
+conv2 = @mx.chain mx.Convolution(conv1, kernel=(5,5), num_filter=50) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.Pooling(pool_type=:max, kernel=(2,2), stride=(2,2))
+```
+
+We basically defined two convolution modules. Each convolution module is
+actually a chain of `Convolution`, `tanh` activation and then max
+`Pooling` operations.
+
+Each sample in the MNIST dataset is a 28x28 single-channel grayscale
+image. In the tensor format used by `NDArray`, a batch of 100 samples is
+a tensor of shape `(28,28,1,100)`. The convolution and pooling operates
+in the spatial axis, so `kernel=(5,5)` indicate a square region of
+5-width and 5-height. The rest of the architecture follows as:
+
+```julia
+# first fully-connected
+fc1 = @mx.chain mx.Flatten(conv2) =>
+ mx.FullyConnected(num_hidden=500) =>
+ mx.Activation(act_type=:tanh)
+
+# second fully-connected
+fc2 = mx.FullyConnected(fc1, num_hidden=10)
+
+# softmax loss
+lenet = mx.Softmax(fc2, name=:softmax)
+```
+
+Note a fully-connected operator expects the input to be a matrix.
+However, the results from spatial convolution and pooling are 4D
+tensors. So we explicitly used a `Flatten` operator to flat the tensor,
+before connecting it to the `FullyConnected` operator.
+
+The rest of the network is the same as the previous MLP example. As
+before, we can now load the MNIST dataset:
+
+```julia
+batch_size = 100
+include("mnist-data.jl")
+train_provider, eval_provider = get_mnist_providers(batch_size; flat=false)
+```
+
+Note we specified `flat=false` to tell the data provider to provide 4D
+tensors instead of 2D matrices because the convolution operators needs
+correct spatial shape information. We then construct a feedforward model
+on GPU, and train it.
+
+```julia
+# fit model
+model = mx.FeedForward(lenet, context=mx.gpu())
+
+# optimizer
+optimizer = mx.SGD(η=0.05, μ=0.9, λ=0.00001)
+
+# fit parameters
+mx.fit(model, optimizer, train_provider, n_epoch=20, eval_data=eval_provider)
+```
+
+And here is a sample of running outputs:
+
+```
+INFO: == Epoch 001 ==========
+INFO: ## Training summary
+INFO: :accuracy = 0.6750
+INFO: time = 4.9814 seconds
+INFO: ## Validation summary
+INFO: :accuracy = 0.9712
+...
+INFO: == Epoch 020 ==========
+INFO: ## Training summary
+INFO: :accuracy = 1.0000
+INFO: time = 4.0086 seconds
+INFO: ## Validation summary
+INFO: :accuracy = 0.9915
+```
+
+Predicting with a trained model
+-------------------------------
+
+Predicting with a trained model is very simple. By calling `mx.predict`
+with the model and a data provider, we get the model output as a Julia
+Array:
+
+```julia
+probs = mx.predict(model, eval_provider)
+```
+
+The following code shows a stupid way of getting all the labels from the
+data provider, and compute the prediction accuracy manually:
+
+```julia
+# collect all labels from eval data
+labels = reduce(
+ vcat,
+ copy(mx.get(eval_provider, batch, :softmax_label)) for batch ∈ eval_provider)
+# labels are 0...9
+labels .= labels .+ 1
+
+# Now we use compute the accuracy
+pred = map(i -> indmax(probs[1:10, i]), 1:size(probs, 2))
+correct = sum(pred .== labels)
+@printf "Accuracy on eval set: %.2f%%\n" 100correct/length(labels)
+```
+
+Alternatively, when the dataset is huge, one can provide a callback to
+`mx.predict`, then the callback function will be invoked with the
+outputs of each mini-batch. The callback could, for example, write the
+data to disk for future inspection. In this case, no value is returned
+from `mx.predict`. See also predict.
diff --git a/julia/docs/src/user-guide/faq.md b/julia/docs/src/user-guide/faq.md
new file mode 100644
index 000000000000..8fd8a6b34551
--- /dev/null
+++ b/julia/docs/src/user-guide/faq.md
@@ -0,0 +1,8 @@
+FAQ
+===
+
+Running MXNet on AWS GPU instances
+----------------------------------
+
+See the discussions and notes
+[here](https://github.com/dmlc/MXNet.jl/issues/43).
diff --git a/julia/docs/src/user-guide/install.md b/julia/docs/src/user-guide/install.md
new file mode 100644
index 000000000000..f1d5eeefacfe
--- /dev/null
+++ b/julia/docs/src/user-guide/install.md
@@ -0,0 +1,92 @@
+Installation Guide
+==================
+
+Automatic Installation
+----------------------
+
+To install MXNet.jl, simply type
+
+```julia
+Pkg.add("MXNet")
+```
+
+In the Julia REPL. Or to use the latest git version of MXNet.jl, use the
+following command instead
+
+```julia
+Pkg.checkout("MXNet")
+```
+
+MXNet.jl is built on top of [libmxnet](https://github.com/dmlc/mxnet).
+Upon installation, Julia will try to automatically download and build
+libmxnet.
+
+There are several environment variables that change this behaviour.
+
+- `MXNET_HOME`: If you already have a pre-installed version of mxnet
+ you can use `MXNET_HOME` to point the build-process in the right direction.
+- `CUDA_HOME`: If the automatic cuda detection fails you can also set `CUDA_HOME`
+ to override the process.
+- `MXNET_COMMIT`: To control which version of libmxnet will be compiled,
+ you can use the`MXNET_COMMIT` variable to point to either a version tag
+ (e.g. `v0.10.0`), a branch name (e.g. `master`) or a specific commit hash
+ (e.g. `a0b1c2d3`).
+- `CC`: The path of C compiler.
+- `CXX`: The path of C++ compiler.
+- `ADD_CFLAGS`: Additional C flags. For instance,
+ if you need to point non-standard include directory, please set it as
+ `ENV["ADD_CFLAGS"] = "-I'/path/to/include/dir'"`.
+- `ADD_LDFLAGS`: Additional linker flags.
+- `USE_JEMALLOC`: Default is enabled if jemalloc available.
+ If you ran into segfault cause by jemalloc,
+ Please try to disable it.
+
+ ```julia
+ # first remove whole libmxnet source: Pkg.dir("MXNet", "deps", "src")
+ ENV["USE_JEMALLOC"] = "0"
+ Pkg.build("MXNet")
+ ```
+
+The libmxnet source is downloaded to `Pkg.dir("MXNet", "deps", "src", "mxnet")`.
+The automatic build is using default configurations, with OpenCV disabled.
+If the compilation failed due to unresolved dependency, or if
+you want to customize the build, you can compile and
+install libmxnet manually. Please see below for more details.
+
+Manual Compilation
+------------------
+
+It is possible to compile libmxnet separately and point MXNet.jl to a
+existing library in case automatic compilation fails due to
+unresolved dependencies in an non-standard environment; Or when one want
+to work with a separate, maybe customized libmxnet.
+
+To build libmxnet, please refer to [the installation guide of
+libmxnet](https://mxnet.incubator.apache.org/install/index.html). After
+successfully installing libmxnet, set the `MXNET_HOME` *environment
+variable* to the location of libmxnet. In other words, the compiled
+`libmxnet.so` should be found in `$MXNET_HOME/lib`.
+
+> **note**
+>
+> The constant `MXNET_HOME` is pre-compiled in MXNet.jl package cache.
+> If you updated the environment variable after installing MXNet.jl,
+> make sure to update the pre-compilation cache by
+> `Base.compilecache("MXNet")`.
+
+When the `MXNET_HOME` environment variable is detected and the
+corresponding `libmxnet.so` could be loaded successfully, MXNet.jl will
+skip automatic building during installation and use the specified
+libmxnet instead.
+
+Basically, MXNet.jl will search `libmxnet.so` or `libmxnet.dll` in the
+following paths (and in that order):
+
+- `$MXNET_HOME/lib`: customized libmxnet builds
+- `Pkg.dir("MXNet", "deps", "usr", "lib")`: automatic builds
+- Any system wide library search path
+
+Note that MXNet.jl can not load `libmxnet.so` even if it is on one of
+the paths above in case a library it depends upon is missing from the
+`LD_LIBRARY_PATH`. Thus, if you are going to compile to add CUDA, the
+path to the CUDA libraries will have to be added to `LD_LIBRARY_PATH`.
diff --git a/julia/docs/src/user-guide/overview.md b/julia/docs/src/user-guide/overview.md
new file mode 100644
index 000000000000..a81d7ff30e9e
--- /dev/null
+++ b/julia/docs/src/user-guide/overview.md
@@ -0,0 +1,406 @@
+# Overview
+
+## MXNet.jl Namespace
+
+Most the functions and types in MXNet.jl are organized in a flat
+namespace. Because many some functions are conflicting with existing
+names in the Julia Base module, we wrap them all in a `mx` module. The
+convention of accessing the MXNet.jl interface is the to use the `mx.`
+prefix explicitly:
+
+```julia
+julia> using MXNet
+
+julia> x = mx.zeros(2, 3) # MXNet NDArray
+2×3 mx.NDArray{Float32} @ CPU0:
+ 0.0 0.0 0.0
+ 0.0 0.0 0.0
+
+julia> y = zeros(eltype(x), size(x)) # Julia Array
+2×3 Array{Float32,2}:
+ 0.0 0.0 0.0
+ 0.0 0.0 0.0
+
+julia> copy!(y, x) # Overloaded function in Julia Base
+2×3 Array{Float32,2}:
+ 0.0 0.0 0.0
+ 0.0 0.0 0.0
+
+julia> z = mx.ones(size(x), mx.gpu()) # MXNet NDArray on GPU
+2×3 mx.NDArray{Float32} @ GPU0:
+ 1.0 1.0 1.0
+ 1.0 1.0 1.0
+
+julia> mx.copy!(z, y) # Same as copy!(z, y)
+2×3 mx.NDArray{Float32} @ GPU0:
+ 0.0 0.0 0.0
+ 0.0 0.0 0.0
+```
+
+Note functions like `size`, `copy!` that is extensively overloaded for
+various types works out of the box. But functions like `zeros` and
+`ones` will be ambiguous, so we always use the `mx.` prefix. If you
+prefer, the `mx.` prefix can be used explicitly for all MXNet.jl
+functions, including `size` and `copy!` as shown in the last line.
+
+## Low Level Interface
+
+### `NDArray`
+
+`NDArray` is the basic building blocks of the actual computations in
+MXNet. It is like a Julia `Array` object, with some important
+differences listed here:
+
+- The actual data could live on different `Context` (e.g. GPUs). For
+ some contexts, iterating into the elements one by one is very slow,
+ thus indexing into NDArray is not recommanded in general. The easiest
+ way to inspect the contents of an NDArray is to use the `copy`
+ function to copy the contents as a Julia `Array`.
+- Operations on `NDArray` (including basic arithmetics and neural
+ network related operators) are executed in parallel with automatic
+ dependency tracking to ensure correctness.
+- There is no generics in `NDArray`, the `eltype` is always
+ `mx.MX_float`. Because for applications in machine learning, single
+ precision floating point numbers are typical a best choice balancing
+ between precision, speed and portability. Also since libmxnet is
+ designed to support multiple languages as front-ends, it is much
+ simpler to implement with a fixed data type.
+
+While most of the computation is hidden in libmxnet by operators
+corresponding to various neural network layers. Getting familiar with
+the `NDArray` API is useful for implementing `Optimizer` or customized
+operators in Julia directly.
+
+The followings are common ways to create `NDArray` objects:
+
+- `mx.empty(shape[, context])`: create on uninitialized array of a
+ given shape on a specific device. For example,
+ `mx.empty(2, 3)`, `mx.((2, 3), mx.gpu(2))`.
+- `mx.zeros(shape[, context])` and `mx.ones(shape[, context])`:
+ similar to the Julia's built-in `zeros` and `ones`.
+- `mx.copy(jl_arr, context)`: copy the contents of a Julia `Array` to
+ a specific device.
+
+Most of the convenient functions like `size`, `length`, `ndims`,
+`eltype` on array objects should work out-of-the-box. Although indexing
+is not supported, it is possible to take *slices*:
+
+```@repl
+using MXNet
+a = mx.ones(2, 3)
+b = mx.slice(a, 1:2)
+b[:] = 2
+a
+```
+
+A slice is a sub-region sharing the same memory with the original
+`NDArray` object. A slice is always a contiguous piece of memory, so only
+slicing on the *last* dimension is supported. The example above also
+shows a way to set the contents of an `NDArray`.
+
+```@repl
+using MXNet
+mx.srand(42)
+a = mx.empty(2, 3)
+a[:] = 0.5 # set all elements to a scalar
+a[:] = rand(size(a)) # set contents with a Julia Array
+copy!(a, rand(size(a))) # set value by copying a Julia Array
+b = mx.empty(size(a))
+b[:] = a # copying and assignment between NDArrays
+```
+
+Note due to the intrinsic design of the Julia language, a normal
+assignment
+
+```julia
+a = b
+```
+
+does **not** mean copying the contents of `b` to `a`. Instead, it just
+make the variable `a` pointing to a new object, which is `b`.
+Similarly, inplace arithmetics does not work as expected:
+
+```@repl inplace-macro
+using MXNet
+a = mx.ones(2)
+r = a # keep a reference to a
+b = mx.ones(2)
+a += b # translates to a = a + b
+a
+r
+```
+
+As we can see, `a` has expected value, but instead of inplace updating,
+a new `NDArray` is created and `a` is set to point to this new object. If
+we look at `r`, which still reference to the old `a`, its content has
+not changed. There is currently no way in Julia to overload the
+operators like `+=` to get customized behavior.
+
+Instead, you will need to write `a[:] = a + b`, or if you want *real*
+inplace `+=` operation, MXNet.jl provides a simple macro `@mx.inplace`:
+
+```@repl inplace-macro
+@mx.inplace a += b
+macroexpand(:(@mx.inplace a += b))
+```
+
+As we can see, it translate the `+=` operator to an explicit `add_to!`
+function call, which invokes into libmxnet to add the contents of `b`
+into `a` directly. For example, the following is the update rule in the
+`SGD Optimizer` (both gradient `∇` and weight `W` are `NDArray` objects):
+
+```julia
+@inplace W .+= -η .* (∇ + λ .* W)
+```
+
+Note there is no much magic in `mx.inplace`: it only does a shallow
+translation. In the SGD update rule example above, the computation like
+scaling the gradient by `grad_scale` and adding the weight decay all
+create temporary `NDArray` objects. To mitigate this issue, libmxnet has a
+customized memory allocator designed specifically to handle this kind of
+situations. The following snippet does a simple benchmark on allocating
+temp `NDArray` vs. pre-allocating:
+
+```julia
+using Benchmark
+using MXNet
+
+N_REP = 1000
+SHAPE = (128, 64)
+CTX = mx.cpu()
+LR = 0.1
+
+function inplace_op()
+ weight = mx.zeros(SHAPE, CTX)
+ grad = mx.ones(SHAPE, CTX)
+
+ # pre-allocate temp objects
+ grad_lr = mx.empty(SHAPE, CTX)
+
+ for i = 1:N_REP
+ copy!(grad_lr, grad)
+ @mx.inplace grad_lr .*= LR
+ @mx.inplace weight -= grad_lr
+ end
+ return weight
+end
+
+function normal_op()
+ weight = mx.zeros(SHAPE, CTX)
+ grad = mx.ones(SHAPE, CTX)
+
+ for i = 1:N_REP
+ weight[:] -= LR * grad
+ end
+ return weight
+end
+
+# make sure the results are the same
+@assert(maximum(abs(copy(normal_op() - inplace_op()))) < 1e-6)
+
+println(compare([inplace_op, normal_op], 100))
+```
+
+The comparison on my laptop shows that `normal_op` while allocating a
+lot of temp NDArray in the loop (the performance gets worse when
+increasing `N_REP`), is only about twice slower than the pre-allocated
+one.
+
+| Row | Function | Average | Relative | Replications |
+| ------ | --------------- | ------------ | ----------- | --------------- |
+| 1 | "inplace\_op" | 0.0074854 | 1.0 | 100 |
+| 2 | "normal\_op" | 0.0174202 | 2.32723 | 100 |
+
+So it will usually not be a big problem unless you are at the bottleneck
+of the computation.
+
+### Distributed Key-value Store
+
+The type `KVStore` and related methods are used for data sharing across
+different devices or machines. It provides a simple and efficient
+integer - NDArray key-value storage system that each device can pull or
+push.
+
+The following example shows how to create a local `KVStore`, initialize
+a value and then pull it back.
+
+```@setup kv
+using MXNet
+```
+
+```@example kv
+kv = mx.KVStore(:local)
+shape = (2, 3)
+key = 3
+
+mx.init!(kv, key, mx.ones(shape) * 2)
+a = mx.empty(shape)
+mx.pull!(kv, key, a) # pull value into a
+a
+```
+
+## Intermediate Level Interface
+
+### Symbols and Composition
+
+The way we build deep learning models in MXNet.jl is to use the powerful
+symbolic composition system. It is like
+[Theano](http://deeplearning.net/software/theano/), except that we
+avoided long expression compilation time by providing *larger* neural
+network related building blocks to guarantee computation performance.
+See also [this note](http://mxnet.readthedocs.org/en/latest/program_model.html)
+for the design and trade-off of the MXNet symbolic composition system.
+
+The basic type is `mx.SymbolicNode`. The following is a trivial example of
+composing two symbols with the `+` operation.
+
+```@setup sym1
+using MXNet
+```
+
+```@example sym1
+A = mx.Variable(:A)
+B = mx.Variable(:B)
+C = A + B
+print(C) # debug printing
+```
+
+We get a new `SymbolicNode` by composing existing `SymbolicNode`s by some
+*operations*. A hierarchical architecture of a deep neural network could
+be realized by recursive composition. For example, the following code
+snippet shows a simple 2-layer MLP construction, using a hidden layer of
+128 units and a `ReLU` activation function.
+
+```@setup fcnet
+using MXNet
+```
+
+```@example fcnet
+net = mx.Variable(:data)
+net = mx.FullyConnected(net, name=:fc1, num_hidden=128)
+net = mx.Activation(net, name=:relu1, act_type=:relu)
+net = mx.FullyConnected(net, name=:fc2, num_hidden=64)
+net = mx.SoftmaxOutput(net, name=:out)
+print(net) # debug printing
+```
+
+Each time we take the previous symbol, and compose with an operation.
+Unlike the simple `+` example above, the *operations* here are "bigger"
+ones, that correspond to common computation layers in deep neural
+networks.
+
+Each of those operation takes one or more input symbols for composition,
+with optional hyper-parameters (e.g. `num_hidden`, `act_type`) to
+further customize the composition results.
+
+When applying those operations, we can also specify a `name` for the
+result symbol. This is convenient if we want to refer to this symbol
+later on. If not supplied, a name will be automatically generated.
+
+Each symbol takes some arguments. For example, in the `+` case above, to
+compute the value of `C`, we will need to know the values of the two
+inputs `A` and `B`. For neural networks, the arguments are primarily two
+categories: *inputs* and *parameters*. *inputs* are data and labels for
+the networks, while *parameters* are typically trainable *weights*,
+*bias*, *filters*.
+
+When composing symbols, their arguments accumulates.
+We can list all the arguments by
+
+```@example fcnet
+mx.list_arguments(net)
+```
+
+Note the names of the arguments are generated according to the provided
+name for each layer. We can also specify those names explicitly:
+
+```@repl
+using MXNet
+net = mx.Variable(:data)
+w = mx.Variable(:myweight)
+net = mx.FullyConnected(net, weight=w, name=:fc1, num_hidden=128)
+mx.list_arguments(net)
+```
+
+The simple fact is that a `Variable` is just a placeholder `mx.SymbolicNode`.
+In composition, we can use arbitrary symbols for arguments. For example:
+
+```@repl
+using MXNet
+net = mx.Variable(:data)
+net = mx.FullyConnected(net, name=:fc1, num_hidden=128)
+net2 = mx.Variable(:data2)
+net2 = mx.FullyConnected(net2, name=:net2, num_hidden=128)
+mx.list_arguments(net2)
+composed_net = net2(data2=net, name=:composed)
+mx.list_arguments(composed_net)
+```
+
+Note we use a composed symbol, `net` as the argument `data2` for `net2`
+to get a new symbol, which we named `:composed`. It also shows that a
+symbol itself is a call-able object, which can be invoked to fill in
+missing arguments and get more complicated symbol compositions.
+
+### Shape Inference
+
+Given enough information, the shapes of all arguments in a composed
+symbol could be inferred automatically. For example, given the input
+shape, and some hyper-parameters like `num_hidden`, the shapes for the
+weights and bias in a neural network could be inferred.
+
+```@repl infer-shape
+using MXNet
+net = mx.Variable(:data)
+net = mx.FullyConnected(net, name=:fc1, num_hidden=10)
+arg_shapes, out_shapes, aux_shapes = mx.infer_shape(net, data=(10, 64))
+```
+
+The returned shapes corresponds to arguments with the same order as
+returned by `mx.list_arguments`. The `out_shapes` are shapes for
+outputs, and `aux_shapes` can be safely ignored for now.
+
+```@repl infer-shape
+for (n, s) in zip(mx.list_arguments(net), arg_shapes)
+ println("$n\t=> $s")
+end
+```
+```@repl infer-shape
+for (n, s) in zip(mx.list_outputs(net), out_shapes)
+ println("$n\t=> $s")
+end
+```
+
+### Binding and Executing
+
+In order to execute the computation graph specified a composed symbol,
+we will *bind* the free variables to concrete values, specified as
+`mx.NDArray`. This will create an `mx.Executor` on a given `mx.Context`.
+A context describes the computation devices (CPUs, GPUs, etc.) and an
+executor will carry out the computation (forward/backward) specified in
+the corresponding symbolic composition.
+
+```@repl
+using MXNet
+A = mx.Variable(:A)
+B = mx.Variable(:B)
+C = A .* B
+a = mx.ones(3) * 4
+b = mx.ones(3) * 2
+c_exec = mx.bind(C, context=mx.cpu(), args=Dict(:A => a, :B => b));
+
+mx.forward(c_exec)
+c_exec.outputs[1]
+copy(c_exec.outputs[1]) # copy turns NDArray into Julia Array
+```
+
+For neural networks, it is easier to use `simple_bind`. By providing the
+shape for input arguments, it will perform a shape inference for the
+rest of the arguments and create the NDArray automatically. In practice,
+the binding and executing steps are hidden under the `Model` interface.
+
+**TODO** Provide pointers to model tutorial and further details about
+binding and symbolic API.
+
+## High Level Interface
+
+The high level interface include model training and prediction API, etc.
diff --git a/julia/examples/char-lstm/.gitignore b/julia/examples/char-lstm/.gitignore
new file mode 100644
index 000000000000..a393ee67b410
--- /dev/null
+++ b/julia/examples/char-lstm/.gitignore
@@ -0,0 +1,6 @@
+input.txt
+vocab.dat
+checkpoints
+visualize.dot
+visualize.svg
+visualize.png
diff --git a/julia/examples/char-lstm/README.md b/julia/examples/char-lstm/README.md
new file mode 100644
index 000000000000..ff16ee0a3ae9
--- /dev/null
+++ b/julia/examples/char-lstm/README.md
@@ -0,0 +1,121 @@
+# LSTM char-rnn
+
+Because we explicitly unroll the LSTM/RNN over time for a fixed sequence length,
+it is easy to fit this model into the existing FeedForward model and re-use everything.
+To get a more flexible LSTM/RNN implementation that avoids explicit unrolling and
+deals with variable-length sequences, we still need to implement another model
+beside the existing FeedForward.
+
+To run this example, you will need to install two extra Julia packages: `Iterators.jl`
+and `StatsBase.jl`.
+
+## Training
+
+This example is adapted from the
+[example in Python binding](https://github.com/dmlc/mxnet/blob/master/example/rnn/char_lstm.ipynb) of
+MXNet. The data `input.txt` can be downloaded [here](https://github.com/dmlc/web-data/tree/master/mxnet/tinyshakespeare).
+
+Modify parameters in [config.jl](config.jl) and then run [train.jl](train.jl). An example output
+of training looks like this:
+```
+...
+INFO: Speed: 357.72 samples/sec
+INFO: == Epoch 020 ==========
+INFO: ## Training summary
+INFO: NLL = 1.4672
+INFO: perplexity = 4.3373
+INFO: time = 87.2631 seconds
+INFO: ## Validation summary
+INFO: NLL = 1.6374
+INFO: perplexity = 5.1418
+INFO: Saved checkpoint to 'char-lstm/checkpoints/ptb-0020.params'
+INFO: Speed: 368.74 samples/sec
+INFO: Speed: 361.04 samples/sec
+INFO: Speed: 360.02 samples/sec
+INFO: Speed: 362.34 samples/sec
+INFO: Speed: 360.80 samples/sec
+INFO: Speed: 362.77 samples/sec
+INFO: Speed: 357.18 samples/sec
+INFO: Speed: 355.30 samples/sec
+INFO: Speed: 362.33 samples/sec
+INFO: Speed: 359.23 samples/sec
+INFO: Speed: 358.09 samples/sec
+INFO: Speed: 356.89 samples/sec
+INFO: Speed: 371.91 samples/sec
+INFO: Speed: 372.24 samples/sec
+INFO: Speed: 356.59 samples/sec
+INFO: Speed: 356.64 samples/sec
+INFO: Speed: 360.24 samples/sec
+INFO: Speed: 360.32 samples/sec
+INFO: Speed: 362.38 samples/sec
+INFO: == Epoch 021 ==========
+INFO: ## Training summary
+INFO: NLL = 1.4655
+INFO: perplexity = 4.3297
+INFO: time = 86.9243 seconds
+INFO: ## Validation summary
+INFO: NLL = 1.6366
+INFO: perplexity = 5.1378
+INFO: Saved checkpoint to 'examples/char-lstm/checkpoints/ptb-0021.params'
+```
+
+## Sampling
+
+Run [sampler.jl](sampler.jl) to generate sample sentences from the trained model. Some example sentences are
+```
+## Sample 1
+all have sir,
+Away will fill'd in His time, I'll keep her, do not madam, if they here? Some more ha?
+
+## Sample 2
+am.
+
+CLAUDIO:
+Hone here, let her, the remedge, and I know not slept a likely, thou some soully free?
+
+## Sample 3
+arrel which noble thing
+The exchnachsureding worns: I ne'er drunken Biancas, fairer, than the lawfu?
+
+## Sample 4
+augh assalu, you'ld tell me corn;
+Farew. First, for me of a loved. Has thereat I knock you presents?
+
+## Sample 5
+ame the first answer.
+
+MARIZARINIO:
+Door of Angelo as her lord, shrield liken Here fellow the fool ?
+
+## Sample 6
+ad well.
+
+CLAUDIO:
+Soon him a fellows here; for her fine edge in a bogms' lord's wife.
+
+LUCENTIO:
+I?
+
+## Sample 7
+adrezilian measure.
+
+LUCENTIO:
+So, help'd you hath nes have a than dream's corn, beautio, I perchas?
+
+## Sample 8
+as eatter me;
+The girlly: and no other conciolation!
+
+BISTRUMIO:
+I have be rest girl. O, that I a h?
+
+## Sample 9
+and is intend you sort:
+What held her all 'clama's for maffice. Some servant.' what I say me the cu?
+
+## Sample 10
+an thoughts will said in our pleasue,
+Not scanin on him that you live; believaries she.
+
+ISABELLLLL?
+```
diff --git a/julia/examples/char-lstm/config.jl b/julia/examples/char-lstm/config.jl
new file mode 100644
index 000000000000..c6ed0ff63b3c
--- /dev/null
+++ b/julia/examples/char-lstm/config.jl
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+const DROPOUT = 0
+const BATCH_SIZE = 32
+const SEQ_LENGTH = 32
+const DIM_HIDDEN = 256
+const DIM_EMBED = 256
+const LSTM_N_LAYER = 2
+const N_EPOCH = 21
+const BASE_LR = 0.01
+const WEIGHT_DECAY = 0.00001
+const CLIP_GRADIENT = 1
+const NAME = :ptb
+const N_GPU = 1
+const USE_GPU = true
+const DATA_TR_RATIO = 0.9
+const CKPOINT_PREFIX = joinpath(@__DIR__, "checkpoints/$NAME")
+
+const BATCH_SIZE_SMP = 10
+const SAMPLE_LENGTH = 100
+const SAMPLE_START = 'a'
+
+const UNKNOWN_CHAR = Char(0)
+const INPUT_FILE = joinpath(@__DIR__, "input.txt")
+const VOCAB_FILE = joinpath(@__DIR__, "vocab.dat")
diff --git a/julia/examples/char-lstm/lstm.jl b/julia/examples/char-lstm/lstm.jl
new file mode 100644
index 000000000000..fc4bcc4b6a91
--- /dev/null
+++ b/julia/examples/char-lstm/lstm.jl
@@ -0,0 +1,166 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# An explicitly unrolled LSTM with fixed sequence length.
+using MXNet
+
+#--LSTMState
+struct LSTMState
+ c :: mx.SymbolicNode
+ h :: mx.SymbolicNode
+end
+#--/LSTMState
+
+#--LSTMParam
+struct LSTMParam
+ i2h_W :: mx.SymbolicNode
+ h2h_W :: mx.SymbolicNode
+ i2h_b :: mx.SymbolicNode
+ h2h_b :: mx.SymbolicNode
+end
+#--/LSTMParam
+
+#--lstm_cell
+function lstm_cell(data::mx.SymbolicNode, prev_state::LSTMState, param::LSTMParam;
+ num_hidden::Int=512, dropout::Real=0, name::Symbol=gensym())
+
+ if dropout > 0
+ data = mx.Dropout(data, p=dropout)
+ end
+
+ i2h = mx.FullyConnected(data, weight=param.i2h_W, bias=param.i2h_b,
+ num_hidden=4num_hidden, name=Symbol(name, "_i2h"))
+ h2h = mx.FullyConnected(prev_state.h, weight=param.h2h_W, bias=param.h2h_b,
+ num_hidden=4num_hidden, name=Symbol(name, "_h2h"))
+
+ gates = mx.SliceChannel(i2h + h2h, num_outputs=4, name=Symbol(name, "_gates"))
+
+ in_gate = mx.Activation(gates[1], act_type=:sigmoid)
+ in_trans = mx.Activation(gates[2], act_type=:tanh)
+ forget_gate = mx.Activation(gates[3], act_type=:sigmoid)
+ out_gate = mx.Activation(gates[4], act_type=:sigmoid)
+
+ next_c = (forget_gate .* prev_state.c) + (in_gate .* in_trans)
+ next_h = out_gate .* mx.Activation(next_c, act_type=:tanh)
+
+ return LSTMState(next_c, next_h)
+end
+#--/lstm_cell
+
+#--LSTM-part1
+function LSTM(n_layer::Int, seq_len::Int, dim_hidden::Int, dim_embed::Int, n_class::Int;
+ dropout::Real=0, name::Symbol=gensym(), output_states::Bool=false)
+
+ # placeholder nodes for all parameters
+ embed_W = mx.Variable(Symbol(name, "_embed_weight"))
+ pred_W = mx.Variable(Symbol(name, "_pred_weight"))
+ pred_b = mx.Variable(Symbol(name, "_pred_bias"))
+
+ layer_param_states = map(1:n_layer) do i
+ param = LSTMParam(mx.Variable(Symbol(name, "_l$(i)_i2h_weight")),
+ mx.Variable(Symbol(name, "_l$(i)_h2h_weight")),
+ mx.Variable(Symbol(name, "_l$(i)_i2h_bias")),
+ mx.Variable(Symbol(name, "_l$(i)_h2h_bias")))
+ state = LSTMState(mx.Variable(Symbol(name, "_l$(i)_init_c")),
+ mx.Variable(Symbol(name, "_l$(i)_init_h")))
+ (param, state)
+ end
+ #...
+ #--/LSTM-part1
+
+ #--LSTM-part2
+ # now unroll over time
+ outputs = mx.SymbolicNode[]
+ for t = 1:seq_len
+ data = mx.Variable(Symbol(name, "_data_$t"))
+ label = mx.Variable(Symbol(name, "_label_$t"))
+ hidden = mx.FullyConnected(data, weight=embed_W, num_hidden=dim_embed,
+ no_bias=true, name=Symbol(name, "_embed_$t"))
+
+ # stack LSTM cells
+ for i = 1:n_layer
+ l_param, l_state = layer_param_states[i]
+ dp = i == 1 ? 0 : dropout # don't do dropout for data
+ next_state = lstm_cell(hidden, l_state, l_param, num_hidden=dim_hidden, dropout=dp,
+ name=Symbol(name, "_lstm_$t"))
+ hidden = next_state.h
+ layer_param_states[i] = (l_param, next_state)
+ end
+
+ # prediction / decoder
+ if dropout > 0
+ hidden = mx.Dropout(hidden, p=dropout)
+ end
+ pred = mx.FullyConnected(hidden, weight=pred_W, bias=pred_b, num_hidden=n_class,
+ name=Symbol(name, "_pred_$t"))
+ smax = mx.SoftmaxOutput(pred, label, name=Symbol(name, "_softmax_$t"))
+ push!(outputs, smax)
+ end
+ #...
+ #--/LSTM-part2
+
+ #--LSTM-part3
+ # append block-gradient nodes to the final states
+ for i = 1:n_layer
+ l_param, l_state = layer_param_states[i]
+ final_state = LSTMState(mx.BlockGrad(l_state.c, name=Symbol(name, "_l$(i)_last_c")),
+ mx.BlockGrad(l_state.h, name=Symbol(name, "_l$(i)_last_h")))
+ layer_param_states[i] = (l_param, final_state)
+ end
+
+ # now group all outputs together
+ if output_states
+ outputs = outputs ∪ [x[2].c for x in layer_param_states] ∪
+ [x[2].h for x in layer_param_states]
+ end
+ return mx.Group(outputs...)
+end
+#--/LSTM-part3
+
+
+# Negative Log-likelihood
+mutable struct NLL <: mx.AbstractEvalMetric
+ nll_sum :: Float64
+ n_sample :: Int
+
+ NLL() = new(0.0, 0)
+end
+
+function mx.update!(metric::NLL, labels::Vector{<:mx.NDArray}, preds::Vector{<:mx.NDArray})
+ @assert length(labels) == length(preds)
+ nll = 0.0
+ for (label, pred) in zip(labels, preds)
+ @mx.nd_as_jl ro=(label, pred) begin
+ nll -= sum(log.(max.(broadcast_getindex(pred, round.(Int,label+1), 1:length(label)), 1e-20)))
+ end
+ end
+
+ nll = nll / length(labels)
+ metric.nll_sum += nll
+ metric.n_sample += length(labels[1])
+end
+
+function mx.get(metric :: NLL)
+ nll = metric.nll_sum / metric.n_sample
+ perp = exp(nll)
+ return [(:NLL, nll), (:perplexity, perp)]
+end
+
+function mx.reset!(metric :: NLL)
+ metric.nll_sum = 0.0
+ metric.n_sample = 0
+end
diff --git a/julia/examples/char-lstm/sampler.jl b/julia/examples/char-lstm/sampler.jl
new file mode 100644
index 000000000000..1a4aada22957
--- /dev/null
+++ b/julia/examples/char-lstm/sampler.jl
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+include(joinpath(@__DIR__, "config.jl"))
+include(joinpath(@__DIR__, "lstm.jl"))
+include(joinpath(@__DIR__, "seq-data.jl"))
+
+using StatsBase
+using MXNet
+
+# load vocabulary
+vocab = build_vocabulary(INPUT_FILE, VOCAB_FILE)
+n_class = length(vocab)
+
+# prepare data provider
+jl_data = Pair[(Symbol(NAME, "_data_$t") => zeros(mx.MX_float, (length(vocab), BATCH_SIZE_SMP)))
+ for t = 1:1]
+jl_c = Pair[(Symbol(NAME, "_l$(l)_init_c") => zeros(mx.MX_float, (DIM_HIDDEN, BATCH_SIZE_SMP)))
+ for l = 1:LSTM_N_LAYER]
+jl_h = Pair[(Symbol(NAME, "_l$(l)_init_h") => zeros(mx.MX_float, (DIM_HIDDEN, BATCH_SIZE_SMP)))
+ for l = 1:LSTM_N_LAYER]
+
+# the first input in the sequence
+jl_data_start = jl_data[1].second
+jl_data_start[char_idx(vocab, SAMPLE_START),:] = 1
+
+# define a LSTM with sequence length 1, also output states so that we could manually copy the states
+# when sampling the next char
+lstm = LSTM(LSTM_N_LAYER, 1, DIM_HIDDEN, DIM_EMBED, n_class, name=NAME, output_states=true)
+model = mx.FeedForward(lstm, context=mx.cpu())
+
+# load parameters from traind LSTM, though the sequence length is different, since the weights are shared
+# over time, this should be compatible.
+model = mx.load_checkpoint(model, CKPOINT_PREFIX, N_EPOCH, allow_different_arch=true)
+
+# prepare outputs
+Base.zero(::Type{Char}) = Char(0)
+output_samples = zeros(Char, (SAMPLE_LENGTH, BATCH_SIZE_SMP))
+output_samples[1, :] = SAMPLE_START
+
+# build inverse vocabulary for convenience
+inv_vocab = Dict(v => k for (k,v) in vocab)
+
+# do prediction and sampling step by step
+for t = 2:SAMPLE_LENGTH-1
+ data = mx.ArrayDataProvider(jl_data ∪ jl_c ∪ jl_h)
+ preds = mx.predict(model, data)
+
+ # the first output is prediction
+ outputs = preds[1]
+
+ # do sampling and init the next inputs
+ jl_data_start[:] = 0
+ for i = 1:BATCH_SIZE_SMP
+ prob = WeightVec(outputs[:, i])
+ k = sample(prob)
+ output_samples[t, i] = inv_vocab[k]
+ jl_data_start[k, i] = 1
+ end
+
+ # copy the states over
+ for l = 1:LSTM_N_LAYER
+ copy!(jl_c[l][2], preds[1+l])
+ copy!(jl_h[l][2], preds[1+LSTM_N_LAYER+l])
+ end
+end
+
+output_texts = [join(output_samples[:,i]) for i = 1:BATCH_SIZE_SMP]
+output_texts = [replace(x, UNKNOWN_CHAR, '?') for x in output_texts]
+
+for (i, text) in enumerate(output_texts)
+ println("## Sample $i")
+ println(text)
+ println()
+end
diff --git a/julia/examples/char-lstm/seq-data.jl b/julia/examples/char-lstm/seq-data.jl
new file mode 100644
index 000000000000..3489e5bc3c39
--- /dev/null
+++ b/julia/examples/char-lstm/seq-data.jl
@@ -0,0 +1,136 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Simple data provider that load text
+using Iterators
+using MXNet
+
+function build_vocabulary(corpus_fn::AbstractString, vocab_fn::AbstractString; max_vocab=10000)
+ if isfile(vocab_fn)
+ info("Vocabulary already exists, reusing $vocab_fn...")
+ vocab = Dict{Char,Int}(w => i for (i,w) in enumerate(readstring(vocab_fn)))
+ else
+ # count symbol frequency
+ dict = Dict{Char,Int}()
+ open(corpus_fn) do io
+ for line in eachline(io)
+ for c in line
+ dict[c] = get(dict, c, 0) + 1
+ end
+ end
+ end
+
+ vocab = sort(collect(dict), by=x->-x.second)
+ vocab = vocab[1:min(max_vocab,length(vocab))]
+ open(vocab_fn, "w") do io
+ for x in vocab
+ print(io, x.first)
+ end
+ end
+
+ vocab = Dict(x.first => i for (i,x) in enumerate(vocab))
+ end
+ vocab[UNKNOWN_CHAR] = length(vocab)
+ return vocab
+end
+
+#--CharSeqProvider
+mutable struct CharSeqProvider <: mx.AbstractDataProvider
+ text :: AbstractString
+ batch_size :: Int
+ seq_len :: Int
+ vocab :: Dict{Char,Int}
+
+ prefix :: Symbol
+ n_layer :: Int
+ dim_hidden :: Int
+end
+#--/CharSeqProvider
+
+function mx.get_batch_size(p :: CharSeqProvider)
+ p.batch_size
+end
+
+#--provide
+function mx.provide_data(p :: CharSeqProvider)
+ [(Symbol(p.prefix, "_data_$t"), (length(p.vocab), p.batch_size)) for t = 1:p.seq_len] ∪
+ [(Symbol(p.prefix, "_l$(l)_init_c"), (p.dim_hidden, p.batch_size)) for l=1:p.n_layer] ∪
+ [(Symbol(p.prefix, "_l$(l)_init_h"), (p.dim_hidden, p.batch_size)) for l=1:p.n_layer]
+end
+function mx.provide_label(p :: CharSeqProvider)
+ [(Symbol(p.prefix, "_label_$t"), (p.batch_size,)) for t = 1:p.seq_len]
+end
+#--/provide
+
+#--eachbatch-part1
+function mx.eachbatch(p::CharSeqProvider)
+ data_all = [mx.zeros(shape) for (name, shape) in mx.provide_data(p)]
+ label_all = [mx.zeros(shape) for (name, shape) in mx.provide_label(p)]
+
+ data_jl = [copy(x) for x in data_all]
+ label_jl= [copy(x) for x in label_all]
+
+ batch = mx.DataBatch(data_all, label_all, p.batch_size)
+ #...
+ #--/eachbatch-part1
+
+ #--eachbatch-part2
+ #...
+ function _text_iter(c::Channel)
+ text = p.text
+
+ n_batch = floor(Int, length(text) / p.batch_size / p.seq_len)
+ text = text[1:n_batch*p.batch_size*p.seq_len] # discard tailing
+ idx_all = 1:length(text)
+
+ for idx_batch in partition(idx_all, p.batch_size*p.seq_len)
+ for i = 1:p.seq_len
+ data_jl[i][:] = 0
+ label_jl[i][:] = 0
+ end
+
+ for (i, idx_seq) in enumerate(partition(idx_batch, p.seq_len))
+ for (j, idx) in enumerate(idx_seq)
+ c_this = text[idx]
+ c_next = idx == length(text) ? UNKNOWN_CHAR : text[idx+1]
+ data_jl[j][char_idx(vocab,c_this),i] = 1
+ label_jl[j][i] = char_idx(vocab,c_next)-1
+ end
+ end
+
+ for i = 1:p.seq_len
+ copy!(data_all[i], data_jl[i])
+ copy!(label_all[i], label_jl[i])
+ end
+
+ put!(c, batch)
+ end
+ end
+
+ return Channel(_text_iter)
+end
+#--/eachbatch-part2
+
+# helper function to convert a char into index in vocabulary
+function char_idx(vocab :: Dict{Char,Int}, c :: Char)
+ if haskey(vocab, c)
+ vocab[c]
+ else
+ vocab[UNKNOWN_CHAR]
+ end
+end
+
diff --git a/julia/examples/char-lstm/train.jl b/julia/examples/char-lstm/train.jl
new file mode 100644
index 000000000000..57bfeb6b6e11
--- /dev/null
+++ b/julia/examples/char-lstm/train.jl
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+include(joinpath(@__DIR__, "config.jl"))
+include(joinpath(@__DIR__, "lstm.jl"))
+include(joinpath(@__DIR__, "seq-data.jl"))
+
+# build vocabulary
+vocab = build_vocabulary(INPUT_FILE, VOCAB_FILE)
+n_class = length(vocab)
+
+#--LSTM
+# define LSTM
+lstm = LSTM(LSTM_N_LAYER, SEQ_LENGTH, DIM_HIDDEN, DIM_EMBED,
+ n_class, dropout=DROPOUT, name=NAME)
+#--/LSTM
+
+#--data
+# load data
+text_all = readstring(INPUT_FILE)
+len_train = round(Int, length(text_all)*DATA_TR_RATIO)
+text_tr = text_all[1:len_train]
+text_val = text_all[len_train+1:end]
+
+data_tr = CharSeqProvider(text_tr, BATCH_SIZE, SEQ_LENGTH, vocab, NAME,
+ LSTM_N_LAYER, DIM_HIDDEN)
+data_val = CharSeqProvider(text_val, BATCH_SIZE, SEQ_LENGTH, vocab, NAME,
+ LSTM_N_LAYER, DIM_HIDDEN)
+#--/data
+
+# set up training
+if USE_GPU
+ context = [mx.gpu(i) for i = 0:N_GPU-1]
+else
+ context = mx.cpu()
+end
+
+#--train
+model = mx.FeedForward(lstm, context=context)
+optimizer = mx.ADAM(η=BASE_LR, λ=WEIGHT_DECAY, clip=CLIP_GRADIENT)
+
+mx.fit(model, optimizer, data_tr, eval_data=data_val, n_epoch=N_EPOCH,
+ initializer=mx.UniformInitializer(0.1),
+ callbacks=[mx.speedometer(), mx.do_checkpoint(CKPOINT_PREFIX)], eval_metric=NLL())
+#--/train
diff --git a/julia/examples/char-lstm/visualize.jl b/julia/examples/char-lstm/visualize.jl
new file mode 100644
index 000000000000..e2a2c87c9c10
--- /dev/null
+++ b/julia/examples/char-lstm/visualize.jl
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+include(joinpath(dirname(@__FILE__), "config.jl"))
+include(joinpath(dirname(@__FILE__), "lstm.jl"))
+
+using MXNet
+
+vis_n_layer = 1
+vis_seq_len = 2
+vis_n_class = 128
+
+lstm = LSTM(vis_n_layer, vis_seq_len, DIM_HIDDEN, DIM_EMBED, vis_n_class, name=NAME, output_states=true)
+
+open("visualize.dot", "w") do io
+ println(io, mx.to_graphviz(lstm))
+end
+run(pipeline(`dot -Tsvg visualize.dot`, stdout="visualize.svg"))
diff --git a/julia/examples/cifar10/cifar10.jl b/julia/examples/cifar10/cifar10.jl
new file mode 100644
index 000000000000..a00664ce3a50
--- /dev/null
+++ b/julia/examples/cifar10/cifar10.jl
@@ -0,0 +1,101 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using MXNet
+
+#--------------------------------------------------------------------------------
+# Helper functions to construct larger networks
+
+# basic Conv + BN + ReLU factory
+function conv_factory(data, num_filter, kernel; stride=(1,1), pad=(0,0), act_type=:relu)
+ conv = mx.Convolution(data, num_filter=num_filter, kernel=kernel, stride=stride, pad=pad)
+ bn = mx.BatchNorm(conv)
+ act = mx.Activation(bn, act_type=act_type)
+ return act
+end
+
+# simple downsampling factory
+function downsample_factory(data, ch_3x3)
+ # conv 3x3
+ conv = conv_factory(data, ch_3x3, (3,3), stride=(2,2), pad=(1,1))
+ # pool
+ pool = mx.Pooling(data, kernel=(3,3), stride=(2,2), pool_type=:max)
+ # concat
+ concat = mx.Concat(conv, pool)
+ return concat
+end
+
+# a simple module
+function simple_factory(data, ch_1x1, ch_3x3)
+ # 1x1
+ conv1x1 = conv_factory(data, ch_1x1, (1,1); pad=(0,0))
+ # 3x3
+ conv3x3 = conv_factory(data, ch_3x3, (3,3); pad=(1,1))
+ # concat
+ concat = mx.Concat(conv1x1, conv3x3)
+ return concat
+end
+
+
+#--------------------------------------------------------------------------------
+# Actual architecture
+data = mx.Variable(:data)
+conv1 = conv_factory(data, 96, (3,3); pad=(1,1), act_type=:relu)
+in3a = simple_factory(conv1, 32, 32)
+in3b = simple_factory(in3a, 32, 48)
+in3c = downsample_factory(in3b, 80)
+in4a = simple_factory(in3c, 112, 48)
+in4b = simple_factory(in4a, 96, 64)
+in4c = simple_factory(in4b, 80, 80)
+in4d = simple_factory(in4b, 48, 96)
+in4e = downsample_factory(in4d, 96)
+in5a = simple_factory(in4e, 176, 160)
+in5b = simple_factory(in5a, 176, 160)
+pool = mx.Pooling(in5b, pool_type=:avg, kernel=(7,7), name=:global_pool)
+flatten = mx.Flatten(pool, name=:flatten1)
+fc = mx.FullyConnected(flatten, num_hidden=10, name=:fc1)
+softmax = mx.SoftmaxOutput(fc, name=:loss)
+
+
+#--------------------------------------------------------------------------------
+# Prepare data
+filenames = mx.get_cifar10()
+batch_size = 128
+num_epoch = 10
+num_gpus = 8
+
+train_provider = mx.ImageRecordProvider(label_name=:loss_label,
+ path_imgrec=filenames[:train], mean_img=filenames[:mean],
+ rand_crop=true, rand_mirror=true, data_shape=(28,28,3),
+ batch_size=batch_size, preprocess_threads=1)
+test_provider = mx.ImageRecordProvider(label_name=:loss_label,
+ path_imgrec=filenames[:test], mean_img=filenames[:mean],
+ rand_crop=false, rand_mirror=false, data_shape=(28,28,3),
+ batch_size=batch_size, preprocess_threads=1)
+
+
+#--------------------------------------------------------------------------------
+# Training model
+gpus = [mx.Context(mx.GPU, i) for i = 0:num_gpus-1]
+model = mx.FeedForward(softmax, context=gpus)
+
+# optimizer
+optimizer = mx.SGD(η=0.05, μ=0.9, λ=0.0001)
+
+# fit parameters
+mx.fit(model, optimizer, train_provider, n_epoch=num_epoch, eval_data=test_provider,
+ initializer=mx.UniformInitializer(0.07), callbacks=[mx.speedometer()])
diff --git a/julia/examples/imagenet/ijulia-pretrained-predict/Prediction with Pre-trained Model.ipynb b/julia/examples/imagenet/ijulia-pretrained-predict/Prediction with Pre-trained Model.ipynb
new file mode 100644
index 000000000000..2d5d0ee7dd97
--- /dev/null
+++ b/julia/examples/imagenet/ijulia-pretrained-predict/Prediction with Pre-trained Model.ipynb
@@ -0,0 +1,249 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Using Pretrained Inception-BatchNorm Network\n",
+ "\n",
+ "In this example we will show how to use a pretrained *Inception-BatchNorm* Network. This network is described in the paper\n",
+ "\n",
+ "> Ioffe, Sergey, and Christian Szegedy. \"Batch normalization: Accelerating deep network training by reducing internal covariate shift.\" arXiv preprint arXiv:1502.03167 (2015).\n",
+ "\n",
+ "The pre-trained Inception-BatchNorm network is able to be downloaded from [this link](http://webdocs.cs.ualberta.ca/~bx3/data/Inception.zip). Run the script `get.sh` in `models/Inception/` will download and unpack it automatically."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us first load and display the demo image (try to use other images you like). You will need to install `Images.jl` and `Colors.jl` to load the image."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ "RGB4 Images.Image with:\n",
+ " data: 256x256 Array{ColorTypes.RGB4{FixedPointNumbers.UFixed{UInt8,8}},2}\n",
+ " properties:\n",
+ " imagedescription: \n",
+ " spatialorder: x y\n",
+ " pixelspacing: 1 1"
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "using Images, Colors, ImageMagick\n",
+ "img = load(\"cat.png\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now let us do some preprocessing. The most important thing is to resize image to 224x224 that the pre-trained neural network model expect. However, since `Images.jl` does not have a `imresize` function yet, we will call Python to do the preprocessing. The helper function is defined in `imagehelper.py` under the same directory."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Image resized to (224,224,3)\n",
+ "('Original Image Shape: ', (256, 256, 3))\n"
+ ]
+ }
+ ],
+ "source": [
+ "img = convert(Array, separate(convert(Image{RGB}, img)))\n",
+ "using PyCall\n",
+ "unshift!(PyVector(pyimport(\"sys\")[\"path\"]), \"\")\n",
+ "@pyimport imagehelper as helper\n",
+ "\n",
+ "img = helper.PreprocessImage(img)\n",
+ "# transform from Python row-major to Julia column-major\n",
+ "img = permutedims(img, [3,2,1])\n",
+ "println(\"Image resized to $(size(img))\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The last thing we need to do to prepare the image is to subtract it from the mean. The mean image is computed on the training set, and it comes with the pre-trained model archive."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Image prepared!\n"
+ ]
+ }
+ ],
+ "source": [
+ "using MXNet\n",
+ "\n",
+ "model_dir = joinpath(Pkg.dir(\"MXNet\"), \"models/Inception/Inception/\")\n",
+ "mean_file = joinpath(model_dir, \"mean_224.nd\")\n",
+ "mean_arr = mx.load(mean_file, mx.NDArray)[:mean_img]\n",
+ "\n",
+ "img = img - copy(mean_arr)\n",
+ "img = reshape(img, 224, 224, 3, 1) # add a mini-batch dim\n",
+ "println(\"Image prepared!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now we can load the pre-trained model, via the `load_checkpoint` function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[09:48:53] src/operator/./softmax_output-inl.h:187: Softmax symbol is renamed to SoftmaxOutput. This API will be deprecated in Dec, 2015\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Model loaded\n"
+ ]
+ }
+ ],
+ "source": [
+ "model_prefix = joinpath(model_dir, \"Inception_BN\")\n",
+ "model_epoch = 39\n",
+ "model = mx.load_checkpoint(model_prefix, model_epoch, mx.FeedForward)\n",
+ "println(\"Model loaded\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "With the loaded model, we can do prediction by wrapping the image with a `ArrayDataProvider`. The output is a 1000-way vector giving the predicted probability of each class. The class names are read from `synset.txt`, and we show the class name with the maximum probability."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "tiger cat\n"
+ ]
+ }
+ ],
+ "source": [
+ "pred = mx.predict(model, mx.ArrayDataProvider(img))\n",
+ "classes = open(joinpath(model_dir, \"synset.txt\")) do s \n",
+ " map(x -> replace(strip(x), r\"^n[0-9]+ \", \"\"), readlines(s))\n",
+ "end\n",
+ "println(classes[indmax(pred)])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also show easily the top-5 classes and the associated probabilities."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " tiger cat w.p. 0.415807\n",
+ " tabby, tabby cat w.p. 0.235859\n",
+ " Egyptian cat w.p. 0.161553\n",
+ " lynx, catamount w.p. 0.136078\n",
+ " Persian cat w.p. 0.007109\n"
+ ]
+ }
+ ],
+ "source": [
+ "K = 5\n",
+ "n_best = sortperm(vec(pred), rev=true)[1:K]\n",
+ "best_probs = pred[n_best]\n",
+ "best_labels = classes[n_best]\n",
+ "\n",
+ "for (l,p) in zip(best_labels, best_probs)\n",
+ " println(mx.format(\"{1:>18} w.p. {2:4f}\", l, p))\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Julia 0.4.0",
+ "language": "julia",
+ "name": "julia-0.4"
+ },
+ "language_info": {
+ "file_extension": ".jl",
+ "mimetype": "application/julia",
+ "name": "julia",
+ "version": "0.4.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/julia/examples/imagenet/ijulia-pretrained-predict/cat.png b/julia/examples/imagenet/ijulia-pretrained-predict/cat.png
new file mode 100644
index 000000000000..5f681ec7e99c
Binary files /dev/null and b/julia/examples/imagenet/ijulia-pretrained-predict/cat.png differ
diff --git a/julia/examples/imagenet/ijulia-pretrained-predict/imagehelper.py b/julia/examples/imagenet/ijulia-pretrained-predict/imagehelper.py
new file mode 100644
index 000000000000..dddef7415f45
--- /dev/null
+++ b/julia/examples/imagenet/ijulia-pretrained-predict/imagehelper.py
@@ -0,0 +1,48 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import numpy as np
+from skimage import io, transform
+
+def PreprocessImage(img):
+ img = np.array(img)
+ print("Original Image Shape: ", img.shape)
+ # we crop image from center
+ short_egde = min(img.shape[:2])
+ yy = int((img.shape[0] - short_egde) / 2)
+ xx = int((img.shape[1] - short_egde) / 2)
+ crop_img = img[yy : yy + short_egde, xx : xx + short_egde]
+ # resize to 224, 224
+ resized_img = transform.resize(crop_img, (224, 224))
+ # convert to numpy.ndarray
+ sample = np.asarray(resized_img) * 256
+
+ #-------------------------------------------------------------------
+ # Note: The decoded image should be in BGR channel (opencv output)
+ # For RGB output such as from skimage, we need to convert it to BGR
+ # WRONG channel will lead to WRONG result
+ #-------------------------------------------------------------------
+ # swap channel from RGB to BGR
+ # sample = sample[:, :, [2,1,0]]
+ sample = sample[:, :, [0,1,2]] # actually, in this pre-trained model RGB is used
+
+ # swap axes to make image from (224, 224, 4) to (3, 224, 224)
+ sample = np.swapaxes(sample, 0, 2)
+ sample = np.swapaxes(sample, 1, 2)
+
+ sample.resize(3,224,224)
+ return sample
diff --git a/julia/examples/mnist/lenet-stn.jl b/julia/examples/mnist/lenet-stn.jl
new file mode 100644
index 000000000000..95cd0955d402
--- /dev/null
+++ b/julia/examples/mnist/lenet-stn.jl
@@ -0,0 +1,81 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using MXNet
+
+#--------------------------------------------------------------------------------
+# define lenet with stn layer
+
+
+
+# input
+data = mx.Variable(:data)
+
+
+# the localisation network in lenet-stn
+# it will increase acc about more than 1%, when num-epoch >=15
+# The localization net just takes the data as input and must output a vector in R^n
+loc_net = @mx.chain mx.Convolution(data, num_filter=10, kernel=(5, 5), stride=(2,2)) =>
+ mx.Activation(act_type=:relu) =>
+ mx.Pooling( kernel=(2, 2), stride=(2, 2), pool_type=:max) =>
+ mx.Convolution( num_filter=10, kernel=(3, 3), stride=(2,2), pad=(1, 1)) =>
+ mx.Activation(act_type=:relu) =>
+ mx.Pooling( global_pool=true, kernel=(2, 2), pool_type=:avg) =>
+ mx.Flatten() =>
+ mx.FullyConnected(num_hidden=6, name=:stn_loc)
+
+data=mx.SpatialTransformer(data,loc_net, target_shape = (28,28), transform_type="affine", sampler_type="bilinear")
+
+# first conv
+conv1 = @mx.chain mx.Convolution(data, kernel=(5,5), num_filter=20) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.Pooling(pool_type=:max, kernel=(2,2), stride=(2,2))
+
+# second conv
+conv2 = @mx.chain mx.Convolution(conv1, kernel=(5,5), num_filter=50) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.Pooling(pool_type=:max, kernel=(2,2), stride=(2,2))
+
+# first fully-connected
+fc1 = @mx.chain mx.Flatten(conv2) =>
+ mx.FullyConnected(num_hidden=500) =>
+ mx.Activation(act_type=:tanh)
+
+# second fully-connected
+fc2 = mx.FullyConnected(fc1, num_hidden=10)
+
+# softmax loss
+lenet = mx.SoftmaxOutput(fc2, name=:softmax)
+
+
+#--------------------------------------------------------------------------------
+
+# load data
+batch_size = 100
+include("mnist-data.jl")
+train_provider, eval_provider = get_mnist_providers(batch_size; flat=false)
+
+#--------------------------------------------------------------------------------
+# fit model
+model = mx.FeedForward(lenet, context=mx.cpu())
+
+# optimizer
+optimizer = mx.ADAM(η=0.01, λ=0.00001)
+
+# fit parameters
+initializer=mx.XavierInitializer(distribution = mx.xv_uniform, regularization = mx.xv_avg, magnitude = 1)
+mx.fit(model, optimizer, train_provider, n_epoch=20, eval_data=eval_provider,initializer=initializer)
diff --git a/julia/examples/mnist/lenet.jl b/julia/examples/mnist/lenet.jl
new file mode 100644
index 000000000000..5ee15d69dd1b
--- /dev/null
+++ b/julia/examples/mnist/lenet.jl
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using MXNet
+
+#--------------------------------------------------------------------------------
+# define lenet
+
+# input
+data = mx.Variable(:data)
+
+# first conv
+conv1 = @mx.chain mx.Convolution(data, kernel=(5,5), num_filter=20) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.Pooling(pool_type=:max, kernel=(2,2), stride=(2,2))
+
+# second conv
+conv2 = @mx.chain mx.Convolution(conv1, kernel=(5,5), num_filter=50) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.Pooling(pool_type=:max, kernel=(2,2), stride=(2,2))
+
+# first fully-connected
+fc1 = @mx.chain mx.Flatten(conv2) =>
+ mx.FullyConnected(num_hidden=500) =>
+ mx.Activation(act_type=:tanh)
+
+# second fully-connected
+fc2 = mx.FullyConnected(fc1, num_hidden=10)
+
+# softmax loss
+lenet = mx.SoftmaxOutput(fc2, name=:softmax)
+
+
+#--------------------------------------------------------------------------------
+# load data
+batch_size = 100
+include("mnist-data.jl")
+train_provider, eval_provider = get_mnist_providers(batch_size; flat=false)
+
+#--------------------------------------------------------------------------------
+# fit model
+model = mx.FeedForward(lenet, context=mx.gpu())
+
+# optimizer
+optimizer = mx.SGD(η=0.05, μ=0.9, λ=0.00001)
+
+# fit parameters
+mx.fit(model, optimizer, train_provider, n_epoch=20, eval_data=eval_provider)
diff --git a/julia/examples/mnist/mlp-test.jl b/julia/examples/mnist/mlp-test.jl
new file mode 100644
index 000000000000..1af84ed3ba8a
--- /dev/null
+++ b/julia/examples/mnist/mlp-test.jl
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# This file is primarily to be included from runtest.jl. We tried to cover various
+# features of MXNet.jl in this example in order to detect regression errors.
+
+module MNISTTest
+
+using MXNet
+using Base.Test
+
+include("mnist-data.jl")
+
+function get_mnist_mlp()
+ @mx.chain mx.Variable(:data) =>
+ mx.FullyConnected(name=:fc1, num_hidden=128) =>
+ mx.Activation(name=:relu1, act_type=:relu) =>
+ mx.FullyConnected(name=:fc2, num_hidden=64) =>
+ mx.Activation(name=:relu2, act_type=:relu) =>
+ mx.FullyConnected(name=:fc3, num_hidden=10) =>
+ mx.SoftmaxOutput(name=:softmax)
+end
+
+get_mnist_data(batch_size = 100) = get_mnist_providers(batch_size)
+
+function mnist_fit_and_predict(optimizer, initializer, n_epoch)
+ mlp = get_mnist_mlp()
+ train_provider, eval_provider = get_mnist_data()
+
+ # setup model
+ model = mx.FeedForward(mlp, context = mx.cpu())
+
+ # fit parameters
+ cp_prefix = "mnist-test-cp"
+ mx.fit(model, optimizer, train_provider, eval_data=eval_provider, n_epoch=n_epoch,
+ initializer=initializer, callbacks=[mx.speedometer(), mx.do_checkpoint(cp_prefix, save_epoch_0=true)])
+
+ # make sure the checkpoints are saved
+ @test isfile("$cp_prefix-symbol.json")
+ for i_epoch = 0:n_epoch
+ @test isfile(mx.format("{1}-{2:04d}.params", cp_prefix, i_epoch))
+ end
+ mlp_load = mx.load("$cp_prefix-symbol.json", mx.SymbolicNode)
+ @test mx.to_json(mlp_load) == mx.to_json(mlp)
+ mlp_load = mx.from_json(readstring("$cp_prefix-symbol.json"), mx.SymbolicNode)
+ @test mx.to_json(mlp_load) == mx.to_json(mlp)
+
+ #--------------------------------------------------------------------------------
+ # the predict API
+ probs = mx.predict(model, eval_provider)
+
+ # collect all labels from eval data
+ labels = Array[]
+ for batch in eval_provider
+ push!(labels, copy(mx.get(eval_provider, batch, :softmax_label)))
+ end
+ labels = cat(1, labels...)
+
+ # Now we use compute the accuracy
+ correct = 0
+ for i = 1:length(labels)
+ # labels are 0...9
+ if indmax(probs[:,i]) == labels[i]+1
+ correct += 1
+ end
+ end
+ accuracy = 100correct/length(labels)
+ println(mx.format("Accuracy on eval set: {1:.2f}%", accuracy))
+
+ # try to call visualization
+ dot_code = mx.to_graphviz(mlp)
+
+ return accuracy
+end
+
+function test_mnist_mlp()
+ info("MNIST::SGD")
+ @test mnist_fit_and_predict(mx.SGD(η=.2), mx.UniformInitializer(.01), 2) > 90
+
+ info("MNIST::SGD::η scheduler")
+ @test mnist_fit_and_predict(mx.SGD(η_sched=mx.LearningRate.Inv(.25)),
+ mx.UniformInitializer(.01), 2) > 90
+
+ info("MNIST::SGD::momentum μ")
+ @test mnist_fit_and_predict(mx.SGD(η=.1, μ=.9), mx.UniformInitializer(.01), 2) > 90
+
+ info("MNIST::ADAM")
+ @test mnist_fit_and_predict(mx.ADAM(), mx.NormalInitializer(), 2) > 90
+
+ info("MNIST::AdaGrad")
+ @test mnist_fit_and_predict(mx.AdaGrad(), mx.NormalInitializer(), 2) > 90
+
+ info("MNIST::AdaDelta")
+ @test mnist_fit_and_predict(mx.AdaDelta(), mx.NormalInitializer(), 2) > 90
+
+ info("MNIST::AdaMax")
+ @test mnist_fit_and_predict(mx.AdaMax(), mx.NormalInitializer(), 2) > 90
+
+ info("MNIST::RMSProp")
+ @test mnist_fit_and_predict(mx.RMSProp(), mx.NormalInitializer(), 2) > 90
+
+ info("MNIST::Nadam")
+ @test mnist_fit_and_predict(mx.Nadam(), mx.NormalInitializer(), 2) > 90
+end
+
+test_mnist_mlp()
+
+end # module MNISTTest
diff --git a/julia/examples/mnist/mlp.jl b/julia/examples/mnist/mlp.jl
new file mode 100644
index 000000000000..20facc9b71b3
--- /dev/null
+++ b/julia/examples/mnist/mlp.jl
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using MXNet
+
+#--------------------------------------------------------------------------------
+# define MLP
+# the following two ways are equivalent
+
+#-- Option 1: explicit composition
+# data = mx.Variable(:data)
+# fc1 = mx.FullyConnected(data, name=:fc1, num_hidden=128)
+# act1 = mx.Activation(fc1, name=:relu1, act_type=:relu)
+# fc2 = mx.FullyConnected(act1, name=:fc2, num_hidden=64)
+# act2 = mx.Activation(fc2, name=:relu2, act_type=:relu)
+# fc3 = mx.FullyConnected(act2, name=:fc3, num_hidden=10)
+# mlp = mx.SoftmaxOutput(fc3, name=:softmax)
+
+#-- Option 2: using the mx.chain macro
+# mlp = @mx.chain mx.Variable(:data) =>
+# mx.FullyConnected(name=:fc1, num_hidden=128) =>
+# mx.Activation(name=:relu1, act_type=:relu) =>
+# mx.FullyConnected(name=:fc2, num_hidden=64) =>
+# mx.Activation(name=:relu2, act_type=:relu) =>
+# mx.FullyConnected(name=:fc3, num_hidden=10) =>
+# mx.SoftmaxOutput(name=:softmax)
+
+#-- Option 3: using nn-factory
+mlp = @mx.chain mx.Variable(:data) =>
+ mx.MLP([128, 64, 10]) =>
+ mx.SoftmaxOutput(name=:softmax)
+
+# data provider
+batch_size = 100
+include("mnist-data.jl")
+train_provider, eval_provider = get_mnist_providers(batch_size)
+
+# setup model
+model = mx.FeedForward(mlp, context=mx.cpu())
+
+# optimizer
+optimizer = mx.SGD(η=0.1, μ=0.9, λ=0.00001)
+
+# fit parameters
+mx.fit(model, optimizer, train_provider, eval_data=eval_provider, n_epoch=20)
+
+#--------------------------------------------------------------------------------
+# Optional, demonstration of the predict API
+probs = mx.predict(model, eval_provider)
+
+# collect all labels from eval data
+labels = reduce(
+ vcat,
+ copy(mx.get(eval_provider, batch, :softmax_label)) for batch ∈ eval_provider)
+# labels are 0...9
+labels .= labels .+ 1
+
+# Now we use compute the accuracy
+pred = map(i -> indmax(probs[1:10, i]), 1:size(probs, 2))
+correct = sum(pred .== labels)
+@printf "Accuracy on eval set: %.2f%%\n" 100correct/length(labels)
diff --git a/julia/examples/mnist/mnist-data.jl b/julia/examples/mnist/mnist-data.jl
new file mode 100644
index 000000000000..12160cf6f18e
--- /dev/null
+++ b/julia/examples/mnist/mnist-data.jl
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+function get_mnist_providers(batch_size::Int; data_name=:data, label_name=:softmax_label, flat=true)
+ # download MNIST into Pkg.dir("MXNet")/data/mnist if not exist
+ filenames = mx.get_mnist_ubyte()
+
+ # data provider
+ train_provider = mx.MNISTProvider(image=filenames[:train_data],
+ label=filenames[:train_label],
+ data_name=data_name, label_name=label_name,
+ batch_size=batch_size, shuffle=true, flat=flat, silent=true)
+ eval_provider = mx.MNISTProvider(image=filenames[:test_data],
+ label=filenames[:test_label],
+ data_name=data_name, label_name=label_name,
+ batch_size=batch_size, shuffle=false, flat=flat, silent=true)
+
+ return (train_provider, eval_provider)
+end
diff --git a/julia/examples/nondefault-example.jl b/julia/examples/nondefault-example.jl
new file mode 100644
index 000000000000..75eff085a459
--- /dev/null
+++ b/julia/examples/nondefault-example.jl
@@ -0,0 +1,147 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+#=
+ Contents: This file contains code for:
+ - Setting the initial values of the biases and weights equal to the final values of a previous run.
+ This is helpful for re-estimating a model on updated training data, where the original and updated training data largely overlap.
+ - Changing the loss function (in our example from Accuracy to ACE)
+
+ Notes:
+ 1. The model is a toy example with 4 outcomes (categories).
+ The model is a poor fit to the data, but this is unimportant. The point of the example is to demonstrate the use of some non-default settings.
+ 2. For categorical outcomes, use 0-based categories! Some of the loss functions assume this, such as ACE.
+ 3. Incomplete batches are padded with repeated instances of an artificial observation.
+ This is bad because the artificial data is over-represented and thus biases the results.
+ The ideal solution is to distribute the observations from the incomplete batch among the complete batches.
+ This would result in batches of variable but similar size, and thus the estimate of the gradient would not be significantly affected.
+ But this doesn't happen.
+ For simplicity we instead drop these extra observations, so that the number of observations in the data set is a multiple of the batch_size.
+=#
+
+
+using RDatasets
+using MXNet
+
+
+################################################################################
+### Data: Exam scores discretised into 4 categories (use zero-based categories!).
+df = dataset("mlmRev", "Gcsemv"); # 1905 x 5
+complete_cases!(df) # 1523 x 5
+n = nrow(df)
+df[:written] = zeros(Int, n)
+df[:course] = zeros(Int, n)
+for i = 1:n
+ # Categorise :Written
+ if df[i, :Written] <= 20.0
+ df[i, :written] = 0
+ elseif df[i, :Written] <= 40.0
+ df[i, :written] = 1
+ elseif df[i, :Written] <= 60.0
+ df[i, :written] = 2
+ else
+ df[i, :written] = 3
+ end
+
+ # Categorise :Course
+ if df[i, :Course] <= 25.0
+ df[i, :course] = 0
+ elseif df[i, :Course] <= 50.0
+ df[i, :course] = 1
+ elseif df[i, :Course] <= 75.0
+ df[i, :course] = 2
+ else
+ df[i, :course] = 3
+ end
+end
+df = df[1:1500, :] # Ensure nrows is a multiple of batch_size (100 in our example, see below)
+
+x = convert(Vector{Float64}, df[:course])
+y = convert(Vector{Float64}, df[:written])
+
+
+################################################################################
+### Hyperparameters
+
+# Architecture
+mlp = @mx.chain mx.Variable(:data) =>
+ mx.FullyConnected(name = :h1, num_hidden = 10) =>
+ mx.Activation(name = :h1_out, act_type = :sigmoid) =>
+ mx.FullyConnected(name = :out, num_hidden = 4) =>
+ mx.SoftmaxOutput(name = :softmax)
+
+# Hyperparameters
+n_epoch = 100
+batch_size = 100
+learn_rate = 0.1
+mom = 0.9
+wt_decay = 0.00001
+
+
+# Connect data, network architecture and hyperparameters
+train_prov = mx.ArrayDataProvider(x, y; batch_size = batch_size)
+eval_prov = mx.ArrayDataProvider(x, y; batch_size = batch_size)
+opt = mx.SGD(lr = learn_rate, momentum = mom, weight_decay = wt_decay) # Optimizing algorithm
+
+################################################################################
+### Run 1: Basic run, storing initial and final state.
+
+# Learn
+mdl1 = mx.FeedForward(mlp, context = mx.cpu()) # Model targets the local CPU
+cb = mx.do_checkpoint("first", frequency = n_epoch, save_epoch_0 = true) # Write initial and final states to disk
+mx.fit(mdl1, opt, train_prov, n_epoch = n_epoch, eval_data = eval_prov, callbacks = [cb]) # Random initial biases and weights
+
+
+################################################################################
+### Run 2: Load the previously trained model and run it some more, starting where Run 1 finished.
+
+# Load final state of 1st run from disk
+arch, arg_params, aux_params = mx.load_checkpoint("first", 100) # arch is the network structure, arg_params contains the weights and biases
+mdl2 = mx.FeedForward(arch, context = mx.cpu()) # Only populates the arch and ctx fields
+mdl2.arg_params = arg_params # Populate the arg_params fields
+cb = mx.do_checkpoint("second", frequency = n_epoch, save_epoch_0 = true)
+mx.fit(mdl2, opt, train_prov, n_epoch = n_epoch, eval_data = eval_prov, callbacks = [cb])
+
+# Test whether the final state of 1st run equals the initial state of 2nd run
+run(`diff first-0100.params second-0000.params`) # Throws error if not true, does nothing otherwise
+
+
+#=
+ # Other useful functions
+ arch = mx.load("first-symbol.json", mx.SymbolicNode)
+ arg_params = mx.load("first-0100.params", mx.NDArray)
+=#
+
+
+################################################################################
+### Run 3: Change the loss function from the default Accuracy to ACE
+
+mdl3 = mx.FeedForward(mlp, context = mx.cpu())
+mx.fit(mdl3, opt, train_prov, n_epoch = n_epoch, eval_data = eval_prov, eval_metric = mx.ACE())
+#mx.fit(mdl3, opt, train_prov, n_epoch = n_epoch, eval_data = eval_prov, eval_metric = mx.Accuracy()) # Default eval_metric
+#mx.fit(mdl3, opt, train_prov, n_epoch = n_epoch, eval_data = eval_prov, eval_metric = mx.MultiACE(4))
+
+# Test manually
+probs = mx.predict(mdl3, eval_prov)
+LL = 0.0
+for i = 1:size(y, 1)
+ LL += log(probs[Int(y[i]) + 1, i])
+end
+-LL / size(y, 1) # Should equal the value of ACE from the final iteration of fit(mdl3, ...)
+
+
+# EOF
diff --git a/julia/examples/regression-example.jl b/julia/examples/regression-example.jl
new file mode 100644
index 000000000000..bbbb415fe664
--- /dev/null
+++ b/julia/examples/regression-example.jl
@@ -0,0 +1,101 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+#=
+This script shows how a simple MLP net may be used
+for regression. It shows how data in memory may be
+used for training and evaluation, and how to obtain
+the predictions from the trained net.
+=#
+using MXNet
+using Distributions
+#using Plots
+
+# data generating process
+generate_inputs(mean, var, size) = rand(MvNormal(mean, var), size)
+output(data) = sin.(data[1:1,:]).*sin.(data[2:2,:])./(data[1:1,:].*data[2:2,:])
+
+# create training and evaluation data sets
+mean=[0.0; 0.0]
+var=[1.0 0.0; 0.0 1.0]
+samplesize = 5000
+TrainInput = generate_inputs(mean, var, samplesize)
+TrainOutput = output(TrainInput)
+ValidationInput = generate_inputs(mean, var, samplesize)
+ValidationOutput = output(ValidationInput)
+
+# how to set up data providers using data in memory
+function data_source(batchsize = 100)
+ train = mx.ArrayDataProvider(
+ :data => TrainInput,
+ :label => TrainOutput,
+ batch_size = batchsize,
+ shuffle = true,
+ )
+ valid = mx.ArrayDataProvider(
+ :data => ValidationInput,
+ :label => ValidationOutput,
+ batch_size = batchsize,
+ shuffle = true,
+ )
+
+ train, valid
+end
+
+# create a two hidden layer MPL: try varying num_hidden, and change tanh to relu,
+# or add/remove a layer
+data = mx.Variable(:data)
+label = mx.Variable(:label)
+net = @mx.chain mx.Variable(:data) =>
+ mx.FullyConnected(num_hidden=10) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.FullyConnected(num_hidden=3) =>
+ mx.Activation(act_type=:tanh) =>
+ mx.FullyConnected(num_hidden=1) =>
+ mx.LinearRegressionOutput(mx.Variable(:label))
+
+# final model definition, don't change, except if using gpu
+model = mx.FeedForward(net, context=mx.cpu())
+
+# set up the optimizer: select one, explore parameters, if desired
+#optimizer = mx.SGD(η=0.01, μ=0.9, λ=0.00001)
+optimizer = mx.ADAM()
+
+# train, reporting loss for training and evaluation sets
+# initial training with small batch size, to get to a good neighborhood
+trainprovider, evalprovider = data_source(#= batchsize =# 200)
+mx.fit(model, optimizer, trainprovider,
+ initializer = mx.NormalInitializer(0.0, 0.1),
+ eval_metric = mx.MSE(),
+ eval_data = evalprovider,
+ n_epoch = 20,
+ callbacks = [mx.speedometer()])
+# more training with the full sample
+trainprovider, evalprovider = data_source(#= batchsize =# samplesize)
+mx.fit(model, optimizer, trainprovider,
+ initializer = mx.NormalInitializer(0.0, 0.1),
+ eval_metric = mx.MSE(),
+ eval_data = evalprovider,
+ n_epoch = 500, # previous setting is batchsize = 200, epoch = 20
+ # implies we did (5000 / 200) * 20 times update in previous `fit`
+ callbacks = [mx.speedometer()])
+
+# obtain predictions
+plotprovider = mx.ArrayDataProvider(:data => ValidationInput, :label => ValidationOutput)
+fit = mx.predict(model, plotprovider)
+println("correlation between fitted values and true regression line: ", cor(vec(fit), vec(ValidationOutput)))
+#scatter(ValidationOutput',fit',w = 3, xlabel="true", ylabel="predicted", title="45º line is what we hope for", show=true)
diff --git a/julia/models/Inception/.gitignore b/julia/models/Inception/.gitignore
new file mode 100644
index 000000000000..3eabb6e80247
--- /dev/null
+++ b/julia/models/Inception/.gitignore
@@ -0,0 +1,2 @@
+Inception
+Inception.zip
diff --git a/julia/models/Inception/get.sh b/julia/models/Inception/get.sh
new file mode 100755
index 000000000000..16452a361d98
--- /dev/null
+++ b/julia/models/Inception/get.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+wget -c http://data.dmlc.ml/mxnet/data/Inception.zip
+unzip Inception.zip
diff --git a/julia/plugins/README.md b/julia/plugins/README.md
new file mode 100644
index 000000000000..38882889f494
--- /dev/null
+++ b/julia/plugins/README.md
@@ -0,0 +1,14 @@
+# Plugins of MXNet.jl
+
+This directory contains *plugins* of MXNet.jl. A plugin is typically a component that could be part of MXNet.jl, but excluded from the `mx` namespace. The plugins are included here primarily for two reasons:
+
+* To minimize the dependency of MXNet.jl on other optional packages.
+* To serve as examples on how to extend some components of MXNet.jl.
+
+The most straightforward way to use a plugin is to `include` the code. For example
+
+```julia
+include(joinpath(Pkg.dir("MXNet"), "plugins", "io", "svmlight.jl"))
+
+provider = SVMLightProvider("/path/to/dataset", 100)
+```
diff --git a/julia/plugins/io/svmlight.jl b/julia/plugins/io/svmlight.jl
new file mode 100644
index 000000000000..f9d9b2ec83db
--- /dev/null
+++ b/julia/plugins/io/svmlight.jl
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+#=doc
+SVMLight / LibSVM is a popular data format for sparse features. Some preprocessed
+datasets in this format could be found at http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/
+=#
+using MXNet
+using SVMLightLoader
+
+mutable struct SVMLightProvider <: mx.AbstractDataProvider
+ filename :: AbstractString
+ batch_size :: Int
+ fea_dim :: Int
+ data_name :: Symbol
+ label_name :: Symbol
+end
+
+function SVMLightProvider(filename::AbstractString, batch_size::Int; fea_dim::Int=-1,
+ data_name::Symbol=:data, label_name::Symbol=:label)
+ if fea_dim == -1
+ info("SVMLightProvider: going over file to get feature dimension of $filename")
+ f = SVMLightFile(filename)
+ for (data, label) in f
+ fea_dim = max(fea_dim, length(data))
+ end
+ end
+
+ return SVMLightProvider(filename, batch_size, fea_dim, data_name, label_name)
+end
+
+mx.get_batch_size(provider :: SVMLightProvider) = provider.batch_size
+function mx.provide_data(provider :: SVMLightProvider)
+ [(provider.data_name, (provider.fea_dim, provider.batch_size))]
+end
+function mx.provide_label(provider :: SVMLightProvider)
+ [(provider.label_name, (provider.batch_size,))]
+end
+
+function mx.eachbatch(provider :: SVMLightProvider)
+ data_jl = zeros(mx.MX_float, (provider.fea_dim, provider.batch_size))
+ data_nd = mx.empty(size(data_jl))
+ label_jl = zeros(mx.MX_float, (provider.batch_size,))
+ label_nd = mx.empty(size(label_jl))
+
+ batch = mx.DataBatch([data_nd], [label_nd], provider.batch_size)
+ function _svmlight_iter()
+ f = SVMLightFile(provider.filename)
+ while true
+ error("This is actually buggy and needs fixing")
+ raw = collect(take(f, provider.batch_size))
+ cnt = length(raw)
+ if cnt == 0
+ # end of file, no more data to see
+ return
+ end
+
+ data_jl[:] = 0
+ for i = 1:provider.batch_size
+ vec, gnd = raw[min(i,cnt)]
+ data_jl[1:length(vec),i] = vec
+ label_jl[i] = gnd
+ end
+ mx.copy!(data_nd, data_jl)
+ mx.copy!(label_nd, label_jl)
+ batch.count = cnt
+ produce(batch)
+ end
+ end
+
+ return Task(_svmlight_iter)
+end
diff --git a/julia/src/MXNet.jl b/julia/src/MXNet.jl
new file mode 100644
index 000000000000..03c3cb89b530
--- /dev/null
+++ b/julia/src/MXNet.jl
@@ -0,0 +1,173 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+__precompile__()
+
+module MXNet
+
+using Reexport
+
+# we put everything in the namespace mx, because there are a lot of
+# functions with the same names as built-in utilities like "zeros", etc.
+export mx
+module mx
+
+import Base.Iterators: filter
+
+using Formatting
+using MacroTools
+using TakingBroadcastSeriously: @unfuse
+import TakingBroadcastSeriously: broadcast_
+
+# Functions from base that we can safely extend and that are defined by libmxnet.
+import Base: round, ceil, floor, cos, sin, abs, sign, exp, sqrt, exp, log, norm,
+ transpose
+
+###############################################################################
+# exports
+###############################################################################
+
+# symbolic-node.jl
+export SymbolicNode,
+ Variable,
+ @var
+
+# ndarray.jl
+export NDArray,
+ clip,
+ clip!,
+ context,
+ empty,
+ expand_dims,
+ @inplace,
+ # activation funcs
+ σ,
+ sigmoid,
+ relu,
+ softmax,
+ log_softmax,
+ # broadcast utils
+ broadcast_to,
+ broadcast_axis,
+ broadcast_axes
+
+# executor.jl
+export Executor,
+ bind,
+ simple_bind,
+ forward,
+ backward
+
+# context.jl
+export Context,
+ cpu,
+ gpu
+
+# model.jl
+export AbstractModel,
+ FeedForward,
+ predict
+
+# nn-factory.jl
+export MLP
+
+# metric.jl
+export AbstractEvalMetric,
+ ACE,
+ Accuracy,
+ MSE,
+ MultiACE,
+ MultiMetric,
+ NMSE,
+ SeqMetric
+
+# kvstore.jl
+export KVStore,
+ init!,
+ pull!,
+ barrier,
+ setoptimizer!,
+ setupdater!
+
+# initializer.jl
+export AbstractInitializer,
+ UniformInitializer,
+ NormalInitializer,
+ XavierInitializer
+
+# optimizer.jl
+export AbstractOptimizer,
+ AdaDelta,
+ AdaGrad,
+ ADAM,
+ AdaMax,
+ Nadam,
+ RMSProp,
+ SGD,
+ getupdater,
+ normgrad!,
+ update!
+
+# io.jl
+export AbstractDataProvider,
+ AbstractDataBatch,
+ DataBatch,
+ ArrayDataProvider,
+ ArrayDataBatch
+
+# visualize.jl
+export to_graphviz
+
+###############################################################################
+# includes
+###############################################################################
+
+include("base.jl")
+
+include("context.jl")
+include("util.jl")
+include("broadcast.jl")
+
+include("ndarray.jl")
+include("random.jl")
+include("autograd.jl")
+
+include("name.jl")
+include("symbolic-node.jl")
+include("executor.jl")
+
+include("metric.jl")
+include("optimizer.jl")
+include("initializer.jl")
+
+include("io.jl")
+include("kvstore.jl")
+
+include("callback.jl")
+include("model.jl")
+
+include("visualize.jl")
+
+include("nn-factory.jl")
+
+include("deprecated.jl")
+
+end # mx
+
+@reexport using .mx
+
+end # module MXNet
diff --git a/julia/src/autograd.jl b/julia/src/autograd.jl
new file mode 100644
index 000000000000..72fb82ba1bbb
--- /dev/null
+++ b/julia/src/autograd.jl
@@ -0,0 +1,404 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Autograd for NDArray
+# this is a port of Python's autograd module
+# https://github.com/apache/incubator-mxnet/blob/master/python/mxnet/autograd.py
+
+###############################################################################
+# Private util functions
+###############################################################################
+
+"""
+ _set_recording(state::Bool)::Bool
+
+Set status to recording/not recording. When recording, graph will be constructed
+for gradient computation.
+
+## Parameters
+
+* `state::Bool`
+
+## Returns
+
+Previous state before this set
+"""
+function _set_recording(state::Bool)::Bool
+ prev = Ref{Cint}(C_NULL)
+ @mxcall(:MXAutogradSetIsRecording, (Cint, Ref{Cint}), state, prev)
+ prev[]
+end
+
+_set_recording(::Void) = nothing
+
+"""
+Set status to training/predicting.
+For example, Dropout will drop inputs randomly when
+`train_mode = true` while simply passing through if `train_mode = false`.
+
+## Parameters
+* `train_mode::Bool`
+
+## Returns
+
+Previous state before this set.
+"""
+function _set_training(train_mode::Bool)::Bool
+ prev = Ref{Cint}(C_NULL)
+ @mxcall(:MXAutogradSetIsTraining, (Cint, Ref{Cint}), train_mode, prev)
+ prev[]
+end
+
+_set_training(::Void) = nothing
+
+###############################################################################
+# Public API
+###############################################################################
+
+"""
+ is_recording()::Bool
+
+Get status on recording/not recording.
+"""
+function is_recording()::Bool
+ state = Ref{Cint}(C_NULL)
+ @mxcall(:MXAutogradIsRecording, (Ref{Cint},), state)
+ state[]
+end
+
+"""
+ is_training()::Bool
+
+Get status on recording/not recording.
+"""
+function is_training()::Bool
+ state = Ref{Cint}(C_NULL)
+ @mxcall(:MXAutogradIsTraining, (Ref{Cint},), state)
+ state[]
+end
+
+@inline function _record(f, is_record::Union{Void,Bool}, train_mode::Union{Void,Bool})
+ # Port from Python's `_RecordingStateScope` context manager
+ # __enter__
+ prev_is_record = _set_recording(is_record)
+ prev_train_mode = _set_training(train_mode)
+
+ try
+ f()
+ finally
+ # __exit__
+ if is_record != nothing && prev_is_record != is_record
+ _set_recording(prev_is_record)
+ end
+ if train_mode != nothing && prev_train_mode != train_mode
+ _set_recording(prev_train_mode)
+ end
+ end
+end
+
+"""
+ record(f, train_mode = true)
+ record(translates = true) do
+ ...
+ end
+
+Returns an autograd recording scope context to be used in `do` block
+and captures code that needs gradients to be calculated.
+
+Parameter `train_mode::Bool` controls whether the forward pass is in training
+or predicting mode.
+This controls the behavior of some layers such as `Dropout`, `BatchNorm`.
+
+!!! note
+ When forwarding with `train_mode = false`, the corresponding backward
+ should also use `train_mode = false`, otherwise gradient is undefined.
+
+```julia
+x = mx.NDArray([1 2; 3 4])
+∇ = mx.attach_grad!(x)
+y = mx.record() do
+ 2x
+end
+mx.backward!(y)
+
+julia> ∇
+2×2 mx.NDArray{Int64,2} @ CPU0:
+ 2 2
+ 2 2
+```
+"""
+record(f, train_mode::Bool = true) = _record(f, true, train_mode)
+
+"""
+ pause(f, train_mode = false)
+ pause(train_mode = false) do
+ ...
+ end
+
+Create a scope context for codes that do not need gradients to be calculated.
+
+```julia
+record() do
+ ...
+ pause() do
+ # testing, IO, gradient updates...
+ end
+end
+```
+"""
+pause(f, train_mode::Bool = false) = _record(f, false, train_mode)
+
+"""
+ train_mode(f)
+ train_mode() do
+ ...
+ end
+
+Create a scope context in which forward pass behavior is set to training mode,
+without changing the recording states.
+
+```julia
+y = model(x)
+train_mode() do
+ z = mx.Dropout(y)
+ ...
+end
+```
+"""
+train_mode(f) = _record(f, nothing, true)
+
+"""
+ predict_mode(f)
+ predict_mode() do
+ ...
+ end
+
+Create a scope context in which forward pass behavior is set to inference mode,
+without changing the recording states.
+
+```julia
+record() do
+ y = model(x)
+ predict_mode() do
+ y = sampling(y)
+ end
+end
+```
+"""
+predict_mode(f) = _record(f, nothing, false)
+
+"""
+ backward!(head, head_grad; retain_graph = false, train_mode = true)
+ backward!(heads, head_grads; retain_graph = false, train_mode = true)
+
+Compute the gradients of heads w.r.t previously marked variables.
+
+## Parameters
+
+- `head::NDArray`: output NDArray
+
+- `head_grad::NDArray` or `Void`: gradient coefficient with respect to head.
+
+- `heads::Vector{NDArray}`: a list of output NDArray
+
+- `head_grads::Vector`: a list of gradient coefficient with respect ot heads.
+ the element should be `NDArray` or `Void`
+
+- `retain_graph::Bool`: whether to keep the graph after backward. e.g:
+ If you want to differentiate the same graph twice,
+ you need to pass `retain_graph=true`.
+
+- `train_mode::Bool`: whether to do backward for training or predicting.
+"""
+backward!(head::NDArray, head_grad::NDArray; kws...) =
+ backward!([head], [head_grad]; kws...)
+
+backward!(head::NDArray, head_grad::Void = nothing; kws...) =
+ backward!([head], head_grad; kws...)
+
+function backward!(heads::VecOfNDArray, head_grad::Void;
+ retain_graph::Bool = false, train_mode::Bool = true)
+ @mxcall(
+ :MXAutogradBackwardEx,
+ (MX_uint,
+ Ptr{MX_handle},
+ Ptr{MX_handle},
+ MX_uint,
+ Ptr{MX_handle},
+ Cint,
+ Cint,
+ Cint,
+ Ptr{MX_handle},
+ Ptr{MX_handle}),
+ length(heads),
+ map(x -> x.handle, heads),
+ C_NULL,
+ 0,
+ C_NULL,
+ retain_graph,
+ false, # create_graph
+ train_mode,
+ C_NULL,
+ C_NULL)
+end
+
+function backward!(heads::VecOfNDArray, head_grads::Vector;
+ retain_graph::Bool = false, train_mode::Bool = true)
+ output_handles = map(x -> x.handle, heads)
+ ograd_handles = map(head_grads) do x
+ if x isa NDArray
+ x.handle
+ elseif x isa Void
+ MX_handle(C_NULL)
+ else
+ throw(ArgumentError("element of head_grads should be NDArray or Void"))
+ end
+ end
+ @assert length(output_handles) == length(ograd_handles)
+ @mxcall(
+ :MXAutogradBackwardEx,
+ (MX_uint,
+ Ptr{MX_handle},
+ Ptr{MX_handle},
+ MX_uint,
+ Ptr{MX_handle},
+ Cint,
+ Cint,
+ Cint,
+ Ptr{MX_handle},
+ Ptr{MX_handle}),
+ length(output_handles),
+ output_handles,
+ ograd_handles,
+ 0,
+ C_NULL,
+ retain_graph,
+ false, # create_graph
+ train_mode,
+ C_NULL,
+ C_NULL)
+end
+
+"""
+ getgrad(arr::NDArray)
+
+Returns the gradient buffer attached to this `NDArray`.
+If the gradient buffer isn't attached yet, return `nothing`.
+"""
+function getgrad(arr::NDArray)
+ out = Ref{MX_handle}(C_NULL)
+ @mxcall(:MXNDArrayGetGrad, (MX_handle, Ref{MX_handle}), arr.handle, out)
+ (out[] == C_NULL) ? nothing : NDArray(MX_NDArrayHandle(out[]))
+end
+
+"""
+ attach_grad!(x::NDArray, grad_req::Symbol = :write)
+
+Attach a gradient buffer to this `NDArray`,
+so that [`backward!`](@ref) can compute gradient with respect to it.
+
+## Parameters
+
+- `x::NDArray`
+- `grad_req::Symbol` (default is `:write`)
+
+## Return
+
+The attached gradient buffer
+
+## See also
+
+- [`getgrad`](@ref)
+"""
+function attach_grad!(x::NDArray, grad_req::Symbol = :write)
+ # TODO: support storage type (stype in Python)
+ # TODO: make sure it works with gpu array
+ grad = zeros_like(x)
+ _mark_variables!([x], [grad], grad_req)
+ grad
+end
+
+"""
+ mark_variables!(var, grad, grad_req)
+ mark_variables!(vars, grads, grad_reqs)
+
+Mark `NDArrays` as variables to compute gradient for autograd.
+
+## Parameters
+
+- `var::NDArray`
+- `grad::NDArray`
+- `grad_req::Symbol`: `:nop`, `:write`, `:inplace` or `:add`
+- `vars::Vector{NDArray}`
+- `grads::Vector{NDArray}`
+- `grad_req::Vector{Symbol}`
+"""
+mark_variables!(var::NDArray, grad::NDArray, grad_reqs::Symbol = :write) =
+ _mark_variables!([var], [grad], grad_reqs)
+
+mark_variables!(var::VecOfNDArray, grads::VecOfNDArray, grad_reqs = :write) =
+ _mark_variables!(var, grads, grad_reqs)
+
+@inline function _getgrad_req(x::Symbol)::GRAD_REQ
+ val = get(grad_req_map, x, false)
+ if val == false
+ throw(ArgumentError("invalid grad_reqs $x"))
+ end
+ val
+end
+
+@inline _getgrad_reqs(x::Symbol, n::Int) =
+ map((_) -> MX_uint(_getgrad_req(x)), Base.OneTo(n))
+
+@inline function _getgrad_reqs(xs::Vector{Symbol}, n::Int)
+ if length(xs) != n
+ throw(ArgumentError("number of variables and grad_reqs not matched"))
+ end
+ map(MX_uint ∘ _getgrad_req, xs)
+end
+
+@inline function _mark_variables!(vars::VecOfNDArray, grads::VecOfNDArray,
+ grad_reqs = :write)
+ n = length(vars)
+ if n != length(grads)
+ throw(ArgumentError("number of variables and gradients not matched"))
+ end
+
+ var_hdls = map(x -> x.handle, vars)
+ grad_hdls = map(x -> x.handle, grads)
+ grad_reqs = _getgrad_reqs(grad_reqs, n)
+
+ @mxcall(:MXAutogradMarkVariables,
+ (MX_uint, Ref{MX_handle}, Ptr{MX_uint}, Ref{MX_handle}),
+ length(vars), var_hdls, grad_reqs, grad_hdls)
+end
+
+"""
+ symbol(x::NDArray)
+
+Retrieve recorded computation history as `SymbolicNode`,
+ where `x` is a `NDArray` representing the head of computation graph.
+ """
+function symbol(x::NDArray)
+ ref = Ref{MX_handle}(C_NULL)
+ @mxcall(:MXAutogradGetSymbol, (MX_handle, Ref{MX_handle}), x, ref)
+ SymbolicNode(MX_SymbolHandle(ref[]))
+end
+
+###############################################################################
+# TODO: User-defined differentiable function
+###############################################################################
diff --git a/julia/src/base.jl b/julia/src/base.jl
new file mode 100644
index 000000000000..ce1c183eafb5
--- /dev/null
+++ b/julia/src/base.jl
@@ -0,0 +1,317 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"Exception thrown when an error occurred calling MXNet API."
+struct MXError <: Exception
+ msg :: AbstractString
+end
+
+Base.show(io::IO, e::MXError) = print(io, e.msg)
+
+################################################################################
+# Common types used in MXNet API
+################################################################################
+const MX_uint = Cuint
+const MX_float = Cfloat
+const MX_handle = Ptr{Void}
+
+const char_p = Ptr{UInt8}
+const char_pp = Ptr{char_p}
+
+################################################################################
+# Enumeration from MXNet headers
+################################################################################
+# OpReqType in include/mxnet/op_attr_types.h
+@enum GRAD_REQ GRAD_NOP=0 GRAD_WRITE=1 GRAD_INPLACE=2 GRAD_ADD=3
+const grad_req_map = Dict{Symbol,GRAD_REQ}(
+ :nop => GRAD_NOP, # no operation, do not write anything
+ :write => GRAD_WRITE, # write gradient to provided space
+ :inplace => GRAD_INPLACE, # perform an inplace write
+ :add => GRAD_ADD, # add to the provided space
+)
+
+################################################################################
+# Initialization and library API entrance
+################################################################################
+const MXNET_LIB = Libdl.find_library(["libmxnet.$(Libdl.dlext)", "libmxnet.so"], # see build.jl
+ [joinpath(get(ENV, "MXNET_HOME", ""), "lib"),
+ get(ENV, "MXNET_HOME", ""),
+ Pkg.dir("MXNet", "deps", "usr", "lib")])
+if isempty(MXNET_LIB)
+ # touch this file, so that after the user properly build libmxnet, the precompiled
+ # MXNet.ji will be re-compiled to get MXNET_LIB properly.
+ touch(@__FILE__)
+ error("Cannot find or load libmxnet.$(Libdl.dlext). " *
+ "Please see the document on how to build it.")
+else
+ include_dependency(MXNET_LIB)
+end
+
+function __init__()
+ # TODO: bug in nnvm, if do not call this, call get handle "_copyto" will fail
+ _get_libmx_op_names()
+ _populate_iter_creator_cache!()
+
+ global const LIB_VERSION = _get_lib_version()
+
+ atexit() do
+ # notify libmxnet we are shutting down
+ ccall( ("MXNotifyShutdown", MXNET_LIB), Cint, () )
+ end
+end
+
+function mx_get_last_error()
+ msg = ccall( ("MXGetLastError", MXNET_LIB), char_p, () )
+ if msg == C_NULL
+ throw(MXError("Failed to get last error message"))
+ end
+ return unsafe_string(msg)
+end
+
+"Utility macro to call MXNet API functions"
+macro mxcall(fv, argtypes, args...)
+ f = eval(fv)
+ args = map(esc, args)
+ quote
+ _mxret = ccall( ($(Meta.quot(f)), $MXNET_LIB),
+ Cint, $argtypes, $(args...) )
+ if _mxret != 0
+ err_msg = mx_get_last_error()
+ throw(MXError(err_msg))
+ end
+ end
+end
+
+"""
+Get libmxnet version
+"""
+function _get_lib_version()
+ ver = Ref{Cint}(0)
+ @mxcall :MXGetVersion (Ref{Cint},) ver
+ ver[]
+end
+
+################################################################################
+# Handle types
+################################################################################
+macro mx_define_handle_t(name, destructor)
+ name = esc(name)
+ quote
+ mutable struct $name
+ value :: MX_handle
+
+ function $name(value = C_NULL)
+ hdr = new(value)
+
+ $(if destructor != :nop
+ :(finalizer(hdr, delete!))
+ end)
+
+ return hdr
+ end
+ end
+
+ $(if finalizer != :nop
+ quote
+ function delete!(h :: $name)
+ if h.value != C_NULL
+ @mxcall($(Meta.quot(destructor)), (MX_handle,), h.value)
+ h.value = C_NULL
+ end
+ end
+ end
+ end)
+
+ function Base.unsafe_convert(::Type{MX_handle}, obj::$name)
+ obj.value
+ end
+ Base.convert(t::Type{MX_handle}, obj::$name) = Base.unsafe_convert(t, obj)
+ Base.cconvert(t::Type{MX_handle}, obj::$name) = Base.unsafe_convert(t, obj)
+
+ function Base.isnull(obj::$name) obj.value == C_NULL end
+ end
+end
+
+@mx_define_handle_t(MX_NDArrayHandle, MXNDArrayFree)
+@mx_define_handle_t(MX_OpHandle, nop)
+@mx_define_handle_t(MX_SymbolHandle, MXSymbolFree)
+@mx_define_handle_t(MX_ExecutorHandle, MXExecutorFree)
+@mx_define_handle_t(MX_DataIterHandle, MXDataIterFree)
+@mx_define_handle_t(MX_KVStoreHandle, MXKVStoreFree)
+
+################################################################################
+# MXNet Params
+#
+# MXNet API use string to pass some common parameters like the configurations
+# when defining layers. Typically, it is enough to use string(obj) to get a
+# recognizable representation for libmxnet. However, there is currently a
+# caveat:
+#
+# Because Julia use column-major ordering for tensors. In order to properly
+# interact with Julia Arrays, the shape will look "reversed" from the Julia
+# side. For example, a typical MNIST mini-batch tensor is of shape (28,28,1,100)
+# from Julia side, while the shape information for the same piece of memory
+# should be interpreted as (100,1,28,28) from C/C++/Python side.
+#
+# Therefore, when passing parameters to libmxnet, we should reverse the shape
+# parameter. For example, when the user specify a non-square kernel size for
+# a convolution or pooling layer. Unfortunately, those operators are automatically
+# imported, and information about the type of each parameter is somehow limited.
+# One hacky way is to match the type description for the string "Shape(tuple)"
+# when importing operators. But currently we simply decided to reverse **all**
+# NTuple{N, Int} passed to libmxnet.
+#
+# TODO: find a better solution in case this cause issues in the future.
+# I made `@_remap` in `ndarray.jl`. (Iblis Lin)
+################################################################################
+dump_mx_param(val::Any) = string(val)
+dump_mx_param(val::Float64) = @sprintf("%.16e", val)
+dump_mx_param(val::Float32) = @sprintf("%.8e", val)
+dump_mx_param(val::Float16) = @sprintf("%.4e", val)
+dump_mx_param(val::Irrational) = @sprintf("%.16e", val)
+dump_mx_param(shape::NTuple{N, <:Integer}) where N =
+ string(tuple(flipdim([shape...], 1)...))
+
+
+"""
+A convenient macro copied from Mocha.jl that could be used to define structs
+with default values and type checks. For example
+```julia
+@defstruct MyStruct Any (
+ field1 :: Int = 0,
+ (field2 :: AbstractString = "", !isempty(field2))
+)
+```
+where each field could be either
+```julia
+field_name :: field_type = default_value
+```
+or put within a tuple, with the second element
+specifying a validation check on the field value.
+In the example above, the default value for
+field2 does not satisfy the assertion, this
+could be used to force user to provide a
+valid value when no meaningful default value
+is available.
+
+The macro will define a constructor that could accept
+the keyword arguments.
+"""
+macro defstruct(name, fields)
+ _defstruct_impl(false, name, fields)
+end
+
+"""A convenient macro to define immutable structs. The same as
+`@defstruct` except that the defined type is immutable.
+"""
+macro defimmutable(name, fields)
+ _defstruct_impl(true, name, fields)
+end
+
+"""Internal use only, this value is used to indicate a required value
+is not specified.
+"""
+struct __Undefined
+end
+
+function _defstruct_impl(is_immutable, name, fields)
+ if isa(fields, Expr) && fields.head == :tuple
+ fields = fields.args
+ else
+ fields = [fields]
+ end
+ @assert length(fields) > 0
+
+ if isa(name, Symbol)
+ name = esc(name)
+ super_name = :Any
+ else
+ @assert(isa(name, Expr) && name.head == :(<:) && length(name.args) == 2 &&
+ isa(name.args[1], Symbol) && isa(name.args[2], Symbol),
+ "name must be of form 'Name <: SuperType'")
+
+ super_name = esc(name.args[2])
+ name = esc(name.args[1])
+ end
+
+ field_defs = Vector{Expr}(length(fields)) # :(field2 :: Int)
+ field_names = Vector{Expr}(length(fields)) # :field2
+ field_defaults = Vector{Expr}(length(fields)) # :(field2 = 0)
+ field_types = Vector{Expr}(length(fields)) # Int
+ field_asserts = Vector{Expr}(length(fields)) # :(field2 >= 0)
+ required_field = Symbol[]
+
+ for i = 1:length(fields)
+ field = fields[i]
+ if field.head == :tuple
+ field_asserts[i] = esc(field.args[2])
+ field = field.args[1]
+ end
+ if field.head == :(=)
+ fname = field.args[1].args[1]
+ field_defs[i] = esc(field.args[1])
+ field_names[i] = esc(fname)
+ field_types[i] = esc(field.args[1].args[2])
+ field_defaults[i] = Expr(:kw, fname, esc(field.args[2]))
+ else
+ # no default value provided, required field
+ fname = field.args[1]
+ field_defs[i] = esc(field)
+ field_names[i] = esc(fname)
+ field_types[i] = esc(field.args[2])
+ field_defaults[i] = Expr(:kw, fname, __Undefined())
+ push!(required_field, fname)
+ end
+ end
+
+ # body of layer type, defining fields
+ type_body = Expr(:block, field_defs...)
+
+ # constructor
+ requires = map(required_field) do fname
+ :(@assert(!isa($fname, __Undefined), "value for " * string($fname) * " is required"))
+ end
+ converts = map(zip(field_names, field_types)) do param
+ f_name, f_type = param
+ :($f_name = convert($f_type, $f_name))
+ end
+ asserts = map(filter(i -> isassigned(field_asserts,i), 1:length(fields))) do i
+ :(@assert($(field_asserts[i])))
+ end
+ construct = Expr(:call, name, field_names...)
+ ctor_body = Expr(:block, requires..., converts..., asserts..., construct)
+ ctor_def = Expr(:call, name, Expr(:parameters, field_defaults...))
+ ctor = Expr(:(=), ctor_def, ctor_body)
+
+ if is_immutable
+ quote
+ struct $(name) <: $(super_name)
+ $type_body
+ end
+
+ $ctor
+ end
+ else
+ quote
+ mutable struct $(name) <: $(super_name)
+ $type_body
+ end
+
+ $ctor
+ end
+ end
+end
diff --git a/julia/src/broadcast.jl b/julia/src/broadcast.jl
new file mode 100644
index 000000000000..fee960a46271
--- /dev/null
+++ b/julia/src/broadcast.jl
@@ -0,0 +1,36 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using TakingBroadcastSeriously: Broadcasted, unwrap
+
+for f in :[%,
+ tan, asin, acos, atan,
+ sinh, cosh, tanh, asinh, acosh, atanh,
+ min, max,
+ hypot].args
+ # copy from TakingBroadcastSeriously
+ @eval Base.$f(a::Broadcasted...) = Broadcasted(broadcast_($f, unwrap.(a)...))
+ @eval Base.$f(a::Broadcasted, b) = Broadcasted(broadcast_($f, unwrap(a), b))
+ @eval Base.$f(b, a::Broadcasted) = Broadcasted(broadcast_($f, b, unwrap(a)))
+end
+
+for f in :[σ, sigmoid, relu, softmax, log_softmax].args
+ # copy from TakingBroadcastSeriously
+ @eval $f(a::Broadcasted...) = Broadcasted(broadcast_($f, unwrap.(a)...))
+ @eval $f(a::Broadcasted, b) = Broadcasted(broadcast_($f, unwrap(a), b))
+ @eval $f(b, a::Broadcasted) = Broadcasted(broadcast_($f, b, unwrap(a)))
+end
diff --git a/julia/src/callback.jl b/julia/src/callback.jl
new file mode 100644
index 000000000000..06e431de06d0
--- /dev/null
+++ b/julia/src/callback.jl
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ AbstractCallback
+
+Abstract type of callback functions used in training.
+"""
+abstract type AbstractCallback end
+
+"""
+ AbstractBatchCallback
+
+Abstract type of callbacks to be called every mini-batch.
+"""
+abstract type AbstractBatchCallback <: AbstractCallback end
+
+"""
+ AbstractEpochCallback
+
+Abstract type of callbacks to be called every epoch.
+"""
+abstract type AbstractEpochCallback <: AbstractCallback end
+
+mutable struct BatchCallback <: AbstractBatchCallback
+ frequency :: Int
+ call_on_0 :: Bool
+ callback :: Function
+end
+
+"""
+ every_n_batch(callback :: Function, n :: Int; call_on_0 = false)
+
+A convenient function to construct a callback that runs every `n` mini-batches.
+
+# Arguments
+* `call_on_0::Bool`: keyword argument, default false. Unless set, the callback
+ will *not* be run on batch 0.
+
+For example, the [`speedometer`](@ref) callback is defined as
+
+```julia
+every_n_batch(frequency, call_on_0=true) do state :: OptimizationState
+ if state.curr_batch == 0
+ # reset timer
+ else
+ # compute and print speed
+ end
+end
+```
+
+See also [`every_n_epoch`](@ref) and [`speedometer`](@ref).
+"""
+function every_n_batch(callback::Function, n::Int; call_on_0::Bool = false)
+ BatchCallback(n, call_on_0, callback)
+end
+function (cb :: BatchCallback)(state :: OptimizationState)
+ if state.curr_batch == 0
+ if cb.call_on_0
+ cb.callback(state)
+ end
+ elseif state.curr_batch % cb.frequency == 0
+ cb.callback(state)
+ end
+end
+
+"""
+ speedometer(;frequency=50)
+
+Create an `AbstractBatchCallback` that measure the training speed
+ (number of samples processed per second) every k mini-batches.
+
+# Arguments
+* `frequency::Int`: keyword argument, default 50. The frequency (number of
+ min-batches) to measure and report the speed.
+"""
+function speedometer(;frequency::Int = 50)
+ cl_tic = 0
+ every_n_batch(frequency, call_on_0 = true) do state::OptimizationState
+ if state.curr_batch == 0
+ # reset timer
+ cl_tic = time()
+ else
+ speed = frequency * state.batch_size / (time() - cl_tic)
+ info(format("Speed: {1:>6.2f} samples/sec", speed))
+ cl_tic = time()
+ end
+ end
+end
+
+
+mutable struct EpochCallback <: AbstractEpochCallback
+ frequency :: Int
+ call_on_0 :: Bool
+ callback :: Function
+end
+
+"""
+ every_n_epoch(callback :: Function, n :: Int; call_on_0 = false)
+
+A convenient function to construct a callback that runs every `n` full data-passes.
+
+* `call_on_0::Bool`: keyword argument, default false. Unless set, the callback
+ will *not* be run on epoch 0. Epoch 0 means no training has been performed
+ yet. This is useful if you want to inspect the randomly initialized model
+ that has not seen any data yet.
+
+See also [`every_n_batch`](@ref).
+"""
+every_n_epoch(callback::Function, n::Int; call_on_0::Bool = false) =
+ EpochCallback(n, call_on_0, callback)
+
+function (cb::EpochCallback)(model::Any, state::OptimizationState,
+ metric::Vector{Tuple{Symbol, T}}) where T<:Real
+ if state.curr_epoch == 0
+ if cb.call_on_0
+ cb.callback(model, state, metric)
+ end
+ elseif state.curr_epoch % cb.frequency == 0
+ cb.callback(model, state, metric)
+ end
+end
+
+"""
+ do_checkpoint(prefix; frequency=1, save_epoch_0=false)
+
+Create an `AbstractEpochCallback` that save checkpoints of the model to disk.
+The checkpoints can be loaded back later on.
+
+# Arguments
+* `prefix::AbstractString`: the prefix of the filenames to save the model.
+ The model architecture will be saved to prefix-symbol.json,
+ while the weights will be saved to prefix-0012.params,
+ for example, for the 12-th epoch.
+* `frequency::Int`: keyword argument, default is 1.
+ The frequency (measured in epochs) to save checkpoints.
+* `save_epoch_0::Bool`: keyword argument, default false. Whether we should save a
+ checkpoint for epoch 0 (model initialized but not seen any data yet).
+"""
+function do_checkpoint(prefix::AbstractString;
+ frequency::Int = 1, save_epoch_0::Bool = false)
+ mkpath(dirname(prefix))
+ every_n_epoch(frequency, call_on_0=save_epoch_0) do model, state, metric
+ save_checkpoint(model, prefix, state)
+ end
+end
diff --git a/julia/src/context.jl b/julia/src/context.jl
new file mode 100644
index 000000000000..c97522b3b846
--- /dev/null
+++ b/julia/src/context.jl
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+@enum CONTEXT_TYPE CPU=1 GPU=2 CPU_PINNED=3
+
+"""
+ Context(dev_type, dev_id)
+
+A context describes the device type and id on which computation should be carried on.
+"""
+struct Context
+ device_type :: CONTEXT_TYPE
+ device_id :: Int
+end
+Context(dev_type :: Union{CONTEXT_TYPE, Int}, dev_id :: Int = 0) =
+ Context(convert(CONTEXT_TYPE, dev_type), dev_id)
+
+Base.show(io::IO, ctx::Context) =
+ print(io, "$(ctx.device_type)$(ctx.device_id)")
+
+"""
+ cpu(dev_id)
+
+Get a CPU context with a specific id. `cpu()` is usually the default context for many
+operations when no context is specified.
+
+# Arguments
+* `dev_id::Int = 0`: the CPU id.
+"""
+cpu(dev_id::Int = 0) = Context(CPU, dev_id)
+
+"""
+ gpu(dev_id)
+
+Get a GPU context with a specific id. The K GPUs on a node is typically numbered as 0,...,K-1.
+
+# Arguments
+* `dev_id :: Int = 0` the GPU device id.
+"""
+gpu(dev_id::Int = 0) = return Context(GPU, dev_id)
diff --git a/julia/src/deprecated.jl b/julia/src/deprecated.jl
new file mode 100644
index 000000000000..12c5345aa198
--- /dev/null
+++ b/julia/src/deprecated.jl
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# NDArray reshape (#272)
+@deprecate reshape(arr::NDArray; shape=()) reshape(arr, shape)
+@deprecate Reshape(arr::NDArray; shape=()) reshape(arr, shape)
+
+# SymbolicNode reshape (#279)
+@deprecate reshape(sym::SymbolicNode; shape=()) reshape(sym, shape)
+@deprecate Reshape(sym::SymbolicNode; shape=()) reshape(sym, shape)
+
+# srand (#282)
+@deprecate srand!(seed_state::Int) srand(seed_state)
+
+# v0.4
+@deprecate sin(x::NDArray) sin.(x)
+@deprecate cos(x::NDArray) cos.(x)
+@deprecate tan(x::NDArray) tan.(x)
+@deprecate arcsin(x::NDArray) asin.(x)
+@deprecate arccos(x::NDArray) acos.(x)
+@deprecate arctan(x::NDArray) atan.(x)
+
+@deprecate sinh(x::NDArray) sinh.(x)
+@deprecate cosh(x::NDArray) cosh.(x)
+@deprecate tanh(x::NDArray) tanh.(x)
+@deprecate arcsinh(x::NDArray) asinh.(x)
+@deprecate arccosh(x::NDArray) acosh.(x)
+@deprecate arctanh(x::NDArray) atanh.(x)
+
+# @deprecate make `randn` exported accidentially
+# so we make the depwarn manually
+function randn(μ, σ, dims::NTuple{N,Int}, ctx::Context = cpu()) where N
+ warn("mx.randn(μ, σ, dims, ctx = cpu()) is deprecated, use " *
+ "mx.randn(dims...; μ = μ, σ = σ, context = ctx) instead.")
+ mx.randn(dims...; μ = μ, σ = σ, context = ctx)
+end
+
+function randn!(μ, σ, x::NDArray)
+ warn("mx.randn!(μ, σ, x::NDArray) is deprecated, use " *
+ "mx.randn!(x; μ = μ, σ = σ) instead.")
+ randn!(x; μ = μ, σ = σ)
+end
+
+function rand!(low::Real, high::Real, x::NDArray)
+ warn("rand!(low, high, x::NDArray) is deprecated, use " *
+ "rand!(x, low = low, high = high) instead.")
+ rand!(x, low = low, high = high)
+end
+
+function rand(low::Real, high::Real, dims::NTuple{N,Int}, context::Context = cpu()) where N
+ warn("rand!(low, high, dims, x::NDArray, context = cpu()) is deprecated, use " *
+ "rand!(dims..., x; low = low, high = high, context = cpu()) instead.")
+ rand(dims...; low = low, high = high, context = context)
+end
+
+@deprecate sigmoid(x::NDArray) sigmoid.(x)
+@deprecate relu(x::NDArray) relu.(x)
+@deprecate softmax(x::NDArray; axis = ndims(x)) softmax.(x, axis)
+@deprecate log_softmax(x::NDArray; axis = ndims(x)) log_softmax.(x, axis)
+
+@deprecate clip(x; a_min = 0, a_max = 0) clip(x, a_min, a_max)
+
+function broadcast_plus(x::NDArray, y::NDArray)
+ warn("broadcast_plus(x, y) is deprecated, use x .+ y instead.")
+ x .+ y
+end
+
+function broadcast_add(x::NDArray, y::NDArray)
+ warn("broadcast_add(x, y) is deprecated, use x .+ y instead.")
+ x .+ y
+end
+
+function broadcast_sub(x::NDArray, y::NDArray)
+ warn("broadcast_sub(x, y) is deprecated, use x .- y instead.")
+ x .- y
+end
+
+function broadcast_minus(x::NDArray, y::NDArray)
+ warn("broadcast_minus(x, y) is deprecated, use x .- y instead.")
+ x .- y
+end
+
+function broadcast_mul(x::NDArray, y::NDArray)
+ warn("broadcast_mul(x, y) is deprecated, use x .* y instead.")
+ x .* y
+end
+
+function broadcast_div(x::NDArray, y::NDArray)
+ warn("broadcast_div(x, y) is deprecated, use x ./ y instead.")
+ x ./ y
+end
+
+function broadcast_mod(x::NDArray, y::NDArray)
+ warn("broadcast_mod(x, y) is deprecated, use x .% y instead.")
+ x .% y
+end
+
+function broadcast_power(x::NDArray, y::NDArray)
+ warn("broadcast_power(x, y) is deprecated, use x.^y instead.")
+ x.^y
+end
+
+function broadcast_equal(x::NDArray, y::NDArray)
+ warn("broadcast_equal(x, y) is deprecated, use x .== y instead.")
+ x .== y
+end
+
+function broadcast_not_equal(x::NDArray, y::NDArray)
+ warn("broadcast_not_equal(x, y) is deprecated, use x .== y instead.")
+ x .!= y
+end
+
+function broadcast_greater(x::NDArray, y::NDArray)
+ warn("broadcast_greater(x, y) is deprecated, use x .== y instead.")
+ x .> y
+end
+
+function broadcast_greater_equal(x::NDArray, y::NDArray)
+ warn("broadcast_greater_equal(x, y) is deprecated, use x .== y instead.")
+ x .>= y
+end
+
+function broadcast_lesser(x::NDArray, y::NDArray)
+ warn("broadcast_lesser(x, y) is deprecated, use x .== y instead.")
+ x .< y
+end
+
+function broadcast_lesser_equal(x::NDArray, y::NDArray)
+ warn("broadcast_lesser_equal(x, y) is deprecated, use x .== y instead.")
+ x .<= y
+end
+
+function broadcast_maximum(x::NDArray, y::NDArray)
+ warn("broadcast_maximum(x, y) is deprecated, use max.(x, y) instead.")
+ max.(x, y)
+end
+
+function broadcast_minimum(x::NDArray, y::NDArray)
+ warn("broadcast_minimum(x, y) is deprecated, use min.(x, y) instead.")
+ min.(x, y)
+end
+
+function broadcast_hypot(x::NDArray, y::NDArray)
+ warn("broadcast_hypot(x, y) is deprecated, use hypot.(x, y) instead.")
+ hypot.(x, y)
+end
diff --git a/julia/src/executor.jl b/julia/src/executor.jl
new file mode 100644
index 000000000000..4bf4339d65d1
--- /dev/null
+++ b/julia/src/executor.jl
@@ -0,0 +1,254 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import Base: bind
+
+"""
+ Executor
+
+An executor is a realization of a symbolic architecture defined by a `SymbolicNode`.
+The actual forward and backward computation specified by the network architecture can
+be carried out with an executor.
+"""
+mutable struct Executor
+ handle :: MX_ExecutorHandle
+ symbol :: SymbolicNode
+ arg_arrays :: VecOfNDArray
+ grad_arrays :: Vector{Union{Void,<:NDArray}}
+ aux_arrays :: VecOfNDArray
+ outputs :: VecOfNDArray
+ arg_dict :: Dict{Symbol}
+ aux_dict :: Dict{Symbol}
+end
+
+function Executor(hdl::MX_ExecutorHandle, sym::SymbolicNode,
+ arg_arrays::VecOfNDArray, grad_arrays::AbstractVector,
+ aux_arrays::VecOfNDArray)
+ # get output arrays
+ ref_size = Ref{MX_uint}(0)
+ ref_hdls = Ref{Ptr{MX_handle}}(C_NULL)
+ @mxcall(:MXExecutorOutputs, (MX_handle, Ref{MX_uint}, Ref{Ptr{MX_handle}}),
+ hdl, ref_size, ref_hdls)
+ out_hdrs = unsafe_wrap(Array, ref_hdls[], ref_size[])
+ out_arrays = [NDArray(MX_NDArrayHandle(x)) for x in out_hdrs]
+
+ arg_names = list_arguments(sym)
+ @assert(length(arg_names) == length(unique(arg_names)), "Duplicated names in arguments: $arg_names")
+ arg_dict = Dict(zip(arg_names, arg_arrays))
+
+ aux_names = list_auxiliary_states(sym)
+ @assert(length(aux_names) == length(unique(aux_names)), "Duplicated names in auxiliary states: $aux_names")
+ aux_dict = Dict(zip(aux_names, aux_arrays))
+
+ Executor(hdl, sym, arg_arrays, grad_arrays, aux_arrays, out_arrays, arg_dict, aux_dict)
+end
+
+Base.unsafe_convert(::Type{MX_handle}, obj::Executor) =
+ Base.unsafe_convert(MX_handle, obj.handle)
+Base.convert(t::Type{MX_handle}, obj::Executor) = Base.unsafe_convert(t, obj)
+Base.cconvert(t::Type{MX_handle}, obj::Executor) = Base.unsafe_convert(t, obj)
+
+function _get_ndarray_inputs(arg_key::AbstractString, args::VecOfNDArray,
+ arg_names::Vector{Symbol}, allow_missing::Bool)
+ @assert(length(args) == length(arg_names), "Length of $arg_key does not match number of arguments")
+ return (MX_handle[args...], args)
+end
+
+function _get_ndarray_inputs(arg_key::AbstractString, args::Dict{Symbol},
+ arg_names::Vector{Symbol}, allow_missing::Bool)
+ args_vec = map(arg_names) do name
+ arr = get(args, name, nothing)
+ if !allow_missing
+ @assert(!isa(arr, Void), "Must specify all arguments in $arg_key ($name is missing)")
+ end
+ arr
+ end
+ # help the type inference
+ if allow_missing
+ args_vec = Union{NDArray,Void}[args_vec...]
+ else
+ args_vec = NDArray[args_vec...]
+ end
+ args_hdr = MX_handle[(isa(x,Void) ? MX_handle(0) : x) for x in args_vec]
+ return (args_hdr, args_vec)
+end
+
+"""
+ bind(sym, ctx, args; args_grad=Dict(), aux_states=Dict(), grad_req=GRAD_WRITE)
+
+Create an `Executor` by binding a `SymbolicNode` to concrete `NDArray`.
+
+# Arguments
+* `sym::SymbolicNode`: the network architecture describing the computation graph.
+* `ctx::Context`: the context on which the computation should run.
+* `args`: either a list of `NDArray` or a dictionary of name-array pairs. Concrete
+ arrays for all the inputs in the network architecture. The inputs typically include
+ network parameters (weights, bias, filters, etc.), data and labels.
+ See [`list_arguments`](@ref) and [`infer_shape`](@ref).
+* `args_grad`: a `Vector` of `NDArray` or a `Dict` contains `NDArray`
+* `aux_states`: a `Vector` of `NDArray` or a `Dict` contains `NDArray`
+* `grad_req`: single value, a `Vector` of `GRAD_REQ` or a `Dict{Symbol,GRAD_REQ}`
+"""
+function bind(self::SymbolicNode, ctx::Context, args;
+ args_grad = Dict{Symbol,NDArray}(),
+ aux_states = Dict{Symbol,NDArray}(),
+ grad_req = GRAD_WRITE)
+
+ arg_names = list_arguments(self)
+
+ args_hdr, args = _get_ndarray_inputs("args", args, arg_names, false)
+ args_grad_hdr, args_grad = _get_ndarray_inputs("args_grad", args_grad, arg_names, true)
+ aux_args_hdr, aux_states = _get_ndarray_inputs("aux_states", aux_states, list_auxiliary_states(self), false)
+
+ if isa(grad_req, GRAD_REQ)
+ reqs = MX_uint[grad_req for i=1:length(args)]
+ elseif isa(grad_req, Vector{GRAD_REQ})
+ @assert(length(grad_req) == length(args))
+ reqs = MX_uint[grad_req...]
+ elseif isa(grad_req, Dict{Symbol, GRAD_REQ})
+ reqs = MX_uint[get(grad_req, name, GRAD_NOP) for name in arg_names]
+ end
+
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXExecutorBind,
+ (MX_handle, Cint, Cint, MX_uint, Ptr{MX_handle}, Ptr{MX_handle}, Ptr{MX_uint},
+ MX_uint, Ptr{MX_handle}, Ref{MX_handle}),
+ self, ctx.device_type, ctx.device_id, length(args), args_hdr,
+ args_grad_hdr, reqs, length(aux_states), aux_args_hdr, ref_hdr)
+ args_grad = convert(Vector{Union{Void,NDArray}}, args_grad)
+ executor = Executor(MX_ExecutorHandle(ref_hdr[]), self,
+ args, args_grad, aux_states)
+end
+
+function bind(x::SymbolicNode; context::Context = cpu(), kwargs...)
+ kwargs = Dict(kwargs)
+ @assert(haskey(kwargs, :args), "Must specify args")
+ args = pop!(kwargs, :args)
+ bind(x, context, args; kwargs...)
+end
+
+function simple_bind(self::SymbolicNode, ctx::Context;
+ grad_req::Union{GRAD_REQ,Dict{Symbol,GRAD_REQ}} = GRAD_WRITE,
+ kwargs...)
+ arg_shapes, out_shapes, aux_shapes = infer_shape(self; kwargs...)
+ @assert(!isa(arg_shapes, Void), "Information not enough to perform complete shape inference")
+
+ arg_arrays = NDArray[zeros(shape, ctx) for shape in arg_shapes]
+ arg_names = list_arguments(self)
+
+ grad_arrays = Dict{Symbol,NDArray}()
+
+ if grad_req != GRAD_NOP
+ shapes = zip(arg_names, arg_shapes)
+
+ # if not in provided data, should be parameters
+ provided_data_names = [x[1] for x in kwargs]
+ shapes = filter(x -> !in(x[1], provided_data_names), shapes)
+
+ # Remove all gradients for nop params
+ # if isa(grad_req, Dict{Symbol, GRAD_REQ})
+ # shapes = filter(x -> grad_req[x[1]] != GRAD_NOP,shapes)
+ # end
+
+ for (name, shape) in shapes
+ grad_arrays[name] = zeros(shape, ctx)
+ end
+ end
+
+ aux_arrays = [zeros(shape, ctx) for shape in aux_shapes]
+ return bind(self, ctx, arg_arrays, args_grad=grad_arrays, grad_req=grad_req, aux_states=aux_arrays)
+end
+
+
+function forward(self::Executor; is_train::Bool = false, kwargs...)
+ for (k,v) in kwargs
+ @assert(k ∈ self.arg_dict, "Unknown argument $k")
+ @assert(isa(v, NDArray), "Keyword argument $k must be an NDArray")
+ copy!(self.arg_dict[k], v)
+ end
+
+ @mxcall(:MXExecutorForward, (MX_handle, Cint), self, is_train)
+
+ self.outputs
+end
+
+backward(x::Executor) = backward(x, NDArray[])
+backward(x::Executor, out_grad::NDArray) = backward(x, [out_grad])
+backward(x::Executor, out_grads::VecOfNDArray) =
+ @mxcall(:MXExecutorBackward, (MX_handle, MX_uint, Ptr{MX_handle}),
+ x, length(out_grads), MX_handle[out_grads...])
+
+function copy_params_from(self::Executor, arg_params::Dict{Symbol},
+ aux_params::Dict{Symbol} = Dict{Symbol,Any}();
+ allow_extra_params::Bool = false)
+ for (name, array) in arg_params
+ if haskey(self.arg_dict, name)
+ copy!(self.arg_dict[name], array)
+ else
+ @assert(allow_extra_params, "Extra params $name not in the arguments")
+ end
+ end
+
+ for (name, array) in aux_params
+ if haskey(self.aux_dict, name)
+ copy!(self.aux_dict[name], array)
+ else
+ @assert(allow_extra_params, "Extra auxiliary state $name not recognized")
+ end
+ end
+end
+
+
+Base.show(io::IO, x::Executor) =
+ print(io, "mx.", split(string(typeof(x)), '.')[end], " ", x.handle.value)
+
+"""
+ print([io::IO], x::Executor)
+
+Get a debug string about internal execution plan.
+
+Can be used to get an estimated about the memory cost.
+
+```julia
+julia> x = mx.Variable(:x)
+MXNet.mx.SymbolicNode x
+
+julia> exec = mx.bind(x + 1, mx.cpu(), Dict(:x => mx.ones(2,3)))
+mx.Executor Ptr{Void} @0x000055c3dee9eb30
+
+julia> print(exec)
+Symbol Outputs:
+ output[0]=_plus_scalar0(0)
+Variable:x
+--------------------
+Op:_plus_scalar, Name=_plus_scalar0
+Inputs:
+ arg[0]=x(0) version=0
+Attrs:
+ scalar=1.00000000e+00
+Total 0 MB allocated
+Total 11 TempSpace resource requested
+```
+"""
+Base.print(io::IO, x::Executor) = print(io, debug_str(x))
+Base.print(x::Executor) = print(STDOUT, x)
+
+function debug_str(x::Executor)
+ s_ref = Ref{Cstring}(C_NULL)
+ @mxcall(:MXExecutorPrint, (MX_handle, Ptr{Cstring}), x.handle, s_ref)
+ unsafe_string(s_ref[])
+end
diff --git a/julia/src/initializer.jl b/julia/src/initializer.jl
new file mode 100644
index 000000000000..95dbeb31febd
--- /dev/null
+++ b/julia/src/initializer.jl
@@ -0,0 +1,197 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ AbstractInitializer
+
+The abstract base class for all initializers.
+
+To define a new initializer, it is
+enough to derive a new type, and implement one or more of the following methods:
+
+ _init_weight(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ _init_bias(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ _init_gamma(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ _init_beta(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+
+Or, if full behavior customization is needed, override the following function
+
+ init(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+"""
+abstract type AbstractInitializer end
+
+function init(self :: T, name :: Base.Symbol, array :: NDArray) where T<:AbstractInitializer
+ strname = string(name)
+ if startswith(strname,"upsampling")
+ _init_bilinear(self,name, array)
+ elseif startswith(strname,"stn_loc") && endswith(strname,"weight")
+ _init_zero(self,name, array)
+ elseif startswith(strname,"stn_loc") && endswith(strname,"bias")
+ _init_loc_bias(self,name, array)
+ elseif endswith(strname, "bias")
+ _init_bias(self, name, array)
+ elseif endswith(strname, "gamma")
+ _init_gamma(self, name, array)
+ elseif endswith(strname, "beta")
+ _init_beta(self, name, array)
+ elseif endswith(strname, "weight")
+ _init_weight(self, name, array)
+ elseif endswith(strname, "moving_mean")
+ _init_zero(self, name, array)
+ elseif endswith(strname, "moving_var")
+ _init_zero(self, name, array)
+ else
+ _init_default(self, name, array)
+ end
+end
+
+function _init_loc_bias(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ assert(size(array) == (6,))
+ array[:]= [1.0, 0, 0, 0, 1.0, 0]
+end
+
+function _init_bilinear(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ @assert ndims(array) == 4
+
+ W, H, C, N = size(array) # Inverse of NCHW layout
+ filter = Base.zeros(eltype(array), W, H)
+
+ @assert H == W
+
+ f = ceil(Int, W / 2) # factor
+ c = (2 * f - 1 - f % 2) / (2 * f) # center
+ for x in 0:(W-1)
+ for y in 0:(H-1)
+ filter[x+1, y+1] = (1 - abs(x / f - c)) * (1 - abs(y / f - c))
+ end
+ end
+
+ @nd_as_jl rw=array begin
+ for i in 1:N
+ for j in 1:C
+ array[:,:, j, i] = filter
+ end
+ end
+ end
+end
+
+function _init_bias(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ array[:] = 0
+end
+function _init_gamma(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ array[:] = 1
+end
+function _init_beta(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ array[:] = 0
+end
+function _init_zero(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ array[:] = 0
+end
+
+function _init_default(self :: AbstractInitializer, name :: Base.Symbol, array :: NDArray)
+ error("Do not know how to init $name")
+end
+
+"""
+ UniformInitializer
+
+Initialize weights according to a uniform distribution within the provided scale.
+"""
+struct UniformInitializer <: AbstractInitializer
+ scale :: AbstractFloat
+end
+"""
+ UniformInitializer(scale=0.07)
+
+Construct a `UniformInitializer` with the specified scale.
+"""
+UniformInitializer() = UniformInitializer(0.07)
+
+_init_weight(i::UniformInitializer, name::Symbol, x::NDArray) =
+ rand!(x, low = -i.scale, high = i.scale)
+
+"""
+ NormalInitializer
+
+Initialize weights according to a univariate Gaussian distribution.
+"""
+struct NormalInitializer <: AbstractInitializer
+ μ :: AbstractFloat
+ σ :: AbstractFloat
+end
+"""
+ NormalInitializer(; mu=0, sigma=0.01)
+
+Construct a `NormalInitializer` with mean `mu` and variance `sigma`.
+"""
+NormalInitializer(; mu=0, sigma=0.01) = NormalInitializer(mu, sigma)
+
+_init_weight(i::NormalInitializer, name::Symbol, x::NDArray) =
+ randn!(x, μ = i.μ, σ = i.σ)
+
+"""
+ XavierInitializer
+
+The initializer documented in the paper [Bengio and Glorot 2010]: *Understanding
+the difficulty of training deep feedforward neuralnetworks*.
+
+There are several different version of the XavierInitializer used in the wild.
+The general idea is that the variance of the initialization distribution is controlled
+by the dimensionality of the input and output. As a distribution one can either choose
+a normal distribution with μ = 0 and σ² or a uniform distribution from -σ to σ.
+
+Several different ways of calculating the variance are given in the literature or are
+used by various libraries.
+
+* [Bengio and Glorot 2010]: `mx.XavierInitializer(distribution = mx.xv_uniform, regularization = mx.xv_avg, magnitude = 1)`
+* [K. He, X. Zhang, S. Ren, and J. Sun 2015]: `mx.XavierInitializer(distribution = mx.xv_gaussian, regularization = mx.xv_in, magnitude = 2)`
+* caffe_avg: `mx.XavierInitializer(distribution = mx.xv_uniform, regularization = mx.xv_avg, magnitude = 3)`
+"""
+
+@enum XavierDistribution xv_uniform xv_normal
+@enum XavierRegularization xv_avg xv_in xv_out
+
+struct XavierInitializer <: AbstractInitializer
+ distribution :: XavierDistribution
+ regularization :: XavierRegularization
+ magnitude :: Float64
+end
+
+XavierInitializer(; distribution = xv_uniform, regularization = xv_avg, magnitude = 3.0) =
+ XavierInitializer(distribution, regularization, magnitude)
+
+function _init_weight(self :: XavierInitializer, name :: Base.Symbol, array :: NDArray)
+ dims = size(array)
+ fan_in = prod(dims[2:end])
+ fan_out = dims[1]
+
+ if self.regularization == xv_avg
+ factor = (fan_in + fan_out) / 2
+ elseif self.regularization == xv_in
+ factor = fan_in
+ elseif self.regularization == xv_out
+ factor = fan_out
+ end
+
+ σ = √(self.magnitude / factor)
+
+ if self.distribution == xv_uniform
+ rand!(array, low = -σ, high = σ)
+ elseif self.distribution == xv_normal
+ randn!(array; μ = 0.0, σ = σ)
+ end
+end
diff --git a/julia/src/io.jl b/julia/src/io.jl
new file mode 100644
index 000000000000..e5f43950754c
--- /dev/null
+++ b/julia/src/io.jl
@@ -0,0 +1,643 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ AbstractDataProvider
+
+The root type for all data provider. A data provider should implement the following interfaces:
+
+* [`get_batch_size`](@ref)
+* [`provide_data`](@ref)
+* [`provide_label`](@ref)
+
+As well as the Julia iterator interface (see [the Julia manual](http://docs.julialang.org/en/stable/manual/interfaces/)).
+Normally this involves defining:
+
+* `Base.eltype(provider) -> AbstractDataBatch`
+* `Base.start(provider) -> AbstractDataProviderState`
+* `Base.done(provider, state) -> Bool`
+* `Base.next(provider, state) -> (AbstractDataBatch, AbstractDataProvider)`
+"""
+abstract type AbstractDataProvider end
+
+"""
+ get_batch_size(provider) -> Int
+
+# Arguments:
+* `provider::AbstractDataProvider`: the data provider.
+
+Returns the mini-batch size of the provided data. All the provided data should have the same mini-batch size (i.e. the last dimension).
+"""
+get_batch_size
+
+"""
+ provide_data(provider) -> Vector{Tuple{Base.Symbol, Tuple}}
+
+# Arguments:
+* `provider::AbstractDataProvider`: the data provider.
+
+Returns a vector of (name, shape) pairs describing the names of the data it provides, and the corresponding shapes.
+
+"""
+provide_data
+
+"""
+ provide_label(provider) -> Vector{Tuple{Base.Symbol, Tuple}}
+
+# Arguments:
+* `provider::AbstractDataProvider`: the data provider.
+
+Returns a vector of (name, shape) pairs describing the names of the labels it provides, and the corresponding shapes.
+"""
+provide_label
+
+"""
+ AbstractDataProviderState
+
+ Base type for data provider states.
+"""
+abstract type AbstractDataProviderState end
+
+"""
+ AbstractDataBatch
+
+ Base type for a data mini-batch. It should implement the following interfaces:
+
+* [`count_samples`](@ref)
+* [`get_data`](@ref)
+* [`get_label`](@ref)
+
+The following utility functions will be automatically defined:
+
+* [`get`](@ref)
+* [`load_data!`](@ref)
+* [`load_label!`](@ref)
+"""
+abstract type AbstractDataBatch end
+
+"""
+ count_samples(provider, batch) -> Int
+
+# Arguments:
+* `batch::AbstractDataBatch`: the data batch object.
+
+Returns the number of samples in this batch. This number should be greater than 0, but less than or equal to the batch size. This is used to indicate at the end of the data set, there might not be enough samples for a whole mini-batch.
+
+"""
+count_samples
+
+"""
+ get_data(provider, batch) -> Vector{NDArray}
+
+# Arguments:
+* `provider::AbstractDataProvider`: the data provider.
+* `batch::AbstractDataBatch`: the data batch object.
+
+Returns a vector of data in this batch, should be in the same order as declared in `provide_data() `.
+
+The last dimension of each `NDArray` should always match the batch_size, even when `count_samples` returns a value less than the batch size. In this case, the data provider is free to pad the remaining contents with any value.
+"""
+get_data
+
+"""
+ get_label(provider, batch) -> Vector{NDArray}
+
+# Arguments:
+* `provider::AbstractDataProvider`: the data provider.
+* `batch::AbstractDataBatch`: the data batch object.
+
+Returns a vector of labels in this batch. Similar to [`get_data`](@ref).
+"""
+get_label
+
+"""
+ DataBatch
+
+A basic subclass of `AbstractDataBatch`, that implement the interface by
+accessing member fields.
+"""
+mutable struct DataBatch{T,S,N,M} <: AbstractDataBatch
+ data :: Vector{NDArray{T,N}}
+ label :: Vector{NDArray{S,M}}
+ count :: Int
+end
+
+count_samples(batch::DataBatch) = batch.count
+
+get_data(::Provider, batch::DataBatch) where {Provider<:AbstractDataProvider} =
+ batch.data
+
+get_label(::Provider, batch::DataBatch) where {Provider<:AbstractDataProvider} =
+ batch.label
+
+"""
+ SlicedNDArray
+
+A alias type of `Tuple{UnitRange{Int},NDArray}`.
+"""
+const SlicedNDArray = Tuple{UnitRange{Int},<:NDArray}
+
+function _load_general!(provider :: AbstractDataProvider, batch :: AbstractDataBatch,
+ targets :: Vector{<:Vector{<:SlicedNDArray}}, loader::Function)
+ data = loader(provider, batch)
+ for (d_src, d_targets) in zip(data, targets)
+ for (slice_idx, d_dst) in d_targets
+ copy!(d_dst, slice(d_src, slice_idx))
+ end
+ end
+end
+
+"""
+ load_data!(provider, batch, targets)
+
+# Arguments:
+* `provider::AbstractDataProvider`: the data provider.
+* `batch::AbstractDataBatch`: the data batch object.
+* `targets::Vector{Vector{SlicedNDArray}}`: the targets to load data into.
+
+The targets is a list of the same length as number of data provided by this provider.
+Each element in the list is a list of `SlicedNDArray`. This list described a
+spliting scheme of this data batch into different slices, each slice is specified by
+a slice-ndarray pair, where *slice* specify the range of samples in the mini-batch
+that should be loaded into the corresponding *ndarray*.
+
+This utility function is used in data parallelization, where a mini-batch is splited
+and computed on several different devices.
+"""
+function load_data!(provider :: AbstractDataProvider, batch :: AbstractDataBatch,
+ targets :: Vector{<:Vector{<:SlicedNDArray}})
+ _load_general!(provider, batch, targets, get_data)
+end
+
+"""
+ load_label!(provider, batch, targets)
+
+* `provider::AbstractDataProvider provider`: the data provider.
+* `batch::AbstractDataBatch batch`: the data batch object.
+* `targets::Vector{Vector{SlicedNDArray}}`: the targets to load label into.
+
+The same as [`load_data!`](@ref), except that this is for loading labels.
+"""
+function load_label!(provider :: AbstractDataProvider, batch :: AbstractDataBatch,
+ targets :: Vector{<:Vector{<:SlicedNDArray}})
+ _load_general!(provider, batch, targets, get_label)
+end
+
+function load_data!(provider :: AbstractDataProvider, batch :: AbstractDataBatch,
+ targets :: Vector{<:NDArray})
+ for (src, dst) in zip(get_data(provider, batch), targets)
+ copy!(dst, src)
+ end
+end
+function load_label!(provider :: AbstractDataProvider, batch :: AbstractDataBatch,
+ targets :: Vector{<:NDArray})
+ for (src, dst) in zip(get_label(provider, batch), targets)
+ copy!(dst, src)
+ end
+end
+
+import Base.get
+"""
+ get(provider, batch, name) -> NDArray
+
+* `provider::AbstractDataProvider`: the data provider.
+* `batch::AbstractDataBatch`: the data batch object.
+* `name::Symbol`: the name of the data to get, should be one of the names
+ provided in either `provide_data() `
+ or `provide_label() `.
+
+Returns the corresponding data array corresponding to that name.
+"""
+function get(provider::AbstractDataProvider, batch::AbstractDataBatch, name::Symbol)
+ for (idx, (k, s)) in enumerate(provide_data(provider))
+ if name == k
+ return get_data(provider, batch)[idx]
+ end
+ end
+ for (idx, (k, s)) in enumerate(provide_label(provider))
+ if name == k
+ return get_label(provider, batch)[idx]
+ end
+ end
+ error("$name is not provided by this data provider")
+end
+
+"""
+ eachbatch(provider::AbstractDataProvider)
+
+Allows you to perform operations on data every epoch. This is especially useful
+when you need to perform real-time augmentation of the data.
+
+# Arguments:
+* `provider`: an instance of the custom DataProvider type. You must return this
+instance after modifying its fields.
+
+"""
+eachbatch(provider::AbstractDataProvider) = provider
+
+"""
+ ArrayDataProvider
+
+A convenient tool to iterate `NDArray` or Julia `Array`.
+
+ ArrayDataProvider(data[, label]; batch_size, shuffle, data_padding, label_padding)
+
+Construct a data provider from `NDArray` or Julia Arrays.
+
+# Arguments:
+* `data`: the data, could be
+ * a `NDArray`, or a Julia Array. This is equivalent to `:data => data`.
+ * a name-data pair, like `:mydata => array`, where `:mydata` is the name of the data
+ * and `array` is an `NDArray` or a Julia Array.
+ * a list of name-data pairs.
+
+* `label`: the same as the `data` parameter. When this argument is omitted, the constructed provider will provide no labels.
+* `batch_size::Int`: the batch size, default is 0, which means treating the whole array as a single mini-batch.
+* `shuffle::Bool`: turn on if the data should be shuffled at every epoch.
+* `data_padding::Real`: when the mini-batch goes beyond the dataset boundary, there might
+ be less samples to include than a mini-batch. This value specify a scalar to pad the
+ contents of all the missing data points.
+* `label_padding::Real`: the same as `data_padding`, except for the labels.
+
+TODO: remove `data_padding` and `label_padding`, and implement rollover that copies
+the last or first several training samples to feed the padding.
+"""
+mutable struct ArrayDataProvider{T,N} <: AbstractDataProvider
+ data_arrays :: Vector{Array{T,N}}
+ data_names :: Vector{Symbol}
+ label_arrays
+ label_names :: Vector{Symbol}
+ batch_size :: Int
+ sample_count :: Int
+ shuffle :: Bool
+ data_padding :: MX_float
+ label_padding :: MX_float
+
+ data_batch
+ label_batch
+end
+
+# Julia's type system is sometimes very frustrating. You cannot specify a function
+# with argument Vector{Pair} to expect to be matched when calling with the parameter
+# [:foo => zeros(2,3), :bar => zeros(3)] because the type inference gives very specific
+# results, about the parametric type in the Pair{T1,T2} type, thus does not match the
+# generic Pair type. In general, Int <: Number but Vector{Int} <: Vector{Number} is not
+# true. So let us just use Any here...
+function ArrayDataProvider(data; batch_size::Int = 0, shuffle::Bool = false,
+ data_padding::Real = 0, label_padding::Real = 0)
+ ArrayDataProvider(data, [], batch_size = batch_size, shuffle = shuffle,
+ data_padding = data_padding, label_padding = label_padding)
+end
+
+function ArrayDataProvider(data, label; batch_size::Int = 0, shuffle::Bool = false,
+ data_padding::Real = 0, label_padding::Real = 0)
+ asarr(arr :: Array{T}) where {T} = convert(Array{MX_float}, arr)
+ asarr(arr :: NDArray) = copy(arr)
+
+ if isa(data, Union{NDArray, Array}) && eltype(data) <: Real
+ data_names = [:data]
+ data_arrays = Array{MX_float}[asarr(data)]
+ elseif isa(data, Pair)
+ @assert isa(data.first, Base.Symbol) && isa(data.second, Union{NDArray, Array})
+ data_names = [data.first]
+ data_arrays = Array{MX_float}[asarr(data.second)]
+ elseif isa(data, Vector) || isa(data, Tuple)
+ map(data) do d
+ @assert isa(d, Pair) && isa(d.first, Base.Symbol) && isa(d.second, Union{NDArray, Array})
+ end
+ data_names = Base.Symbol[d.first for d in data]
+ data_arrays = Array{MX_float}[asarr(d.second) for d in data]
+ else
+ error("Invalid data argument type")
+ end
+
+ if isa(label, Union{NDArray, Array}) && eltype(label) <: Real
+ label_names = [:softmax_label]
+ label_arrays = Array{MX_float}[asarr(label)]
+ elseif isa(label, Pair)
+ @assert isa(label.first, Base.Symbol) && isa(label.second, Union{NDArray, Array})
+ label_names = [label.first]
+ label_arrays = Array{MX_float}[asarr(label.second)]
+ elseif isa(label, Vector) || isa(label, Tuple)
+ map(label) do d
+ @assert isa(d, Pair) && isa(d.first, Base.Symbol) && isa(d.second, Union{NDArray, Array})
+ end
+ label_names = Base.Symbol[d.first for d in label]
+ label_arrays = Array{MX_float}[asarr(d.second) for d in label]
+ else
+ error("Invalid label argument type")
+ end
+
+ @assert length(data_arrays) > 0
+ sample_count = size(data_arrays[1])[end]
+ for i = 1:length(data_names)
+ @assert(size(data_arrays[i])[end] == sample_count,
+ "Number of samples in $(data_names[i]) is mismatch with $(data_names[1])")
+ end
+ for i = 1:length(label_names)
+ @assert(size(label_arrays[i])[end] == sample_count,
+ "Number of samples in $(label_names[i]) is mismatch with $(data_names[1])")
+ end
+
+ if batch_size == 0
+ batch_size = sample_count
+ end
+ @assert 0 < batch_size <= sample_count
+
+ function gen_batch_nds(arrs :: Vector{Array{MX_float}}, bsize :: Int)
+ map(arrs) do arr
+ shape = size(arr)
+ empty(shape[1:end-1]..., bsize)
+ end
+ end
+
+ data_batch = gen_batch_nds(data_arrays, batch_size)
+ label_batch = gen_batch_nds(label_arrays, batch_size)
+
+ # reshape data and labels into 2D tensors, so that it is easier to work with them
+ data_arrays = map(data_arrays) do arr
+ reshape(arr, prod(size(arr)[1:end-1]), size(arr)[end])
+ end
+ label_arrays = map(label_arrays) do arr
+ reshape(arr, prod(size(arr)[1:end-1]), size(arr)[end])
+ end
+
+ ArrayDataProvider(data_arrays, data_names, label_arrays, label_names, batch_size,
+ sample_count, shuffle, MX_float(data_padding), MX_float(label_padding),
+ data_batch, label_batch)
+end
+
+provide_data(provider::ArrayDataProvider) =
+ collect(zip(provider.data_names, map(size, provider.data_batch)))
+
+provide_label(provider::ArrayDataProvider) =
+ collect(zip(provider.label_names, map(size, provider.label_batch)))
+
+get_batch_size(provider::ArrayDataProvider) = provider.batch_size
+
+struct ArrayDataProviderState <: AbstractDataProviderState
+ curr_idx :: Int
+end
+
+Base.eltype(provider :: ArrayDataProvider) = ArrayDataProviderState
+
+function Base.start(provider :: ArrayDataProvider)
+ if provider.shuffle
+ # re-shuffle all data
+ idx_perm = randperm(provider.sample_count)
+ provider.data_arrays = map(x->x[:,idx_perm], provider.data_arrays)
+ provider.label_arrays = map(x->x[:,idx_perm], provider.label_arrays)
+ end
+
+ return ArrayDataProviderState(1)
+end
+
+Base.done(provider::ArrayDataProvider, state::ArrayDataProviderState) =
+ state.curr_idx > provider.sample_count
+
+struct ArrayDataBatch <: AbstractDataBatch
+ idx :: UnitRange{Int}
+end
+function Base.next(provider :: ArrayDataProvider, state :: ArrayDataProviderState)
+ idx = state.curr_idx:Base.min(state.curr_idx+provider.batch_size-1, provider.sample_count)
+ return (ArrayDataBatch(idx), ArrayDataProviderState(idx.stop+1))
+end
+
+function count_samples(provider :: ArrayDataProvider, batch :: ArrayDataBatch)
+ return length(batch.idx)
+end
+
+function get_data(provider :: ArrayDataProvider, batch :: ArrayDataBatch)
+ for (src, dst) in zip(provider.data_arrays, provider.data_batch)
+ copy_ignore_shape!(dst[1:length(batch.idx)], src[:, batch.idx])
+ if length(batch.idx) < provider.batch_size
+ dst[length(batch.idx)+1:provider.batch_size] = provider.data_padding
+ end
+ end
+ return provider.data_batch
+end
+function get_label(provider :: ArrayDataProvider, batch :: ArrayDataBatch)
+ for (src, dst) in zip(provider.label_arrays, provider.label_batch)
+ copy_ignore_shape!(dst[1:length(batch.idx)], src[:, batch.idx])
+ if length(batch.idx) < provider.batch_size
+ dst[length(batch.idx)+1:provider.batch_size] = provider.label_padding
+ end
+ end
+ return provider.label_batch
+end
+
+
+"""
+ MXDataProvider
+
+A data provider that wrap built-in data iterators from libmxnet. See below for
+a list of built-in data iterators.
+"""
+mutable struct MXDataProvider <: AbstractDataProvider
+ handle :: MX_DataIterHandle
+ data_shape :: Vector{Tuple{Symbol,Tuple}}
+ label_shape:: Vector{Tuple{Symbol,Tuple}}
+ batch_size :: Int
+
+ # those two a auxiliary variables to help avoid calling reset
+ # but still pre-fetch first batch to get shape information
+ first_epoch:: Bool
+ first_batch:: Bool
+end
+
+function _reset_data_iter(handle :: MX_DataIterHandle)
+ @mxcall(:MXDataIterBeforeFirst, (MX_handle,), handle)
+end
+function _iter_next(handle :: MX_DataIterHandle)
+ ref_ret = Ref{Cint}(0)
+ @mxcall(:MXDataIterNext, (MX_handle, Ref{Cint}), handle, ref_ret)
+ return Bool(ref_ret[])
+end
+function _get_data(handle :: MX_DataIterHandle)
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXDataIterGetData, (MX_handle, Ref{MX_handle}), handle, ref_hdr)
+ return NDArray(MX_NDArrayHandle(ref_hdr[]), false)
+end
+function _get_label(handle :: MX_DataIterHandle)
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXDataIterGetLabel, (MX_handle, Ref{MX_handle}), handle, ref_hdr)
+ return NDArray(MX_NDArrayHandle(ref_hdr[]), false)
+end
+
+function MXDataProvider(handle :: MX_DataIterHandle;
+ data_name :: Symbol = :data,
+ label_name :: Union{Symbol,Void} = :softmax_label,
+ kwargs...) # for convenience, we ignore the rest keyword arguments
+ # init iterator, load the first batch and get shapes
+ @assert(_iter_next(handle), "Failed to load the first batch in MXDataProvider")
+ data_shape = Tuple{Base.Symbol, Tuple}[(data_name, size(_get_data(handle)))]
+ if !isa(label_name, Void)
+ label_shape = Tuple{Base.Symbol, Tuple}[(label_name::Base.Symbol, size(_get_label(handle)))]
+ else
+ label_shape = Tuple{Base.Symbol, Tuple}[]
+ end
+
+ MXDataProvider(handle, data_shape, label_shape, data_shape[1][2][end], true, true)
+end
+
+provide_data(provider::MXDataProvider) = provider.data_shape
+provide_label(provider::MXDataProvider) = provider.label_shape
+get_batch_size(provider::MXDataProvider) = provider.batch_size
+
+mutable struct MXDataProviderState <: AbstractDataProviderState
+ has_next :: Bool
+end
+struct MXDataBatch <: AbstractDataBatch
+end
+
+function Base.eltype(provider :: MXDataProvider)
+ MXDataBatch
+end
+function Base.start(provider :: MXDataProvider)
+ if !provider.first_epoch
+ _reset_data_iter(provider.handle)
+ else
+ provider.first_epoch = false
+ end
+
+ return MXDataProviderState(true)
+end
+function Base.done(provider :: MXDataProvider, state :: MXDataProviderState)
+ if provider.first_batch
+ state.has_next = true
+ provider.first_batch = false
+ else
+ state.has_next = _iter_next(provider.handle)
+ end
+ return !state.has_next
+end
+function Base.next(provider :: MXDataProvider, state :: MXDataProviderState)
+ return (MXDataBatch(), state)
+end
+
+function get_data(provider :: MXDataProvider, batch :: MXDataBatch)
+ return NDArray[_get_data(provider.handle)]
+end
+function get_label(provider :: MXDataProvider, batch :: MXDataBatch)
+ return NDArray[_get_label(provider.handle)]
+end
+function count_samples(provider :: MXDataProvider, batch :: MXDataBatch)
+ ref_pad = Ref{Cint}(0)
+ @mxcall(:MXDataIterGetPadNum, (MX_handle, Ref{Cint}), provider.handle, ref_pad)
+ return provider.batch_size - Int(ref_pad[])
+end
+
+function _get_iter_creators()
+ n_ref = Ref{MX_uint}(0)
+ h_ref = Ref{Ptr{MX_handle}}(0)
+ @mxcall(:MXListDataIters, (Ref{MX_uint}, Ref{Ptr{MX_handle}}), n_ref, h_ref)
+
+ return unsafe_wrap(Array, h_ref[], n_ref[])
+end
+
+function _get_iter_name(hdr :: MX_handle)
+ ref_name = Ref{char_p}(0)
+ ref_desc = Ref{char_p}(0)
+ ref_narg = Ref{MX_uint}(0)
+ ref_arg_names = Ref{char_pp}(0)
+ ref_arg_types = Ref{char_pp}(0)
+ ref_arg_descs = Ref{char_pp}(0)
+
+ @mxcall(:MXDataIterGetIterInfo,
+ (MX_handle, Ref{char_p}, Ref{char_p}, Ref{MX_uint}, Ref{char_pp}, Ref{char_pp}, Ref{char_pp}),
+ hdr, ref_name, ref_desc, ref_narg, ref_arg_names, ref_arg_types, ref_arg_descs)
+
+ return Symbol(unsafe_string(ref_name[]))
+end
+
+const _iter_creator_cache = Dict{Symbol,MX_handle}()
+function _populate_iter_creator_cache!()
+ empty!(_iter_creator_cache)
+ h_creators = _get_iter_creators()
+ for handle in h_creators
+ name = _get_iter_name(handle)
+ _iter_creator_cache[name] = handle
+ end
+end
+
+_get_iter_creator(name :: Symbol) = _iter_creator_cache[name]
+
+function _define_data_iter_creator(hdr :: MX_handle)
+ ref_name = Ref{char_p}(0)
+ ref_desc = Ref{char_p}(0)
+ ref_narg = Ref{MX_uint}(0)
+ ref_arg_names = Ref{char_pp}(0)
+ ref_arg_types = Ref{char_pp}(0)
+ ref_arg_descs = Ref{char_pp}(0)
+
+ @mxcall(:MXDataIterGetIterInfo,
+ (MX_handle, Ref{char_p}, Ref{char_p}, Ref{MX_uint}, Ref{char_pp}, Ref{char_pp}, Ref{char_pp}),
+ hdr, ref_name, ref_desc, ref_narg, ref_arg_names, ref_arg_types, ref_arg_descs)
+
+ iter_name = Symbol(unsafe_string(ref_name[]))
+
+ isprovider = endswith(string(iter_name), "Iter")
+ signature = _format_signature(Int(ref_narg[]), ref_arg_names)
+ f_desc = " " * string(iter_name) * "(" *signature * ")\n\n"
+ if isprovider
+ f_desc *= "Can also be called with the alias `$(string(iter_name)[1:end-4] * "Provider")`.\n"
+ end
+ f_desc *= unsafe_string(ref_desc[]) * "\n\n"
+ f_desc *= "# Arguments:\n"
+ f_desc *= "* `data_name::Symbol`: keyword argument, default `:data`. The name of the data.\n"
+ f_desc *= "* `label_name::Symbol`: keyword argument, default `:softmax_label`. " *
+ "The name of the label. Could be `nothing` if no label is presented in this dataset.\n\n"
+ f_desc *= _format_docstring(Int(ref_narg[]), ref_arg_names, ref_arg_types, ref_arg_descs) * "\n"
+ f_desc *= "Returns the constructed `MXDataProvider`."
+
+ if isprovider
+ alias_name = Symbol(string(iter_name)[1:end-4] * "Provider")
+ else
+ alias_name = nothing
+ end
+
+ defun = quote
+ @doc $f_desc ->
+ function $iter_name(; kwargs...)
+ arg_keys = String[string(k) for (k,v) in kwargs]
+ arg_vals = String[dump_mx_param(v) for (k,v) in kwargs]
+ ref_hdr = Ref{MX_handle}(0)
+
+ local hdr = _get_iter_creator($(QuoteNode(iter_name)))
+ @mxcall(:MXDataIterCreateIter, (MX_handle, MX_uint, char_pp, char_pp, Ref{MX_handle}),
+ hdr, length(arg_keys), arg_keys, arg_vals, ref_hdr)
+
+ return MXDataProvider(MX_DataIterHandle(ref_hdr[]); kwargs...)
+ end
+ $(isprovider ? :(const $alias_name = $iter_name) : :())
+
+ end
+ defun
+end
+
+macro _import_io_iterators()
+ creators = _get_iter_creators()
+ defs = Expr[]
+ for handle in creators
+ push!(defs, _define_data_iter_creator(handle))
+ end
+ esc(quote
+ $(defs...)
+ end)
+end
+
+@_import_io_iterators()
diff --git a/julia/src/kvstore.jl b/julia/src/kvstore.jl
new file mode 100644
index 000000000000..ac0367144384
--- /dev/null
+++ b/julia/src/kvstore.jl
@@ -0,0 +1,353 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import Base.push!
+
+"""
+ KVStore(kv_type = :local)
+
+For single machine training, there are two commonly used types:
+
+- `local`: Copies all gradients to CPU memory and updates weights there.
+
+- `device`: Aggregates gradients and updates weights on GPU(s).
+ With this setting, the `KVStore` also attempts to use GPU peer-to-peer
+ communication, potentially accelerating the communication.
+
+For distributed training, `KVStore` also supports a number of types:
+
+- `dist_sync`: Behaves similarly to `local` but with one major difference.
+ With `dist_sync`, batch-size now means the batch size used on each machine.
+ So if there are `n` machines and we use batch size ``b``,
+ then `dist_sync` behaves like `local` with batch size `n * b`.
+
+- `dist_device_sync`: Identical to `dist_sync` with the difference similar
+ to `device` vs `local`.
+
+- `dist_async`: Performs asynchronous updates.
+ The weights are updated whenever gradients are received from any machine.
+ No two updates happen on the same weight at the same time.
+ However, the order is not guaranteed.
+"""
+mutable struct KVStore
+ handle :: MX_KVStoreHandle
+ updater_c :: Ptr{Void}
+ updater :: Function
+
+ KVStore(hdr::MX_KVStoreHandle) = new(hdr, Ptr{Void}(0))
+end
+
+function KVStore(kv_type::Symbol = :local)
+ @assert kv_type ∈ (:local, :device, :dist_sync, :dist_device_sync, :dist_async)
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXKVStoreCreate, (char_p, Ref{MX_handle}), dump_mx_param(kv_type), ref_hdr)
+ KVStore(MX_KVStoreHandle(ref_hdr[]))
+end
+
+Base.unsafe_convert(::Type{MX_handle}, obj::KVStore) =
+ Base.unsafe_convert(MX_handle, obj.handle)
+Base.convert(t::Type{MX_handle}, obj::KVStore) = Base.unsafe_convert(t, obj)
+Base.cconvert(t::Type{MX_handle}, obj::KVStore) = Base.unsafe_convert(t, obj)
+
+Base.show(io::IO, kv::KVStore) =
+ print(io, "mx.KVStore @ $(get_type(kv))")
+
+function _flatten_kvlist(keys::Vector{Int}, vals::Vector{<:Vector{<:NDArray}})
+ @assert length(keys) == length(vals)
+ keys_flt = Int[]
+ vals_flt = NDArray[]
+ for (k,v) in zip(keys, vals)
+ append!(keys_flt, Base.ones(Int, length(v))*k)
+ append!(vals_flt, v)
+ end
+ return (keys_flt, vals_flt)
+end
+
+"""
+ init!(kv::KVStore, key::Int, val::NDArray)
+ init!(kv::KVStore, keys, vals)
+
+Initializes a single or a sequence of key-value pairs into the store.
+
+For each key, one must `init!` it before calling `push!` or `pull!`.
+When multiple workers invoke `init!` for the same key, only
+the value supplied by worker with rank `0` is used. This function returns
+after data has been initialized successfully.
+
+```jldoctest
+julia> kv = KVStore(:local)
+mx.KVStore @ local
+
+julia> init!(kv, 42, mx.rand(2, 3))
+```
+"""
+init!(kv::KVStore, key::Int, val::NDArray) = init!(kv, [key], [val])
+init!(kv::KVStore, key::Int, vals::Vector{<:NDArray}) =
+ init!(kv, Base.ones(Int, length(vals)) * key, vals)
+init!(kv::KVStore, keys::Vector{Int}, vals::Vector{<:Vector{<:NDArray}}) =
+ init!(kv, _flatten_kvlist(keys, vals)...)
+
+function init!(kv::KVStore, keys::Vector{Int}, vals::VecOfNDArray)
+ @assert length(keys) == length(vals)
+ keys = Cint[keys...]
+ vals = MX_handle[vals...]
+ @mxcall(:MXKVStoreInit, (MX_handle, MX_uint, Ptr{Cint}, Ptr{MX_handle}),
+ kv, length(keys), keys, vals)
+end
+
+"""
+ push!(kv::KVStore, key, val; priority = 0)
+ push!(kv::KVStore, key, vals; priority = 0)
+ push!(kv::KVStore, keys, vals; priority = 0)
+
+Pushes a single or a sequence of key-value pairs into the store.
+
+This function returns immediately after adding an operator to the engine.
+The actual operation is executed asynchronously. If there are consecutive
+pushes to the same key, there is no guarantee on the serialization of pushes.
+The execution of a push does not guarantee that all previous pushes are
+finished. There is no synchronization between workers by default.
+One can use ``barrier()`` to sync all workers.
+
+`push!` and `pull!` single `NDArray`:
+```jldoctest
+julia> kv = KVStore(:local)
+mx.KVStore @ local
+
+julia> x = mx.empty(2, 3);
+
+julia> init!(kv, 3, x)
+
+julia> push!(kv, 3, mx.ones(2, 3) * 8)
+
+julia> pull!(kv, 3, x)
+
+julia> x
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 8.0 8.0 8.0
+ 8.0 8.0 8.0
+```
+
+Aggregate values and `push!`:
+```jldoctest
+julia> vals = [mx.ones((2, 3), gpu(0)) * 3, mx.ones((2, 3), gpu(1)) * 4];
+
+julia> push!(kv, 3, vals)
+
+julia> pull!(kv, 3, x)
+
+julia> x
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 7.0 7.0 7.0
+ 7.0 7.0 7.0
+```
+
+`push!` a list of key to single device:
+
+```jldoctest
+julia> keys = [4, 5];
+
+julia> init!(kv, keys, [empty(2, 3), empty(2, 3)])
+
+julia> push!(kv, keys, [x, x])
+
+julia> y, z = empty(2, 3), empty(2, 3);
+
+julia> pull!(kv, keys, [y, z])
+```
+"""
+push!(kv::KVStore, key::Int, val::NDArray; priority::Int = 0) =
+ push!(kv, [key], [val]; priority = priority)
+push!(kv::KVStore, key::Int, vals::Vector{<:NDArray}; priority::Int = 0) =
+ push!(kv, Base.ones(Int, length(vals)) * key, vals; priority = priority)
+push!(kv:: KVStore, keys::Vector{Int}, vals::Vector{<:Vector{<:NDArray}};
+ priority::Int = 0) =
+ push!(kv, _flatten_kvlist(keys, vals)...; priority = priority)
+
+function push!(kv::KVStore, keys::Vector{Int}, vals::Vector{<:NDArray}; priority::Int = 0)
+ @assert length(keys) == length(vals)
+ keys = Cint[keys...]
+ vals = MX_handle[vals...]
+ @mxcall(:MXKVStorePush, (MX_handle, MX_uint, Ptr{Cint}, Ptr{MX_handle}, Cint),
+ kv, length(keys), keys, vals, priority)
+end
+
+""" Pulls a single value or a sequence of values from the store.
+
+This function returns immediately after adding an operator to the engine.
+Subsequent attempts to read from the `out` variable will be blocked until the
+pull operation completes.
+
+`pull` is executed asynchronously after all previous `pull` calls and only
+the last `push` call for the same input key(s) are finished.
+
+The returned values are guaranteed to be the latest values in the store.
+
+See [`pull!`](@ref) for more examples.
+"""
+pull!(kv::KVStore, key::Int, out::NDArray; priority::Int = 0) =
+ pull!(kv, [key], [out], priority = priority)
+pull!(kv::KVStore, key::Int, outs::Vector{<:NDArray}; priority::Int = 0) =
+ pull!(kv, Base.ones(Int, length(outs))*key, outs; priority = priority)
+pull!(kv::KVStore, keys::Vector{Int}, outs::Vector{<:Vector{<:NDArray}};
+ priority::Int = 0) =
+ pull!(kv, _flatten_kvlist(keys, outs)...; priority = priority)
+
+function pull!(kv::KVStore, keys::Vector{Int}, outs::Vector{<:NDArray}; priority::Int = 0)
+ @assert length(keys) == length(outs)
+ keys = Cint[keys...]
+ outs = MX_handle[outs...]
+ @mxcall(:MXKVStorePull, (MX_handle, MX_uint, Ptr{Cint}, Ptr{MX_handle}, Cint),
+ kv, length(keys), keys, outs, priority)
+end
+
+
+function get_type(kv::KVStore)
+ type_ref = Ref{char_p}(0)
+ @mxcall(:MXKVStoreGetType, (MX_handle, Ref{char_p}), kv, type_ref)
+ return Symbol(unsafe_string(type_ref[]))
+end
+
+function get_num_workers(kv::KVStore)
+ ref_size = Ref{Cint}(0)
+ @mxcall(:MXKVStoreGetGroupSize, (MX_handle, Ref{Cint}), kv, ref_size)
+ return Int(ref_size[])
+end
+
+function get_rank(kv::KVStore)
+ ref_rank = Ref{Cint}(0)
+ @mxcall(:MXKVStoreGetRank, (MX_handle, Ref{Cint}), kv, ref_rank)
+ return Int(ref_rank[])
+end
+
+"""
+ barrier(kv::KVStore)
+
+Invokes global barrier among all worker nodes.
+
+For example, assume there are `n` machines. We would like machine `0` to first
+`init` the values and then have all the workers `pull` the initialized value.
+Before pulling, we can place invoke `barrier(kv)` to guarantee that the
+initialization is finished.
+"""
+barrier(kv::KVStore) = @mxcall(:MXKVStoreBarrier, (MX_handle,), kv)
+
+
+# TODO: Currently Julia does not support closure in c-callbacks, so we are making use of the
+# extra handle parameter of the API to pass the updater object around. Fix this when someday
+# full closure cfunction is supported in Julia.
+function _kvstore_update_wrapper(key::Cint, nd_recv::MX_handle, nd_local::MX_handle,
+ updater::Ptr{Void})
+ updater_func = unsafe_pointer_to_objref(updater)
+ updater_func(Int(key), NDArray(MX_NDArrayHandle(nd_recv)),
+ NDArray(MX_NDArrayHandle(nd_local)))
+ nothing
+end
+
+"""
+ setupdater!(kv, updater)
+
+Sets a `push!` updater into the store.
+
+This function only changes the local store.
+When running on multiple machines one must use `set_optimizer`.
+
+```jldoctest
+julia> update(key, val, orig) = mx.@inplace orig += val .* .2
+update (generic function with 1 method)
+
+julia> kv = KVStore(:local)
+mx.KVStore @ local
+
+julia> mx.setupdater!(kv, update)
+
+julia> init!(kv, 42, mx.ones(2, 3))
+
+julia> push!(kv, 42, mx.ones(2, 3))
+
+julia> x = empty(2, 3);
+
+julia> pull!(kv, 42, x)
+
+julia> x
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 1.2 1.2 1.2
+ 1.2 1.2 1.2
+```
+"""
+function setupdater!(kv::KVStore, updater)
+ kv.updater = updater # keep a reference to the julia object so that updater_c is kept valid
+ kv.updater_c = cfunction(_kvstore_update_wrapper, Void,
+ (Cint, MX_handle, MX_handle, Ptr{Void}))
+ @mxcall(:MXKVStoreSetUpdater, (MX_handle, Ptr{Void}, Any),
+ kv, kv.updater_c, updater)
+end
+
+"""
+ setoptimizer!(kv::KVStore, opt)
+
+Registers an optimizer with the kvstore.
+
+When using a single machine, this function updates the local optimizer.
+If using multiple machines and this operation is invoked from a worker node,
+it will serialized the optimizer with pickle and send it to all servers.
+The function returns after all servers have been updated.
+
+```jldoctest
+julia> kv = KVStore()
+mx.KVStore @ local
+
+julia> W = mx.zeros(2, 3) # 2×3 weight matrix
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 0.0 0.0 0.0
+ 0.0 0.0 0.0
+
+julia> init!(kv, 42, W)
+
+julia> setoptimizer!(kv, SGD(η = .2)) # SGD with .2 as learning rate
+
+julia> ∇W = mx.ones(2, 3) # assume it's the gradient
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 1.0 1.0 1.0
+ 1.0 1.0 1.0
+
+julia> push!(kv, 42, ∇W)
+
+julia> pull!(kv, 42, W) # fetch weight and write back to `W`
+
+julia> W
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ -0.2 -0.2 -0.2
+ -0.2 -0.2 -0.2
+```
+"""
+function setoptimizer!(kv::KVStore, opt::AbstractOptimizer)
+ if ismatch(r"dist", string(get_type(kv))) && _isworker()
+ # TODO
+ error("not implemented")
+ else
+ setupdater!(kv, getupdater(opt))
+ end
+end
+
+function _isworker()::Bool
+ ref = Ref{Cint}(0)
+ @mxcall(:MXKVStoreIsWorkerNode, (Ref{Cint},), ref)
+ ref_is_worker[]
+end
+
+# TODO: sparse support?
diff --git a/julia/src/metric.jl b/julia/src/metric.jl
new file mode 100644
index 000000000000..772eb3b3e680
--- /dev/null
+++ b/julia/src/metric.jl
@@ -0,0 +1,478 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ AbstractEvalMetric
+
+The base class for all evaluation metrics. The sub-types should implement the following
+interfaces:
+
+* [`update!`](@ref)
+* [`reset!`](@ref)
+* [`get`](@ref)
+"""
+abstract type AbstractEvalMetric end
+
+"""
+ hasNDArraySupport(metric) -> Val{true/false}
+
+Trait for `_update_single_output` should return `Val{true}() if metric can handle `NDArray`
+directly and `Val{false}()` if requires `Array`. Metric that work with NDArrays can be
+async, while native Julia arrays require that we copy the output of the network, which is
+a blocking operation.
+"""
+hasNDArraySupport(::AbstractEvalMetric) = Val{true}()
+
+"""
+ update!(metric, labels, preds)
+
+Update and accumulate metrics.
+
+# Arguments:
+* `metric::AbstractEvalMetric`: the metric object.
+* `labels::Vector{NDArray}`: the labels from the data provider.
+* `preds::Vector{NDArray}`: the outputs (predictions) of the network.
+"""
+function update!(metric::T, labels::VecOfNDArray, preds::VecOfNDArray) where T <: AbstractEvalMetric
+ _update!(metric, labels, preds, hasNDArraySupport(metric))
+end
+
+function _update!(metric::T, labels::VecOfNDArray, preds::VecOfNDArray,
+ ::Val{true}) where T<: AbstractEvalMetric
+ if length(labels) != length(preds)
+ Base.warn_once(
+ "The number of labels ($(length(labels))) does not correspond to the\
+ number of outputs ($(length(preds))). The calculated metric might not be accuracte.")
+ end
+ for (label, pred) in zip(labels, preds)
+ _update_single_output(metric, label, pred)
+ end
+end
+
+function _update!(metric::T, labels::VecOfNDArray, preds::VecOfNDArray,
+ ::Val{false}) where T<: AbstractEvalMetric
+ if length(labels) != length(preds)
+ Base.warn_once(
+ "The number of labels ($(length(labels))) does not correspond to the\
+ number of outputs ($(length(preds))). The calculated metric might not be accuracte.")
+ end
+ for (label, pred) in zip(labels, preds)
+ @nd_as_jl ro=(label, pred) begin
+ # This is a dynamic dispatch since the conversion from NDArray to
+ # Array is not type-stable.
+ _update_single_output(metric, label, pred)
+ end
+ end
+end
+
+"""
+ reset!(metric)
+
+Reset the accumulation counter.
+"""
+reset!(metric::AbstractEvalMetric) = throw(MethodError(reset!, (typeof(metric),)))
+
+
+import Base: get
+"""
+ get(metric)
+
+Get the accumulated metrics.
+
+Returns `Vector{Tuple{Base.Symbol, Real}}`, a list of name-value pairs.
+For example, `[(:accuracy, 0.9)]`.
+"""
+get(metric::AbstractEvalMetric) = throw(MethodError(get, (typeof(metric),)))
+
+"""
+ NullMetric()
+
+A metric that calculates nothing. Can be used to ignore an output during training.
+"""
+mutable struct NullMetric <: mx.AbstractEvalMetric
+end
+
+update!(metric::NullMetric, labels::VecOfNDArray, preds::VecOfNDArray) = nothing
+
+reset!(metric::NullMetric) = nothing
+
+get(metric::NullMetric) = Tuple{Symbol, Float64}[]
+
+"""
+ MultiMetric(metrics::Vector{AbstractEvalMetric})
+
+Combine multiple metrics in one and get a result for all of them.
+
+# Usage
+To calculate both mean-squared error [`Accuracy`](@ref) and log-loss [`ACE`](@ref):
+```julia
+ mx.fit(..., eval_metric = mx.MultiMetric([mx.Accuracy(), mx.ACE()]))
+```
+"""
+mutable struct MultiMetric <: AbstractEvalMetric
+ metrics :: Vector{mx.AbstractEvalMetric}
+end
+
+function update!(metric :: MultiMetric, labels :: Vector{<:NDArray}, preds :: Vector{<:NDArray})
+ for m in metric.metrics
+ update!(m, labels, preds)
+ end
+ nothing
+end
+
+function reset!(metric :: MultiMetric)
+ map(reset!, metric.metrics)
+ nothing
+end
+
+get(metric::MultiMetric) = mapreduce(get, append!, metric.metrics)
+
+"""
+ SeqMetric(metrics::Vector{AbstractEvalMetric})
+
+Apply a different metric to each output. This is especially useful for `mx.Group`.
+
+# Usage
+Calculate accuracy [`Accuracy`](@ref) for the first output
+and log-loss [`ACE`](@ref) for the second output:
+```julia
+ mx.fit(..., eval_metric = mx.SeqMetric([mx.Accuracy(), mx.ACE()]))
+```
+"""
+mutable struct SeqMetric <: AbstractEvalMetric
+ metrics :: Vector{AbstractEvalMetric}
+end
+
+function update!(metric::SeqMetric, labels::VecOfNDArray, preds::VecOfNDArray)
+ @assert length(metric.metrics) == length(labels)
+ @assert length(metric.metrics) == length(preds)
+ for (m, l, p) in zip(metric.metrics, labels, preds)
+ update!(m, [l], [p])
+ end
+ nothing
+end
+
+function reset!(metric::SeqMetric)
+ map(reset!, metric.metrics)
+ nothing
+end
+
+get(metric::SeqMetric) = mapreduce(get, append!, metric.metrics)
+
+"""
+ Accuracy
+
+Multiclass classification accuracy.
+
+Calculates the mean accuracy per sample for softmax in one dimension.
+For a multi-dimensional softmax the mean accuracy over all dimensions is calculated.
+"""
+mutable struct Accuracy <: AbstractEvalMetric
+ acc_sum :: Float64
+ n_sample :: Int
+
+ Accuracy() = new(0.0, 0)
+end
+
+hasNDArraySupport(::Accuracy) = Val{false}()
+
+function _update_single_output(metric::Accuracy, label::Array, pred::Array)
+ # Samples are stored in the last dimension
+ @assert size(label, ndims(label)) == size(pred, ndims(pred))
+
+ if ndims(pred) == 4 # Multidimensional case
+ # Reshape label to be of the same shape as pred.
+ # Except for the third dimension where the predictions are stored.
+ labels = reshape(label, size(pred, 1, 2)..., 1, size(pred, 4))
+
+ for sample in 1:size(labels, 4)
+ for j in 1:size(labels, 2)
+ for i in 1:size(labels, 1)
+ label = labels[i, j, 1, sample]
+ klasses = view(pred, i, j, :, sample)
+ klass = indmax(klasses) - 1 # Classes start at 0...k-1
+
+ metric.acc_sum += klass == label
+ metric.n_sample += 1
+ end
+ end
+ end
+ elseif ndims(pred) == 2 # 1-dimensional case
+ for sample in 1:size(label, 1)
+ klass = indmax(view(pred, :, sample)) - 1
+ metric.acc_sum += klass == label[sample]
+ metric.n_sample += 1
+ end
+ else
+ error("Can't handle prediction with dimensions $(ndims(pred)).")
+ end
+end
+
+get(metric::Accuracy) = [(:accuracy, metric.acc_sum / metric.n_sample)]
+
+function reset!(metric :: Accuracy)
+ metric.acc_sum = 0.0
+ metric.n_sample = 0
+end
+
+"""
+ MSE
+
+Mean Squared Error.
+
+Calculates the mean squared error regression loss.
+Requires that label and prediction have the same shape.
+"""
+
+mutable struct MSE{N} <: AbstractEvalMetric
+ mse_sum :: Vector{NDArray{MX_float,N}}
+ n_sample :: Int
+
+ MSE{N}() where {N} = new(Vector{NDArray{MX_float,N}}(), 0)
+end
+
+MSE() = MSE{1}() # backward compat?
+
+hasNDArraySupport(::MSE) = Val{true}()
+
+function _update_single_output(metric::MSE, label::NDArray{T,N},
+ pred::NDArray{T,N}) where {T,N}
+ @assert size(label) == size(pred)
+ metric.n_sample += length(label)
+ mse_sum = mx.sum((label .- pred).^2)
+ push!(metric.mse_sum, mse_sum)
+ nothing
+end
+
+function get(metric::MSE)
+ # Delay copy until last possible moment
+ mse_sum = mapreduce(nda->copy(nda)[1], +, 0.0, metric.mse_sum)
+ [(:MSE, mse_sum / metric.n_sample)]
+end
+
+function reset!(metric::MSE{N}) where N
+ metric.mse_sum = Vector{NDArray{Float32,N}}()
+ metric.n_sample = 0
+end
+
+doc"""
+ NMSE
+
+Normalized Mean Squared Error
+
+```math
+\sum_i (\frac{label_i - pred_i}{label_i})^2
+```
+
+Note that there are various ways to do the *normalization*.
+It depends on your own context. Please judge the problem setting you have
+first. If the current implementation do not suitable for you,
+feel free to file it on GitHub.
+
+Let me show you a use case of this kind of normalization:
+
+Bob is training a network for option pricing. The option pricing problem is
+a regression problem (pirce predicting). There are lots of option contracts
+on same target stock but different strike price.
+For example, there is a stock `S`; it's market price is 1000.
+And, there are two call option contracts with different strike price.
+Assume Bob obtains the outcome as following table:
+
+```
++--------+----------------+----------------+--------------+
+| | Strike Price | Market Price | Pred Price |
++--------+----------------+----------------+--------------+
+| Op 1 | 1500 | 100 | 80 |
++--------+----------------+----------------+--------------+
+| Op 2 | 500 | 10 | 8 |
++--------+----------------+----------------+--------------+
+```
+
+Now, obviously, Bob will calculate the normalized MSE as:
+
+```math
+ (\frac{100 - 80}{100})^2
+ \text{ vs }
+ (\frac{10 - 8}{10}) ^2
+```
+
+Both of the pred prices got the same degree of error.
+
+For more discussion about normalized MSE, please see
+[#211](https://github.com/dmlc/MXNet.jl/pull/211) also.
+
+"""
+mutable struct NMSE <: AbstractEvalMetric
+ nmse_sum :: Float64
+ n_sample :: Int
+
+ NMSE() = new(0.0, 0)
+end
+
+hasNDArraySupport(::NMSE) = Val{false}()
+
+function _update_single_output(metric::NMSE, label::Array, pred::Array)
+ n_sample = size(pred)[end]
+ metric.n_sample += n_sample
+
+ for i = 1:n_sample
+ if label[i] == 0.0f0 # in case of batch padding
+ continue
+ end
+
+ metric.nmse_sum += ((label[i] - pred[i]) / label[i])^2
+ end
+end
+
+get(metric::NMSE) = [(:NMSE, metric.nmse_sum / metric.n_sample)]
+
+function reset!(metric::NMSE)
+ metric.nmse_sum = 0.0
+ metric.n_sample = 0
+end
+
+"""
+ ACE
+
+Calculates the averaged cross-entropy (logloss) for classification.
+
+# Arguments:
+* `eps::Float64`: Prevents returning `Inf` if `p = 0`.
+"""
+mutable struct ACE <: AbstractEvalMetric
+ ace_sum :: Float64
+ n_sample :: Int
+ eps :: Float64
+
+ ACE(eps=1.0e-8) = new(0.0, 0, eps)
+end
+
+get(metric::ACE) = [(:ACE, - metric.ace_sum / metric.n_sample)]
+
+function reset!(metric::ACE)
+ metric.ace_sum = 0.0
+ metric.n_sample = 0
+end
+
+hasNDArraySupport(::ACE) = Val{false}()
+
+function _update_single_output(metric :: ACE, label :: Array{T}, pred :: Array{T}) where T
+ eps = convert(T, metric.eps)
+ # Samples are stored in the last dimension
+ @assert size(label, ndims(label)) == size(pred, ndims(pred))
+ if size(label) == size(pred) # simply calculate the cross entropy of the probabilities
+ for (q, p) in zip(pred, label)
+ # p == true probability
+ # q == "unnatural" probability
+ metric.ace_sum += p * log(q + eps)
+ metric.n_sample += 1
+ end
+ elseif ndims(pred) == 4
+ labels = reshape(label, size(pred, 1, 2)..., 1, size(pred, 4))
+ for sample in 1:size(labels, 4)
+ for j in 1:size(labels, 2)
+ for i in 1:size(labels, 1)
+ # Cross-entropy reduces to -(ln(p_1)*0 + ln(p_2)*1) for classification
+ # Since we can only target labels right now this is the only thing we can do.
+ target = Int(labels[i, j, 1, sample]) + 1 # klasses are 0...k-1 => julia indexing
+ p_k = pred[i, j, target, sample]
+ metric.ace_sum += log(p_k + eps)
+ metric.n_sample += 1
+ end
+ end
+ end
+ elseif ndims(pred) == 2 # 1-dimensional case
+ for sample in 1:size(label, 1)
+ target = Int(label[sample]) + 1 # 0-based indexing => 1-based indexing
+ p_k = pred[target, sample]
+ metric.ace_sum += log(p_k +eps)
+ metric.n_sample += 1
+ end
+ else
+ error("Can't handle prediction with dimensions $(ndims(pred)).")
+ end
+end
+
+"""
+ MultiACE
+
+Calculates the averaged cross-entropy per class and overall (see [`ACE`](@ref)).
+This can be used to quantify the influence of different classes on the overall loss.
+"""
+mutable struct MultiACE <: AbstractEvalMetric
+ aces :: Vector{Float64}
+ counts :: Vector{Int}
+ eps :: Float64
+
+ MultiACE(nclasses, eps=1.0e-8) = new(Base.zeros(nclasses), Base.zeros(Int, nclasses), eps)
+end
+
+function get(metric :: MultiACE)
+ aces = [(Symbol("ACE_$(i-0)"), - metric.aces[i] / metric.counts[i]) for i in 1:length(metric.aces)]
+ push!(aces, (:ACE, - Base.sum(metric.aces) / Base.sum(metric.counts)))
+ return aces
+end
+
+function reset!(metric :: MultiACE)
+ metric.aces = Base.zero(metric.aces)
+ metric.counts = Base.zero(metric.counts)
+end
+
+hasNDArraySupport(::MultiACE) = Val{false}()
+
+function _update_single_output(metric :: MultiACE, label :: Array{T}, pred :: Array{T}) where T
+ eps = convert(T, metric.eps)
+ # Samples are stored in the last dimension
+ @assert size(label, ndims(label)) == size(pred, ndims(pred))
+ @assert size(metric.aces) == size(metric.counts)
+ if size(label) == size(pred) # simply calculate the cross entropy of the probabilities
+ for k in 1:length(metric.aces)
+ kpred = view(pred, ntuple(d->:, ndims(pred) - 2)..., k, :)
+ klabel = view(label, ntuple(d->:, ndims(label) - 2)..., k, :)
+ for (q, p) in zip(kpred, klabel)
+ # p == true probability
+ # q == "unnatural" probability
+ metric.aces[k] += p * log(q + eps)
+ metric.counts[k] += 1
+ end
+ end
+ elseif ndims(pred) == 4
+ labels = reshape(label, size(pred, 1, 2)..., 1, size(pred, 4))
+ for sample in 1:size(labels, 4)
+ for j in 1:size(labels, 2)
+ for i in 1:size(labels, 1)
+ # Cross-entropy reduces to -(ln(p_1)*0 + ln(p_2)*1) for classification
+ # Since we can only target labels right now this is the only thing we can do.
+ target = Int(labels[i, j, 1, sample]) + 1 # klasses are 0...k-1 => julia indexing
+ p_k = pred[i, j, target, sample]
+
+ metric.aces[target] += log(p_k + eps)
+ metric.counts[target] += 1
+ end
+ end
+ end
+ elseif ndims(pred) == 2
+ for sample in 1:size(label, 1)
+ target = Int(label[sample]) + 1
+ p_k = pred[target, sample]
+ metric.aces[target] += log(p_k + eps)
+ metric.counts[target] += 1
+ end
+ else
+ error("Can't handle prediction with dimensions $(ndims(pred)).")
+ end
+end
diff --git a/julia/src/model.jl b/julia/src/model.jl
new file mode 100644
index 000000000000..109cb35e38a6
--- /dev/null
+++ b/julia/src/model.jl
@@ -0,0 +1,671 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ AbstractModel
+
+The abstract super type of all models in MXNet.jl.
+"""
+abstract type AbstractModel end
+
+"""
+ FeedForward
+
+The feedforward model provides convenient interface to train and predict on
+feedforward architectures like multi-layer MLP, ConvNets, etc. There is no
+explicitly handling of *time index*, but it is relatively easy to implement
+unrolled RNN / LSTM under this framework (*TODO*: add example). For models
+that handles sequential data explicitly, please use *TODO*...
+"""
+mutable struct FeedForward <: AbstractModel
+ arch :: SymbolicNode
+ ctx :: Vector{Context}
+
+ arg_params :: Dict{Symbol}
+ aux_params :: Dict{Symbol}
+
+ pred_exec :: Union{Executor,Void}
+
+ # leave the rest fields undefined
+ FeedForward(arch::SymbolicNode, ctx::Vector{Context}) = new(arch, ctx)
+ FeedForward(arch::SymbolicNode, ctx::Context) = new(arch, [ctx])
+end
+
+"""
+Get a split of `batch_size` into `n_split` pieces for data parallelization. Returns a vector
+of length `n_split`, with each entry a `UnitRange{Int}` indicating the slice index for that
+piece.
+"""
+function _split_inputs(batch_size::Int, n_split::Int)
+ @assert(batch_size >= n_split)
+ per_split = floor(Int, batch_size / n_split)
+ counts = Base.zeros(Int, n_split)+per_split
+ extra = batch_size - Base.sum(counts)
+ counts[1:extra] += 1
+
+ cum = [0, cumsum(counts)...]
+ idx = [cum[i-1]+1:cum[i] for i = 2:length(cum)]
+ return idx
+end
+
+"""
+ FeedForward(arch :: SymbolicNode, ctx)
+
+# Arguments:
+* `arch`: the architecture of the network constructed using the symbolic API.
+* `ctx`: the devices on which this model should do computation. It could be a single `Context`
+ or a list of `Context` objects. In the latter case, data parallelization will be used
+ for training. If no context is provided, the default context `cpu()` will be used.
+"""
+FeedForward(arch::SymbolicNode; context::Union{Context,Vector{Context}} = [cpu()]) =
+ FeedForward(arch, context)
+
+"""
+ init_model(self, initializer; overwrite=false, input_shapes...)
+
+Initialize the weights in the model.
+
+This method will be called automatically when training a model. So there is usually no
+need to call this method unless one needs to inspect a model with only randomly initialized
+weights.
+
+# Arguments:
+* `self::FeedForward`: the model to be initialized.
+* `initializer::AbstractInitializer`: an initializer describing how the weights should be initialized.
+* `overwrite::Bool`: keyword argument, force initialization even when weights already exists.
+* `input_shapes`: the shape of all data and label inputs to this model, given as keyword arguments.
+ For example, `data=(28,28,1,100), label=(100,)`.
+"""
+function init_model(self::FeedForward, initializer::AbstractInitializer; overwrite::Bool=false, input_shapes...)
+ # all arg names, including data, label, and parameters
+ arg_names = list_arguments(self.arch)
+
+ input_names = [x[1] for x in input_shapes]
+
+ param_names = setdiff(arg_names, input_names)
+ aux_names = list_auxiliary_states(self.arch)
+
+ arg_shapes, out_shapes, aux_shapes = infer_shape(self.arch; input_shapes...)
+
+ # If target dict is not yet defined set a temporary one
+ if !isdefined(self, :arg_params)
+ self.arg_params = Dict{Symbol, NDArray}()
+ end
+ if !isdefined(self, :aux_params)
+ self.aux_params = Dict{Symbol, NDArray}()
+ end
+
+ arg_params = Dict{Symbol,NDArray}()
+ aux_params = Dict{Symbol,NDArray}()
+
+ for (name, shape) in filter(x -> in(x[1],param_names), zip(arg_names, arg_shapes))
+ if haskey(self.arg_params, name)
+ if shape == size(self.arg_params[name])
+ arg_params[name] = self.arg_params[name]
+ continue
+ else
+ warn("Shape mismatch for $name. Overwriting with new one.")
+ delete!(self.arg_params, name)
+ end
+ end
+ arg_params[name] = empty(shape)
+ end
+
+ for (name, shape) in zip(aux_names, aux_shapes)
+ if haskey(self.aux_params, name)
+ if shape == size(self.aux_params[name])
+ aux_params[name] = self.aux_params[name]
+ continue
+ else
+ warn("Shape mismatch for $name. Overwriting with new one.")
+ delete!(self.aux_params, name)
+ end
+ end
+ aux_params[name] = empty(shape)
+ end
+
+ for (k,v) in arg_params
+ if overwrite || !haskey(self.arg_params, k)
+ init(initializer, k, v)
+ end
+ end
+ for (k,v) in aux_params
+ if overwrite || !haskey(self.aux_params, k)
+ init(initializer, k, v)
+ end
+ end
+
+ self.arg_params = arg_params
+ self.aux_params = aux_params
+
+ return (arg_names, param_names, aux_names)
+end
+
+function _setup_predictor(self::FeedForward, overwrite::Bool=false; verbosity::Integer = 1, data_shapes...)
+ if !isdefined(self, :pred_exec) || isa(self.pred_exec, Void) || overwrite
+ if !isdefined(self, :arg_params) || !isdefined(self, :aux_params)
+ @assert(false, "Model weights not defined, please init or train the model, or load from file")
+ end
+
+ # the predictor use only the first device
+ self.pred_exec = simple_bind(self.arch, self.ctx[1]; grad_req=GRAD_NOP, data_shapes...)
+ dbg_str = mx.debug_str(self.pred_exec)
+ verbosity >= 1 && info(string("TempSpace: ", split(dbg_str, ['\n'])[end-2]..., " on ", self.ctx[1]))
+ copy_params_from(self.pred_exec, self.arg_params, self.aux_params)
+ else
+ # make sure the new setup is compatible with the existing one
+ for (d_name, d_shape) in data_shapes
+ @assert(d_shape == size(self.pred_exec.arg_dict[d_name]),
+ "Shape of $d_name mismatch with existing predictor, use overwrite=true overwrite existing predictor")
+ end
+ end
+end
+
+"""
+ predict(self, data; overwrite=false, callback=nothing)
+
+Predict using an existing model. The model should be already initialized, or trained or loaded from
+a checkpoint. There is an overloaded function that allows to pass the callback as the first argument,
+so it is possible to do
+
+```julia
+predict(model, data) do batch_output
+ # consume or write batch_output to file
+end
+```
+
+# Arguments:
+* `self::FeedForward`: the model.
+* `data::AbstractDataProvider`: the data to perform prediction on.
+* `overwrite::Bool`: an `Executor` is initialized the first time predict is called. The memory
+ allocation of the `Executor` depends on the mini-batch size of the test
+ data provider. If you call predict twice with data provider of the same batch-size,
+ then the executor can be potentially be re-used. So, if `overwrite` is false,
+ we will try to re-use, and raise an error if batch-size changed. If `overwrite`
+ is true (the default), a new `Executor` will be created to replace the old one.
+* `verbosity::Integer`: Determines the verbosity of the print messages. Higher numbers
+ leads to more verbose printing. Acceptable values are
+ - `0`: Do not print anything during prediction
+ - `1`: Print allocation information during prediction
+
+!!! note
+ Prediction is computationally much less costly than training, so the bottleneck sometimes becomes the IO
+ for copying mini-batches of data. Since there is no concern about convergence in prediction, it is better
+ to set the mini-batch size as large as possible (limited by your device memory) if prediction speed is a
+ concern.
+
+ For the same reason, currently prediction will only use the first device even if multiple devices are
+ provided to construct the model.
+
+!!! note
+ If you perform further after prediction. The weights are not automatically synchronized if `overwrite`
+ is set to false and the old predictor is re-used. In this case
+ setting `overwrite` to true (the default) will re-initialize the predictor the next time you call
+ predict and synchronize the weights again.
+
+See also [`train`](@ref), [`fit`](@ref), [`init_model`](@ref), and [`load_checkpoint`](@ref)
+"""
+function predict(callback::Function, self::FeedForward, data::AbstractDataProvider;
+ overwrite::Bool = true, verbosity::Integer = 1)
+ predict(self, data; overwrite = overwrite, callback=callback, verbosity = verbosity)
+end
+function predict(self::FeedForward, data::AbstractDataProvider;
+ overwrite::Bool = true, callback::Union{Function,Void}=nothing, verbosity::Integer = 1)
+ data_shapes = provide_data(data)
+ data_names = [x[1] for x in data_shapes]
+ _setup_predictor(self, overwrite; verbosity = verbosity, data_shapes...)
+
+ batch_size = get_batch_size(data)
+ data_arrays = [self.pred_exec.arg_dict[name] for name in data_names]
+ output_list = [Array{MX_float}[] for i=1:length(self.pred_exec.outputs)]
+ for batch in eachbatch(data)
+ load_data!(data, batch, data_arrays)
+ forward(self.pred_exec, is_train=false)
+ if isa(callback, Void)
+ # no callback, accumulate the data and return at the end
+ for (o_list, o_nd) in zip(output_list, self.pred_exec.outputs)
+ push!(o_list, copy(slice(o_nd, 1:count_samples(data, batch))))
+ end
+ else
+ outputs = self.pred_exec.outputs
+ if length(outputs) == 1
+ outputs = outputs[1]
+ end
+ callback(outputs)
+ end
+ end
+
+ if !isa(callback, Void)
+ # callback exists, do not accumulate data
+ return nothing
+ end
+
+ if isempty(output_list)
+ # maybe model does not have outputs
+ return nothing
+ end
+ if isempty(output_list[1])
+ # maybe no output because data is empty
+ return length(output_list) == 1 ? output_list[1] : output_list
+ end
+
+ # concatenate along mini-batches
+ output_arrays = [cat(ndims(x[1]), x...) for x in output_list]
+ if length(output_arrays) == 1
+ # only 1 output, return it directly, instead of a list
+ output_arrays = output_arrays[1]
+ end
+ return output_arrays
+end
+
+function _init_model(self::FeedForward, data::AbstractDataProvider,
+ initializer::AbstractInitializer, overwrite::Bool)
+ init_model(self, initializer; overwrite=overwrite,
+ [provide_data(data)..., provide_label(data)...]...)
+end
+
+function _create_kvstore(kv_type::Symbol, num_device::Int, arg_params::Dict{Symbol}, verbosity::Int)
+ if num_device == 1 && !ismatch(r"dist", string(kv_type))
+ return nothing
+ else
+ if kv_type == :local
+ max_size = maximum([prod(size(param)) for (k,param) in arg_params])
+ if max_size < 1024 * 1024 * 16
+ kv_type = :local_update_cpu
+ else
+ kv_type = :local_allreduce_cpu
+ end
+ verbosity >= 2 && info("Auto-select kvstore type = $kv_type")
+ end
+ return KVStore(kv_type)
+ end
+end
+
+@defstruct TrainingOptions (
+ initializer :: AbstractInitializer = UniformInitializer(0.01),
+ n_epoch :: Int = 10,
+ eval_data :: Union{Void,AbstractDataProvider} = nothing,
+ eval_metric :: AbstractEvalMetric = Accuracy(),
+ kvstore :: Union{Symbol,KVStore} = :local,
+ force_init :: Bool = false,
+ callbacks :: Vector{AbstractCallback} = AbstractCallback[],
+ verbosity :: Int = 3,
+ η_decay :: Symbol = :epoch,
+)
+
+function _invoke_callbacks(m::FeedForward, callbacks::Vector{AbstractCallback},
+ state::OptimizationState, type_filter::Type;
+ metric = Vector{Tuple{Symbol,Real}}())
+ map(callbacks) do cb
+ !isa(cb, type_filter) && return
+
+ # epoch callback have extra access to the model object
+ type_filter == AbstractEpochCallback && return cb(m, state, metric)
+
+ cb(state)
+ end
+end
+
+"""
+ train(model :: FeedForward, ...)
+
+Alias to [`fit`](@ref).
+"""
+train(m::FeedForward, opt::AbstractOptimizer, data::AbstractDataProvider; kw...) =
+ fit(m, opt, data; kw...)
+
+"""
+ fit(model::FeedForward, optimizer, data; kwargs...)
+
+Train the `model` on `data` with the `optimizer`.
+
+* `model::FeedForward`: the model to be trained.
+* `optimizer::AbstractOptimizer`: the optimization algorithm to use.
+* `data::AbstractDataProvider`: the training data provider.
+* `n_epoch::Int`: default 10, the number of full data-passes to run.
+* `eval_data::AbstractDataProvider`: keyword argument, default `nothing`. The data provider for
+ the validation set.
+* `eval_metric::AbstractEvalMetric`: keyword argument, default [`Accuracy()`](@ref). The metric used
+ to evaluate the training performance. If `eval_data` is provided, the same metric is also
+ calculated on the validation set.
+* `kvstore`: keyword argument, default `:local`. The key-value store used to synchronize gradients
+ and parameters when multiple devices are used for training.
+ :type kvstore: `KVStore` or `Symbol`
+* `initializer::AbstractInitializer`: keyword argument, default `UniformInitializer(0.01)`.
+* `force_init::Bool`: keyword argument, default false. By default, the random initialization using the
+ provided `initializer` will be skipped if the model weights already exists, maybe from a previous
+ call to [`train`](@ref) or an explicit call to [`init_model`](@ref) or [`load_checkpoint`](@ref). When
+ this option is set, it will always do random initialization at the begining of training.
+* `callbacks::Vector{AbstractCallback}`: keyword argument, default `[]`. Callbacks to be invoked at each epoch or mini-batch,
+ see `AbstractCallback`.
+* `verbosity::Int`: Determines the verbosity of the print messages. Higher numbers
+ leads to more verbose printing. Acceptable values are
+ - `0`: Do not print anything during training
+ - `1`: Print starting and final messages
+ - `2`: Print one time messages and a message at the start of each epoch
+ - `3`: Print a summary of the training and validation accuracy for each epoch
+* `η_decay::Symbol`: `:epoch` or `:batch`, decay learning rate on epoch or batch.
+"""
+function fit(self::FeedForward, optimizer::AbstractOptimizer, data::AbstractDataProvider;
+ kwargs...)
+ opts = TrainingOptions(; kwargs...)
+
+ opts.verbosity >= 1 && info("Start training on $(self.ctx)")
+
+ batch_size = get_batch_size(data)
+ num_dev = length(self.ctx)
+ slices = _split_inputs(batch_size, num_dev)
+
+ # initialize parameters
+ opts.verbosity >= 2 && info("Initializing parameters...")
+ arg_names, param_names, aux_names = _init_model(self, data, opts.initializer, opts.force_init)
+
+ # setup kvstore
+ kvstore = opts.kvstore
+ if isa(kvstore, Symbol)
+ opts.verbosity >= 2 && info("Creating KVStore...")
+ kvstore = _create_kvstore(kvstore, length(self.ctx), self.arg_params, opts.verbosity)
+ end
+
+ update_on_kvstore = true
+ if isa(kvstore, Void) || ismatch(r"local_allreduce", string(get_type(kvstore)))
+ update_on_kvstore = false
+ end
+
+ # get grad attribute to allow for freezing
+ freeze_names = Symbol[]
+ for (attr, value) in list_all_attr(self.arch)
+ sattr = string(attr)
+ if endswith(sattr, "grad") && value == "freeze"
+ push!(freeze_names, Symbol(sattr[1:end-5]))
+ end
+ end
+ # Needs to correspond to the correct id in the update loop layer idx=1:length(param_names).
+ freeze_idx = filter(i -> in(param_names[i], freeze_names), 1:length(param_names))
+
+ # Setup grad_req as a dictionary
+ grad_req = Dict{Symbol,GRAD_REQ}()
+ for param in param_names
+ if in(param, freeze_names)
+ grad_req[param] = GRAD_NOP
+ else
+ grad_req[param] = GRAD_WRITE
+ end
+ end
+
+ train_execs = Array{Executor}(num_dev)
+ for i = 1:num_dev
+ data_shapes = Dict(map((x) -> x[1] => tuple(x[2][1:end-1]...,length(slices[i])), provide_data(data)))
+ label_shapes = Dict(map((x) -> x[1] => tuple(x[2][1:end-1]...,length(slices[i])), provide_label(data)))
+ train_execs[i] = simple_bind(self.arch, self.ctx[i]; grad_req=grad_req, data_shapes..., label_shapes...)
+ dbg_str = mx.debug_str(train_execs[i])
+ opts.verbosity >= 2 && info(string("TempSpace: ", split(dbg_str, ['\n'])[end-2]..., " on ", self.ctx[i]))
+
+ copy_params_from(train_execs[i], self.arg_params, self.aux_params)
+ end
+
+ # set up input data structures
+ data_names = [x[1] for x in provide_data(data)]
+ label_names = [x[1] for x in provide_label(data)]
+
+ data_arrays = [SlicedNDArray[(slices[i], exec.arg_dict[name]) for (i,exec) in enumerate(train_execs)]
+ for name in data_names]
+ label_arrays = [SlicedNDArray[(slices[i], exec.arg_dict[name]) for (i,exec) in enumerate(train_execs)]
+ for name in label_names]
+
+ param_idx = filter(i -> in(arg_names[i], param_names), 1:length(arg_names))
+
+ param_arrays = [NDArray[exec.arg_arrays[i] for exec in train_execs] for i in param_idx]
+ grad_arrays = [NDArray[exec.grad_arrays[i] for exec in train_execs] for i in param_idx]
+ aux_arrays = [NDArray[exec.aux_arrays[i] for exec in train_execs] for i = 1:length(aux_names)]
+
+ op_state = OptimizationState(batch_size)
+ # set up the gradient rescaling if user not set
+ iszero(optimizer.scale) && (optimizer.scale = 1 / batch_size)
+
+ if !update_on_kvstore
+ updater = getupdater(optimizer)
+ end
+
+ if !isa(kvstore, Void)
+ if update_on_kvstore
+ set_optimizer(kvstore, optimizer)
+ end
+
+ opts.verbosity >= 2 && info("Initializing KVStore...")
+ # init kv with gradients
+ for idx = 1:length(param_arrays)
+ param_on_devs = param_arrays[idx]
+
+ init!(kvstore, idx, self.arg_params[param_names[idx]])
+
+ if update_on_kvstore
+ # pull weights back
+ pull!(kvstore, idx, param_on_devs, priority=-idx)
+ end
+ end
+ end
+
+ # set up output and labels in CPU for evaluation metric
+ output_shapes = [tuple(size(x)[1:end-1]...,batch_size) for x in train_execs[1].outputs]
+ cpu_dev = Context(CPU)
+ cpu_output_arrays = [empty(shape, cpu_dev) for shape in output_shapes]
+ cpu_label_arrays = [empty(shape, cpu_dev) for (name,shape) in provide_label(data)]
+
+ # invoke callbacks on epoch 0
+ _invoke_callbacks(self, opts.callbacks, op_state, AbstractEpochCallback)
+
+ opts.verbosity >= 2 && info("Start training...")
+ for i_epoch = 1:opts.n_epoch
+ time_start = time()
+ reset!(opts.eval_metric)
+
+ op_state.curr_epoch = i_epoch
+ op_state.curr_batch = 0
+
+ # invoke callbacks on iteration 0
+ _invoke_callbacks(self, opts.callbacks, op_state, AbstractBatchCallback)
+
+ for batch in eachbatch(data)
+ load_data!(data, batch, data_arrays)
+ load_label!(data, batch, label_arrays)
+
+ # forward and backward
+ for (texec, islice) in zip(train_execs, slices)
+ forward(texec, is_train=true)
+
+ # copy outputs into cpu ndarray, for evaluation metric
+ for (cpu_out, dev_out) in zip(cpu_output_arrays, texec.outputs)
+ copy!(slice(cpu_out, islice), dev_out)
+ end
+
+ backward(texec)
+ end
+
+ op_state.curr_iter += 1
+ op_state.curr_batch += 1
+
+ # update parameters
+ for idx = 1:length(param_names)
+ if in(idx, freeze_idx)
+ continue # Skip parameter update entirely
+ end
+
+ # gradient synchronization
+ if !isa(kvstore, Void)
+ # push gradient, priority is negative index
+ push!(kvstore, idx, grad_arrays[idx], priority=-idx)
+ if update_on_kvstore
+ # pull back the weights
+ pull!(kvstore, idx, param_arrays[idx], priority=-idx)
+ else
+ # pull back the sum-ed gradients, to the same locations
+ pull!(kvstore, idx, grad_arrays[idx], priority=-idx)
+ end
+ end
+
+ if !update_on_kvstore
+ # manual updating
+ for i_dev = 1:num_dev
+ # create a fake index, so that the updater create states
+ # for different param AND different devices, TODO(mli)
+ # use a better solution later
+ fake_idx = idx * num_dev + i_dev
+ updater(fake_idx, grad_arrays[idx][i_dev], param_arrays[idx][i_dev])
+ end
+ end
+ end
+
+ # trigger learning rate decay
+ opts.η_decay == :batch && update!(optimizer.η_sched)
+
+ # invoke callbacks after finishing each iteration
+ _invoke_callbacks(self, opts.callbacks, op_state, AbstractBatchCallback)
+
+ # update evaluation metric on training set
+ load_label!(data, batch, cpu_label_arrays)
+ update!(opts.eval_metric, cpu_label_arrays, cpu_output_arrays)
+ end # end of one epoch
+
+ time_stop = time()
+ metric = get(opts.eval_metric)
+ opts.verbosity >= 2 && info(format("== Epoch {1:0>3d}/{2:0>3d} ==========", i_epoch, opts.n_epoch))
+ if opts.verbosity >= 3
+ info("## Training summary")
+ for (name, value) in metric
+ info(format("{1:>18s} = {2:.4f}", string(name), value))
+ end
+ info(format("{1:>18s} = {2:.4f} seconds", "time", time_stop-time_start))
+ end
+
+ # evaluation on validation set
+ if !isa(opts.eval_data, Void)
+ # because we are re-using the memory allocated for the training network,
+ # the batch_size of the validation dataset must be the same as the training
+ # batch_size
+ @assert(get_batch_size(opts.eval_data) == batch_size)
+
+ reset!(opts.eval_metric)
+ for batch in eachbatch(opts.eval_data)
+ load_data!(opts.eval_data, batch, data_arrays)
+
+ # forward and backward
+ for (texec, islice) in zip(train_execs, slices)
+ forward(texec, is_train=true)
+
+ # copy outputs into cpu ndarray, for evaluation metric
+ for (cpu_out, dev_out) in zip(cpu_output_arrays, texec.outputs)
+ copy!(slice(cpu_out, islice), dev_out)
+ end
+ end
+ load_label!(opts.eval_data, batch, cpu_label_arrays)
+ update!(opts.eval_metric, cpu_label_arrays, cpu_output_arrays)
+ end
+
+ if opts.verbosity >= 3
+ info("## Validation summary")
+ for (name, value) in get(opts.eval_metric)
+ info(format("{1:>18s} = {2:.4f}", string(name), value))
+ end
+ end
+ end
+
+ if i_epoch == opts.n_epoch || any(x->isa(x, AbstractEpochCallback), opts.callbacks)
+ # copy data back to cpu
+ for (name, weights) in zip(param_names, param_arrays)
+ # average parameters across devices
+ weight = +([copy(w, cpu()) for w in weights]...) / length(weights)
+ copy!(self.arg_params[name], weight)
+ end
+ for (name, aux_devs) in zip(aux_names, aux_arrays)
+ aux_avg = +([copy(aux, cpu()) for aux in aux_devs]...) / length(aux_devs)
+ copy!(self.aux_params[name], aux_avg)
+ end
+ end
+
+ # trigger learning rate decay
+ opts.η_decay == :epoch && update!(optimizer.η_sched)
+
+ _invoke_callbacks(self, opts.callbacks, op_state, AbstractEpochCallback; metric=metric)
+ end # end of all epochs
+
+ opts.verbosity >= 1 && info("Finish training on $(self.ctx)")
+ nothing
+end
+
+save_checkpoint(self::FeedForward, prefix::AbstractString, state::OptimizationState) =
+ save_checkpoint(self.arch, self.arg_params, self.aux_params, prefix, state.curr_epoch)
+
+function save_checkpoint(sym::SymbolicNode, arg_params::Dict{Symbol},
+ aux_params::Dict{Symbol}, prefix::AbstractString, epoch::Int)
+ save("$prefix-symbol.json", sym)
+ save_dict = Dict{Symbol, NDArray}(map((x) -> Symbol("arg:$(x[1])") => x[2], arg_params))
+ if !isempty(aux_params)
+ merge!(save_dict, Dict(map((x) -> Symbol("aux:$(x[1])") => x[2], aux_params)))
+ end
+ save_filename = format("{1}-{2:04d}.params", prefix, epoch)
+ save(save_filename, save_dict)
+ info("Saved checkpoint to '$save_filename'")
+end
+
+function load_checkpoint(prefix::AbstractString, epoch::Int)
+ arch = load("$prefix-symbol.json", SymbolicNode)
+ saved_dict = load(format("{1}-{2:04d}.params", prefix, epoch), NDArray)
+ arg_params = Dict{Symbol,Any}()
+ aux_params = Dict{Symbol,Any}()
+ for (k,v) in saved_dict
+ tp, name = split(string(k), ':')
+ name = Symbol(name)
+ if tp == "arg"
+ arg_params[name] = v
+ else
+ aux_params[name] = v
+ end
+ end
+
+ return (arch, arg_params, aux_params)
+end
+
+"""
+ load_checkpoint(prefix, epoch, ::mx.FeedForward; context)
+
+Load a mx.FeedForward model from the checkpoint *prefix*, *epoch* and optionally provide a context.
+"""
+function load_checkpoint(prefix::AbstractString, epoch::Int, ::Type{FeedForward}; context = nothing)
+ arch, arg_params, aux_params = load_checkpoint(prefix, epoch)
+ model = FeedForward(arch, context = context)
+ model.arg_params = arg_params
+ model.aux_params = aux_params
+ return model
+end
+
+function load_checkpoint(self::FeedForward, prefix::AbstractString, epoch::Int;
+ overwrite::Bool = true, allow_different_arch::Bool = false)
+ if isdefined(self, :arg_params) && isdefined(self, :aux_params) && !overwrite
+ info("model weights already exists, skip loading... (call with overwrite=true if needed)")
+ return self
+ end
+
+ arch, arg_params, aux_params = load_checkpoint(prefix, epoch)
+ if !allow_different_arch
+ # TODO: is there better way to compare two symbols
+ @assert(to_json(self.arch) == to_json(arch), "Cannot load from a checkpoint with different network architecture")
+ end
+ self.arg_params = arg_params
+ self.aux_params = aux_params
+ return self
+end
diff --git a/julia/src/name.jl b/julia/src/name.jl
new file mode 100644
index 000000000000..8180886c869c
--- /dev/null
+++ b/julia/src/name.jl
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+abstract type AbstractNameManager end
+const NameType = Union{Base.Symbol, AbstractString}
+const NameCounter = Dict{Base.Symbol, Int}
+
+import Base: get!
+
+# Default implementation for generating a name for a symbol.
+# When a name is specified by the user, it will be used. Otherwise, a name
+# is automatically generated based on the hint string.
+function _default_get_name!(counter :: NameCounter, name :: NameType, hint :: NameType)
+ if isa(name, Base.Symbol) || !isempty(name)
+ return Symbol(name)
+ end
+
+ hint = Symbol(hint)
+ if !haskey(counter, hint)
+ counter[hint] = 0
+ end
+ name = Symbol("$hint$(counter[hint])")
+ counter[hint] += 1
+ return name
+end
+
+mutable struct BasicNameManager <: AbstractNameManager
+ counter :: NameCounter
+end
+BasicNameManager() = BasicNameManager(NameCounter())
+
+function get!(manager :: BasicNameManager, name :: NameType, hint :: NameType)
+ _default_get_name!(manager.counter, name, hint)
+end
+
+mutable struct PrefixNameManager <: AbstractNameManager
+ prefix :: Base.Symbol
+ counter :: NameCounter
+end
+PrefixNameManager(prefix :: NameType) = PrefixNameManager(Symbol(prefix), NameCounter())
+
+function get!(manager :: PrefixNameManager, name :: NameType, hint :: NameType)
+ name = _default_get_name!(manager.counter, name, hint)
+ return Symbol("$(manager.prefix)$name")
+end
+
+DEFAULT_NAME_MANAGER = BasicNameManager()
diff --git a/julia/src/ndarray.jl b/julia/src/ndarray.jl
new file mode 100644
index 000000000000..9e47150a1a00
--- /dev/null
+++ b/julia/src/ndarray.jl
@@ -0,0 +1,1780 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# All the types supported by mshadow. See `mshadow/base.h`
+const DType = Union{Float32, Float64, Float16, UInt8, Int32, Int8, Int64}
+@enum TypeFlag kFloat32 kFloat64 kFloat16 kUint8 kInt32 kInt8 kInt64
+const DEFAULT_DTYPE = Float32 # MSHADOW_DEFAULT_DTYPE
+
+function toTypeFlag(T::Type{<:DType})
+ if T == Float32
+ return kFloat32
+ elseif T == Float64
+ return kFloat64
+ elseif T == Float16
+ return kFloat16
+ elseif T == UInt8
+ return kUint8
+ elseif T == Int32
+ return kInt32
+ elseif T == Int8
+ return kInt8
+ elseif T == Int64
+ return kInt64
+ else
+ throw(ArgumentError("Can't convert $T to DType."))
+ end
+end
+
+function fromTypeFlag(T::TypeFlag)
+ if T == kFloat32
+ return Float32
+ elseif T == kFloat64
+ return Float64
+ elseif T == kFloat16
+ return Float16
+ elseif T == kUint8
+ return UInt8
+ elseif T == kInt32
+ return Int32
+ elseif T == kInt8
+ return Int8
+ elseif T == kInt64
+ return Int64
+ else
+ throw(ArgumentError("Can't convert DType $T."))
+ end
+end
+
+# create a NDArray handle of specific shape
+function _ndarray_alloc(shape :: NTuple{N, Int}, ctx :: Context, delay_alloc :: Bool) where N
+ h_ref = Ref{MX_handle}(0)
+ shape = flipdim(MX_uint[shape...],1)
+ @mxcall(:MXNDArrayCreate, (Ptr{MX_uint}, MX_uint, Cint, Cint, Cint, Ref{MX_handle}),
+ shape, length(shape), ctx.device_type, ctx.device_id, delay_alloc, h_ref)
+ handle = MX_NDArrayHandle(h_ref[])
+ return handle
+end
+
+# create a NDArray handle of specific shape type
+function _ndarray_alloc(:: Type{T}, shape :: NTuple{N, Int}, ctx :: Context, delay_alloc :: Bool) where {T <: DType,N}
+ h_ref = Ref{MX_handle}(0)
+ shape = flipdim(MX_uint[shape...],1)
+ dtype = toTypeFlag(T)
+ @mxcall(:MXNDArrayCreateEx, (Ptr{MX_uint}, MX_uint, Cint, Cint, Cint, Cint, Ref{MX_handle}),
+ shape, length(shape), ctx.device_type, ctx.device_id, delay_alloc, dtype, h_ref)
+ handle = MX_NDArrayHandle(h_ref[])
+ return handle
+end
+
+# create a handle to an empty NDArray, this handle can be used to hold
+# results returned by libmx API calls
+function _ndarray_alloc()
+ h_ref = Ref{MX_handle}(0)
+ @mxcall(:MXNDArrayCreateNone, (Ref{MX_handle},), h_ref)
+ return MX_NDArrayHandle(h_ref[])
+end
+
+################################################################################
+# NDArray Type
+################################################################################
+"""
+ NDArray{T,N}
+
+Wrapper of the `NDArray` type in `libmxnet`. This is the basic building block
+of tensor-based computation.
+
+!!! note
+ since C/C++ use row-major ordering for arrays while Julia follows a
+ column-major ordering. To keep things consistent, we keep the underlying data
+ in their original layout, but use *language-native* convention when we talk
+ about shapes. For example, a mini-batch of 100 MNIST images is a tensor of
+ C/C++/Python shape (100,1,28,28), while in Julia, the same piece of memory
+ have shape (28,28,1,100).
+"""
+mutable struct NDArray{T,N}
+ handle :: MX_NDArrayHandle
+ writable :: Bool
+
+ NDArray{T,N}(handle, writable = true) where {T,N} = new(handle, writable)
+end
+
+NDArray(x::AbstractArray{T}) where {T<:DType} = copy(collect(x), cpu())
+NDArray(x::Array{T}) where {T<:DType} = copy(x, cpu())
+NDArray(::Type{T}, x::AbstractArray) where {T<:DType} =
+ copy(convert(AbstractArray{T}, x), cpu())
+NDArray(handle, writable = true) =
+ NDArray{eltype(handle), ndims(handle)}(handle, writable)
+
+# type aliases
+const NDArrayOrReal = Union{NDArray, Real}
+const VecOfNDArray = AbstractVector{<:NDArray}
+
+@unfuse NDArray
+
+function Base.show(io::IO, x::NDArray)
+ print(io, "NDArray ")
+ Base.showarray(io, try_get_shared(x, sync = :read), header = false)
+end
+
+# for REPL
+function Base.show(io::IO, ::MIME{Symbol("text/plain")}, x::NDArray{T, N}) where {T, N}
+ type_ = split(string(typeof(x)), '.', limit=2)[end]
+ size_ = N == 1 ? "$(length(x))-element" : join(size(x), "×")
+ println(io, "$size_ $type_ @ $(context(x)):")
+ Base.showarray(io, try_get_shared(x, sync = :read), false, header = false)
+end
+
+Base.unsafe_convert(::Type{MX_handle}, obj::NDArray) =
+ Base.unsafe_convert(MX_handle, obj.handle)
+Base.convert(T::Type{MX_handle}, obj::NDArray) = Base.unsafe_convert(T, obj)
+Base.cconvert(T::Type{MX_handle}, obj::NDArray) = Base.unsafe_convert(T, obj)
+
+################################################################################
+# NDArray functions exported to the users
+################################################################################
+"""
+ context(arr::NDArray)
+
+Get the context that this `NDArray` lives on.
+"""
+function context(arr::NDArray)
+ ref_typeid = Ref{Cint}(0)
+ ref_devid = Ref{Cint}(0)
+ @mxcall(:MXNDArrayGetContext, (MX_handle, Ref{Cint}, Ref{Cint}),
+ arr, ref_typeid, ref_devid)
+ return Context(ref_typeid[], ref_devid[])
+end
+
+"""
+ empty(DType, dims[, ctx::Context = cpu()])
+ empty(DType, dims)
+ empty(DType, dim1, dim2, ...)
+
+Allocate memory for an uninitialized `NDArray` with a specified type.
+"""
+empty(::Type{T}, dims::NTuple{N,Int}, ctx::Context = cpu()) where {N,T<:DType} =
+ NDArray{T, N}(_ndarray_alloc(T, dims, ctx, false))
+empty(::Type{T}, dims::Int...) where {T<:DType} = empty(T, dims)
+
+"""
+ empty(dims::Tuple[, ctx::Context = cpu()])
+ empty(dim1, dim2, ...)
+
+Allocate memory for an uninitialized `NDArray` with specific shape of type Float32.
+"""
+empty(dims::NTuple{N,Int}, ctx::Context = cpu()) where N =
+ NDArray(_ndarray_alloc(dims, ctx, false))
+empty(dims::Int...) = empty(dims)
+
+"""
+ similar(x::NDArray)
+
+Create an `NDArray` with similar shape, data type,
+and context with the given one.
+Note that the returned `NDArray` is uninitialized.
+"""
+Base.similar(x::NDArray{T}) where {T} = empty(T, size(x), context(x))
+
+"""
+ zeros([DType], dims, [ctx::Context = cpu()])
+ zeros([DType], dims...)
+ zeros(x::NDArray)
+
+Create zero-ed `NDArray` with specific shape and type.
+"""
+function zeros(::Type{T}, dims::NTuple{N,Int}, ctx::Context = cpu()) where {N,T<:DType}
+ arr = empty(T, dims, ctx)
+ arr[:] = zero(T)
+ arr
+end
+
+zeros(::Type{T}, dims::Int...) where {T<:DType} = zeros(T, dims)
+
+zeros(dims::NTuple{N,Int}, ctx::Context = cpu()) where N =
+ zeros(MX_float, dims, ctx)
+zeros(dims::Int...) = zeros(dims)
+
+zeros(x::NDArray)::typeof(x) = zeros_like(x)
+Base.zeros(x::NDArray)::typeof(x) = zeros_like(x)
+
+"""
+ ones([DType], dims, [ctx::Context = cpu()])
+ ones([DType], dims...)
+ ones(x::NDArray)
+
+Create an `NDArray` with specific shape & type, and initialize with 1.
+"""
+function ones(::Type{T}, dims::NTuple{N,Int}, ctx::Context = cpu()) where {N,T<:DType}
+ arr = empty(T, dims, ctx)
+ arr[:] = one(T)
+ arr
+end
+
+ones(::Type{T}, dims::Int...) where T<:DType = ones(T, dims)
+
+ones(dims::NTuple{N,Int}, ctx::Context = cpu()) where N =
+ ones(MX_float, dims, ctx)
+ones(dims::Int...) = ones(dims)
+
+ones(x::NDArray)::typeof(x) = ones_like(x)
+Base.ones(x::NDArray)::typeof(x) = ones_like(x)
+
+import Base: size, length, ndims, eltype
+
+"""
+ size(x::NDArray)
+ size(x::NDArray, dims...)
+
+Get the shape of an `NDArray`. The shape is in Julia's column-major convention.
+See also the notes on NDArray shapes [`NDArray`](@ref).
+"""
+function size(x::NDArray)
+ ref_ndim = Ref{MX_uint}(0)
+ ref_shape = Ref{Ptr{MX_uint}}(0)
+ @mxcall(:MXNDArrayGetShape, (MX_handle, Ref{MX_uint}, Ref{Ptr{MX_uint}}),
+ x, ref_ndim, ref_shape)
+ tuple(map(Int, flipdim(unsafe_wrap(Array, ref_shape[], ref_ndim[]),1))...)
+end
+
+function size(x::NDArray{T,N}, dim::Int) where {T,N}
+ if dim > N
+ 1
+ else
+ size(x)[dim]
+ end
+end
+
+size(x::NDArray, dims::Int...) = map(d -> size(x, d), dims)
+
+"""
+ length(x::NDArray)
+
+Get the number of elements in an `NDArray`.
+"""
+length(x::NDArray) = prod(size(x))
+
+"""
+ ndims(x::NDArray)
+
+Get the number of dimensions of an `NDArray`.
+Is equivalent to `length(size(arr))`.
+"""
+ndims(x::NDArray) = ndims(x.handle)
+
+function ndims(x::MX_NDArrayHandle)::Int
+ ref_ndim = Ref{MX_uint}(0)
+ ref_shape = Ref{Ptr{MX_uint}}(0)
+ @mxcall(:MXNDArrayGetShape, (MX_handle, Ref{MX_uint}, Ref{Ptr{MX_uint}}),
+ x, ref_ndim, ref_shape)
+ ref_ndim[]
+end
+
+"""
+ eltype(x::NDArray)
+
+Get the element type of an `NDArray`.
+"""
+function eltype(x::Union{NDArray, MX_NDArrayHandle})
+ dtype_ref = Ref{Cint}(0)
+ @mxcall(:MXNDArrayGetDType, (MX_handle, Ptr{Cint}), x, dtype_ref)
+
+ if dtype_ref[] == -1 # x->is_none()
+ warn("Eltype of $x is not defined")
+ Base.show_backtrace(STDOUT, backtrace())
+ println()
+ Float32
+ else
+ fromTypeFlag(TypeFlag(dtype_ref[]))
+ end
+end
+
+@inline _first(x::NDArray) = try_get_shared(x, sync = :read) |> first
+
+Base.first(x::NDArray) = _first(x)
+
+Base.endof(x::NDArray) = length(x)
+
+"""
+ slice(arr :: NDArray, start:stop)
+
+Create a view into a sub-slice of an `NDArray`. Note only slicing at the slowest
+changing dimension is supported. In Julia's column-major perspective, this is the last
+dimension. For example, given an `NDArray` of shape (2,3,4), `slice(array, 2:3)` will create
+a `NDArray` of shape (2,3,2), sharing the data with the original array. This operation is
+used in data parallelization to split mini-batch into sub-batches for different devices.
+"""
+function slice(arr::NDArray, ::Colon)
+ arr
+end
+function slice(arr::NDArray, slice::UnitRange{Int})
+ dim1 = size(arr)[end]
+ @assert(1 <= slice.start <= slice.stop <= dim1)
+ if slice.start == 1 && slice.stop == dim1
+ return arr
+ end
+
+ hdr_ref = Ref{MX_handle}(0)
+ # note Julia is 1-based, inclusive-inclusive indexing, while C++ is
+ # 0-based, inclusive-exclusive indexing. So 1:3 in Julia should
+ # translates into 0:3 in C++.
+ @mxcall(:MXNDArraySlice, (MX_handle, MX_uint, MX_uint, Ref{MX_handle}),
+ arr, slice.start-1, slice.stop, hdr_ref)
+ return NDArray(MX_NDArrayHandle(hdr_ref[]), arr.writable)
+end
+
+function _at(handle::Union{MX_NDArrayHandle, MX_handle}, idx::Integer)
+ h_ref = Ref{MX_handle}(C_NULL)
+ @mxcall(:MXNDArrayAt, (MX_handle, MX_uint, Ref{MX_handle}),
+ handle, idx, h_ref)
+ h_ref[]
+end
+
+import Base: setindex!
+
+"""
+ setindex!(arr::NDArray, val, idx)
+
+Assign values to an `NDArray`.
+The following scenarios are supported
+
+* single value assignment via linear indexing: `arr[42] = 24`
+
+* `arr[:] = val`: whole array assignment, `val` could be a scalar or an array (Julia `Array`
+ or `NDArray`) of the same shape.
+* `arr[start:stop] = val`: assignment to a *slice*, `val` could be a scalar or an array of
+ the same shape to the slice. See also [`slice`](@ref).
+"""
+function setindex!(arr::NDArray, val::Real, idx::Integer)
+ # linear indexing
+ @assert arr.writable
+ _set_value(out=arr[idx], src=val)
+end
+
+function setindex!(arr::NDArray, val::Real, ::Colon)
+ @assert arr.writable
+ _set_value(out = arr, src = dump_mx_param(val))
+end
+
+function setindex!(arr::NDArray, val::Array{T}, ::Colon) where T<:Real
+ @assert arr.writable
+ copy!(arr, val)
+end
+
+function setindex!(arr::NDArray, val::NDArray, ::Colon)
+ @assert arr.writable
+ copy!(arr, val)
+end
+
+function setindex!(arr::NDArray, val::Union{T,Array{T},NDArray},
+ idx::UnitRange{Int}) where T<:Real
+ @assert arr.writable
+ setindex!(slice(arr, idx), val, Colon())
+end
+
+import Base: getindex
+"""
+ getindex(arr::NDArray, idx)
+
+Shortcut for [`slice`](@ref). A typical use is to write
+
+```julia
+ arr[:] += 5
+```
+
+which translates into
+
+```julia
+ arr[:] = arr[:] + 5
+```
+
+which furthur translates into
+
+```julia
+ setindex!(getindex(arr, Colon()), 5, Colon())
+```
+
+!!! note
+ The behavior is quite different from indexing into Julia's `Array`. For example, `arr[2:5]`
+ create a **copy** of the sub-array for Julia `Array`, while for `NDArray`, this is
+ a *slice* that shares the memory.
+"""
+getindex(arr::NDArray, ::Colon) = arr
+
+"""
+Shortcut for [`slice`](@ref).
+**NOTE** the behavior for Julia's built-in index slicing is to create a
+copy of the sub-array, while here we simply call `slice`,
+which shares the underlying memory.
+"""
+getindex(arr::NDArray, idx::UnitRange{Int}) = slice(arr, idx)
+
+getindex(arr::NDArray) = _first(arr)
+
+function getindex(arr::NDArray, idx::Integer)
+ # linear indexing
+ len = length(arr)
+ size_ = size(arr)
+
+ if idx <= 0 || idx > len
+ throw(BoundsError(
+ "attempt to access $(join(size_, 'x')) NDArray at index $(idx)"))
+ end
+
+ idx -= 1
+ offsets = size_[1:end-1] |> reverse ∘ cumprod ∘ collect
+ handle = arr.handle
+ for offset ∈ offsets
+ handle = _at(handle, idx ÷ offset)
+ idx %= offset
+ end
+
+ _at(handle, idx) |> MX_NDArrayHandle |> x -> NDArray(x, arr.writable)
+end
+
+import Base: copy!, copy, convert, deepcopy
+
+"""
+ copy!(dst::Union{NDArray, Array}, src::Union{NDArray, Array})
+
+Copy contents of `src` into `dst`.
+"""
+function copy!(dst::NDArray, src::NDArray)
+ @assert(dst.writable)
+ if dst.handle == src.handle
+ warn("Copying an NDArray to itself")
+ return
+ end
+
+ _copyto(src, out=dst)
+ return dst
+end
+
+function copy!(dst::Array{T}, src::NDArray{T}) where T<:DType
+ @assert size(dst) == size(src)
+ @mxcall(:MXNDArraySyncCopyToCPU, (MX_handle, Ptr{Void}, Csize_t),
+ src, pointer(dst), length(dst))
+ dst
+end
+
+copy!(dst::Array{<:Real}, src::NDArray) = copy!(dst, copy(src))
+copy!(dst::NDArray, src::AbstractArray) = copy!(dst, collect(src))
+
+function copy!(dst::NDArray{T}, src::Array{<:Real}) where {T}
+ @assert dst.writable
+ @assert size(dst) == size(src)
+ src = convert(Array{T}, src) # this might involve copying
+ @mxcall(:MXNDArraySyncCopyFromCPU, (MX_handle, Ptr{Void}, Csize_t),
+ dst.handle, pointer(src), length(src))
+ dst
+end
+
+function copy_ignore_shape!(dst::NDArray{T}, src::Array{<:Real}) where {T}
+ @assert dst.writable
+ @assert length(dst) == length(src)
+ src = convert(Array{T}, src) # this might involve copying
+ @mxcall(:MXNDArraySyncCopyFromCPU, (MX_handle, Ptr{Void}, Csize_t),
+ dst.handle, pointer(src), length(src))
+ dst
+end
+
+
+"""
+ copy(arr :: NDArray)
+ copy(arr :: NDArray, ctx :: Context)
+ copy(arr :: Array, ctx :: Context)
+
+Create a copy of an array. When no `Context` is given, create a Julia `Array`.
+Otherwise, create an `NDArray` on the specified context.
+"""
+# Create copy: NDArray -> Julia Array
+copy(x::NDArray{T,D}) where{T,D} = copy!(Array{T,D}(size(x)), x)
+
+# Create copy: NDArray -> NDArray in a given context
+copy(x::NDArray{T,D}, ctx::Context) where {T,D} =
+ copy!(NDArray{T,D}(_ndarray_alloc(T, size(x), ctx, true)), x)
+
+# Create copy: Julia Array -> NDArray in a given context
+copy(x::Array{T}, ctx::Context) where {T<:DType} =
+ copy!(empty(T, size(x), ctx), x)
+
+copy(x::AbstractArray, ctx::Context) =
+ copy!(empty(eltype(x), size(x), ctx), collect(x))
+
+"""
+ convert(::Type{Array{<:Real}}, x::NDArray)
+
+Convert an `NDArray` into a Julia `Array` of specific type.
+Data will be copied.
+"""
+convert(T::Type{Array{<:Real}}, x::NDArray) = convert(T, copy(x))
+
+"""
+ deepcopy(arr::NDArray)
+
+Get a deep copy of the data blob in the form of an NDArray of default storage
+type. This function blocks. Do not use it in performance critical code.
+"""
+function deepcopy(arr::NDArray)
+ out_ref = Ref{MX_handle}(C_NULL)
+ @mxcall(:MXNDArrayGetDataNDArray, (MX_handle, Ref{MX_handle}), arr, out_ref)
+ NDArray(MX_NDArrayHandle(out_ref[]))
+end
+
+"""
+ hcat(x::NDArray...)
+"""
+Base.hcat(xs::NDArray{T}...) where T = cat(2, xs...)
+
+"""
+ vcat(x::NDArray...)
+"""
+Base.vcat(xs::NDArray{T}...) where T = cat(1, xs...)
+
+"""
+ cat(dim, xs::NDArray...)
+
+Concate the `NDArray`s which have the same element type along the `dim`.
+Building a diagonal matrix is not supported yet.
+"""
+function Base.cat(dim::Int, xs::NDArray{T}...) where T
+ ns = ndims.(xs)
+ d = Base.max(dim, maximum(ns))
+ xs′ = map(zip(ns, xs)) do i
+ n, x = i
+ (d > n) ? reshape(x, -2, Base.ones(Int, d - n)...) : x
+ end
+ concat(xs′..., dim = d - dim)
+end
+
+"""
+ @inplace
+
+Julia does not support re-definiton of `+=` operator (like `__iadd__` in python),
+When one write `a += b`, it gets translated to `a = a+b`. `a+b` will allocate new
+memory for the results, and the newly allocated `NDArray` object is then assigned
+back to a, while the original contents in a is discarded. This is very inefficient
+when we want to do inplace update.
+
+This macro is a simple utility to implement this behavior. Write
+
+```julia
+ @mx.inplace a += b
+```
+
+will translate into
+
+```julia
+ mx.add_to!(a, b)
+```
+
+which will do inplace adding of the contents of `b` into `a`.
+"""
+macro inplace(ex)
+ f = if ex.head == :+= || ex.head == :.+=
+ :add_to!
+ elseif ex.head == :-= || ex.head == :.-=
+ :sub_from!
+ elseif ex.head == :.*=
+ :mul_to!
+ elseif ex.head == :./=
+ :div_from!
+ elseif ex.head == :.%=
+ :mod_from!
+ else
+ error("unsupported inplace translation for $ex")
+ end
+ Expr(:call, f, esc(ex.args[1]), esc(ex.args[2]))
+end
+
+"""
+ add_to!(dst::NDArray, args::NDArrayOrReal...)
+
+Add a bunch of arguments into `dst`. Inplace updating.
+"""
+function add_to!(dst::NDArray, args::NDArrayOrReal...)
+ @assert dst.writable
+ for arg in args
+ if isa(arg, Real)
+ _plus_scalar(dst, scalar = arg, out = dst)
+ else
+ _plus!(dst, arg)
+ end
+ end
+ dst
+end
+
+import Base: +
+
+"""
+ +(args...)
+ .+(args...)
+
+Summation. Multiple arguments of either scalar or `NDArray` could be
+added together. Note at least the first or second argument needs to be an
+`NDArray` to avoid ambiguity of built-in summation.
+"""
++(x::NDArray) = x
++(x::NDArray, y::NDArray) = _plus(x, y)
++(x::NDArray, y::Real) = _plus_scalar(x, scalar = y)
++(y::Real, x::NDArray) = _plus_scalar(x, scalar = y)
+
+broadcast_(::typeof(+), x::NDArray, y::Real) = x + y
+broadcast_(::typeof(+), x::Real, y::NDArray) = x + y
+
+broadcast_(::typeof(+), x::NDArray{T,N}, y::NDArray{T,N}) where {T,N} = x + y
+broadcast_(::typeof(+), x::NDArray{T,N}, y::NDArray{T,M}) where {T,N,M} =
+ _broadcast_add(x, y)
+
+"""
+ sub_from!(dst::NDArray, args::NDArrayOrReal...)
+
+Subtract a bunch of arguments from `dst`. Inplace updating.
+"""
+function sub_from!(dst::NDArray, arg::NDArrayOrReal)
+ @assert dst.writable
+ if isa(arg, Real)
+ _minus_scalar(dst, scalar = arg, out = dst)
+ else
+ _minus!(dst, arg)
+ end
+ dst
+end
+
+import Base: -
+
+"""
+ -(x::NDArray)
+ -(x, y)
+ .-(x, y)
+
+Subtraction `x - y`, of scalar types or `NDArray`.
+Or create the negative of `x`.
+"""
+-(x::NDArray) = _mul_scalar(x, scalar = -one(eltype(x)))
+-(x::NDArray, y::NDArray) = _minus(x, y)
+-(x::NDArray, y::Real) = _minus_scalar(x, scalar = y)
+-(y::Real, x::NDArray) = _rminus_scalar(x, scalar = y)
+
+broadcast_(::typeof(-), x::NDArray, y::Real) = x - y
+broadcast_(::typeof(-), x::Real, y::NDArray) = x - y
+
+broadcast_(::typeof(-), x::NDArray{T,N}, y::NDArray{T,N}) where {T,N} = x - y
+broadcast_(::typeof(-), x::NDArray{T,N}, y::NDArray{T,M}) where {T,N,M} =
+ _broadcast_minus(x, y)
+
+"""
+ mul_to!(dst::NDArray, arg::NDArrayOrReal)
+
+Elementwise multiplication into `dst` of either a scalar or an `NDArray` of the same shape.
+Inplace updating.
+"""
+function mul_to!(dst::NDArray, arg::NDArrayOrReal)
+ @assert dst.writable
+ if isa(arg, Real)
+ _mul_scalar(dst, scalar = arg, out = dst)
+ else
+ _mul(dst, arg, out = dst)
+ end
+ dst
+end
+
+import Base: *
+
+"""
+ .*(x, y)
+
+Elementwise multiplication for `NDArray`.
+"""
+*(x::NDArray, y::Real) = _mul_scalar(x, scalar = y)
+*(y::Real, x::NDArray) = _mul_scalar(x, scalar = y)
+
+broadcast_(::typeof(*), x::NDArray, y::Real) = x * y
+broadcast_(::typeof(*), y::Real, x::NDArray) = x * y
+
+broadcast_(::typeof(*), x::NDArray{T,N}, y::NDArray{T,N}) where {T,N} =
+ _mul(x, y)
+broadcast_(::typeof(*), x::NDArray{T,N}, y::NDArray{T,M}) where {T,N,M} =
+ _broadcast_mul(x, y)
+
+"""
+ *(A::NDArray, B::NDArray)
+
+Matrix/tensor multiplication.
+"""
+*(x::NDArray{T}, y::NDArray{T}) where T = x ⋅ y
+
+"""
+ div_from!(dst::NDArray, arg::NDArrayOrReal)
+
+Elementwise divide a scalar or an `NDArray` of the same shape from `dst`. Inplace updating.
+"""
+function div_from!(dst::NDArray, arg::NDArrayOrReal)
+ @assert dst.writable
+ if isa(arg, Real)
+ _div_scalar(dst, scalar = arg, out = dst)
+ else
+ _div(dst, arg, out = dst)
+ end
+ dst
+end
+
+function div_from!(dst::NDArray{T}, arg::Real) where {T<:Integer}
+ @assert dst.writable
+ @assert(round(T, arg) != zero(T), "Integer divided by zero")
+ _div_scalar(dst, scalar = arg, out = dst)
+ dst
+end
+
+"""
+ rdiv_from!(x:: Real, y::NDArray)
+
+Elementwise divide a scalar by an `NDArray`. Inplace updating.
+"""
+function rdiv_from!(x::Real, y::NDArray)
+ @assert y.writable
+ _rdiv_scalar(y, scalar = x, out = y)
+ y
+end
+
+import Base: /
+
+"""
+ ./(x::NDArray, y::NDArray)
+ ./(x::NDArray, y::Real)
+ ./(x::Real, y::NDArray)
+
+* Elementwise dividing an `NDArray` by a scalar or another `NDArray`
+of the same shape.
+
+* Elementwise divide a scalar by an `NDArray`.
+
+* Matrix division (solving linear systems) is not implemented yet.
+"""
+/(x::NDArray, y::Real) = _div_scalar(x, scalar = y)
+
+broadcast_(::typeof(/), x::NDArray, y::Real) = _div_scalar(x, scalar = y)
+broadcast_(::typeof(/), y::Real, x::NDArray) = _rdiv_scalar(x, scalar = y)
+
+broadcast_(::typeof(/), x::NDArray{T,N}, y::NDArray{T,N}) where {T,N} =
+ _div(x, y)
+broadcast_(::typeof(/), x::NDArray{T,N}, y::NDArray{T,M}) where {T,N,M} =
+ _broadcast_div(x, y)
+
+function broadcast_(::typeof(/), x::NDArray{T}, y::Real) where {T<:Integer}
+ @assert(round(T, y) != zero(T), "Integer divided by zero")
+ _div_scalar(x, scalar = y)
+end
+
+"""
+ mod_from!(x::NDArray, y::NDArray)
+ mod_from!(x::NDArray, y::Real)
+
+Elementwise modulo for `NDArray`.
+Inplace updating.
+"""
+mod_from!(x::NDArray, y::NDArray) = _mod!(x, y)
+mod_from!(x::NDArray, y::Real) = _mod_scalar!(x, y)
+
+"""
+ rmod_from!(y::Real, x::NDArray)
+
+Elementwise modulo for `NDArray`.
+Inplace updating.
+"""
+rmod_from!(y::Real, x::NDArray) = _rmod_scalar!(x, y)
+
+import Base: %
+
+"""
+ .%(x::NDArray, y::NDArray)
+ .%(x::NDArray, y::Real)
+ .%(x::Real, y::NDArray)
+
+Elementwise modulo for `NDArray`.
+"""
+%(x::NDArray, y::Real) = _mod_scalar(x, scalar = y)
+
+broadcast_(::typeof(%), x::NDArray, y::Real) = _mod_scalar(x, y)
+broadcast_(::typeof(%), y::Real, x::NDArray) = _rmod_scalar(x, y)
+
+broadcast_(::typeof(%), x::NDArray{T,N}, y::NDArray{T,N}) where {T,N} =
+ _mod(x, y)
+broadcast_(::typeof(%), x::NDArray{T,N}, y::NDArray{T,M}) where {T,N,M} =
+ _broadcast_mod(x, y)
+
+import Base: ^
+
+# document of `.^` is merged into SymbolicNode's
+
+broadcast_(::typeof(^), x::NDArray, s::Real) = _power_scalar(x, scalar = s)
+broadcast_(::typeof(^), s::Real, x::NDArray) = _rpower_scalar(x, scalar = s)
+
+broadcast_(::typeof(^), ::Irrational{:e}, x::NDArray) = exp(x)
+broadcast_(::typeof(^), x::NDArray, s::Irrational) = _power_scalar(x, scalar = s)
+broadcast_(::typeof(^), s::Irrational, x::NDArray) = _rpower_scalar(x, scalar = s)
+
+broadcast_(::typeof(^), x::NDArray{T,N}, y::NDArray{T,N}) where {T,N} =
+ _power(x, y)
+broadcast_(::typeof(^), x::NDArray{T,N}, y::NDArray{T,M}) where {T,N,M} =
+ _broadcast_power(x, y)
+
+###############################################################################
+# comparison
+###############################################################################
+
+broadcast_(::typeof(==), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_equal(x, y)
+
+broadcast_(::typeof(!=), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_not_equal(x, y)
+
+broadcast_(::typeof(>), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_greater(x, y)
+
+broadcast_(::typeof(>=), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_greater_equal(x, y)
+
+broadcast_(::typeof(<), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_lesser(x, y)
+
+broadcast_(::typeof(<=), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_lesser_equal(x, y)
+
+
+###############################################################################
+# min/max
+###############################################################################
+
+import Base: min, max
+
+broadcast_(::typeof(max), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_maximum(x, y)
+
+broadcast_(::typeof(min), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_minimum(x, y)
+
+"""
+ fill!(arr::NDArray, x)
+
+Create an `NDArray` filled with the value `x`, like `Base.fill!`.
+"""
+function Base.fill!(arr::NDArray, x)
+ arr[:] = x
+ arr
+end
+
+"""
+ fill(x, dims, ctx=cpu())
+ fill(x, dims...)
+
+Create an `NDArray` filled with the value `x`, like `Base.fill`.
+"""
+function fill(x, dims::NTuple{N,Integer}, ctx::Context=cpu()) where N
+ arr = empty(typeof(x), dims, ctx)
+ arr[:] = x
+ arr
+end
+
+fill(x, dims::Integer...) = fill(x, dims)
+
+import Base: hypot
+
+broadcast_(::typeof(hypot), x::NDArray{T}, y::NDArray{T}) where {T} =
+ _broadcast_hypot(x, y)
+
+"""
+Manipulating as Julia Arrays
+----------------------------
+
+ @nd_as_jl(captures..., statement)
+
+A convenient macro that allows to operate `NDArray` as Julia Arrays. For example,
+
+```julia
+ x = mx.zeros(3,4)
+ y = mx.ones(3,4)
+ z = mx.zeros((3,4), mx.gpu())
+
+ @mx.nd_as_jl ro=(x,y) rw=z begin
+ # now x, y, z are just ordinary Julia Arrays
+ z[:,1] = y[:,2]
+ z[:,2] = 5
+ end
+```
+
+Under the hood, the macro convert all the declared captures from `NDArray` into Julia
+Arrays, by using `try_get_shared`. And automatically commit the modifications back into
+the `NDArray` that is declared as `rw`. This is useful for fast prototyping and when
+implement non-critical computations, such as `AbstractEvalMetric`.
+
+!!! note
+* Multiple `rw` and / or `ro` capture declaration could be made.
+* The macro does **not** check to make sure that `ro` captures are not modified. If the
+ original `NDArray` lives in CPU memory, then it is very likely the corresponding
+ Julia Array shares data with the `NDArray`, so modifying the Julia Array will also
+ modify the underlying `NDArray`.
+* More importantly, since the `NDArray` is
+ asynchronized, we will wait for *writing* for `rw` variables but wait only for *reading*
+ in `ro` variables. If we write into those `ro` variables, **and** if the memory is
+ shared, racing condition might happen, and the behavior is undefined.
+* When an `NDArray` is declared to be captured as `rw`, its contents is always sync
+ back in the end.
+* The execution results of the expanded macro is always `nothing`.
+* The statements are wrapped in a `let`, thus locally introduced new variables will not be
+ available after the statements. So you will need to declare the variables before calling the
+ macro if needed.
+"""
+macro nd_as_jl(m_args...)
+ @assert(length(m_args) > 0)
+ stmts = m_args[end]
+ @assert(isa(stmts, Expr) && stmts.head == :block,
+ "The last argument should be a statement block (begin-end); but get $stmts")
+ stmts = esc(stmts)
+
+ dclrs = m_args[1:end-1]
+ nd_ro = []
+ nd_rw = []
+ nd_all = []
+ for declr in dclrs
+ @assert(isa(declr, Expr) && declr.head == :(=) && length(declr.args)==2 && declr.args[1] ∈ (:ro,:rw),
+ "Invalid declaration, should be rw=(x,y) or ro=z; but get $declr")
+
+ declr_vars = declr.args[2]
+ if isa(declr_vars, Symbol)
+ declr_vars = (declr_vars,)
+ elseif isa(declr_vars, Expr)
+ @assert(declr_vars.head ∈ (:tuple, :vect),
+ "Capture declaration should be a variable or a tuple of variables; but got $declr_vars")
+ declr_vars = declr_vars.args
+ else
+ @assert(false, "Capture declaration should be a variable or a tuple of variables; but got $declr_vars")
+ end
+ for declr_var in declr_vars
+ @assert(isa(declr_var, Symbol),
+ "Captured ndarrays in ro/rw declaration should be variables, but get $(declr_var)")
+ end
+ append!(nd_all, [declr_vars...])
+ if declr.args[1] == :ro
+ append!(nd_ro, [declr_vars...])
+ else
+ append!(nd_rw, [declr_vars...])
+ end
+ end
+
+ nd_ro = map(esc, nd_ro)
+ nd_rw = map(esc, nd_rw)
+ nd_all = map(esc, nd_all)
+ rw_origs = [gensym() for _ in nd_rw]
+
+ save_statements = Expr(:block, [:($v_orig = $v) for (v_orig, v) in zip(rw_origs, nd_rw)]...)
+ wait_statements = Expr(:block, [:(_wait_to_read($v)) for v in nd_ro]...,
+ [:(_wait_to_write($v)) for v in nd_rw]...)
+ clear_statements = Expr(:block, [:($v_orig = nothing) for v_orig in rw_origs]...)
+ let_assignments = [:($v = try_get_shared($v)) for v in nd_all]
+ sync_statements = map(rw_origs, nd_rw) do v_orig, v
+ quote
+ if !is_shared($v, $v_orig)
+ # copy data back if not or no longer sharing data
+ copy!($v_orig, $v)
+ end
+ end
+ end
+ sync_statements = Expr(:block, sync_statements...)
+
+ let_statement = Expr(:let, quote
+ $stmts
+ $sync_statements
+ end, let_assignments...)
+ m_body = quote
+ $wait_statements
+ $save_statements
+ $let_statement
+ $clear_statements
+ nothing # the final results is always nothing
+ end
+
+ m_body
+end
+
+# NOTE: internal use only. Accessing pointers on a different device (e.g. accessing GPU
+# pointers from CPU) leads to undefined behavior.
+import Base.pointer
+function pointer(arr :: NDArray)
+ pdata = Ref{Ptr{Void}}(0)
+ @mxcall(:MXNDArrayGetData, (MX_handle, Ref{Ptr{Void}}), arr, pdata)
+ return convert(Ptr{eltype(arr)}, pdata[])
+end
+
+@inline _wait_to_read(arr :: NDArray) =
+ @mxcall(:MXNDArrayWaitToRead, (MX_handle,), arr)
+@inline _wait_to_write(arr :: NDArray) =
+ @mxcall(:MXNDArrayWaitToWrite, (MX_handle,), arr)
+
+"""
+ try_get_shared(arr; sync=:nop)
+
+Try to create a Julia array by sharing the data with the underlying `NDArray`.
+
+# Arguments:
+
+* `arr::NDArray`: the array to be shared.
+
+!!! note
+ The returned array does not guarantee to share data with the underlying `NDArray`.
+ In particular, data sharing is possible only when the `NDArray` lives on CPU.
+
+* `sync::Symbol`: `:nop`,`:write`, `:read`
+ On CPU, invoke `_wait_to_read` if `:read`;
+ invoke `_wait_to_write` if `:write`.
+"""
+function try_get_shared(x::NDArray; sync::Symbol=:nop)
+ if context(x).device_type == CPU
+ # try to do data sharing
+ if sync == :read
+ _wait_to_read(x)
+ elseif sync == :write
+ _wait_to_write(x)
+ end
+
+ unsafe_wrap(Array, pointer(x), size(x))
+ else
+ # impossible to share, just copying
+ copy(x)
+ end
+end
+
+"""
+ is_shared(j_arr, arr)
+
+Test whether `j_arr` is sharing data with `arr`.
+
+# Arguments:
+
+* `j_arr::Array`: the Julia Array.
+* `arr::NDArray`: the `NDArray`.
+"""
+is_shared(::Array, ::NDArray) = false
+
+function is_shared(j_arr::Array{T}, arr::NDArray{T}) where {T<:DType}
+ if length(j_arr) != length(arr)
+ return false
+ end
+ if context(arr).device_type != CPU
+ return false
+ end
+ pointer(j_arr) == pointer(arr)
+end
+
+"""
+ load(filename, ::Type{NDArray})
+
+Load NDArrays from binary file.
+
+# Arguments:
+* `filename::String`: the path of the file to load. It could be S3 or HDFS address.
+
+Returns either `Dict{Symbol, NDArray}` or `Vector{NDArray}`.
+
+`filename` can point to `s3` or `hdfs` resources if the `libmxnet` is built with the
+corresponding components enabled. Examples:
+* `s3://my-bucket/path/my-s3-ndarray`
+* `hdfs://my-bucket/path/my-hdfs-ndarray`
+* `/path-to/my-local-ndarray`
+"""
+function load(filename::AbstractString, ::Type{<:NDArray})
+ out_size = Ref{MX_uint}(0)
+ out_hdrs = Ref{Ptr{MX_handle}}(0)
+ out_name_size = Ref{MX_uint}(0)
+ out_names = Ref{char_pp}(0)
+ @mxcall(:MXNDArrayLoad, (char_p, Ref{MX_uint}, Ref{Ptr{MX_handle}}, Ref{MX_uint}, Ref{char_pp}),
+ filename, out_size, out_hdrs, out_name_size, out_names)
+ out_name_size = out_name_size[]
+ out_size = out_size[]
+ if out_name_size == 0
+ return [NDArray(MX_NDArrayHandle(hdr)) for hdr in unsafe_wrap(Array, out_hdrs[], out_size)]
+ else
+ @assert out_size == out_name_size
+ return Dict([(Symbol(unsafe_string(k)), NDArray(MX_NDArrayHandle(hdr))) for (k,hdr) in
+ zip(unsafe_wrap(Array, out_names[], out_size), unsafe_wrap(Array, out_hdrs[], out_size))])
+ end
+end
+
+"""
+ save(filename::AbstractString, data)
+
+Save NDarrays to binary file. Filename could be S3 or HDFS address, if `libmxnet` is built
+with corresponding support (see `load`).
+
+* `filename::String`: path to the binary file to write to.
+* `data`: data to save to file. Data can be a`NDArray`, a `Vector` of `NDArray`,
+ or a `Dict{Symbol}` contains `NDArray`s.
+"""
+save(filename::String, data::NDArray) = save(filename, [data])
+
+save(filename::String, data::VecOfNDArray) =
+ @mxcall(:MXNDArraySave, (char_p, MX_uint, Ptr{MX_handle}, char_pp),
+ filename, length(data), MX_handle[data...], char_pp(0))
+
+function save(filename::String, data::Dict{Symbol})
+ names = keys(data)
+ arrays = MX_handle.(collect(values(data)))
+ names = String.(collect(names))
+
+ @mxcall(:MXNDArraySave, (char_p, MX_uint, Ptr{MX_handle}, char_pp),
+ filename, length(names), arrays, names)
+end
+
+################################################################################
+# Mapping NDArray functions to Base-like API
+################################################################################
+
+const _ndsig = Dict{Symbol,Expr}()
+const _nddoc = Dict{Symbol,Any}()
+
+function _autoimport(name::Symbol, sig::Expr)
+ if name == :broadcast_
+ name = _broadcast_target(sig)
+ end
+
+ if isdefined(Base, name)
+ :(import Base: $name)
+ else
+ :()
+ end
+end
+
+_isinplace(name::Symbol) = endswith(string(name), "!")
+
+_writable(name::Symbol, x) =
+ _isinplace(name) ? :(@assert $x.writable "this NDArray isn't writable") : :()
+
+function _outexpr(name::Symbol, x #= the first arg of `sig` =#)
+ if _isinplace(name) # `func!`
+ Ptr, 1, :([[MX_handle(x.handle)]]), :($x)
+ else
+ retexpr = :(NDArray(MX_NDArrayHandle(unsafe_load(hdls_ref[], 1))))
+ Ref, 0, :(Ref{Ptr{MX_handle}}(C_NULL)), retexpr
+ end
+end
+
+_broadcast_target(sig::Expr) = sig.args[2].args[].args[end]
+
+"""
+Generate docstring from function signature
+"""
+function _docsig(fname::Symbol, sig::Expr, opname::String)
+ if fname !== :broadcast_
+ get(_nddoc, fname, " $sig") * "\n" * _getdocdefine(opname)
+ else
+ name = _broadcast_target(sig)
+ str = get(_nddoc, name, "")
+ _nddoc[name] = false # change to false, denote docstring has been set up
+ if isempty(str)
+ sig_ = Expr(:call, Symbol(name, "."), sig.args[3:end]...)
+ str = " $sig_"
+ end
+ if str ≠ false
+ # append "Defined in ..."
+ def = _getdocdefine(opname)
+ str = if str isa Markdown.MD
+ str = Markdown.MD(copy(str.content), copy(str.meta))
+ push!(str, Markdown.Paragraph(def))
+ str
+ else
+ str * def
+ end
+
+ @eval @doc $str $name
+ end
+ ""
+ end
+end
+
+macro _remap(sig::Expr, imp::Expr)
+ fname = (sig.head == :call) ? sig.args[1] : sig.args[1].args[1] # case of `where`
+ opname = string(imp.args[1])
+
+ import_expr = _autoimport(fname, sig)
+
+ if isa(imp.args[2], Expr) && imp.args[2].head == :parameters
+ ndin = imp.args[3:end]
+ mxargs = imp.args[2].args
+ else # no keyword arguments
+ ndin = imp.args[2:end]
+ mxargs = []
+ end
+
+ mxkeys = map(x -> string(x.args[1]), mxargs)
+ mxvals = Expr(:vect, map(x -> :(dump_mx_param($(x.args[2]))), mxargs)...)
+ ndhlds = Expr(:vect, map(x -> :($(x).handle), ndin)...)
+
+ # handler for `func!` which has side effect on first argument.
+ T, n_output, hdls_ref, retexpr = _outexpr(fname, _firstarg(sig))
+
+ assert_expr = _writable(fname, _firstarg(sig))
+
+ func_body = quote
+ $assert_expr
+ op_handle = _get_cached_libmx_op_handle($opname)
+ n_output = Ref(Cint($n_output))
+ hdls_ref = $hdls_ref
+ @mxcall(:MXImperativeInvoke,
+ (MX_handle,
+ Cint,
+ Ptr{MX_handle},
+ Ref{Cint},
+ $T{Ptr{MX_handle}},
+ Cint,
+ char_pp,
+ char_pp),
+ op_handle,
+ $(length(ndin)),
+ $(ndhlds),
+ n_output,
+ hdls_ref,
+ $(length(mxargs)),
+ $mxkeys,
+ $mxvals)
+ $retexpr
+ end
+
+ docstr = _docsig(fname, sig, opname)
+ func_def = Expr(:function, sig, func_body)
+
+ esc(quote
+ $import_expr
+ @doc $docstr ->
+ $func_def
+ end)
+end
+
+macro _remap(sig::Expr, imp::Symbol)
+ imp = _ndsig[imp]
+
+ esc(quote
+ @_remap($sig, $imp)
+ end)
+end
+
+_ndsig[:reshape] = :(reshape(arr; shape = dim, reverse = !reverse))
+@_remap reshape(arr::NDArray, dim...; reverse = false) reshape
+@_remap reshape(arr::NDArray, dim; reverse = false) reshape
+
+@_remap mean(arr::NDArray) mean(arr)
+@_remap mean(arr::NDArray, region) mean(arr; axis = 0 .- region, keepdims = true)
+
+@_remap sum(arr::NDArray) sum(arr)
+@_remap sum(arr::NDArray, dims) sum(arr; axis = 0 .- dims, keepdims = true)
+
+@_remap maximum(arr::NDArray) max(arr)
+@_remap maximum(arr::NDArray, dims) max(arr; axis = 0 .- dims, keepdims = true)
+
+@_remap minimum(arr::NDArray) min(arr)
+@_remap minimum(arr::NDArray, dims) min(arr; axis = 0 .- dims, keepdims = true)
+
+# See https://github.com/dmlc/MXNet.jl/issues/55
+@_remap dot(x::NDArray, y::NDArray) dot(y, x)
+
+# See https://github.com/dmlc/MXNet.jl/pull/123
+@_remap transpose(arr::NDArray{T,1}) where T reshape(arr; shape = (1, length(arr)), reverse = true)
+@_remap transpose(arr::NDArray{T,2}) where T transpose(arr)
+@_remap permutedims(arr::NDArray, axes) transpose(arr; axes = length(axes) .- tuple(axes...))
+
+@_remap prod(arr::NDArray) prod(arr)
+@_remap prod(arr::NDArray, dims) prod(arr; axis = 0 .- dims, keepdims = true)
+
+_nddoc[:clip] = _nddoc[:clip!] =
+"""
+ clip(x::NDArray, min, max)
+ clip!(x::NDArray, min, max)
+
+Clips (limits) the values in `NDArray`.
+Given an interval, values outside the interval are clipped to the interval edges.
+Clipping `x` between `min` and `x` would be:
+
+```julia
+clip(x, min_, max_) = max(min(x, max_), min_))
+```
+
+```jldoctest
+julia> x = NDArray(1:9);
+
+julia> mx.clip(x, 2, 8)'
+1×9 mx.NDArray{Int64,2} @ CPU0:
+ 2 2 3 4 5 6 7 8 8
+```
+
+The storage type of clip output depends on storage types of inputs and the
+`min`, `max` parameter values:
+
+- clip(default) = default
+- clip(row_sparse, min <= 0, max >= 0) = row_sparse
+- clip(csr, min <= 0, max >= 0) = csr
+- clip(row_sparse, min < 0, max < 0) = default
+- clip(row_sparse, min > 0, max > 0) = default
+- clip(csr, min < 0, max < 0) = csr
+- clip(csr, min > 0, max > 0) = csr
+"""
+@_remap clip(x::NDArray, min::Real, max::Real) clip(x; a_min = min, a_max = max)
+@_remap clip!(x::NDArray, min::Real, max::Real) clip(x; a_min = min, a_max = max)
+
+_nddoc[:expand_dims] =
+"""
+ expand_dims(x::NDArray, dim)
+
+Insert a new axis into `dim`.
+
+```julia
+julia> x
+4 mx.NDArray{Float64,1} @ CPU0:
+ 1.0
+ 2.0
+ 3.0
+ 4.0
+
+julia> mx.expand_dims(x, 1)
+1×4 mx.NDArray{Float64,2} @ CPU0:
+ 1.0 2.0 3.0 4.0
+
+julia> mx.expand_dims(x, 2)
+4×1 mx.NDArray{Float64,2} @ CPU0:
+ 1.0
+ 2.0
+ 3.0
+ 4.0
+```
+"""
+@_remap expand_dims(x::NDArray, dim) expand_dims(x; axis = -dim)
+
+# trigonometric functions, remap to keep consistent with Base
+@_remap broadcast_(::typeof(sin), x::NDArray) sin(x)
+@_remap broadcast_(::typeof(cos), x::NDArray) cos(x)
+@_remap broadcast_(::typeof(tan), x::NDArray) tan(x)
+@_remap broadcast_(::typeof(asin), x::NDArray) arcsin(x)
+@_remap broadcast_(::typeof(acos), x::NDArray) arccos(x)
+@_remap broadcast_(::typeof(atan), x::NDArray) arctan(x)
+
+# hyperbolic funcs, remap to keep consistent with Base
+@_remap broadcast_(::typeof(sinh), x::NDArray) sinh(x)
+@_remap broadcast_(::typeof(cosh), x::NDArray) cosh(x)
+@_remap broadcast_(::typeof(tanh), x::NDArray) tanh(x)
+@_remap broadcast_(::typeof(asinh), x::NDArray) arcsinh(x)
+@_remap broadcast_(::typeof(acosh), x::NDArray) arccosh(x)
+@_remap broadcast_(::typeof(atanh), x::NDArray) arctanh(x)
+
+# activation functions
+_nddoc[:σ] = _nddoc[:sigmoid] = doc"""
+ σ.(x::NDArray)
+ sigmoid.(x::NDArray)
+
+Computes sigmoid of x element-wise.
+
+```math
+σ(x) = \frac{1}{(1 + exp(-x))}
+```
+
+The storage type of `sigmoid` output is always dense.
+"""
+@_remap broadcast_(::typeof(σ), x::NDArray) sigmoid(x)
+@_remap broadcast_(::typeof(sigmoid), x::NDArray) sigmoid(x)
+
+_nddoc[:relu] = doc"""
+ relu.(x::NDArray)
+
+Computes rectified linear.
+
+```math
+\max(x, 0)
+```
+"""
+@_remap broadcast_(::typeof(relu), x::NDArray) relu(x)
+
+_nddoc[:softmax] = doc"""
+ softmax.(x::NDArray, [dim = ndims(x)])
+
+Applies the softmax function.
+
+The resulting array contains elements in the range `(0, 1)`
+and the elements along the given axis sum up to 1.
+
+```math
+softmax(\mathbf{z})_j = \frac{e^{z_j}}{\sum_{k=1}^K e^{z_k}}
+```
+"""
+@_remap broadcast_(::typeof(softmax), x::NDArray) softmax(x; axis = -ndims(x))
+@_remap broadcast_(::typeof(softmax), x::NDArray, dim::Int) softmax(x; axis = -dim)
+
+_nddoc[:log_softmax] = """
+ log_softmax.(x::NDArray, [dim = ndims(x)])
+
+Computes the log softmax of the input.
+This is equivalent to computing softmax followed by log.
+
+julia> x
+2×3 mx.NDArray{Float64,2} @ CPU0:
+ 1.0 2.0 0.1
+ 0.1 2.0 1.0
+
+julia> mx.log_softmax.(x)
+2×3 mx.NDArray{Float64,2} @ CPU0:
+ -1.41703 -0.41703 -2.31703
+ -2.31703 -0.41703 -1.41703
+"""
+@_remap broadcast_(::typeof(log_softmax), x::NDArray) log_softmax(x; axis = -ndims(x))
+@_remap broadcast_(::typeof(log_softmax), x::NDArray, dim::Int) log_softmax(x; axis = -dim)
+
+################################################################################
+# remapping to solving type unstablility
+################################################################################
+
+@_remap _plus(x::NDArray, y::NDArray) _plus(x, y)
+@_remap _plus!(x::NDArray, y::NDArray) _plus(x, y)
+
+@_remap _minus(x::NDArray, y::NDArray) _minus(x, y)
+@_remap _minus!(x::NDArray, y::NDArray) _minus(x, y)
+
+@_remap _mod(x::NDArray, y::NDArray) _mod(x, y)
+@_remap _mod!(x::NDArray, y::NDArray) _mod(x, y)
+
+@_remap _mod_scalar(x::NDArray, y::Real) _mod_scalar(x; scalar = y)
+@_remap _mod_scalar!(x::NDArray, y::Real) _mod_scalar(x; scalar = y)
+
+@_remap _rmod_scalar(x::NDArray, y::Real) _rmod_scalar(x; scalar = y)
+@_remap _rmod_scalar!(x::NDArray, y::Real) _rmod_scalar(x; scalar = y)
+
+@_remap _broadcast_add(x::NDArray, y::NDArray) broadcast_add(x, y)
+@_remap _broadcast_add!(x::NDArray, y::NDArray) broadcast_add(x, y)
+
+@_remap _broadcast_minus(x::NDArray, y::NDArray) broadcast_minus(x, y)
+@_remap _broadcast_minus!(x::NDArray, y::NDArray) broadcast_minus(x, y)
+
+@_remap _broadcast_mul(x::NDArray, y::NDArray) broadcast_mul(x, y)
+@_remap _broadcast_mul!(x::NDArray, y::NDArray) broadcast_mul(x, y)
+
+@_remap _broadcast_div(x::NDArray, y::NDArray) broadcast_div(x, y)
+@_remap _broadcast_div!(x::NDArray, y::NDArray) broadcast_div(x, y)
+
+@_remap _broadcast_mod(x::NDArray, y::NDArray) broadcast_mod(x, y)
+@_remap _broadcast_mod!(x::NDArray, y::NDArray) broadcast_mod(x, y)
+
+@_remap _broadcast_power(x::NDArray, y::NDArray) broadcast_power(x, y)
+@_remap _broadcast_power!(x::NDArray, y::NDArray) broadcast_power(x, y)
+
+@_remap _broadcast_equal(x::NDArray, y::NDArray) broadcast_equal(x, y)
+@_remap _broadcast_equal!(x::NDArray, y::NDArray) broadcast_equal(x, y)
+
+@_remap _broadcast_not_equal(x::NDArray, y::NDArray) broadcast_not_equal(x, y)
+@_remap _broadcast_not_equal!(x::NDArray, y::NDArray) broadcast_not_equal(x, y)
+
+@_remap _broadcast_greater(x::NDArray, y::NDArray) broadcast_greater(x, y)
+@_remap _broadcast_greater!(x::NDArray, y::NDArray) broadcast_greater(x, y)
+
+@_remap _broadcast_greater_equal(x::NDArray, y::NDArray) broadcast_greater_equal(x, y)
+@_remap _broadcast_greater_equal!(x::NDArray, y::NDArray) broadcast_greater_equal(x, y)
+
+@_remap _broadcast_lesser(x::NDArray, y::NDArray) broadcast_lesser(x, y)
+@_remap _broadcast_lesser!(x::NDArray, y::NDArray) broadcast_lesser(x, y)
+
+@_remap _broadcast_lesser_equal(x::NDArray, y::NDArray) broadcast_lesser_equal(x, y)
+@_remap _broadcast_lesser_equal!(x::NDArray, y::NDArray) broadcast_lesser_equal(x, y)
+
+@_remap _broadcast_maximum(x::NDArray, y::NDArray) broadcast_maximum(x, y)
+@_remap _broadcast_maximum!(x::NDArray, y::NDArray) broadcast_maximum(x, y)
+
+@_remap _broadcast_minimum(x::NDArray, y::NDArray) broadcast_minimum(x, y)
+@_remap _broadcast_minimum!(x::NDArray, y::NDArray) broadcast_minimum(x, y)
+
+@_remap _broadcast_hypot(x::NDArray, y::NDArray) broadcast_hypot(x, y)
+@_remap _broadcast_hypot!(x::NDArray, y::NDArray) broadcast_hypot(x, y)
+
+_nddoc[:broadcast_to] = """
+ broadcast_to(x::NDArray, dims)
+ broadcast_to(x::NDArray, dims...)
+
+Broadcasts the input array to a new shape.
+
+In the case of broacasting doesn't work out of box,
+you can expand the NDArray first.
+
+```jldoctest
+julia> x = mx.ones(2, 3, 4);
+
+julia> y = mx.ones(1, 1, 4);
+
+julia> x .+ mx.broadcast_to(y, 2, 3, 4)
+2×3×4 mx.NDArray{Float32,3} @ CPU0:
+[:, :, 1] =
+ 2.0 2.0 2.0
+ 2.0 2.0 2.0
+
+[:, :, 2] =
+ 2.0 2.0 2.0
+ 2.0 2.0 2.0
+
+[:, :, 3] =
+ 2.0 2.0 2.0
+ 2.0 2.0 2.0
+
+[:, :, 4] =
+ 2.0 2.0 2.0
+ 2.0 2.0 2.0
+```
+"""
+@_remap broadcast_to(x::NDArray, dims) broadcast_to(x; shape = dims)
+@_remap broadcast_to(x::NDArray, dims...) broadcast_to(x; shape = dims)
+
+_nddoc[:broadcast_axis] = _nddoc[:broadcast_axes] = """
+ broadcast_axis(x::NDArray, dim, size)
+ broadcast_axes(x::NDArray, dim, size)
+
+Broadcasts the input array over particular axis(axes).
+Parameter `dim` and `size` could be a scalar, a Tuple or an Array.
+
+`broadcast_axes` is just an alias.
+
+```jldoctest
+julia> x
+1×2×1 mx.NDArray{Int64,3} @ CPU0:
+[:, :, 1] =
+ 1 2
+
+julia> mx.broadcast_axis(x, 1, 2)
+2×2×1 mx.NDArray{Int64,3} @ CPU0:
+[:, :, 1] =
+ 1 2
+ 1 2
+
+julia> mx.broadcast_axis(x, 3, 2)
+1×2×2 mx.NDArray{Int64,3} @ CPU0:
+[:, :, 1] =
+ 1 2
+
+[:, :, 2] =
+ 1 2
+```
+"""
+@_remap(broadcast_axis(x::NDArray, dim, size),
+ broadcast_axis(x; axis = ndims(x) .- dim, size = size))
+@_remap(broadcast_axes(x::NDArray, dim, size),
+ broadcast_axes(x; axis = ndims(x) .- dim, size = size))
+
+################################################################################
+# NDArray functions dynamically imported from libmxnet
+################################################################################
+function _invoke_mxfunction(func_handle::MX_handle, use_vars, scalars, mut_vars; kwargs...)
+ names = String[string(entry[1]) for entry in kwargs]
+ args = String[string(entry[2]) for entry in kwargs]
+ @mxcall(:MXFuncInvokeEx,
+ (MX_handle, Ptr{MX_handle}, Ptr{MX_float}, Ptr{MX_handle}, Cint, char_pp, char_pp),
+ func_handle, use_vars, scalars, mut_vars, length(names), names, args)
+end
+
+@enum(LIBMX_FUNC_TYPE_MASK,
+ NDARRAY_ARG_BEFORE_SCALAR = 1,
+ ACCEPT_EMPTY_MUTATE_TARGET = (1 << 2)
+)
+
+# Import corresponding math functions from base so the automatically defined libmxnet
+# functions can overload them
+import Base: sqrt
+
+"""
+The libxmnet APIs are automatically imported from `libmxnet.so`. The functions listed
+here operate on `NDArray` objects. The arguments to the functions are typically ordered
+as
+
+```julia
+ func_name(arg_in1, arg_in2, ..., scalar1, scalar2, ..., arg_out1, arg_out2, ...)
+```
+
+unless `NDARRAY_ARG_BEFORE_SCALAR` is not set. In this case, the scalars are put before the input arguments:
+
+```julia
+ func_name(scalar1, scalar2, ..., arg_in1, arg_in2, ..., arg_out1, arg_out2, ...)
+```
+
+If `ACCEPT_EMPTY_MUTATE_TARGET` is set. An overloaded function without the output arguments will also be defined:
+
+```julia
+ func_name(arg_in1, arg_in2, ..., scalar1, scalar2, ...)
+```
+
+Upon calling, the output arguments will be automatically initialized with empty NDArrays.
+
+Those functions always return the output arguments. If there is only one output (the typical situation), that
+object (`NDArray`) is returned. Otherwise, a tuple containing all the outputs will be returned.
+"""
+function _get_ndarray_function_def(name :: String)
+ func_name = Symbol(name)
+
+ func_def = quote
+ function $func_name(::Type{<:NDArray}, args::NDArray...; out=nothing, kwargs...)
+ if out != nothing
+ output_vars = out
+ if isa(output_vars, NDArray)
+ output_vars = NDArray[output_vars]
+ end
+ num_outputs = length(output_vars)
+ else
+ output_vars = NDArray[]
+ num_outputs = 0
+ end
+
+ args = collect(args) # tuple to list
+ if length(args) == 0
+ args = MX_handle[]
+ end
+
+ output_handles_pp = if length(output_vars) > 0
+ [map(x -> x.handle, output_vars)]
+ else
+ [Ptr{MX_handle}(C_NULL)]
+ end
+ num_outputs_p = [convert(Cint, num_outputs)]
+
+ kw_keys_str = String[string(x[1]) for x in kwargs]
+ kw_vals_str = String[dump_mx_param(x[2]) for x in kwargs]
+
+ op_handle = _get_cached_libmx_op_handle($(name))
+ @mxcall(:MXImperativeInvoke,
+ (MX_handle, Cint, Ptr{MX_handle},
+ Ptr{Cint}, Ptr{Ptr{MX_handle}},
+ Cint, char_pp, char_pp),
+ op_handle, length(args), args,
+ num_outputs_p, output_handles_pp,
+ length(kwargs), kw_keys_str, kw_vals_str)
+
+ if out == nothing
+ n = num_outputs_p[]
+ hdls = unsafe_wrap(Array{MX_handle}, output_handles_pp[], n)
+ xs = NDArray[NDArray(MX_NDArrayHandle(x)) for x in hdls]
+ if n == 1
+ return xs[]
+ else
+ return xs
+ end
+ else
+ return out
+ end
+ end
+ end
+
+ func_def2 = quote
+ function $func_name(args::NDArray...; out=nothing, kwargs...)
+ $func_name(NDArray, args...; out=out, kwargs...)
+ end
+ end
+
+ return func_def, func_def2
+end
+
+const _op_import_bl = [ # import black list; do not import these funcs
+ "_full", # we already have `mx.fill`
+ "_ones", # we already have `mx.ones`
+ "_zeros", # we already have `mx.zeros`
+ "clip",
+ "expand_dims",
+
+ # arithmetic
+ "_plus",
+ "_minus",
+ "_mod",
+ "_mod_scalar",
+ "_rmod_scalar",
+
+ "dot",
+ "max",
+ "max_axis",
+ "mean",
+ "min",
+ "min_axis",
+ "prod",
+ "reshape",
+ "sum",
+ "transpose",
+
+ # trigonometric
+ "sin",
+ "cos",
+ "tan",
+ "arcsin",
+ "arccos",
+ "arctan",
+
+ # hyperbolic
+ "sinh",
+ "cosh",
+ "tanh",
+ "arcsinh",
+ "arccosh",
+ "arctanh",
+
+ # activation
+ "sigmoid",
+ "relu",
+ "softmax",
+ "log_softmax",
+
+ # broadcast
+ "broadcast_add",
+ "broadcast_plus",
+ "broadcast_minus",
+ "broadcast_sub",
+ "broadcast_mul",
+ "broadcast_div",
+ "broadcast_mod",
+ "broadcast_power",
+ "broadcast_equal",
+ "broadcast_not_equal",
+ "broadcast_greater",
+ "broadcast_greater_equal",
+ "broadcast_lesser",
+ "broadcast_lesser_equal",
+ "broadcast_maximum",
+ "broadcast_minimum",
+ "broadcast_to",
+ "broadcast_axis",
+ "broadcast_axes",
+ "broadcast_hypot",
+]
+
+macro _import_ndarray_functions()
+ names = filter(n -> ∉(lowercase(n), _op_import_bl), _get_libmx_op_names())
+
+ func_exprs = map(names) do name
+ op_handle = _get_libmx_op_handle(name)
+
+ desc, key_narg = _get_libmx_op_description(name, op_handle)
+ func_def, func_def2 = _get_ndarray_function_def(name)
+
+ func_name = Symbol(name)
+ expr = quote
+ # TODO the explicit exclusion of take will no longer be necessary when it is removed from Base
+ $((isdefined(Base, func_name) && func_name ≠ :take) ? :(import Base.$func_name) : :())
+ $func_def
+ @doc $desc ->
+ $func_def2
+ end
+ end
+
+ esc(quote
+ $(func_exprs...)
+ end)
+end
+
+@_import_ndarray_functions()
diff --git a/julia/src/nn-factory.jl b/julia/src/nn-factory.jl
new file mode 100644
index 000000000000..b5134f9d2bd5
--- /dev/null
+++ b/julia/src/nn-factory.jl
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ MLP(input, spec; hidden_activation = :relu, prefix)
+
+Construct a multi-layer perceptron. A MLP is a multi-layer neural network with
+fully connected layers.
+
+# Arguments:
+* `input::SymbolicNode`: the input to the mlp.
+* `spec`: the mlp specification, a list of hidden dimensions. For example,
+ `[128, (512, :sigmoid), 10]`. The number in the list indicate the
+ number of hidden units in each layer. A tuple could be used to specify
+ the activation of each layer. Otherwise, the default activation will
+ be used (except for the last layer).
+* `hidden_activation::Symbol`: keyword argument, default `:relu`, indicating
+ the default activation for hidden layers. The specification here could be overwritten
+ by layer-wise specification in the `spec` argument. Also activation is not
+ applied to the last, i.e. the prediction layer. See [`Activation`](@ref) for a
+ list of supported activation types.
+* `prefix`: keyword argument, default `gensym()`, used as the prefix to
+ name the constructed layers.
+
+Returns the constructed MLP.
+"""
+function MLP(input, spec; hidden_activation::Symbol=:relu, prefix=gensym())
+ spec = convert(Vector{Union{Int,Tuple}}, spec)
+
+ n_layer = length(spec)
+ for (i, s) in enumerate(spec)
+ if isa(s, Tuple)
+ n_unit, act_type = s
+ else
+ n_unit = s
+ act_type = hidden_activation
+ end
+ input = FullyConnected(input, name=Symbol(prefix, "fc$i"), num_hidden=n_unit)
+ if i < n_layer || isa(s, Tuple)
+ # will not add activation unless the user explicitly specified
+ input = Activation(input, name=Symbol(prefix, "$act_type$i"), act_type=act_type)
+ end
+ end
+
+ return input
+end
diff --git a/julia/src/optimizer.jl b/julia/src/optimizer.jl
new file mode 100644
index 000000000000..6436878df678
--- /dev/null
+++ b/julia/src/optimizer.jl
@@ -0,0 +1,308 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+###############################################################################
+# Types
+###############################################################################
+
+"""
+ AbstractOptimizer
+
+Base type for all optimizers.
+"""
+abstract type AbstractOptimizer end
+
+"""
+ AbstractLearningRateScheduler
+
+Base type for all learning rate scheduler.
+"""
+abstract type AbstractLearningRateScheduler end
+
+"""
+ AbstractMomentumScheduler
+
+Base type for all momentum scheduler.
+"""
+abstract type AbstractMomentumScheduler end
+
+"""
+ OptimizationState
+
+### Attributes
+* `batch_size`: The size of the mini-batch used in stochastic training.
+* `curr_epoch`:
+ The current epoch count. Epoch 0 means no training yet, during the first
+ pass through the data, the epoch will be 1; during the second pass, the
+ epoch count will be 1, and so on.
+* `curr_batch`:
+ The current mini-batch count. The batch count is reset during every epoch.
+ The batch count 0 means the beginning of each epoch, with no mini-batch
+ seen yet. During the first mini-batch, the mini-batch count will be 1.
+* `curr_iter`:
+ The current iteration count. One iteration corresponds to one mini-batch,
+ but unlike the mini-batch count, the iteration count does **not** reset
+ in each epoch. So it track the *total* number of mini-batches seen so far.
+"""
+mutable struct OptimizationState
+ batch_size :: Int
+ curr_epoch :: Int
+ curr_batch :: Int
+ curr_iter :: Int
+end
+
+OptimizationState(batch_size::Int) = OptimizationState(batch_size, 0, 0, 0)
+
+###############################################################################
+# LearningRate module
+###############################################################################
+
+module LearningRate
+
+import Base: get
+import ..mx: AbstractLearningRateScheduler, OptimizationState, update!
+
+export initlrsched
+
+initlrsched(η::Real) = LearningRate.Fixed(η)
+
+update!(a::AbstractLearningRateScheduler) = (isdefined(a, :t) && (a.t += 1))
+
+"""
+ get(sched::AbstractLearningRateScheduler)
+
+Returns the current learning rate.
+"""
+get(::AbstractLearningRateScheduler) = nothing
+
+"""
+ LearningRate.Fixed(η)
+
+Fixed learning rate scheduler always return the same learning rate.
+"""
+struct Fixed <: AbstractLearningRateScheduler
+ η::Float64
+end
+
+get(f::Fixed) = f.η
+
+doc"""
+ LearningRate.Exp(η₀; γ = 0.9)
+
+```math
+\eta_t = \eta_0\gamma^t
+```
+
+Where `t` is the epoch count, or the iteration count.
+"""
+mutable struct Exp <: AbstractLearningRateScheduler
+ η₀::Float64
+ γ ::Float64
+ t ::Int
+end
+
+function Exp(η₀; γ = 0.9, t = 0)
+ @assert 0 < γ < 1
+ Exp(η₀, γ, t)
+end
+
+get(a::Exp) = a.η₀ * a.γ^a.t
+
+doc"""
+ LearningRate.Inv(η₀; γ = 0.9, p = 0.5)
+
+```math
+\eta_t = \eta_0 (1 + \gamma t)^{-p}
+```
+
+Where `t` is the epoch count, or the iteration count.
+"""
+mutable struct Inv <: AbstractLearningRateScheduler
+ η₀::Float64
+ γ ::Float64
+ p ::Float64
+ t ::Int
+end
+
+function Inv(η₀; γ = 0.9, p = 0.5, t = 0)
+ @assert 0 < γ < 1
+ @assert 0 <= p
+ Inv(η₀, γ, p, t)
+end
+
+get(i::Inv) = i.η₀ * (1 + i.γ*i.t)^(-i.p)
+
+end # module LearningRate
+
+using .LearningRate
+
+###############################################################################
+# Momentum module
+###############################################################################
+
+module Momentum
+
+import Base: get
+import ..mx: AbstractMomentumScheduler, OptimizationState
+
+export initmomsched
+
+"""
+ get(sched)
+
+* `sched::AbstractMomentumScheduler`: the momentum scheduler.
+
+Returns the current momentum.
+"""
+get
+
+initmomsched(μ::Real) = iszero(μ) ? Momentum.Null() : Momentum.Fixed(μ)
+
+"""
+ Momentum.Null
+
+The null momentum scheduler always returns 0 for momentum. It is also used to
+explicitly indicate momentum should not be used.
+"""
+struct Null <: AbstractMomentumScheduler
+end
+
+get(::Null) = 0.0
+
+"""
+ Momentum.Fixed
+
+Fixed momentum scheduler always returns the same value.
+"""
+mutable struct Fixed <: AbstractMomentumScheduler
+ μ::Float64
+end
+
+get(f::Fixed) = f.μ
+
+doc"""
+ NadamScheduler(; μ = 0.99, δ = 0.004, γ = 0.5, α = 0.96)
+
+Nesterov-accelerated adaptive momentum scheduler.
+
+Description in [Incorporating Nesterov Momentum into Adam]
+(http://cs229.stanford.edu/proj2015/054_report.pdf).
+
+```math
+\mu_t = \mu_0 * (1 - \gamma * \alpha^{t * \delta})
+```
+
+Where
+* `t`: iteration count
+* `μ`: default `0.99`, μ₀
+* `δ`: default `0.004` is scheduler decay.
+* `γ`: default `0.5`
+* `α`: default `0.96`
+"""
+struct NadamScheduler <: AbstractMomentumScheduler
+ μ::Float64
+ δ::Float64
+ γ::Float64
+ α::Float64
+end
+
+function NadamScheduler(; μ = 0.99, δ = 0.004, γ = 0.5, α = 0.96)
+ @assert 0.0 <= μ < 1.0
+ @assert 0.0 <= δ
+ @assert 0.0 <= γ <= 1.0
+ @assert 0.0 <= α <= 1.0
+ NadamScheduler(μ, δ, γ, α)
+end
+
+"""
+ get(n::NadamScheduler, t)
+
+Where `t` is the iteration count.
+"""
+get(n::NadamScheduler, t) =
+ n.μ * (1.0 - n.γ * n.α^( t * n.δ)),
+ n.μ * (1.0 - n.γ * n.α^((t + 1) * n.δ))
+
+end # module Momentum
+
+using .Momentum
+
+###############################################################################
+# Public APIs
+###############################################################################
+
+"""
+ getupdater(optimizer)
+
+A utility function to create an updater function of `KVStore`,
+that uses its closure to store all the states needed for each weights.
+
+Ther returned function has following signature:
+
+```julia
+decend!(index::Int, ∇::NDArray, x::NDArray)
+```
+
+If the optimizer is stateful and need access/store states during updating,
+`index` will be the key to access/store states.
+"""
+function getupdater(optimizer::AbstractOptimizer)
+ states = Dict{Int,Any}()
+ function updater(index::Int, ∇::NDArray, x::NDArray)
+ if !haskey(states, index)
+ states[index] = create_state(optimizer, index, x)
+ end
+ update!(optimizer, index, x, ∇, states[index])
+ end
+ updater
+end
+
+"""
+ normgrad(optimizer, W, ∇)
+
+Get the properly normalized gradient (re-scaled and clipped if necessary).
+
+* `optimizer`: the optimizer,
+ should contain the field `scale`, `clip` and `λ`.
+* `W::NDArray`: the trainable weights.
+* `∇::NDArray`: the original gradient of the weights.
+"""
+function normgrad!(opt::AbstractOptimizer, W::NDArray, ∇::NDArray)
+ # rescaling
+ s = opt.scale
+ !iszero(s) && @inplace ∇ .*= s
+ # gradient clipping
+ c = opt.clip
+ c > 0 && clip!(∇, -c, c)
+ # weight decay
+ λ = opt.λ
+ λ > 0 && @inplace ∇ += λ .* W
+
+ ∇
+end
+
+###############################################################################
+# Builtin Optimizers
+###############################################################################
+
+include("optimizers/sgd.jl")
+include("optimizers/adam.jl")
+include("optimizers/adagrad.jl")
+include("optimizers/adadelta.jl")
+include("optimizers/adamax.jl")
+include("optimizers/rmsprop.jl")
+include("optimizers/nadam.jl")
diff --git a/julia/src/optimizers/adadelta.jl b/julia/src/optimizers/adadelta.jl
new file mode 100644
index 000000000000..2b0cd0060261
--- /dev/null
+++ b/julia/src/optimizers/adadelta.jl
@@ -0,0 +1,104 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+doc"""
+ AdaDelta(; kwargs...)
+
+Scale learning rates by the ratio of accumulated gradients to accumulated
+updates, see [1] and notes for further description.
+
+### Attributes
+* `η`: default `1.0`, learning rate.
+* `ρ`: default `0.95`, squared gradient moving average decay factor.
+* `ϵ`: default `1e-6`, small value added for numerical stability.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `λ`: default `0.00001`, weight decay is equivalent
+ to adding a global l2 regularizer for all the parameters.
+
+### Notes
+`ρ` should be between 0 and 1. A value of `ρ` close to 1 will decay the
+moving average slowly and a value close to 0 will decay the moving average
+fast.
+
+`ρ = 0.95` and `ϵ = 1e-6` are suggested in the paper and reported to
+work for multiple datasets (MNIST, speech). In the paper, no learning rate is
+considered (so `η = 1.0`). Probably best to keep it at this value.
+
+`ϵ` is important for the very first update (so the numerator does not become 0).
+
+Using the step size `η` and a decay factor `ρ` the learning rate is
+calculated as:
+
+```math
+\begin{align*}
+ r_t &= ρ r_{t-1} + (1 - ρ) g^2 \\
+ η_t &= η \frac{\sqrt{s_{t-1} + ϵ}} {\sqrt{r_t + ϵ}} \\
+ s_t &= ρ s_{t-1} + (1 - ρ) (η_t \times g)^2
+\end{align*}
+```
+
+### References
+1. Zeiler, M. D. (2012):
+ ADADELTA: An Adaptive Learning Rate Method. arXiv Preprint arXiv:1212.5701.
+"""
+AdaDelta
+
+@defstruct AdaDelta <: AbstractOptimizer (
+ (η :: Real = 1.0, η > 0),
+ (ρ :: Real = 0.95, 0 < ρ < 1 ),
+ (ϵ :: Real = 1e-6, ϵ > 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 1e-5, λ >= 0),
+ η_sched :: Any = initlrsched(η)
+)
+
+mutable struct AdaDeltaState
+ x :: NDArray
+ Δx :: NDArray
+end
+
+create_state(::AdaDelta, ::Int, W::NDArray) =
+ AdaDeltaState(zeros(size(W), context(W)), zeros(size(W), context(W)))
+
+function update!(ada::AdaDelta, ::Int, W::NDArray, ∇::NDArray, s::AdaDeltaState)
+ η = get(ada.η_sched)
+ x = s.x
+ Δx = s.Δx
+ ρ = ada.ρ
+ ϵ = ada.ϵ
+
+ normgrad!(ada, W, ∇)
+
+ # Update s.acc as in RMSProp
+ @inplace x .*= ρ
+ @inplace x .+= (1 - ρ) .* ∇.^2
+
+ # Compute update using the "old" Δx
+ Δxₜ = ∇ .* sqrt(Δx .+ ϵ) ./ sqrt(x .+ ϵ) # FIXME: sqrt dot-call
+ @inplace W .+= -η .* Δxₜ
+
+ # update Δx using update
+ @inplace Δx .*= ρ
+ @inplace Δx .+= (1 - ρ) .* Δxₜ.^2
+end
diff --git a/julia/src/optimizers/adagrad.jl b/julia/src/optimizers/adagrad.jl
new file mode 100644
index 000000000000..4236cb8cda20
--- /dev/null
+++ b/julia/src/optimizers/adagrad.jl
@@ -0,0 +1,77 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+doc"""
+ AdaGrad(; kwargs...)
+
+Scale learning rates by dividing with the square root of accumulated
+squared gradients. See [1] for further description.
+
+### Arguments
+* `η`: default `0.1`, learning rate.
+* `ϵ`: default `1e-6`, small value added for numerical stability.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `λ`: default `0.00001`, weight decay is equivalent
+ to adding a global l2 regularizer for all the parameters.
+
+### Notes
+Using step size `η` AdaGrad calculates the learning rate for feature `i` at
+time step t as:
+
+```math
+η_{t,i} = \frac{lr}{\sqrt{\sum^t_{t^\prime} g^2_{t^\prime,i} + ϵ}} g_{t,i}
+```
+
+as such the learning rate is monotonically decreasing.
+Epsilon is not included in the typical formula, see [2].
+
+### References
+1. Duchi, J., Hazan, E., & Singer, Y. (2011):
+ Adaptive subgradient methods for online learning and
+ stochastic optimization. JMLR, 12:2121-2159.
+2. Chris Dyer: Notes on AdaGrad.
+ [http://www.ark.cs.cmu.edu/cdyer/adagrad.pdf]
+ (http://www.ark.cs.cmu.edu/cdyer/adagrad.pdf)
+"""
+AdaGrad
+
+@defstruct AdaGrad <: AbstractOptimizer (
+ (η :: Real = 0.1, η > 0),
+ (ϵ :: Real = 1e-6, ϵ > 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 1e-5, λ >= 0),
+ η_sched :: Any = initlrsched(η)
+)
+
+create_state(::AdaGrad, ::Int, W::NDArray) = zeros(size(W), context(W))
+
+function update!(ada::AdaGrad, ::Int, W::NDArray, ∇::NDArray, x::NDArray)
+ η = get(ada.η_sched)
+ ϵ = ada.ϵ
+
+ normgrad!(ada, W, ∇)
+
+ @inplace x .+= ∇.^2 # update state
+ @inplace W .+= -η .* ∇ ./ sqrt(x .+ ϵ) # FIXME: sqrt dot-call
+end
diff --git a/julia/src/optimizers/adam.jl b/julia/src/optimizers/adam.jl
new file mode 100644
index 000000000000..c6aa99ba71fb
--- /dev/null
+++ b/julia/src/optimizers/adam.jl
@@ -0,0 +1,88 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ ADAM
+
+The solver described in Diederik Kingma, Jimmy Ba: *Adam: A Method for
+Stochastic Optimization*. arXiv:1412.6980 [cs.LG].
+
+ ADAM(; kwargs...)
+
+### Arguments
+* `η`: default `0.001`, learning rate.
+* `β1`: default `0.9`.
+* `β2`: default `0.999`.
+* `ϵ`: default `1e-8`.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `λ`: default `0.00001`, weight decay is equivalent
+ to adding a global l2 regularizer for all the parameters.
+* `η_sched::AbstractLearningRateScheduler`: default `LearningRate.Fixed(η)`, a
+ dynamic learning rate scheduler. If set, will overwrite the `η` parameter.
+"""
+ADAM
+
+@defstruct ADAM <: AbstractOptimizer (
+ (η :: Real = 0.001, η > 0),
+ (β1 :: Real = 0.9, 0 <= β1 < 1),
+ (β2 :: Real = 0.999, 0 <= β2 < 1),
+ (ϵ :: Real = 1e-8, ϵ > 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 1e-5, λ >= 0),
+ η_sched :: Any = initlrsched(η)
+)
+
+mutable struct ADAMState
+ η :: Float64 # current learning rate
+ mₜ :: NDArray
+ vₜ :: NDArray
+ β1ᵗ :: Float64
+ β2ᵗ :: Float64
+end
+
+create_state(adam::ADAM, ::Int, W::NDArray) =
+ ADAMState(get(adam.η_sched),
+ zeros(size(W), context(W)),
+ zeros(size(W), context(W)),
+ adam.β1, adam.β2)
+
+function update!(adam::ADAM, ::Int, W::NDArray, ∇:: NDArray, s::ADAMState)
+ η = s.η
+ β1 = adam.β1
+ β2 = adam.β2
+ ϵ = adam.ϵ
+
+ normgrad!(adam, W, ∇)
+
+ s.mₜ = β1 * s.mₜ + (1 - β1) .* ∇
+ s.vₜ = β2 * s.vₜ + (1 - β2) .* ∇.^2
+
+ aₜ= sqrt(1.0 - s.β2ᵗ)/(1.0 - s.β1ᵗ)
+
+ # update βᵗ to βᵗ⁺¹
+ s.β1ᵗ *= β1
+ s.β2ᵗ *= β2
+
+ @inplace W .+= -η * aₜ * s.mₜ ./ (sqrt(s.vₜ) .+ ϵ)
+end
diff --git a/julia/src/optimizers/adamax.jl b/julia/src/optimizers/adamax.jl
new file mode 100644
index 000000000000..de6a1ab759b3
--- /dev/null
+++ b/julia/src/optimizers/adamax.jl
@@ -0,0 +1,84 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ AdaMax(; kwargs...)
+
+This is a variant of of the Adam algorithm based on the infinity norm.
+See [1] for further description.
+
+### Arguments
+* `η`: default `0.002`, learning rate.
+* `β1`: default `0.9`, exponential decay rate for the first moment estimates.
+* `β2`: default `0.999`, exponential decay rate for the weighted
+ infinity norm estimates.
+* `ϵ`: default `1e-8`, small value added for numerical stability.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `λ`: default `0.00001`, weight decay is equivalent
+ to adding a global l2 regularizer for all the parameters.
+
+### References
+1. Kingma, Diederik, and Jimmy Ba (2014):
+ Adam: A Method for Stochastic Optimization. Section 7.
+ [http://arxiv.org/abs/1412.6980]
+ (http://arxiv.org/abs/1412.6980).
+"""
+AdaMax
+
+@defstruct AdaMax <: AbstractOptimizer (
+ (η :: Real = 0.002, η > 0),
+ (β1 :: Real = 0.9, 0 <= β1 < 1),
+ (β2 :: Real = 0.999, 0 <= β2 < 1),
+ (ϵ :: Real = 1e-8, ϵ > 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 1e-5, λ >= 0),
+ η_sched :: Any = initlrsched(η)
+)
+
+mutable struct AdaMaxState
+ mₜ :: NDArray
+ uₜ :: NDArray
+ β1ᵗ :: Float64
+end
+
+create_state(ada::AdaMax, ::Int, W::NDArray) =
+ AdaMaxState(zeros(size(W), context(W)),
+ zeros(size(W), context(W)),
+ ada.β1)
+
+function update!(ada::AdaMax, ::Int, W::NDArray, ∇::NDArray, s::AdaMaxState)
+ η = get(ada.η_sched)
+ β1 = ada.β1
+ β2 = ada.β2
+ ϵ = ada.ϵ
+
+ normgrad!(ada, W, ∇)
+
+ s.mₜ = β1 * s.mₜ .+ (1 - β1) .* ∇
+ s.uₜ = _maximum(β2 * s.uₜ, abs(∇)) # FIXME abs dot-call
+
+ @inplace W .+= -η / (1 - s.β1ᵗ) * s.mₜ ./ (s.uₜ + ϵ)
+
+ s.β1ᵗ *= ada.β1
+end
diff --git a/julia/src/optimizers/nadam.jl b/julia/src/optimizers/nadam.jl
new file mode 100644
index 000000000000..fdcd1ea7e7ab
--- /dev/null
+++ b/julia/src/optimizers/nadam.jl
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+doc"""
+ Nadam(; kwargs...)
+
+Nesterov Adam optimizer: Adam RMSprop with Nesterov momentum,
+see [1] and notes for further description.
+
+
+### Arguments
+* `η`: default `0.001`, learning rate.
+* `β1`: default `0.99`.
+* `β2`: default `0.999`.
+* `ϵ`: default `1e-8`, small value added for numerical stability.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `λ`: default `0.00001`, weight decay is equivalent
+ to adding a global l2 regularizer for all the parameters.
+* `η_sched::AbstractLearningRateScheduler`: default `nothing`, a
+ dynamic learning rate scheduler. If set, will overwrite the `η`
+ parameter.
+* `μ_sched::NadamScheduler` default `NadamScheduler()` of the form.
+
+ ```math
+ \mu_t = β_1 (1 - 0.5 \times 0.96^{t \times 0.004})
+ ```
+
+### Notes
+Default parameters follow those provided in the paper.
+It is recommended to leave the parameters of this optimizer
+at their default values.
+
+### References
+1. [Incorporating Nesterov Momentum into Adam]
+ (http://cs229.stanford.edu/proj2015/054_report.pdf).
+
+2. [On the importance of initialization and momentum in deep learning]
+ (http://www.cs.toronto.edu/~fritz/absps/momentum.pdf).
+"""
+Nadam
+
+@defstruct Nadam <: AbstractOptimizer (
+ (η :: Real = 0.001, η > 0),
+ (β1 :: Real = 0.99, 0 <= β1 < 1),
+ (β2 :: Real = 0.999, 0 <= β2 < 1),
+ (ϵ :: Real = 1e-8, ϵ > 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 1e-5, λ >= 0),
+ η_sched :: Any = initlrsched(η),
+ μ_sched :: Momentum.NadamScheduler = Momentum.NadamScheduler(μ = β1)
+)
+
+mutable struct NadamState
+ m :: NDArray
+ n :: NDArray
+ Πμ :: Float64
+ β2ᵗ :: Float64
+ t :: Int # use in NadamScheduler.
+ # we store `t` in state because state is created for each `index`
+end
+
+create_state(n::Nadam, ::Int, W::NDArray) =
+ NadamState(zeros(size(W), context(W)), zeros(size(W), context(W)),
+ 1.0, n.β2, 1)
+
+function update!(na::Nadam, ::Int, W::NDArray, ∇::NDArray, s::NadamState)
+ η = get(na.η_sched)
+ μₜ, μₜ₁= get(na.μ_sched, s.t)
+ β1, β2 = na.β1, na.β2
+ ϵ = na.ϵ
+
+ normgrad!(na, W, ∇)
+ s.t += 1
+
+ s.Πμ *= μₜ
+ Πμ′ = s.Πμ * μₜ₁
+
+ ∇′ = ∇ / (1.0 - s.Πμ)
+ @inplace s.m .*= β1
+ @inplace s.m .+= (1.0 - β1) * ∇
+ m̂ = s.m / (1.0 - Πμ′)
+
+ @inplace s.n .*= β2
+ @inplace s.n .+= (1.0 - β2) .* ∇.^2
+ n̂ = s.n / (1.0 - s.β2ᵗ)
+ s.β2ᵗ *= β2
+
+ m̄ = (1.0 - μₜ) * ∇′+ μₜ₁ * m̂
+ @inplace W .+= -η * m̄ ./ (sqrt(n̂) + ϵ)
+end
diff --git a/julia/src/optimizers/rmsprop.jl b/julia/src/optimizers/rmsprop.jl
new file mode 100644
index 000000000000..8351142681b1
--- /dev/null
+++ b/julia/src/optimizers/rmsprop.jl
@@ -0,0 +1,84 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+doc"""
+ RMSProp(; kwargs...)
+
+Scale learning rates by dividing with the moving average of the root mean
+squared (RMS) gradients. See [1] for further description.
+
+### Arguments
+
+* `η`: default `0.1`, learning rate.
+* `ρ`: default `0.9`, gradient moving average decay factor.
+* `ϵ`: default `1e-8`, small value added for numerical stability.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `λ`: default `0.00001`, weight decay is equivalent
+ to adding a global l2 regularizer for all the parameters.
+
+### Notes
+`ρ` should be between 0 and 1. A value of `ρ` close to 1 will decay the
+moving average slowly and a value close to 0 will decay the moving average
+fast.
+
+Using the step size `η` and a decay factor `ρ the
+learning rate `ηₜ` is calculated as:
+
+```math
+\begin{align*}
+ r_t &= ρ r_{t-1} + (1 - ρ)g^2 \\
+ η_t &= \frac{η}{\sqrt{r_t + ϵ}}
+\end{align*}
+```
+
+### References
+1. Tieleman, T. and Hinton, G. (2012):
+ Neural Networks for Machine Learning, Lecture 6.5 - rmsprop.
+ Coursera. [http://www.youtube.com/watch?v=O3sxAc4hxZU]
+ (http://www.youtube.com/watch?v=O3sxAc4hxZU) (formula @5:20)
+"""
+RMSProp
+
+@defstruct RMSProp <: AbstractOptimizer (
+ (η :: Real = 0.001, η > 0),
+ (ρ :: Real = 0.9, 0 < ρ < 1),
+ (ϵ :: Real = 1e-8, ϵ > 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 1e-5, λ >= 0),
+ η_sched :: Any = initlrsched(η)
+)
+
+create_state(::RMSProp, ::Int, W::NDArray) = zeros(size(W), context(W))
+
+function update!(rms::RMSProp, ::Int, W::NDArray, ∇::NDArray, s::NDArray)
+ η = get(rms.η_sched)
+ ρ = rms.ρ
+ ϵ = rms.ϵ
+
+ normgrad!(rms, W, ∇)
+
+ @inplace s .*= ρ
+ @inplace s .+= (1 - ρ) .* (∇.^2)
+ @inplace W .+= -η .* ∇ ./ sqrt(s .+ ϵ) # FIXME: sqrt should be dot-call
+end
diff --git a/julia/src/optimizers/sgd.jl b/julia/src/optimizers/sgd.jl
new file mode 100644
index 000000000000..dfd3d954baa3
--- /dev/null
+++ b/julia/src/optimizers/sgd.jl
@@ -0,0 +1,88 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+doc"""
+ SGD(; kwargs...)
+
+Stochastic gradient descent optimizer.
+
+Vanilla SGD:
+
+```math
+\theta \leftarrow \theta - \eta \nabla
+```
+
+SGD with momentum::
+
+```math
+\begin{align*}
+ \nu & \leftarrow \mu \nu_{t-1} - \eta \nabla \\
+ \theta & \leftarrow \theta + \nu_t
+\end{align*}
+```
+
+### Arguments
+
+* `η`: default `0.01`, learning rate.
+* `μ`: default `0`, the momentum, usually set to `0.9` in this implementation.
+* `λ`: default `0.0001`, weight decay is equivalent to
+ adding a global l2 regularizer to the parameters.
+* `clip`: default `0`, gradient clipping.
+ If positive, will clip the gradient into the bounded range `[-clip, clip]`.
+* `scale`: default `0`, gradient rescaling.
+ If != 0, multiply the gradient with `scale` before updating.
+ Often choose to be `1.0 / batch_size`.
+ If leave it default, high-level API like `fit!` will set it to
+ `1.0 / batch_size`, since `fit!` knows the `batch_size`.
+* `μ_sched::AbstractMomentumScheduler`: default `Momentum.Null()`,
+ a dynamic momentum scheduler. If set, will overwrite the `momentum`
+ parameter.
+* `η_sched::AbstractLearningRateScheduler`: default `LearningRate.Fixed(η)`, a
+ dynamic learning rate scheduler. If set, will overwrite the `η` parameter.
+"""
+SGD
+
+@defstruct SGD <: AbstractOptimizer (
+ (η :: Real = 0.01, η > 0),
+ (μ :: Real = 0.0, μ >= 0),
+ (clip :: Real = 0, clip >= 0),
+ scale :: Real = 0,
+ (λ :: Real = 0.0001, λ >= 0),
+ η_sched :: Any = initlrsched(η),
+ μ_sched :: Any = initmomsched(μ)
+)
+
+create_state(sgd::SGD, ::Int, W::NDArray) =
+ isa(sgd.μ_sched, Momentum.Null) ? nothing : zeros(size(W), context(W))
+
+function update!(sgd::SGD, ::Int, W::NDArray, ∇::NDArray, ::Void)
+ η = get(sgd.η_sched)
+ normgrad!(sgd, W, ∇)
+ @inplace W += -η * ∇
+end
+
+# update with momentum
+function update!(sgd::SGD, ::Int, W::NDArray, ∇::NDArray, ν::NDArray)
+ η = get(sgd.η_sched)
+ μ = get(sgd.μ_sched)
+
+ normgrad!(sgd, W, ∇)
+
+ @inplace ν .*= μ
+ @inplace ν .+= -η .* ∇
+ @inplace W .+= ν
+end
diff --git a/julia/src/random.jl b/julia/src/random.jl
new file mode 100644
index 000000000000..b9b32a42ecff
--- /dev/null
+++ b/julia/src/random.jl
@@ -0,0 +1,83 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ rand!(x::NDArray; low = 0, high = 1)
+
+Draw random samples from a uniform distribution.
+Samples are uniformly distributed over the half-open interval [low, high)
+(includes low, but excludes high).
+
+```julia
+julia> mx.rand!(empty(2, 3))
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 0.385748 0.839275 0.444536
+ 0.0879585 0.215928 0.104636
+
+julia> mx.rand!(empty(2, 3), low = 1, high = 10)
+2×3 mx.NDArray{Float32,2} @ CPU0:
+ 6.6385 4.18888 2.07505
+ 8.97283 2.5636 1.95586
+```
+"""
+rand!(x::NDArray; low = 0, high = 1) =
+ _random_uniform(NDArray, low = low, high = high, shape = size(x), out = x)
+
+"""
+ rand(dims...; low = 0, high = 1, context = cpu())
+
+Draw random samples from a uniform distribution.
+Samples are uniformly distributed over the half-open interval [low, high)
+(includes low, but excludes high).
+
+```julia
+julia> mx.rand(2, 2)
+2×2 mx.NDArray{Float32,2} @ CPU0:
+ 0.487866 0.825691
+ 0.0234245 0.794797
+
+julia> mx.rand(2, 2; low = 1, high = 10)
+2×2 mx.NDArray{Float32,2} @ CPU0:
+ 5.5944 5.74281
+ 9.81258 3.58068
+```
+"""
+rand(dims::Int...; low = 0, high = 1, context = cpu()) =
+ rand!(empty(dims, context), low = low, high = high)
+
+"""
+ randn!(x::NDArray; μ = 0, σ = 1)
+
+Draw random samples from a normal (Gaussian) distribution.
+"""
+randn!(x::NDArray; μ = 0, σ = 1) =
+ _random_normal(NDArray, loc = μ, scale = σ, shape = size(x), out = x)
+
+"""
+ randn(dims...; μ = 0, σ = 1, context = cpu())
+
+Draw random samples from a normal (Gaussian) distribution.
+"""
+randn(dims::Int...; μ = 0, σ = 1, context = cpu()) =
+ randn!(empty(dims, context), μ = μ, σ = σ)
+
+"""
+ srand(seed::Int)
+
+Set the random seed of libmxnet
+"""
+srand(seed_state::Int) = @mxcall(:MXRandomSeed, (Cint,), seed_state)
diff --git a/julia/src/symbolic-node.jl b/julia/src/symbolic-node.jl
new file mode 100644
index 000000000000..508f9d449028
--- /dev/null
+++ b/julia/src/symbolic-node.jl
@@ -0,0 +1,1003 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+ SymbolicNode
+
+SymbolicNode is the basic building block of the symbolic graph in MXNet.jl.
+
+ (self :: SymbolicNode)(args :: SymbolicNode...)
+ (self :: SymbolicNode)(; kwargs...)
+
+Make a new node by composing `self` with `args`. Or the arguments
+can be specified using keyword arguments.
+"""
+mutable struct SymbolicNode
+ handle::MX_SymbolHandle
+end
+
+const SymbolicNodeOrReal = Union{SymbolicNode, Real}
+
+@unfuse SymbolicNode # for broadcasting
+
+Base.unsafe_convert(::Type{MX_handle}, obj::SymbolicNode) =
+ Base.unsafe_convert(MX_handle, obj.handle)
+Base.convert(t::Type{MX_handle}, obj::SymbolicNode) = Base.unsafe_convert(t, obj)
+Base.cconvert(t::Type{MX_handle}, obj::SymbolicNode) = Base.unsafe_convert(t, obj)
+
+"""
+ deepcopy(self :: SymbolicNode)
+
+Make a deep copy of a SymbolicNode.
+"""
+function Base.deepcopy(self :: SymbolicNode)
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXSymbolCopy, (MX_handle, Ref{MX_handle}), self, ref_hdr)
+ return SymbolicNode(MX_SymbolHandle(ref_hdr[]))
+end
+
+"""
+ copy(self :: SymbolicNode)
+
+Make a copy of a SymbolicNode. The same as making a deep copy.
+"""
+function Base.copy(self :: SymbolicNode)
+ Base.deepcopy(self)
+end
+
+function (self::SymbolicNode)(args :: SymbolicNode...)
+ s = deepcopy(self)
+ _compose!(s, args...)
+end
+function (self::SymbolicNode)(;kwargs...)
+ s = deepcopy(self)
+ _compose!(s; kwargs...)
+end
+
+macro _list_symbol_info(self, func_name)
+ quote
+ ref_sz = Ref{MX_uint}(0)
+ ref_names = Ref{char_pp}(0)
+ @mxcall($func_name, (MX_handle, Ref{MX_uint}, Ref{char_pp}),
+ $(esc(self)), ref_sz, ref_names)
+ narg = ref_sz[]
+ names = unsafe_wrap(Array, ref_names[], narg)
+ names = [Symbol(unsafe_string(x)) for x in names]
+ return names
+ end
+end
+
+"""
+ list_arguments(self :: SymbolicNode)
+
+List all the arguments of this node. The argument for a node contains both
+the inputs and parameters. For example, a `FullyConnected` node will
+have both data and weights in its arguments. A composed node (e.g. a MLP) will
+list all the arguments for intermediate nodes.
+
+Returns a list of symbols indicating the names of the arguments.
+"""
+function list_arguments(self :: SymbolicNode)
+ @_list_symbol_info(self, :MXSymbolListArguments)
+end
+
+"""
+ list_outputs(self :: SymbolicNode)
+
+List all the outputs of this node.
+
+Returns a list of symbols indicating the names of the outputs.
+"""
+function list_outputs(self :: SymbolicNode)
+ @_list_symbol_info(self, :MXSymbolListOutputs)
+end
+
+
+"""
+ list_auxiliary_states(self :: SymbolicNode)
+
+
+List all auxiliary states in the symbool.
+
+Auxiliary states are special states of symbols that do not corresponds to an argument,
+and do not have gradient. But still be useful for the specific operations.
+A common example of auxiliary state is the moving_mean and moving_variance in BatchNorm.
+Most operators do not have Auxiliary states.
+
+Returns a list of symbols indicating the names of the auxiliary states.
+"""
+function list_auxiliary_states(self :: SymbolicNode)
+ @_list_symbol_info(self, :MXSymbolListAuxiliaryStates)
+end
+
+"""
+ get_internals(self :: SymbolicNode)
+
+Get a new grouped `SymbolicNode` whose output contains all the internal outputs of
+this `SymbolicNode`.
+"""
+function get_internals(self :: SymbolicNode)
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXSymbolGetInternals, (MX_handle, Ref{MX_handle}), self, ref_hdr)
+ return SymbolicNode(MX_SymbolHandle(ref_hdr[]))
+end
+
+"""
+ get_children(x::SymbolicNode)
+
+Gets a new grouped `SymbolicNode` whose output contains inputs to output
+nodes of the original symbol.
+
+```julia
+julia> x = mx.Variable(:x)
+MXNet.mx.SymbolicNode x
+
+julia> y = mx.Variable(:y)
+MXNet.mx.SymbolicNode y
+
+julia> z = x + y
+MXNet.mx.SymbolicNode _plus1
+
+julia> a |> mx.get_children |> mx.list_outputs
+2-element Array{Symbol,1}:
+ :x
+ :y
+```
+"""
+function get_children(x::SymbolicNode)
+ hdl = Ref{MX_handle}(C_NULL)
+ @mxcall(:MXSymbolGetChildren, (MX_handle, Ref{MX_handle}), x, hdl)
+ sym = hdl[] |> MX_SymbolHandle |> SymbolicNode
+ isempty(list_outputs(sym)) ? nothing : sym
+end
+
+"""
+ get_attr(self :: SymbolicNode, key :: Symbol)
+
+Get attribute attached to this `SymbolicNode` belonging to key.
+
+Returns the value belonging to key as a `Nullable`.
+"""
+function get_attr(self :: SymbolicNode, key :: Symbol)
+ key_s = string(key)
+ ref_out = Ref{Cstring}()
+ ref_success = Ref{Cint}(-1)
+ @mxcall(:MXSymbolGetAttr, (MX_handle, Cstring, Ref{Cstring}, Ref{Cint}),
+ self, key_s, ref_out, ref_success)
+ if ref_success[] == 1
+ return Nullable{String}(unsafe_string(ref_out[]))
+ else
+ return Nullable{String}()
+ end
+end
+
+"""
+ list_attr(self :: SymbolicNode)
+
+Get all attributes from a symbol.
+
+Returns a dictionary of attributes.
+"""
+function list_attr(self :: SymbolicNode)
+ ref_sz = Ref{MX_uint}(0)
+ ref_strings = Ref{char_pp}(0)
+ @mxcall(:MXSymbolListAttrShallow, (MX_handle, Ref{MX_uint}, Ref{char_pp}),
+ self, ref_sz, ref_strings)
+ narg = 2*ref_sz[]
+ strings = unsafe_wrap(Array, ref_strings[], narg)
+ out = Dict{Symbol, String}()
+ for i in 1:2:narg
+ key = Symbol(unsafe_string(strings[i]))
+ value = unsafe_string(strings[i+1]) # Creates a copy of string
+ out[key] = value
+ end
+ return out
+end
+
+"""
+ list_all_attr(self :: SymbolicNode)
+
+Get all attributes from the symbol graph.
+
+Returns a dictionary of attributes.
+"""
+function list_all_attr(self :: SymbolicNode)
+ ref_sz = Ref{MX_uint}(0)
+ ref_strings = Ref{char_pp}(0)
+ @mxcall(:MXSymbolListAttr, (MX_handle, Ref{MX_uint}, Ref{char_pp}),
+ self, ref_sz, ref_strings)
+ narg = 2*ref_sz[]
+ strings = unsafe_wrap(Array, ref_strings[], narg)
+ out = Dict{Symbol, String}()
+ for i in 1:2:narg
+ key = Symbol(unsafe_string(strings[i]))
+ value = unsafe_string(strings[i+1])
+ out[key] = value
+ end
+ return out
+end
+
+"""
+ set_attr(self:: SymbolicNode, key :: Symbol, value :: AbstractString)
+
+Set the attribute key to value for this `SymbolicNode`.
+
+!!! note
+ It is encouraged not to call this function directly, unless you know exactly what you are doing. The
+ recommended way of setting attributes is when creating the `SymbolicNode`. Changing
+ the attributes of a `SymbolicNode` that is already been used somewhere else might
+ cause unexpected behavior and inconsistency.
+"""
+function set_attr(self :: SymbolicNode, key :: Symbol, value :: AbstractString)
+ key_s = string(key)
+ value_s = String(value)
+
+ @mxcall(:MXSymbolSetAttr, (MX_handle, Cstring, Cstring), self, key_s, value_s)
+end
+
+"""
+ get_name(self :: SymbolicNode)
+
+Get the name of the symbol.
+
+ julia> x = mx.Variable(:data)
+ julia> mx.get_name(x)
+ :data
+
+ julia> y = mx.FullyConnected(x, num_hidden = 128)
+ julia> mx.get_name(y)
+ :fullyconnected0
+"""
+function get_name(self :: mx.SymbolicNode)
+ name = Ref{mx.char_p}(0)
+ success = Ref(0)
+ @mxcall(:MXSymbolGetName, (MX_handle, Ref{char_p}, Ref{Int}), self.handle.value, name, success)
+ @assert success[] != -1
+
+ str = name[]
+ if str == C_NULL # e.g. the symbol returned via get_internals
+ string(self.handle.value)
+ else
+ Symbol(unsafe_string(str))
+ end
+end
+
+Base.show(io::IO, sym::SymbolicNode) =
+ print(io, "$(typeof(sym)) $(get_name(sym))")
+
+import Base: print
+
+function print(io::IO, sym::SymbolicNode)
+ out = Ref{mx.char_p}(C_NULL)
+ @mx.mxcall(:MXSymbolPrint, (mx.MX_SymbolHandle, Ref{mx.char_p}), sym.handle, out)
+ print(io, unsafe_string(out[]))
+end
+
+print(sym::SymbolicNode) = print(STDOUT, sym)
+
+"""
+ print([io::IO], sym::SymbolicNode)
+
+Print the content of symbol, used for debug.
+
+```julia
+julia> layer = @mx.chain mx.Variable(:data) =>
+ mx.FullyConnected(name=:fc1, num_hidden=128) =>
+ mx.Activation(name=:relu1, act_type=:relu)
+MXNet.mx.SymbolicNode(MXNet.mx.MX_SymbolHandle(Ptr{Void} @0x000055b29b9c3520))
+
+julia> print(layer)
+Symbol Outputs:
+ output[0]=relu1(0)
+Variable:data
+Variable:fc1_weight
+Variable:fc1_bias
+--------------------
+Op:FullyConnected, Name=fc1
+Inputs:
+ arg[0]=data(0) version=0
+ arg[1]=fc1_weight(0) version=0
+ arg[2]=fc1_bias(0) version=0
+Attrs:
+ num_hidden=128
+--------------------
+Op:Activation, Name=relu1
+Inputs:
+ arg[0]=fc1(0)
+Attrs:
+ act_type=relu
+```
+"""
+print
+
+"""
+ grad(self :: SymbolicNode, wrt :: Vector{SymbolicNode})
+
+Get the autodiff gradient of the current `SymbolicNode`. This function can
+only be used if the current symbol is a loss function.
+
+# Arguments:
+* `self::SymbolicNode`: current node.
+* `wrt::Vector{Symbol}`: the names of the arguments to the gradient.
+
+Returns a gradient symbol of the corresponding gradient.
+"""
+function grad(self :: SymbolicNode, wrt :: Vector{Symbol})
+ hdr_ref = Ref{MX_handle}(0)
+ keys = String[string(key) for key in wrt]
+
+ @mxcall(:MXSymbolGrad, (MX_handle, MX_uint, char_pp, Ptr{MX_handle}), self, length(keys), keys, hdr_ref)
+ return SymbolicNode(MX_SymbolHandle(hdr_ref[]))
+end
+
+"""
+ Variable(name :: Union{Symbol, AbstractString})
+
+Create a symbolic variable with the given name. This is typically used as a placeholder.
+For example, the data node, acting as the starting point of a network architecture.
+
+# Arguments
+* Dict{Symbol, AbstractString} attrs: The attributes associated with this `Variable`.
+"""
+function Variable(name :: Union{Symbol, AbstractString}; attrs = Dict())
+ attrs = convert(Dict{Symbol, AbstractString}, attrs)
+ hdr_ref = Ref{MX_handle}(0)
+ @mxcall(:MXSymbolCreateVariable, (char_p, Ref{MX_handle}), name, hdr_ref)
+ node = SymbolicNode(MX_SymbolHandle(hdr_ref[]))
+ for (k, v) in attrs
+ set_attr(node, k, v)
+ end
+ node
+end
+
+"""
+ @var ...
+
+A handy macro for creating `mx.Variable`.
+
+```julia
+julia> x = @mx.var x
+MXNet.mx.SymbolicNode x
+
+julia> x, y, z = @mx.var x y z
+(MXNet.mx.SymbolicNode x, MXNet.mx.SymbolicNode y, MXNet.mx.SymbolicNode z)
+```
+"""
+macro var(n::Symbol)
+ Expr(:call, :Variable, QuoteNode(n))
+end
+
+macro var(names::Symbol...)
+ Expr(:tuple, map(n -> Expr(:call, :Variable, QuoteNode(n)), names)...)
+end
+
+"""
+ Group(nodes :: SymbolicNode...)
+
+Create a `SymbolicNode` by grouping nodes together.
+"""
+function Group(nodes :: SymbolicNode...)
+ handles = MX_handle[nodes...]
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXSymbolCreateGroup, (MX_uint, Ptr{MX_handle}, Ref{MX_handle}),
+ length(handles), handles, ref_hdr)
+ SymbolicNode(MX_SymbolHandle(ref_hdr[]))
+end
+
+function _build_shapes(shape_size::MX_uint, shape_ndim::Ptr{MX_uint}, shape_data::Ptr{Ptr{MX_uint}})
+ shape_ndim = unsafe_wrap(Array, shape_ndim, shape_size)
+ shape_data = unsafe_wrap(Array, shape_data, shape_size)
+ shapes = map(1:shape_size) do i
+ my_shape = unsafe_wrap(Array, shape_data[i], shape_ndim[i])
+ tuple(flipdim(Int[my_shape...],1)...)
+ end
+ convert(Vector{Tuple}, shapes)
+end
+
+function _infer_shape(self, keys, indptr, sdata)
+ ref_arg_shape_size = Ref{MX_uint}(0)
+ ref_arg_shape_ndim = Ref{Ptr{MX_uint}}(0)
+ ref_arg_shape_data = Ref{Ptr{Ptr{MX_uint}}}(0)
+ ref_out_shape_size = Ref{MX_uint}(0)
+ ref_out_shape_ndim = Ref{Ptr{MX_uint}}(0)
+ ref_out_shape_data = Ref{Ptr{Ptr{MX_uint}}}(0)
+ ref_aux_shape_size = Ref{MX_uint}(0)
+ ref_aux_shape_ndim = Ref{Ptr{MX_uint}}(0)
+ ref_aux_shape_data = Ref{Ptr{Ptr{MX_uint}}}(0)
+ ref_complete = Ref{Cint}(0)
+ @mxcall(:MXSymbolInferShape,
+ (MX_handle, MX_uint, char_pp, Ptr{MX_uint}, Ptr{MX_uint},
+ Ref{MX_uint}, Ref{Ptr{MX_uint}}, Ref{Ptr{Ptr{MX_uint}}},
+ Ref{MX_uint}, Ref{Ptr{MX_uint}}, Ref{Ptr{Ptr{MX_uint}}},
+ Ref{MX_uint}, Ref{Ptr{MX_uint}}, Ref{Ptr{Ptr{MX_uint}}},
+ Ref{Cint}),
+ self, length(indptr)-1, keys, indptr, sdata,
+ ref_arg_shape_size, ref_arg_shape_ndim, ref_arg_shape_data,
+ ref_out_shape_size, ref_out_shape_ndim, ref_out_shape_data,
+ ref_aux_shape_size, ref_aux_shape_ndim, ref_aux_shape_data,
+ ref_complete)
+ if ref_complete[] == 0
+ return (nothing, nothing, nothing)
+ else
+ return (
+ _build_shapes(ref_arg_shape_size[], ref_arg_shape_ndim[], ref_arg_shape_data[]),
+ _build_shapes(ref_out_shape_size[], ref_out_shape_ndim[], ref_out_shape_data[]),
+ _build_shapes(ref_aux_shape_size[], ref_aux_shape_ndim[], ref_aux_shape_data[])
+ )
+ end
+end
+
+"""
+ infer_shape(self :: SymbolicNode, args...)
+ infer_shape(self :: SymbolicNode; kwargs...)
+
+Do shape inference according to the input shapes. The input shapes could be provided
+as a list of shapes, which should specify the shapes of inputs in the same order as
+the arguments returned by [`list_arguments`](@ref). Alternatively, the shape information
+could be specified via keyword arguments.
+
+Returns a 3-tuple containing shapes of all the arguments, shapes of all the outputs and
+shapes of all the auxiliary variables. If shape inference failed due to incomplete
+or incompatible inputs, the return value will be `(nothing, nothing, nothing)`.
+"""
+function infer_shape(self :: SymbolicNode; kwargs...)
+ sdata = MX_uint[]
+ indptr = MX_uint[0]
+ for (k,v) in kwargs
+ append!(sdata, flipdim([v...],1))
+ push!(indptr, length(sdata))
+ end
+ keys = AbstractString[string(x[1]) for x in kwargs]
+ _infer_shape(self, keys, indptr, sdata)
+end
+function infer_shape(self :: SymbolicNode, args :: Union{Tuple, Void}...)
+ sdata = MX_uint[]
+ indptr = MX_uint[0]
+ for arg in args
+ if isa(arg, Void); continue; end
+ append!(sdata, flipdim([arg...],1))
+ push!(indptr, length(sdata))
+ end
+ keys = Ptr{char_p}(0)
+ _infer_shape(self, keys, indptr, sdata)
+end
+
+function _infer_type(self, keys, arg_type_data)
+ ref_in_type_size = Ref{MX_uint}()
+ ref_in_type_data = Ref{Ptr{Cint}}()
+ ref_out_type_size = Ref{MX_uint}()
+ ref_out_type_data = Ref{Ptr{Cint}}()
+ ref_aux_type_size = Ref{MX_uint}()
+ ref_aux_type_data = Ref{Ptr{Cint}}()
+ ref_complete = Ref{Cint}()
+
+ @mxcall(:MXSymbolInferType,
+ (MX_handle, MX_uint, char_pp, Ptr{Cint},
+ Ref{MX_uint}, Ref{Ptr{Cint}},
+ Ref{MX_uint}, Ref{Ptr{Cint}},
+ Ref{MX_uint}, Ref{Ptr{Cint}},
+ Ref{Cint}),
+ self, length(arg_type_data)-1, keys, arg_type_data,
+ ref_in_type_size, ref_in_type_data,
+ ref_out_type_size, ref_out_type_data,
+ ref_aux_type_size, ref_aux_type_data,
+ ref_complete)
+
+ if ref_complete[] == 0
+ return (nothing, nothing, nothing)
+ else
+ in_type = unsafe_wrap(Array, ref_in_type_data[], ref_in_type_size[])
+ out_type = unsafe_wrap(Array, ref_out_type_data[], ref_out_type_size[])
+ aux_type = unsafe_wrap(Array, ref_aux_type_data[], ref_aux_type_size[])
+ return ([fromTypeFlag(TypeFlag(t)) for t in in_type],
+ [fromTypeFlag(TypeFlag(t)) for t in out_type],
+ [fromTypeFlag(TypeFlag(t)) for t in aux_type])
+ end
+end
+
+"""
+ infer_type(self :: SymbolicNode; kwargs...)
+ infer_type(self :: SymbolicNode, args...)
+
+Do type inference according to the input types. The input types could be provided
+as a list of types, which should specify the types of inputs in the same order as
+the arguments returned by [`list_arguments`](@ref). Alternatively, the type information
+could be specified via keyword arguments.
+
+Returns a 3-tuple containing types of all the arguments, types of all the outputs and
+types of all the auxiliary variables. If type inference failed due to incomplete
+or incompatible inputs, the return value will be `(nothing, nothing, nothing)`.
+"""
+function infer_type(self :: SymbolicNode; kwargs...)
+ types = Cint[toTypeFlag(x[2]) for x in kwargs]
+ keys = AbstractString[string(x[1]) for x in kwargs]
+ _infer_type(self, keys, types)
+end
+
+function infer_type(self :: SymbolicNode, args :: Union{Tuple, Void}...)
+ types = Cint[]
+ keys = Ptr{char_p}(0)
+
+ for arg in args
+ if isa(arg, Void); continue; end
+ push!(types, toTypeFlag(arg))
+ end
+ _infer_type(self, keys, types)
+end
+
+"""
+ getindex(self :: SymbolicNode, idx :: Union{Int, Base.Symbol, AbstractString})
+
+Get a node representing the specified output of this node. The index could be
+a symbol or string indicating the name of the output, or a 1-based integer
+indicating the index, as in the list of [`list_outputs`](@ref).
+"""
+function Base.getindex(self :: SymbolicNode, idx :: Union{Base.Symbol, AbstractString})
+ idx = Symbol(idx)
+ i_idx = find(idx .== list_outputs(self))
+ @assert(length(i_idx) > 0, "Cannot find output with name '$idx'")
+ @assert(length(i_idx) < 2, "Found duplicated output with name '$idx'")
+ Base.getindex(self, i_idx[1])
+end
+function Base.getindex(self :: SymbolicNode, idx :: Int)
+ ref_hdr = Ref{MX_handle}(0)
+ # note Julia is 1-based, while MXNet is 0-based
+ @mxcall(:MXSymbolGetOutput, (MX_handle, MX_uint, Ref{MX_handle}), self, idx-1, ref_hdr)
+ return SymbolicNode(MX_SymbolHandle(ref_hdr[]))
+end
+
+import Base: +
+
+"""
+ +(args...)
+ .+(args...)
+
+Elementwise summation of `SymbolicNode`.
+"""
+function +(x::SymbolicNode, ys::SymbolicNodeOrReal...)
+ ret = x
+ for y ∈ ys
+ if y isa SymbolicNode
+ ret = _plus(ret, y)
+ else
+ ret = _plus_scalar(ret, scalar=MX_float(y))
+ end
+ end
+ ret
+end
+
++(s::Real, x::SymbolicNode, ys::SymbolicNodeOrReal...) = +(x + s, ys...)
+
+broadcast_(::typeof(+), x::SymbolicNode, ys::SymbolicNodeOrReal...) = +(x, ys...)
+broadcast_(::typeof(+), s::Real, x::SymbolicNode, ys::SymbolicNodeOrReal...) = +(x + s, ys...)
+
+import Base: -
+
+"""
+ -(x, y)
+ .-(x, y)
+
+Elementwise substraction of `SymbolicNode`.
+Operating with `Real` is available.
+"""
+x::SymbolicNode - y::SymbolicNode = _minus(x, y)
+x::SymbolicNode - s::Real = _minus_scalar(x, scalar=MX_float(s))
+s::Real - x::SymbolicNode = _rminus_scalar(x, scalar=MX_float(s))
+
+-(x::SymbolicNode) = 0 - x
+
+broadcast_(::typeof(-), x::SymbolicNode, y::SymbolicNodeOrReal) = x - y
+broadcast_(::typeof(-), s::Real, x::SymbolicNode) = s - x
+
+import Base: *
+
+"""
+ .*(x, y)
+
+Elementwise multiplication of `SymbolicNode`.
+"""
+x::SymbolicNode * s::Real = _mul_scalar(x, scalar=MX_float(s))
+s::Real * x::SymbolicNode = _mul_scalar(x, scalar=MX_float(s))
+
+function broadcast_(::typeof(*), x::SymbolicNode, ys::SymbolicNodeOrReal...)
+ ret = x
+ for y in ys
+ if y isa SymbolicNode
+ ret = _mul(ret, y)
+ else
+ ret = _mul_scalar(ret, scalar=MX_float(y))
+ end
+ end
+ ret
+end
+
+broadcast_(::typeof(*), s::Real, x::SymbolicNode, ys::SymbolicNodeOrReal...) =
+ broadcast_(*, x * s, ys...)
+
+import Base: /
+
+"""
+ ./(x, y)
+
+* Elementwise dividing a `SymbolicNode` by a scalar or another `SymbolicNode`
+of the same shape.
+
+* Elementwise divide a scalar by an `SymbolicNode`.
+
+* Matrix division (solving linear systems) is not implemented yet.
+"""
+x::SymbolicNode / s::Real = _DivScalar(x, scalar=MX_float(s))
+
+broadcast_(::typeof(/), x::SymbolicNode, y::SymbolicNode) = _div(x, y)
+broadcast_(::typeof(/), x::SymbolicNode, s::Real) = _div_scalar(x, scalar=MX_float(s))
+broadcast_(::typeof(/), s::Real, x::SymbolicNode) = _rdiv_scalar(x, scalar=MX_float(s))
+
+
+import Base: ^
+
+"""
+ .^(x, y)
+
+Elementwise power of `SymbolicNode` and `NDArray`.
+Operating with `Real` is available.
+"""
+^
+
+broadcast_(::typeof(^), x::SymbolicNode, y::SymbolicNode) = _power(x, y)
+broadcast_(::typeof(^), x::SymbolicNode, s::Real) = _power_scalar(x, scalar=MX_float(s))
+broadcast_(::typeof(^), s::Real, x::SymbolicNode) = _rpower_scalar(x, scalar=MX_float(s))
+
+broadcast_(::typeof(^), ::Irrational{:e}, x::SymbolicNode) = exp(x)
+broadcast_(::typeof(^), x::SymbolicNode, s::Irrational) =
+ _power_scalar(x, scalar=MX_float(s))
+broadcast_(::typeof(^), s::Irrational, x::SymbolicNode) =
+ _rpower_scalar(x, scalar=MX_float(s))
+
+function _compose!(node :: SymbolicNode; kwargs...)
+ name = char_p(0)
+ arg_keys = AbstractString[]
+ arg_vals = MX_handle[]
+
+ for (k,v) in kwargs
+ if k == :name
+ name = string(v)
+ else
+ @assert(isa(v, SymbolicNode), "Compose expect `SymbolicNode` as arguments")
+ push!(arg_keys, string(k))
+ push!(arg_vals, v)
+ end
+ end
+
+ @mxcall(:MXSymbolCompose,
+ (MX_handle, char_p, MX_uint, Ptr{char_p}, Ptr{MX_handle}),
+ node, name, length(arg_keys), arg_keys, arg_vals)
+ return node
+end
+function _compose!(node :: SymbolicNode, args::SymbolicNode...)
+ _compose!(node, char_p(0), args...)
+end
+function _compose!(node :: SymbolicNode, name :: Union{Base.Symbol, char_p}, args::SymbolicNode...)
+ if isa(name, Base.Symbol); name = string(name); end
+ arg_keys = Ptr{char_p}(0)
+ arg_vals = MX_handle[args...]
+
+ @mxcall(:MXSymbolCompose,
+ (MX_handle, char_p, MX_uint, Ptr{char_p}, Ptr{MX_handle}),
+ node, name, length(arg_vals), arg_keys, arg_vals)
+ return node
+end
+
+"""
+ to_json(self :: SymbolicNode)
+
+Convert a `SymbolicNode` into a JSON string.
+"""
+function to_json(self :: SymbolicNode)
+ ref_json = Ref{char_p}(0)
+ @mxcall(:MXSymbolSaveToJSON, (MX_handle, Ref{char_p}), self, ref_json)
+ return unsafe_string(ref_json[])
+end
+
+"""
+ from_json(repr :: AbstractString, ::Type{SymbolicNode})
+
+Load a `SymbolicNode` from a JSON string representation.
+"""
+function from_json(repr :: AbstractString, ::Type{SymbolicNode})
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXSymbolCreateFromJSON, (char_p, Ref{MX_handle}), repr, ref_hdr)
+ return SymbolicNode(MX_SymbolHandle(ref_hdr[]))
+end
+
+"""
+ load(filename :: AbstractString, ::Type{SymbolicNode})
+
+Load a `SymbolicNode` from a JSON file.
+"""
+function load(filename :: AbstractString, ::Type{SymbolicNode})
+ ref_hdr = Ref{MX_handle}(0)
+ @mxcall(:MXSymbolCreateFromFile, (char_p, Ref{MX_handle}), filename, ref_hdr)
+ return SymbolicNode(MX_SymbolHandle(ref_hdr[]))
+end
+
+"""
+ save(filename :: AbstractString, node :: SymbolicNode)
+
+Save a `SymbolicNode` to a JSON file.
+"""
+function save(filename :: AbstractString, node :: SymbolicNode)
+ @mxcall(:MXSymbolSaveToFile, (MX_handle, char_p), node, filename)
+end
+
+import Base: reshape
+
+"""
+ reshape(sym::SymbolicNode, dim; reverse=false, name)
+ reshape(sym::SymbolicNode, dim...; reverse=false, name)
+
+Reshape SymbolicNode operator
+
+Some dimensions of the shape can take special values from the set
+{0, -1, -2, -3, -4}.
+The significance of each is explained below:
+
+- `0` copy this dimension from the input to the output shape.
+
+ Example:
+
+ - input shape = (2,3,4), shape = (4,0,2), output shape = (4,3,2)
+ - input shape = (2,3,4), shape = (2,0,0), output shape = (2,3,4)
+
+- `-1` infers the dimension of the output shape by using the remainder of the
+ input dimensions keeping the size of the new array same as that of the input
+ array. At most one dimension of shape can be -1.
+
+ Example:
+
+ - input shape = (2,3,4), shape = (6,1,-1), output shape = (6,1,4)
+ - input shape = (2,3,4), shape = (3,-1,8), output shape = (3,1,8)
+ - input shape = (2,3,4), shape=(-1,), output shape = (24,)
+
+- `-2` copy all/remainder of the input dimensions to the output shape.
+
+ Example:
+
+ - input shape = (2,3,4), shape = (-2,), output shape = (2,3,4)
+ - input shape = (2,3,4), shape = (2,-2), output shape = (2,3,4)
+ - input shape = (2,3,4), shape = (-2,1,1), output shape = (2,3,4,1,1)
+
+- `-3` use the product of two consecutive dimensions of the input shape as the
+ output dimension.
+
+ Example:
+
+ - input shape = (2,3,4), shape = (-3,4), output shape = (6,4)
+ - input shape = (2,3,4,5), shape = (-3,-3), output shape = (6,20)
+ - input shape = (2,3,4), shape = (0,-3), output shape = (2,12)
+ - input shape = (2,3,4), shape = (-3,-2), output shape = (6,4)
+
+- `-4` split one dimension of the input into two dimensions passed subsequent
+ to -4 in shape (can contain -1).
+
+ Example:
+
+ - input shape = (2,3,4), shape = (-4,1,2,-2), output shape = (1,2,3,4)
+ - input shape = (2,3,4), shape = (2,-4,-1,3,-2), output shape = (2,1,3,4)
+
+If the argument `reverse` is set to `1`, then the special values are inferred
+from right to left.
+
+ Example:
+
+ - with `reverse=false`, for input shape = (10,5,4), shape = (-1,0),
+ output shape would be (40,5)
+ - with `reverse=true`, output shape will be (50,4).
+"""
+reshape(sym::SymbolicNode, dim::NTuple{N, Integer}; kwargs...) where {N} =
+ _reshape(sym, dim; kwargs...)
+reshape(sym::SymbolicNode, dim::Integer...; kwargs...) =
+ _reshape(sym, dim; kwargs...)
+
+@inline function _reshape(sym::SymbolicNode, dim::NTuple{N, Integer};
+ reverse::Bool=false, name::String="") where N
+ op = _get_cached_libmx_op_handle("reshape")
+ node = _create_atomic_symbol(op.value, ["shape", "reverse"],
+ [dump_mx_param(dim), dump_mx_param(!reverse)])
+ name = get!(DEFAULT_NAME_MANAGER, name, "reshape")
+ _compose!(node, name=name, data=sym)
+end
+
+################################################################################
+# Atomic SymbolicNode functions dynamically imported from libmxnet
+################################################################################
+@inline function _create_atomic_symbol(creator::MX_handle, keys::Vector{String},
+ vals::Vector{String})
+ ref_sym_hdr = Ref{MX_handle}(C_NULL)
+ @mxcall(:MXSymbolCreateAtomicSymbol,
+ (MX_handle, MX_uint, Ptr{char_p}, Ptr{char_p}, Ref{MX_handle}),
+ creator, length(keys), keys, vals, ref_sym_hdr)
+ SymbolicNode(MX_SymbolHandle(ref_sym_hdr[]))
+end
+
+@inline function _create_atomic_symbol(creator::MX_handle, keys::Vector{String},
+ vals::Vector{String},
+ attrs::Dict{Symbol, String})
+ node = _create_atomic_symbol(creator, keys, vals)
+ # set attrs
+ for (k, v) in attrs
+ set_attr(node, k, v)
+ end
+ node
+end
+
+function _define_atomic_symbol_creator(name :: String)
+ handle = _get_libmx_op_handle(name)
+ f_desc, key_narg = _get_libmx_op_description(name, handle)
+
+ f_desc *= "* `name::Symbol`: The name of the `SymbolicNode`. (e.g. `:my_symbol`), optional.\n"
+ f_desc *= "* `attrs::Dict{Symbol, AbstractString}`: The attributes associated with this `SymbolicNode`.\n\n"
+
+ func_name = Symbol(name)
+ func_def = quote
+ function $func_name(::Type{SymbolicNode}, args::SymbolicNode...; kwargs...)
+ idx = findfirst(x -> x[1] == :name, kwargs)
+ if idx > 0
+ name = kwargs[idx][2]
+ else
+ name = ""
+ end
+
+ # XXX: hacky way of solving the problem that the arguments of `dot` should be swapped
+ # See https://github.com/dmlc/MXNet.jl/issues/55
+ if $name == "dot"
+ args = reverse(args)
+ end
+
+ # XXX: hacky way of solving the semantic difference of the axes parameter in Julia
+ # and in libmxnet.
+ # See https://github.com/dmlc/MXNet.jl/pull/123
+ if $name == "transpose"
+ kwargs = Any[key != :axes ? (key, arg) : (key, reverse(map(i->length(arg)-i, arg))) for (key, arg) in kwargs]
+ end
+
+ param_keys = String[]
+ param_vals = String[]
+ symbol_kws = Dict{Symbol, SymbolicNode}()
+ attrs = Dict{Symbol, String}()
+
+ $(if key_narg != ""
+ quote
+ if !in($key_narg, param_keys)
+ push!(param_keys, $key_narg)
+ push!(param_vals, string(length(args)))
+ end
+ end
+ end)
+
+ for (k,v) in kwargs
+ if k == :name; continue; end
+ if isa(v, SymbolicNode)
+ symbol_kws[k] = v
+ elseif k == :attrs
+ if isa(v, Dict)
+ attrs = convert(Dict{Symbol, String}, v)
+ else
+ throw(ArgumentError("attrs needs to be a Dictionary"))
+ end
+ else
+ push!(param_keys, string(k))
+ push!(param_vals, dump_mx_param(v))
+ end
+ end
+
+ if length(args) > 1 && length(symbol_kws) != 0
+ @assert(false, $name * " only accepts SymbolicNode either as positional or keyword arguments with optional positional `data` argument, not both.")
+ end
+ $(if key_narg != ""
+ quote
+ if length(symbol_kws) > 0
+ @assert(false, $name * " takes variable number of SymbolicNode arguments, " *
+ "please pass input Symbols via positional arguments, instead of keyword arguments.")
+ end
+ end
+ end)
+
+ local op = _get_cached_libmx_op_handle($name)
+ node = _create_atomic_symbol(op.value, param_keys, param_vals, attrs)
+
+ # generate a new name for the new symbol if user not provided in kwargs
+ hint = lowercase($name)
+ name = get!(DEFAULT_NAME_MANAGER, name, hint)
+
+ if length(symbol_kws) == 0
+ _compose!(node, name, args...)
+ elseif length(args) == 1
+ _compose!(node; name=name, data=args[1], symbol_kws...)
+ else
+ _compose!(node; name=name, symbol_kws...)
+ end
+
+ return node
+ end # function
+ end # quote
+
+ func_def2 = quote
+ @doc $f_desc ->
+ function $func_name(args::SymbolicNode...; kwargs...)
+ $func_name(SymbolicNode, args...; kwargs...)
+ end # function
+ end # quote
+
+ return quote
+ $func_def
+ $func_def2
+ end
+end
+
+macro _import_atomic_symbol_creators()
+ # XXX: those are operators defined for NDArray, we exclude them here
+ # because the calling convention for the type signature is not strong
+ # enough to disambiguate the method for NDArray and SymbolicNode
+ const ignored_ops = ["_set_value", "reshape"] # in lowercase
+
+ op_names = _get_libmx_op_names()
+ func_exprs = map(op_names) do name
+ if lowercase(name) ∉ ignored_ops
+ expr = _define_atomic_symbol_creator(name)
+ end
+ end
+
+ esc(quote
+ $(func_exprs...)
+ end)
+end
+
+@_import_atomic_symbol_creators()
+
+################################################################################
+# Utility macros to chain up symbols
+################################################################################
+macro chain(layers)
+ exprs = []
+ last_layer = nothing
+
+ function _chain_layer(layer, last_layer)
+ if isa(last_layer, Void)
+ return esc(layer)
+ else
+ if @capture(layer, f_(x__))
+ x′ = esc.(x)
+ return :($f($last_layer, $(x′...)))
+ else
+ throw(AssertionError("$layer is not a valid function call and cannot be chained."))
+ end
+ end
+ end
+
+ while true
+ if @capture(layers, l1_=>l2_)
+ new_layer = gensym()
+ push!(exprs, :($new_layer = $(_chain_layer(l1, last_layer))))
+ last_layer = new_layer
+ layers = l2
+ else
+ push!(exprs, _chain_layer(layers, last_layer))
+ break
+ end
+ end
+ Expr(:block, exprs...)
+end
diff --git a/julia/src/util.jl b/julia/src/util.jl
new file mode 100644
index 000000000000..c07c9ec910fb
--- /dev/null
+++ b/julia/src/util.jl
@@ -0,0 +1,255 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+################################################################################
+# Dataset related utilities
+################################################################################
+function get_data_dir()
+ data_dir = joinpath(Pkg.dir("MXNet"), "data")
+ mkpath(data_dir)
+ data_dir
+end
+
+function get_mnist_ubyte()
+ data_dir = get_data_dir()
+ mnist_dir = joinpath(data_dir, "mnist")
+ mkpath(mnist_dir)
+ filenames = Dict(:train_data => "train-images-idx3-ubyte",
+ :train_label => "train-labels-idx1-ubyte",
+ :test_data => "t10k-images-idx3-ubyte",
+ :test_label => "t10k-labels-idx1-ubyte")
+ filenames = Dict(map((x) -> x[1] => joinpath(mnist_dir, x[2]), filenames))
+ if !all(isfile, values(filenames))
+ cd(mnist_dir) do
+ mnist_dir = download("http://data.mxnet.io/mxnet/data/mnist.zip", "mnist.zip")
+ try
+ run(`unzip -u $mnist_dir`)
+ catch
+ try
+ run(pipe(`7z x $mnist_dir`,stdout=DevNull))
+ catch
+ error("Extraction Failed:No extraction program found in path")
+ end
+ end
+ end
+ end
+ return filenames
+end
+
+function get_cifar10()
+ data_dir = get_data_dir()
+ cifar10_dir = joinpath(data_dir, "cifar10")
+ mkpath(cifar10_dir)
+ filenames = Dict(:train => "cifar/train.rec", :test => "cifar/test.rec")
+ filenames = Dict(map((x) -> x[1] => joinpath(cifar10_dir, x[2]), filenames))
+ if !all(isfile, values(filenames))
+ cd(cifar10_dir) do
+ download("http://data.mxnet.io/mxnet/data/cifar10.zip", "cifar10.zip")
+ try
+ run(`unzip -u cifar10.zip`)
+ catch
+ try
+ run(pipeline(`7z x cifar10.zip`, stdout=DevNull))
+ catch
+ error("Extraction Failed:No extraction program found in path")
+ end
+ end
+ end
+ end
+
+ filenames[:mean] = joinpath(cifar10_dir, "cifar/cifar_mean.bin")
+ return filenames
+end
+
+
+################################################################################
+# Internal Utilities
+################################################################################
+function _get_libmx_op_names()
+ n = Ref{MX_uint}(0)
+ names = Ref{char_pp}(0)
+
+ @mxcall(:MXListAllOpNames, (Ref{MX_uint}, Ref{char_pp}), n, names)
+
+ names = unsafe_wrap(Array, names[], n[])
+ return [unsafe_string(x) for x in names]
+end
+function _get_libmx_op_handle(name :: String)
+ handle = Ref{MX_handle}(0)
+ @mxcall(:NNGetOpHandle, (char_p, Ref{MX_handle}), name, handle)
+ return MX_OpHandle(handle[])
+end
+
+# We keep a cache and retrieve the address everytime
+# we run Julia, instead of pre-compiling with macro,
+# because the actual handle might change in different
+# runs
+const _libmx_op_cache = Dict{String, MX_OpHandle}()
+function _get_cached_libmx_op_handle(name :: String)
+ if !haskey(_libmx_op_cache, name)
+ handle = _get_libmx_op_handle(name)
+ _libmx_op_cache[name] = handle
+ return handle
+ else
+ return _libmx_op_cache[name]
+ end
+end
+
+function _get_libmx_op_description(name::String, handle::MX_OpHandle)
+ # get operator information (human readable)
+ ref_real_name = Ref{char_p}(0)
+ ref_desc = Ref{char_p}(0)
+ ref_narg = Ref{MX_uint}(0)
+
+ ref_arg_names = Ref{char_pp}(0)
+ ref_arg_types = Ref{char_pp}(0)
+ ref_arg_descs = Ref{char_pp}(0)
+
+ ref_key_narg = Ref{char_p}(0)
+ ref_ret_type = Ref{char_p}(0)
+
+ @mxcall(:MXSymbolGetAtomicSymbolInfo,
+ (MX_handle, Ref{char_p}, Ref{char_p}, Ref{MX_uint}, Ref{char_pp},
+ Ref{char_pp}, Ref{char_pp}, Ref{char_p}, Ref{char_p}),
+ handle, ref_real_name, ref_desc, ref_narg, ref_arg_names,
+ ref_arg_types, ref_arg_descs, ref_key_narg, ref_ret_type)
+
+ real_name = unsafe_string(ref_real_name[])
+ signature = _format_signature(Int(ref_narg[]), ref_arg_names)
+ desc = " " * name * "(" * signature * ")\n\n"
+ if real_name != name
+ desc *= name * " is an alias of " * real_name * ".\n\n"
+ end
+
+ key_narg = unsafe_string(ref_key_narg[])
+ if key_narg != ""
+ desc *= "**Note**: " * name * " takes variable number of positional inputs. "
+ desc *= "So instead of calling as $name([x, y, z], $key_narg=3), "
+ desc *= "one should call via $name(x, y, z), and $key_narg will be "
+ desc *= "determined automatically.\n\n"
+ end
+
+ desc *= unsafe_string(ref_desc[]) * "\n\n"
+ desc *= "# Arguments\n"
+ desc *= _format_docstring(Int(ref_narg[]), ref_arg_names, ref_arg_types, ref_arg_descs)
+ return desc, key_narg
+end
+
+function _format_typestring(typestr :: String)
+ replace(typestr, r"\bSymbol\b", "SymbolicNode")
+end
+function _format_docstring(narg::Int, arg_names::Ref{char_pp}, arg_types::Ref{char_pp}, arg_descs::Ref{char_pp}, remove_dup::Bool=true)
+ param_keys = Set{String}()
+
+ arg_names = unsafe_wrap(Array, arg_names[], narg)
+ arg_types = unsafe_wrap(Array, arg_types[], narg)
+ arg_descs = unsafe_wrap(Array, arg_descs[], narg)
+ docstrings = String[]
+
+ for i = 1:narg
+ arg_name = unsafe_string(arg_names[i])
+ if arg_name ∈ param_keys && remove_dup
+ continue
+ end
+ push!(param_keys, arg_name)
+
+ arg_type = _format_typestring(unsafe_string(arg_types[i]))
+ arg_desc = unsafe_string(arg_descs[i])
+ push!(docstrings, "* `$arg_name::$arg_type`: $arg_desc\n")
+ end
+ return join(docstrings, "\n")
+end
+
+function _format_signature(narg::Int, arg_names::Ref{char_pp})
+ arg_names = unsafe_wrap(Array, arg_names[], narg)
+
+ return join([unsafe_string(name) for name in arg_names] , ", ")
+end
+
+"""
+Extract the line of `Defined in ...`
+
+julia> mx._getdocdefine("sgd_update")
+"Defined in src/operator/optimizer_op.cc:L53"
+```
+"""
+function _getdocdefine(name::String)
+ op = _get_libmx_op_handle(name)
+ str = _get_libmx_op_description(name, op)[1]
+ lines = split(str, '\n')
+ for m ∈ match.(r"^Defined in .*$", lines)
+ m != nothing && return m.match
+ end
+ ""
+end
+
+"""
+libmxnet operators signature checker.
+
+C/Python have different convernsion of accessing array. Those languages
+handle arrays in row-major and zero-indexing which differs from Julia's
+colume-major and 1-indexing.
+
+This function scans the docstrings of NDArray's APIs,
+filter out the signature which contain `axis`, `axes`, `keepdims` and `shape`
+as its function argument.
+
+We invoks this checker in Travis CI build and pop up the warning message
+if the functions does not get manually mapped
+(imply it's dimension refering may looks weird).
+
+If you found any warning in Travis CI build, please open an issue on GitHub.
+"""
+function _sig_checker()
+ names = filter(n -> ∉(lowercase(n), _op_import_bl), _get_libmx_op_names())
+ foreach(names) do name
+ op_handle = _get_libmx_op_handle(name)
+
+ desc, key_narg = _get_libmx_op_description(name, op_handle)
+ _sig = desc |> s -> split(s, '\n') |> first |> strip
+ _m = match(r"(axis|axes|keepdims|shape)", _sig)
+
+ if _m === nothing
+ return
+ end
+
+ warn(_sig)
+
+ end
+end
+
+"""
+Get first position argument from function sig
+"""
+function _firstarg(sig::Expr)
+ if sig.head ∈ (:where, :(::))
+ _firstarg(sig.args[1])
+ elseif sig.head == :call
+ i = if sig.args[2] isa Expr && sig.args[2].head == :parameters
+ # there are some keyward arguments locate at args[2]
+ 3
+ elseif sig.args[1] === :broadcast_
+ # case of broadcasting, skip the first arg `::typeof(...)`
+ 3
+ else
+ 2
+ end
+ _firstarg(sig.args[i])
+ end
+end
+
+_firstarg(s::Symbol) = s
diff --git a/julia/src/visualize.jl b/julia/src/visualize.jl
new file mode 100644
index 000000000000..b41db7e9e5a7
--- /dev/null
+++ b/julia/src/visualize.jl
@@ -0,0 +1,213 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import JSON
+
+"""
+ to_graphviz(network)
+
+* `network::SymbolicNode`: the network to visualize.
+* `title::AbstractString:` keyword argument, default "Network Visualization",
+ the title of the GraphViz graph.
+* `input_shapes`: keyword argument, default `nothing`. If provided,
+ will run shape inference and plot with the shape information. Should
+ be either a dictionary of name-shape mapping or an array of shapes.
+
+Returns the graph description in GraphViz `dot` language.
+"""
+function to_graphviz(network :: SymbolicNode; title="Network Visualization", input_shapes=nothing)
+ if !isa(input_shapes, Void)
+ internals = get_internals(network)
+ if isa(input_shapes, Dict)
+ _, out_shapes, _ = infer_shape(internals; input_shapes...)
+ else
+ _, out_shapes, _ = infer_shape(internals, input_shapes...)
+ end
+ @assert(!isa(out_shapes, Void), "Failed to do shape inference, input shapes are incomplete")
+ shape_dict = Dict(zip(list_outputs(internals), out_shapes))
+ draw_shape = true
+ else
+ draw_shape = false
+ end
+
+ conf = JSON.parse(to_json(network))
+ nodes = conf["nodes"]
+ heads = unique([x[1]+1 for x in conf["heads"]])
+ node_attr = Dict(:shape => :box, :fixedsize => true, :width => 1.3,
+ :height => 0.8034, :style => (:rounded, :filled), :penwidth => 2)
+ io = IOBuffer()
+ println(io, "digraph $(_simple_escape(title)) {")
+ println(io, "node [fontsize=10];")
+ println(io, "edge [fontsize=10];")
+
+ # color map
+ fillcolors = ("#8dd3c7", "#fb8072", "#ffffb3", "#bebada", "#80b1d3",
+ "#fdb462", "#b3de69", "#fccde5")
+ edgecolors = ("#245b51", "#941305", "#999900", "#3b3564", "#275372",
+ "#975102", "#597d1c", "#90094e")
+
+ # make nodes
+ for i = 1:length(nodes)
+ node = nodes[i]
+ op = node["op"]
+ name = node["name"]
+ attr = deepcopy(node_attr)
+ label = op
+
+ # Up to 0.11.0 version of mxnet additional info was stored in
+ # node["attr"]. Staring from 0.12 `attr` was changed to `attrs`.
+ # See: https://github.com/dmlc/nnvm/pull/152
+ if haskey(node, "attrs")
+ node_info = node["attrs"]
+ elseif haskey(node, "attr")
+ node_info = node["attr"]
+ end
+
+ if op == "null"
+ if i ∈ heads
+ # heads are output nodes
+ label = node["name"]
+ colorkey = 1
+ else
+ # otherwise, input nodes, might be data, label or parameters
+ continue
+ end
+ elseif op == "Convolution"
+ if haskey(node_info,"stride")
+ stride_info=_extract_shape(node_info["stride"])
+ else
+ stride_info="1"
+ end
+
+ label = format("Convolution\nkernel={1}\nstride={2}\nn-filter={3}",
+ _extract_shape(node_info["kernel"]),
+ stride_info,
+ node_info["num_filter"])
+ colorkey = 2
+ elseif op == "FullyConnected"
+ label = format("FullyConnected\nnum-hidden={1}", node_info["num_hidden"])
+ colorkey = 2
+ elseif op == "Activation"
+ label = format("Activation\nact-type={1}", node_info["act_type"])
+ colorkey = 3
+ elseif op == "BatchNorm"
+ colorkey = 4
+ elseif op == "Pooling"
+ if haskey(node_info,"stride")
+ stride_info=_extract_shape(node_info["stride"])
+ else
+ stride_info="1"
+ end
+ label = format("Pooling\ntype={1}\nkernel={2}\nstride={3}",
+ node_info["pool_type"],
+ _extract_shape(node_info["kernel"]),
+ stride_info)
+ colorkey = 5
+ elseif op ∈ ("Concat", "Flatten", "Reshape")
+ colorkey = 6
+ elseif endswith(op, "Output") || op == "BlockGrad"
+ colorkey = 7
+ else
+ colorkey = 8
+ end
+
+ if op != "null"
+ label = "$name\n$label"
+ end
+ attr[:fillcolor] = fillcolors[colorkey]
+ attr[:color] = edgecolors[colorkey]
+ attr[:label] = label
+ _format_graphviz_node(io, name, attr)
+ end
+
+ # add edges
+ for i = 1:length(nodes)
+ node = nodes[i]
+ op = node["op"]
+ name = node["name"]
+ if op == "null"
+ continue
+ end
+ inputs = node["inputs"]
+ for item in inputs
+ input_node = nodes[item[1]+1]
+ input_name = input_node["name"]
+ if input_node["op"] != "null" || (item[1]+1) ∈ heads
+ attr = Dict(:dir => :back, :arrowtail => :open, :color => "#737373")
+ if draw_shape
+ if input_node["op"] != "null"
+ key = Symbol(input_name, "_output")
+ shape = shape_dict[key][1:end-1]
+ else
+ key = Symbol(input_name)
+ shape = shape_dict[key][1:end-1]
+ end
+ label = "(" * join([string(x) for x in shape], ",") * ")"
+ attr[:label] = label
+ end
+ _format_graphviz_edge(io, name, input_name, attr)
+ end
+ end
+ end
+ println(io, "}")
+
+ return String(take!(io))
+end
+
+function _format_graphviz_attr(io::IOBuffer, attrs)
+ label = get(attrs, :label, nothing)
+ if isa(label, Void)
+ print(io, " [")
+ else
+ print(io, " [label=$(_simple_escape(label)),")
+ end
+ first_attr = true
+ for (k,v) in attrs
+ if k != :label
+ if !first_attr
+ print(io, ",")
+ end
+ first_attr = false
+
+ if isa(v, AbstractString) && v[1] == '#'
+ # color
+ v = _simple_escape(v)
+ elseif isa(v, Tuple)
+ v = _simple_escape(join([string(x) for x in v], ","))
+ end
+ print(io, "$k=$v")
+ end
+ end
+ println(io, "];")
+end
+function _simple_escape(str)
+ str = replace(string(str), r"\n", "\\n")
+ return "\"$str\""
+end
+function _format_graphviz_node(io::IOBuffer, name::AbstractString, attrs)
+ print(io, "$(_simple_escape(name)) ")
+ _format_graphviz_attr(io, attrs)
+end
+function _format_graphviz_edge(io::IOBuffer, head, tail, attrs)
+ print(io, """$(_simple_escape(head)) -> $(_simple_escape(tail)) """)
+ _format_graphviz_attr(io, attrs)
+end
+function _extract_shape(str :: AbstractString)
+ shape = matchall(r"\d+", str)
+ shape = reverse(shape) # JSON in libmxnet has reversed shape (column vs row majoring)
+ return "(" * join(shape, ",") * ")"
+end
diff --git a/julia/test/common.jl b/julia/test/common.jl
new file mode 100644
index 000000000000..5ac5f905143a
--- /dev/null
+++ b/julia/test/common.jl
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+################################################################################
+# Common models used in testing
+################################################################################
+function rand_dims(max_ndim=6)
+ tuple(rand(1:10, rand(1:max_ndim))...)
+end
+
+function mlp2()
+ data = mx.Variable(:data)
+ out = mx.FullyConnected(data, name=:fc1, num_hidden=1000)
+ out = mx.Activation(out, act_type=:relu)
+ out = mx.FullyConnected(out, name=:fc2, num_hidden=10)
+ return out
+end
+
+function mlpchain()
+ mx.@chain mx.Variable(:data) =>
+ mx.FullyConnected(name=:fc1, num_hidden=1000) =>
+ mx.Activation(act_type=:relu) =>
+ mx.FullyConnected(name=:fc2, num_hidden=10)
+end
+
+"""
+execution helper of SymbolicNode
+"""
+function exec(x::mx.SymbolicNode; feed...)
+ ks, vs = zip(feed...)
+ vs′ = mx.NDArray.(vs)
+
+ e = mx.bind(x, context = mx.cpu(), args = Dict(zip(ks, vs′)))
+ mx.forward(e)
+ e.outputs
+end
diff --git a/julia/test/runtests.jl b/julia/test/runtests.jl
new file mode 100644
index 000000000000..8b46bfda6726
--- /dev/null
+++ b/julia/test/runtests.jl
@@ -0,0 +1,43 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+using MXNet
+using Base.Test
+
+# run test in the whole directory, latest modified files
+# are run first, this makes waiting time shorter when writing
+# or modifying unit-tests
+function test_dir(dir)
+ jl_files = sort(filter(x -> ismatch(r".*\.jl$", x), readdir(dir)), by = fn -> stat(joinpath(dir,fn)).mtime)
+ map(reverse(jl_files)) do file
+ include("$dir/$file")
+ end
+end
+
+info("libmxnet version => $(mx.LIB_VERSION)")
+
+include(joinpath(dirname(@__FILE__), "common.jl"))
+@testset "MXNet Test" begin
+ test_dir(joinpath(dirname(@__FILE__), "unittest"))
+
+ # run the basic MNIST mlp example
+ if haskey(ENV, "CONTINUOUS_INTEGRATION")
+ @testset "MNIST Test" begin
+ include(joinpath(Pkg.dir("MXNet"), "examples", "mnist", "mlp-test.jl"))
+ end
+ end
+end
diff --git a/julia/test/travis/run_coverage.sh b/julia/test/travis/run_coverage.sh
new file mode 100644
index 000000000000..ee22b258b549
--- /dev/null
+++ b/julia/test/travis/run_coverage.sh
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+julia -e 'cd(Pkg.dir("MXNet")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'
diff --git a/julia/test/travis/run_test.sh b/julia/test/travis/run_test.sh
new file mode 100755
index 000000000000..414b1450b554
--- /dev/null
+++ b/julia/test/travis/run_test.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -e
+
+if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
+julia -e 'Pkg.clone(pwd())'
+(
+ cd `julia -e 'println(Pkg.dir("MXNet", "deps"))'` &&
+ ln -fs $TRAVIS_BUILD_DIR/deps/src
+)
+julia -e 'Pkg.build("MXNet"); Pkg.test("MXNet"; coverage=true)'
diff --git a/julia/test/travis/setup_env.sh b/julia/test/travis/setup_env.sh
new file mode 100755
index 000000000000..9d2f7341998d
--- /dev/null
+++ b/julia/test/travis/setup_env.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+echo "##########################"
+echo $TRAVIS_OS_NAME
+
+if [ ${TRAVIS_OS_NAME} == "osx" ]; then
+ brew update >/dev/null 2>&1
+ brew tap homebrew/science
+ brew info opencv
+ brew install graphviz
+ brew install opencv
+fi
+
+if [ ${TRAVIS_OS_NAME} == "linux" ]; then
+ mkdir shadow_bin
+ ln -s `which gcc-4.8` shadow_bin/gcc
+ ln -s `which g++-4.8` shadow_bin/g++
+
+ export PATH=$PWD/shadow_bin:$PATH
+fi
diff --git a/julia/test/unittest/autograd.jl b/julia/test/unittest/autograd.jl
new file mode 100644
index 000000000000..96f275b626af
--- /dev/null
+++ b/julia/test/unittest/autograd.jl
@@ -0,0 +1,403 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestAutoGrad
+
+using Base.Test
+
+using MXNet
+
+
+function checkgradient(f, x, y, ∇)
+ ∇x = mx.attach_grad!(x)
+ y′ = mx.record(f)
+ @test copy(y′) ≈ y
+ @test copy(∇x) |> sum == 0
+ mx.backward!(y′)
+ @test copy(mx.getgrad(x)) ≈ ∇
+end # function checkgradient
+
+
+function test_getgrad()
+ info("AutoGrad::getgrad")
+
+ info("AutoGrad::getgrad::unattached")
+ @test nothing == mx.getgrad(mx.zeros(10))
+
+ info("AutoGrad::getgrad::attached")
+ x = mx.NDArray([1 2; 3 4])
+ grad = mx.attach_grad!(x)
+ @test eltype(grad) ≡ Int
+ @test copy(grad) == [0 0; 0 0]
+
+ grad[:] = 42
+ @test copy(mx.getgrad(x)) == [42 42; 42 42]
+end
+
+
+function test_mark_variables!()
+ info("AutoGrad::mark_variables!")
+ x = mx.zeros(4)
+ ẋ = mx.zeros(4)
+ y = mx.zeros(4)
+ ẏ = mx.zeros(4)
+ mx.mark_variables!([x, y], [ẋ, ẏ], [:nop, :nop])
+ ẋ[:] = 42
+ ẏ[:] = 24
+
+ @test copy(mx.getgrad(x)) == [42, 42, 42, 42]
+ @test copy(mx.getgrad(y)) == [24, 24, 24, 24]
+
+ info("AutoGrad::mark_variables!::invalid grad_reqs")
+ x = mx.zeros(4)
+ y = mx.zeros(4)
+ @test_throws ArgumentError mx.mark_variables!(x, y, :magic)
+ @test_throws ArgumentError mx.mark_variables!([x], [y], [:magic])
+
+ info("AutoGrad::mark_variables!::args length mismatch")
+ x = mx.zeros(4)
+ y = mx.zeros(4)
+ z = mx.zeros(4)
+ @test_throws ArgumentError mx.mark_variables!([x], [y, z])
+ @test_throws ArgumentError mx.mark_variables!([x], [y], [:write, :nop])
+end
+
+
+function test_record()
+ let x = mx.NDArray([1 2; 3 4])
+ info("AutoGrad::record::backward!")
+
+ y = [1 4; 9 16]
+ ∇ = [2 4; 6 8] # gradient is 2x
+ checkgradient(x, y, ∇) do
+ mx.square(x)
+ end
+ end
+
+ let x = mx.NDArray([1 2; 3 4])
+ info("AutoGrad::record::symbol")
+
+ mx.attach_grad!(x)
+ y = mx.record() do
+ mx.square(x)
+ end
+
+ @test copy(y) == [1 4; 9 16]
+
+ @test isa(mx.symbol(y), mx.SymbolicNode)
+ end
+
+ let x = mx.NDArray([1 2; 3 4])
+ info("AutoGrad::record::backward!(retain_graph=true)")
+
+ mx.attach_grad!(x)
+ y = mx.record() do
+ mx.square(x)
+ end
+
+ @test copy(y) == [1 4; 9 16]
+
+ mx.backward!(y, retain_graph=true)
+ # gradient is 2x
+ @test copy(mx.getgrad(x)) == [2 4; 6 8]
+
+ @test isa(mx.symbol(y), mx.SymbolicNode)
+ end
+
+ mx._record(nothing, nothing) do # no error with edage case
+ @test true
+ end
+end # function test_record
+
+
+function test_is_recording()
+ info("AutoGrad::is_recording")
+ mx.record() do
+ @test mx.is_recording()
+ end
+end # function test_is_recording
+
+
+function test_is_training()
+ info("AutoGrad::is_training")
+ mx.record() do
+ @test mx.is_training()
+ end
+
+ mx.record(false) do
+ @test !mx.is_training()
+ end
+end # function test_is_training
+
+
+function test_pause()
+ info("AutoGrad::pause")
+ let x = mx.NDArray([1 2; 3 4])
+ ∇ = mx.attach_grad!(x)
+ y = mx.record() do
+ y = mx.square(x)
+ mx.pause() do
+ z = mx.square(y)
+ @test copy(z) == [1 16; 81 256]
+ end
+ y
+ end
+
+ @test copy(y) == [1 4; 9 16]
+
+ mx.backward!(y)
+ @test copy(∇) == [2 4; 6 8]
+ end
+end # function test_pause
+
+
+function test_train_mode()
+ info("AutoGrad::train_mode")
+ let x = mx.NDArray(Float32[1 2; 3 4])
+ y = mx.train_mode() do
+ mx.Dropout(x, p = 1)
+ end
+
+ @test all(isnan.(copy(y)))
+ end
+end # function test_train_mode
+
+
+function test_predict_mode()
+ info("AutoGrad::predict_mode")
+ let x = mx.NDArray(Float32[1 2; 3 4])
+ y = mx.predict_mode() do
+ mx.Dropout(x, p = 1)
+ end
+
+ @test copy(y) ≈ Float32[1 2; 3 4]
+ end
+end # function test_train_mode
+
+
+function test_backward!()
+ info("AutoGrad::backward!::with head_grad")
+ let x = mx.NDArray(Float32[1 2; 3 4]), A = Float32[.2 .4; 0 .1]
+ ∇ = mx.attach_grad!(x)
+ y = mx.record() do
+ mx.square(x)
+ end
+ mx.backward!(y, mx.NDArray(A))
+ @test copy(∇) ≈ [2 4; 6 8] .* A
+ end
+
+ info("AutoGrad::backward!::with head_grads")
+ let x = mx.NDArray(Float32[1 2; 3 4])
+ ∇ = mx.attach_grad!(x)
+ mx.record() do
+ x′ = mx.square(x)
+ y = mx.square(x)
+ z = mx.square(x) .+ 42
+ mx.backward!([x′, y, z], [nothing,
+ mx.NDArray(Float32[.01 .01; 1 1]),
+ mx.NDArray(Float32[1 1; .1 .1])])
+ end
+ ans = [4.02 8.04
+ 12.6 16.8]
+ @test copy(∇) ≈ ans
+ end
+
+ info("AutoGrad::backward!::ArgumentError")
+ let x = mx.NDArray([42])
+ @test_throws ArgumentError mx.backward!([x], [24])
+ end
+end # function test_backward!
+
+
+function test_symbol()
+ info("AutoGrad::symbol")
+
+ let x = mx.zeros(4)
+ mx.attach_grad!(x)
+ @test isa(mx.symbol(x), mx.SymbolicNode)
+ end
+end
+
+
+function test_add()
+ info("AutoGrad::add")
+
+ info("AutoGrad::add::x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [1 2; 3 4]
+ ∇ = [1 1; 1 1] # gradient is 1
+ checkgradient(x, y, ∇) do
+ x
+ end
+ end
+
+ info("AutoGrad::add::+x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [1 2; 3 4]
+ ∇ = [1 1; 1 1] # gradient is 1
+ checkgradient(x, y, ∇) do
+ +x
+ end
+ end
+
+ info("AutoGrad::add::x .+ 42")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [43 44; 45 46]
+ ∇ = [1 1; 1 1] # gradient is 1
+ checkgradient(x, y, ∇) do
+ x .+ 42
+ end
+ end
+
+ info("AutoGrad::add::42 .+ x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [43 44; 45 46]
+ ∇ = [1 1; 1 1]
+ checkgradient(x, y, ∇) do
+ 42 .+ x
+ end
+ end
+
+ # TODO: info("AutoGrad::add::x .+ y")
+end # function test_add
+
+
+function test_sub()
+ info("AutoGrad::sub")
+
+ info("AutoGrad::sub::-x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [-1 -2; -3 -4]
+ ∇ = [-1 -1; -1 -1] # gradient is -1
+ checkgradient(x, y, ∇) do
+ -x
+ end
+ end
+
+ info("AutoGrad::sub::x .- 42")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [-41 -40; -39 -38]
+ ∇ = [1 1; 1 1]
+ checkgradient(x, y, ∇) do
+ x .- 42
+ end
+ end
+
+ info("AutoGrad::sub::42 .- x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [41 40; 39 38]
+ ∇ = -[1 1; 1 1]
+ checkgradient(x, y, ∇) do
+ 42 .- x
+ end
+ end
+
+ # TODO: info("AutoGrad::add::x .- y")
+end # function test_sub
+
+
+function test_mul()
+ info("AutoGrad::mul")
+
+ info("AutoGrad::mul::2x .* x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [2 8; 18 32]
+ ∇ = [4 8; 12 16] # 4x
+ checkgradient(x, y, ∇) do
+ 2x .* x
+ end
+ end
+
+ info("AutoGrad::mul::x * 2 .* x")
+ let x = mx.NDArray([1 2; 3 4])
+ y = [2 8; 18 32]
+ ∇ = [4 8; 12 16] # 4x
+ checkgradient(x, y, ∇) do
+ x * 2 .* x
+ end
+ end
+end
+
+
+function test_div()
+ info("AutoGrad::div")
+
+ info("AutoGrad::div::x ./ 2")
+ let x = mx.NDArray(Float32[1 2; 3 4])
+ y = Float32[.5 1; 1.5 2]
+ ∇ = [.5 .5; .5 .5]
+ checkgradient(x, y, ∇) do
+ x ./ 2
+ end
+ end
+
+ info("AutoGrad::rdiv::2 ./ x")
+ let A = Float32[1 2; 3 4], x = mx.NDArray(A)
+ y = 2 ./ A
+ ∇ = @. -2 / A^2 # -2 / x²
+ checkgradient(x, y, ∇) do
+ 2 ./ x
+ end
+ end
+end # function test_div
+
+
+function test_power()
+ info("AutoGrad::power")
+
+ info("AutoGrad::power::x.^3")
+ let A = Float32[1 2; 3 4]
+ x = mx.NDArray(A)
+ y = A.^3
+ ∇ = 3(A.^2)
+ checkgradient(x, y, ∇) do
+ x.^3
+ end
+ end
+
+ info("AutoGrad::power::x.^.5")
+ let A = Float32[1 2; 3 4]
+ x = mx.NDArray(A)
+ y = A.^.5
+ ∇ = .5(A.^-.5)
+ checkgradient(x, y, ∇) do
+ x.^.5
+ end
+ end
+end
+
+
+@testset "AutoGrad Test" begin
+ test_getgrad()
+ test_mark_variables!()
+ test_record()
+ test_is_recording()
+ test_is_training()
+ test_pause()
+ test_train_mode()
+ test_predict_mode()
+ test_backward!()
+ test_symbol()
+ test_add()
+ test_sub()
+ test_mul()
+ test_div()
+ test_power()
+end
+
+
+end # model TestAutoGrad
diff --git a/julia/test/unittest/bind.jl b/julia/test/unittest/bind.jl
new file mode 100644
index 000000000000..538b556bf474
--- /dev/null
+++ b/julia/test/unittest/bind.jl
@@ -0,0 +1,95 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestBind
+using MXNet
+using Base.Test
+
+using ..Main: rand_dims
+
+################################################################################
+# Test Implementations
+################################################################################
+function test_arithmetic(::Type{T}, uf, gf) where T <: mx.DType
+ shape = rand_dims()
+ info("Bind::arithmetic::$T::$uf::dims = $shape")
+
+ lhs = mx.Variable(:lhs)
+ rhs = mx.Variable(:rhs)
+ ret = uf(lhs, rhs)
+ @test mx.list_arguments(ret) == [:lhs, :rhs]
+
+ lhs_arr = mx.NDArray(rand(T, shape))
+ rhs_arr = mx.NDArray(rand(T, shape))
+ lhs_grad = mx.empty(T, shape)
+ rhs_grad = mx.empty(T, shape)
+
+ exec2 = mx.bind(ret, mx.Context(mx.CPU), [lhs_arr, rhs_arr], args_grad=[lhs_grad, rhs_grad])
+ exec3 = mx.bind(ret, mx.Context(mx.CPU), [lhs_arr, rhs_arr])
+ exec4 = mx.bind(ret, mx.Context(mx.CPU), Dict(:lhs=>lhs_arr, :rhs=>rhs_arr),
+ args_grad=Dict(:rhs=>rhs_grad, :lhs=>lhs_grad))
+
+ mx.forward(exec2)
+ mx.forward(exec3)
+ mx.forward(exec4)
+
+ out1 = uf(copy(lhs_arr), copy(rhs_arr))
+ out2 = copy(exec2.outputs[1])
+ out3 = copy(exec3.outputs[1])
+ out4 = copy(exec4.outputs[1])
+ @test isapprox(out1, out2)
+ @test isapprox(out1, out3)
+ @test isapprox(out1, out4)
+
+ # test gradients
+ out_grad = mx.NDArray(ones(T, shape))
+ lhs_grad2, rhs_grad2 = gf(copy(out_grad), copy(lhs_arr), copy(rhs_arr))
+ mx.backward(exec2, out_grad)
+ @test isapprox(copy(lhs_grad), lhs_grad2)
+ @test isapprox(copy(rhs_grad), rhs_grad2)
+
+ # reset grads
+ lhs_grad[:] = 0
+ rhs_grad[:] = 0
+ # compute using another binding
+ mx.backward(exec4, out_grad)
+ @test isapprox(copy(lhs_grad), lhs_grad2)
+ @test isapprox(copy(rhs_grad), rhs_grad2)
+end
+
+function test_arithmetic()
+ for T in [mx.fromTypeFlag(TF) for TF in instances(mx.TypeFlag)]
+ test_arithmetic(T, (x,y) -> x .+ y, (g,x,y) -> (g,g))
+ test_arithmetic(T, (x,y) -> x .- y, (g,x,y) -> (g,-g))
+ test_arithmetic(T, (x,y) -> x .* y, (g,x,y) -> (y.*g, x.*g))
+ if T <: Integer || T == Float16
+ warn("Not running division test for $T")
+ else
+ test_arithmetic(T, (x,y) -> x ./ y, (g,x,y) -> (g ./ y, -x .* g ./ (y.^2)))
+ end
+ end
+end
+
+################################################################################
+# Run tests
+################################################################################
+@testset "Bind Test" begin
+ test_arithmetic()
+end
+
+end
+
diff --git a/julia/test/unittest/initializer.jl b/julia/test/unittest/initializer.jl
new file mode 100644
index 000000000000..fa528c9f8e13
--- /dev/null
+++ b/julia/test/unittest/initializer.jl
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+@testset "Initializers" begin
+ @testset "Bilinear initializer" begin
+ # Setup a filter with scale = 2
+ expectedFilter = Float32[
+ 0.0625 0.1875 0.1875 0.0625;
+ 0.1875 0.5625 0.5625 0.1875;
+ 0.1875 0.5625 0.5625 0.1875;
+ 0.0625 0.1875 0.1875 0.0625]
+ filter = mx.zeros(Float32, 4, 4, 1, 4)
+ mx.init(mx.XavierInitializer(), :upsampling0_weight, filter)
+
+ mx.@nd_as_jl ro=filter begin
+ for s in 1:size(filter, 4)
+ @test all(filter[:, :, 1, s] .== expectedFilter)
+ end
+ end
+ end
+end
diff --git a/julia/test/unittest/io.jl b/julia/test/unittest/io.jl
new file mode 100644
index 000000000000..81f2ff79a83b
--- /dev/null
+++ b/julia/test/unittest/io.jl
@@ -0,0 +1,145 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestIO
+
+using MXNet
+using Base.Test
+
+using ..Main: rand_dims
+
+function test_mnist()
+ info("IO::MNIST")
+ filenames = mx.get_mnist_ubyte()
+
+ batch_size = 10
+ mnist_provider = mx.MNISTProvider(image=filenames[:train_data],
+ label=filenames[:train_label],
+ batch_size=batch_size, silent=true, shuffle=false)
+ data_spec = mx.provide_data(mnist_provider)
+ label_spec = mx.provide_label(mnist_provider)
+ @test data_spec == [(:data, (28,28,1,batch_size))]
+ @test label_spec == [(:softmax_label, (batch_size,))]
+
+ n_batch = 0
+ for batch in mnist_provider
+ if n_batch == 0
+ data_array = mx.empty(28,28,1,batch_size)
+ label_array = mx.empty(batch_size)
+ # have to use "for i=1:1" to get over the legacy "feature" of using
+ # [ ] to do concatenation in Julia
+ data_targets = [[(1:batch_size, data_array)] for i = 1:1]
+ label_targets = [[(1:batch_size, label_array)] for i = 1:1]
+
+ mx.load_data!(mnist_provider, batch, data_targets)
+ mx.load_label!(mnist_provider, batch, label_targets)
+
+ true_labels = [5,0,4,1,9,2,1,3,1,4] # the first 10 labels in MNIST train
+ got_labels = Int[copy(label_array)...]
+ @test true_labels == got_labels
+ end
+
+ n_batch += 1
+ end
+
+ @test n_batch == 60000 / batch_size
+end
+
+function test_arrays_impl(data::Vector, label::Vector, provider::mx.ArrayDataProvider)
+ data = convert(Vector{Array{Float64}}, data)
+ label = convert(Vector{Array{Float64}}, label)
+
+ sample_count = size(data[1])[end]
+ batch_size = mx.get_batch_size(provider)
+ idx_all = 1:batch_size:sample_count
+
+ for (d1, (_, d2)) in zip(data, mx.provide_data(provider))
+ @test size(d1)[1:end-1] == d2[1:end-1]
+ @test batch_size == d2[end]
+ end
+ for (d1, (_, d2)) in zip(label, mx.provide_label(provider))
+ @test size(d1)[1:end-1] == d2[1:end-1]
+ @test batch_size == d2[end]
+ end
+
+ info("IO::Array::#data=$(length(data)),#label=$(length(label)),batch_size=$batch_size")
+ for (idx, batch) in zip(idx_all, provider)
+ data_batch = [x[[Colon() for i=1:ndims(x)-1]..., idx:min(idx+batch_size-1,sample_count)] for x in data]
+ data_get = mx.get_data(provider, batch)
+
+ for (d_real, d_get) in zip(data_batch, data_get)
+ @test d_real ≈ copy(d_get)[[1:n for n in size(d_real)]...]
+ @test mx.count_samples(provider, batch) == size(d_real)[end]
+ end
+ end
+end
+
+function test_arrays()
+ sample_count = 15
+ batch_size = 4
+ dims_data = [rand_dims()..., sample_count]
+ data = rand(dims_data...)
+ provider = mx.ArrayDataProvider(data, batch_size=batch_size)
+ test_arrays_impl(Array[data], [], provider)
+
+ dims_label = [rand_dims()..., sample_count]
+ label = rand(dims_label...)
+ provider = mx.ArrayDataProvider(data, label, batch_size=batch_size)
+ test_arrays_impl(Array[data], Array[label], provider)
+
+ provider = mx.ArrayDataProvider(:data=>data, :my_label=>label, batch_size=batch_size)
+ test_arrays_impl(Array[data], Array[label], provider)
+
+ dims_data2 = [rand_dims()..., sample_count]
+ data2 = rand(dims_data2...)
+ provider = mx.ArrayDataProvider((:data=>data, :data2=>data2), label, batch_size=batch_size)
+ test_arrays_impl(Array[data,data2], Array[label], provider)
+end
+
+function test_arrays_shuffle()
+ info("IO::Array::shuffle")
+
+ sample_count = 15
+ batch_size = 4
+ data = rand(mx.MX_float, 1, sample_count)
+ label = collect(1:sample_count)
+ provider = mx.ArrayDataProvider(data, :index => label, batch_size=batch_size, shuffle=true)
+
+ idx_all = 1:batch_size:sample_count
+ data_got = similar(data)
+ label_got = similar(label)
+ for (idx, batch) in zip(idx_all, provider)
+ data_batch = mx.get(provider, batch, :data)
+ label_batch = mx.get(provider, batch, :index)
+ ns_batch = mx.count_samples(provider, batch)
+ data_got[idx:idx+ns_batch-1] = copy(data_batch)[1:ns_batch]
+ label_got[idx:idx+ns_batch-1] = copy(label_batch)[1:ns_batch]
+ end
+
+ @test label_got != label
+ @test sort(label_got) == label
+ @test size(data_got) == size(data[:, Int[label_got...]])
+ @test data_got ≈ data[:, Int[label_got...]]
+end
+
+@testset "IO Test" begin
+ test_arrays_shuffle()
+ test_arrays()
+ test_mnist()
+end
+
+end
diff --git a/julia/test/unittest/kvstore.jl b/julia/test/unittest/kvstore.jl
new file mode 100644
index 000000000000..96e1643d8d83
--- /dev/null
+++ b/julia/test/unittest/kvstore.jl
@@ -0,0 +1,114 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestKVStore
+using MXNet
+using Base.Test
+
+using ..Main: rand_dims
+
+SHAPE = rand_dims()
+KEYS = [5,7,11]
+
+function init_kv()
+ kv = mx.KVStore()
+ mx.init!(kv, 3, mx.zeros(SHAPE))
+
+ vals = [mx.zeros(SHAPE) for k in KEYS]
+ mx.init!(kv, KEYS, vals)
+ kv
+end
+
+function test_kv_basic()
+ info("KVStore::basic")
+
+ kv = init_kv()
+ @test mx.get_type(kv) == :local
+ @test mx.get_rank(kv) == 0
+ @test mx.get_num_workers(kv) == 1
+end
+
+function test_single_kv_pair()
+ info("KVStore::single")
+
+ kv = init_kv()
+ mx.push!(kv, 3, mx.ones(SHAPE))
+ val = mx.empty(SHAPE)
+ mx.pull!(kv, 3, val)
+ @test maximum(abs.(copy(val) .- 1)) == 0
+end
+
+function test_aggregator()
+ info("KVStore::aggregator")
+
+ kv = init_kv()
+
+ num_devs = 4
+ devs = [mx.Context(mx.CPU, i) for i=0:num_devs-1]
+ vals = [mx.ones(SHAPE, dev) for dev in devs]
+
+ mx.push!(kv, 3, vals)
+ mx.pull!(kv, 3, vals)
+ for v in vals
+ @test maximum(abs.(copy(v)) - num_devs) == 0
+ end
+
+ # list
+ vals = [mx.NDArray[mx.ones(SHAPE, dev)*2 for dev in devs] for k in KEYS]
+ mx.push!(kv, KEYS, vals)
+ mx.pull!(kv, KEYS, vals)
+
+ for vv in vals
+ for v in vv
+ @test maximum(abs.(copy(v)) - 2 * num_devs) == 0
+ end
+ end
+end
+
+function check_setupdater!(f)
+ kv = KVStore(:local)
+ setupdater!(kv, f)
+
+ A = Float32[1, 2, 3, 4]
+ B = Float32[.5, .6, .7, .8]
+ x = NDArray(A)
+ Δ = NDArray(B)
+ init!(kv, 42, x)
+ push!(kv, 42, Δ)
+ pull!(kv, 42, x)
+
+ @test copy(x) ≈ A + 2B
+end # function check_setupdater!
+
+function test_setupdater!()
+ info("KVStore::setupdater!")
+
+ f(key, Δ, x) = @mx.inplace x += 2Δ
+ g(key, Δ, x) = (x[:] += 2Δ)
+
+ check_setupdater!(f)
+ check_setupdater!(g)
+end # test_setupdater!
+
+@testset "KVStore Test" begin
+ test_kv_basic()
+ test_single_kv_pair()
+ test_aggregator()
+ test_setupdater!()
+end
+
+end
diff --git a/julia/test/unittest/metric.jl b/julia/test/unittest/metric.jl
new file mode 100644
index 000000000000..32c4538b608c
--- /dev/null
+++ b/julia/test/unittest/metric.jl
@@ -0,0 +1,116 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestMetric
+
+using MXNet
+using Base.Test
+
+################################################################################
+# Supporting functions
+################################################################################
+
+"""
+Returns a random n x m array in which each column defines a discrete probability distribution.
+Each column contains numbers between 0 and 1, and each column sums to 1.
+"""
+function generate_probs(n, m)
+ # Init
+ result = rand(n, m)
+
+ # Normalize: ensure each column sums to 1
+ for j = 1:m
+ colsum = sum(result[:, j])
+ for i = 1:n
+ result[i, j] /= colsum
+ end
+ end
+ result
+end
+
+
+function loglikelihood(labels::Vector{T}, probs::Array{T, 2}) where T <: AbstractFloat
+ LL = 0.0
+ eps = convert(T, 1.0e-8)
+ for i = 1:size(labels, 1)
+ LL += log(probs[Int(labels[i]) + 1, i] + eps) # labels are zero-based
+ end
+ LL / size(labels, 1)
+end
+
+
+################################################################################
+# Test Implementations
+################################################################################
+
+function test_ace()
+ info("EvalMetric::ACE")
+ n_categories = 4
+ n_observations = 100
+ labels = convert(Vector{Float32}, rand(0:(n_categories - 1), n_observations)) # MXNet uses Float32
+ probs = convert(Array{Float32}, generate_probs(n_categories, n_observations))
+ LL = loglikelihood(labels, probs)
+ metric = mx.ACE() # For categorical variables, ACE == -LL
+ mx._update_single_output(metric, labels, probs)
+ LL_v2 = metric.ace_sum / metric.n_sample
+ @test LL ≈ LL_v2 atol=1e-12
+end
+
+
+function test_nmse()
+ info("EvalMetric::NMSE")
+
+ @testset "EvalMetric::NMSE::update!" begin
+ metric = mx.NMSE()
+ labels = Array{mx.NDArray}(
+ [mx.NDArray([100.0, 0.0]), mx.NDArray([10.0, 0.0])])
+ preds = Array{mx.NDArray}(
+ [mx.NDArray([20.0, 0.0]), mx.NDArray([2.0, 0.0])])
+
+ mx.update!(metric, labels, preds)
+ @test metric.nmse_sum ≈ 0.64 * 2
+ end
+
+ @testset "EvalMetric::NMSE::reset!" begin
+ metric = mx.NMSE()
+ metric.nmse_sum = sum(rand(10))
+ metric.n_sample = 42
+
+ mx.reset!(metric)
+
+ @test metric.nmse_sum == 0.0
+ @test metric.n_sample == 0
+ end
+
+ @testset "EvalMetric::NMSE::get" begin
+ metric = mx.NMSE()
+ metric.nmse_sum = 100.0
+ metric.n_sample = 20
+
+ @test mx.get(metric) == [(:NMSE, 5.0)]
+ end
+end
+
+
+################################################################################
+# Run tests
+################################################################################
+test_ace()
+test_nmse()
+
+
+end
diff --git a/julia/test/unittest/model.jl b/julia/test/unittest/model.jl
new file mode 100644
index 000000000000..dbe97093dc68
--- /dev/null
+++ b/julia/test/unittest/model.jl
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestModel
+
+using Base.Test
+using MXNet
+
+
+function test_feedforward()
+ info("Model::FeedForward::constructor")
+ let x = @mx.var x
+ m = mx.FeedForward(x)
+ @test m.arch === x
+ @test length(m.ctx) == 1
+ end
+
+ info("Model::FeedForward::constructor::keyword context")
+ let x = @mx.var x
+ m = mx.FeedForward(x, context = mx.cpu())
+ @test m.arch === x
+ @test length(m.ctx) == 1
+ end
+
+ let x = @mx.var x
+ m = mx.FeedForward(x, context = [mx.cpu(), mx.cpu(1)])
+ @test m.arch === x
+ @test length(m.ctx) == 2
+ end
+end
+
+
+@testset "Model Test" begin
+ test_feedforward()
+end
+
+end # module TestModel
diff --git a/julia/test/unittest/name.jl b/julia/test/unittest/name.jl
new file mode 100644
index 000000000000..eea73efb43c9
--- /dev/null
+++ b/julia/test/unittest/name.jl
@@ -0,0 +1,50 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestNameManager
+using MXNet
+using Base.Test
+
+function test_default()
+ info("NameManager::default")
+
+ name = :_____aaaaa_____
+ @test get!(mx.DEFAULT_NAME_MANAGER, name, "") == name
+ @test get!(mx.DEFAULT_NAME_MANAGER, string(name), "") == name
+
+ hint = name
+ @test get!(mx.DEFAULT_NAME_MANAGER, "", hint) == Symbol("$(hint)0")
+ @test get!(mx.DEFAULT_NAME_MANAGER, "", string(hint)) == Symbol("$(hint)1")
+end
+
+function test_prefix()
+ info("NameManager::prefix")
+
+ name = :_____bbbbb_____
+ prefix = :_____foobar_____
+
+ prefix_manager = mx.PrefixNameManager(prefix)
+ @test get!(prefix_manager, name, "") == Symbol("$prefix$name")
+ @test get!(prefix_manager, "", name) == Symbol("$prefix$(name)0")
+end
+
+@testset "Name Test" begin
+ test_default()
+ test_prefix()
+end
+
+end
diff --git a/julia/test/unittest/ndarray.jl b/julia/test/unittest/ndarray.jl
new file mode 100644
index 000000000000..3bacbb009dfc
--- /dev/null
+++ b/julia/test/unittest/ndarray.jl
@@ -0,0 +1,1475 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestNDArray
+
+using MXNet
+using Base.Test
+
+using ..Main: rand_dims
+
+################################################################################
+# Test Implementations
+################################################################################
+rand_tensors(dims::NTuple{N,Int}) where {N} = rand_tensors(mx.MX_float, dims)
+function rand_tensors(::Type{T}, dims::NTuple{N,Int}) where {N,T}
+ tensor = rand(T, dims)
+ array = copy(tensor, mx.cpu())
+ return (tensor, array)
+end
+
+function test_constructor()
+ info("NDArray::NDArray(x::AbstractArray)")
+ function check_absarray(x)
+ y = mx.NDArray(x)
+ @test ndims(x) == ndims(y)
+ @test eltype(x) == eltype(y)
+ @test x[3] == y[3][]
+ end
+
+ check_absarray(1:10)
+ check_absarray(1.0:10)
+
+ info("NDArray::NDArray(Type, AbstractArray)")
+ let
+ x = mx.NDArray(Float32, [1, 2, 3])
+ @test eltype(x) == Float32
+ @test copy(x) == [1, 2, 3]
+ end
+ let
+ x = mx.NDArray(Float32, [1.1, 2, 3])
+ @test eltype(x) == Float32
+ @test copy(x) ≈ [1.1, 2, 3]
+ end
+end # function test_constructor
+
+
+function test_ones_zeros_like()
+ info("NDArray::Base.zeros")
+ let x = mx.rand(1, 3, 2, 4, low = 1, high = 10)
+ y = zeros(x)
+ @test sum(copy(y)) == 0
+
+ y = mx.zeros(x)
+ @test sum(copy(y)) == 0
+ end
+
+ info("NDArray::Base.ones")
+ let x = mx.rand(1, 3, 2, 4, low = 1, high = 10)
+ y = ones(x)
+ @test sum(copy(y)) == 1 * 3 * 2 * 4
+
+ y = mx.ones(x)
+ @test sum(copy(y)) == 1 * 3 * 2 * 4
+ end
+end # function test_ones_zeros_like
+
+
+function test_copy()
+ dims = rand_dims()
+ tensor = rand(mx.MX_float, dims)
+
+ info("NDArray::copy::dims = $dims")
+
+ # copy to NDArray and back
+ array = copy(tensor, mx.cpu())
+ tensor2 = copy(array)
+ @test tensor ≈ tensor2
+
+ # copy between NDArray
+ array2 = copy(array, mx.cpu())
+ tensor2 = copy(array2)
+ @test tensor ≈ tensor2
+
+ info("NDArray::copy::AbstractArray")
+ let x = copy(1:4, mx.cpu())
+ @test eltype(x) == Int
+ @test copy(x) == [1, 2, 3, 4]
+ end
+
+ let x = copy(1.:4, mx.cpu())
+ @test eltype(x) == Float64
+ @test copy(x) ≈ [1., 2, 3, 4]
+ end
+
+ info("NDArray::copy!::AbstractArray")
+ let
+ x = mx.zeros(4)
+ copy!(x, 1:4)
+
+ @test eltype(x) == Float32
+ @test copy(x) == [1, 2, 3, 4]
+ end
+end
+
+function test_deepcopy()
+ info("NDArray::deepcopy")
+
+ x = mx.zeros(2, 5)
+ y = deepcopy(x)
+ x[:] = 42
+ @test copy(x) != copy(y)
+end
+
+function test_assign()
+ dims = rand_dims()
+ tensor = rand(mx.MX_float, dims)
+
+ info("NDArray::assign::dims = $dims")
+
+ # Julia Array -> NDArray assignment
+ array = mx.empty(size(tensor))
+ array[:]= tensor
+ @test tensor ≈ copy(array)
+
+ array2 = mx.zeros(size(tensor))
+ @test zeros(size(tensor)) ≈ copy(array2)
+
+ array3 = mx.zeros(Float16, size(tensor))
+ @test zeros(Float16, size(tensor)) ≈ copy(array2)
+
+ # scalar -> NDArray assignment
+ scalar = rand()
+ array2[:] = scalar
+ @test zeros(size(tensor)) + scalar ≈ copy(array2)
+
+ scalar = rand(Float16)
+ array2[:] = scalar
+ @test zeros(size(tensor)) + scalar ≈ copy(array2)
+
+ scalar = rand(Float64)
+ array2[:] = scalar
+ array3[:] = scalar
+ @test zeros(size(tensor)) + scalar ≈ copy(array2)
+ @test zeros(Float16, size(tensor)) + scalar ≈ copy(array3)
+
+ # NDArray -> NDArray assignment
+ array[:] = array2
+ @test zeros(size(tensor)) + scalar ≈ copy(array)
+end
+
+function test_slice()
+ array = mx.zeros((2, 4))
+ array[2:3] = ones(2, 2)
+ @test copy(array) == [0 1 1 0; 0 1 1 0]
+ @test copy(mx.slice(array, 2:3)) == [1 1; 1 1]
+end
+
+function test_linear_idx()
+ info("NDArray::getindex::linear indexing")
+ let A = reshape(1:30, 3, 10)
+ x = mx.NDArray(A)
+
+ @test copy(x) == A
+ @test copy(x[1]) == [1]
+ @test copy(x[2]) == [2]
+ @test copy(x[3]) == [3]
+ @test copy(x[12]) == [12]
+ @test copy(x[13]) == [13]
+ @test copy(x[14]) == [14]
+
+ @test_throws BoundsError x[-1]
+ @test_throws BoundsError x[0]
+ @test_throws BoundsError x[31]
+ @test_throws BoundsError x[42]
+ end
+
+ let A = reshape(1:24, 3, 2, 4)
+ x = mx.NDArray(A)
+
+ @test copy(x) == A
+ @test copy(x[1]) == [1]
+ @test copy(x[2]) == [2]
+ @test copy(x[3]) == [3]
+ @test copy(x[11]) == [11]
+ @test copy(x[12]) == [12]
+ @test copy(x[13]) == [13]
+ @test copy(x[14]) == [14]
+ end
+
+ info("NDArray::setindex!::linear indexing")
+ let A = reshape(1:24, 3, 2, 4)
+ x = mx.NDArray(A)
+
+ @test copy(x) == A
+
+ x[4] = -4
+ @test copy(x[4]) == [-4]
+
+ x[11] = -11
+ @test copy(x[11]) == [-11]
+
+ x[24] = 42
+ @test copy(x[24]) == [42]
+ end
+
+ info("NDArray::setindex!::type convert")
+ let
+ x = NDArray([1, 2, 3])
+ @test eltype(x) == Int
+ x[:] = π
+ @test copy(x) == [3, 3, 3]
+ end
+end # function test_linear_idx
+
+function test_first()
+ info("NDArray::first")
+ let A = reshape(1:30, 3, 10)
+ x = mx.NDArray(A)
+
+ @test x[] == 1
+ @test x[5][] == 5
+
+ @test first(x) == 1
+ @test first(x[5]) == 5
+ end
+end # function test_first
+
+function test_endof()
+ info("NDArray::endof")
+ let A = [1 2; 3 4; 5 6], x = mx.NDArray(A)
+ @test endof(A) == endof(x)
+ end
+end # function test_endof
+
+function test_cat()
+ function check_cat(f, A, B = 2A)
+ C = [A B]
+ D = [A; B]
+ x = NDArray(A)
+ y = NDArray(B)
+ z = NDArray(C)
+ d = NDArray(D)
+
+ if f == :hcat
+ @test copy([x y]) == [A B]
+ @test copy([x y 3y x]) == [A B 3B A]
+ @test copy([z y x]) == [C B A]
+ elseif f == :vcat
+ @test copy([x; y]) == [A; B]
+ @test copy([x; y; 3y; x]) == [A; B; 3B; A]
+ @test copy([x; d]) == [A; D]
+ @test copy([d; x]) == [D; A]
+ else
+ @assert false
+ end
+ end
+
+ let A = [1, 2, 3, 4]
+ info("NDArray::hcat::1D")
+ check_cat(:hcat, A)
+
+ info("NDArray::vcat::1D")
+ check_cat(:vcat, A)
+ end
+
+ let A = [1 2; 3 4]
+ info("NDArray::hcat::2D")
+ check_cat(:hcat, A)
+
+ info("NDArray::vcat::2D")
+ check_cat(:vcat, A)
+ end
+
+ let A = rand(4, 3, 2)
+ info("NDArray::hcat::3D")
+ check_cat(:hcat, A)
+
+ info("NDArray::vcat::3D")
+ check_cat(:vcat, A)
+ end
+
+ let A = rand(4, 3, 2, 2)
+ info("NDArray::hcat::4D")
+ check_cat(:hcat, A)
+
+ info("NDArray::vcat::4D")
+ check_cat(:vcat, A)
+ end
+
+ let A = [1, 2, 3, 4]
+ info("NDArray::cat::3D/1D")
+ check_cat(:vcat, reshape(A, 4, 1, 1), 2A)
+ end
+end # function test_cat
+
+function test_plus()
+ dims = rand_dims()
+ t1, a1 = rand_tensors(dims)
+ t2, a2 = rand_tensors(dims)
+ t3, a3 = rand_tensors(dims)
+
+ info("NDArray::plus::dims = $dims")
+
+ @test t1 + t2 ≈ copy(a1 + a2)
+ @test t1 .+ t2 ≈ copy(a1 .+ a2)
+
+ @test t1 + t2 + t3 ≈ copy(a1 + a2 + a3)
+
+ # test inplace += operation
+ a0 = a1 # keep a reference to a1
+ @mx.inplace a1 += a2 # perform inplace +=
+ @test a0 == a1 # make sure they are still the same object
+ @test copy(a0) ≈ copy(a1)
+ @test copy(a1) ≈ t1 + t2
+
+ # test scalar
+ scalar = rand()
+ @test t3 + scalar ≈ copy(a3 + scalar)
+ @test t2 + scalar + t3 ≈ copy(a2 + scalar + a3)
+
+ # test small and large scalar
+ t4 = zeros(Float32, dims)
+ a4 = copy(t4, mx.cpu())
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t4 + scalar_small ≈ copy(a4 .+ scalar_small)
+ @test t4 + scalar_large ≈ copy(a4 .+ scalar_large)
+
+ t5 = zeros(Float64, dims)
+ a5 = copy(t5, mx.cpu())
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t5 + scalar_small ≈ copy(a5 .+ scalar_small)
+ @test t5 + scalar_large ≈ copy(a5 .+ scalar_large)
+
+ t6 = zeros(Float16, dims)
+ a6 = copy(t6, mx.cpu())
+ scalar_small = Float16(1e-5)
+ scalar_large = Float16(1e4)
+ @test t6 + scalar_small ≈ copy(a6 .+ scalar_small)
+ @test t6 + scalar_large ≈ copy(a6 .+ scalar_large)
+
+ let x = mx.NDArray([1 2; 3 4]), y = mx.NDArray([1 1; 1 1])
+ @test copy(42 .+ x) == [43 44; 45 46]
+ @test copy(x .+ 42) == [43 44; 45 46]
+ @test copy(0 .+ x .+ y .+ 41) == [43 44; 45 46]
+ end
+
+ info("NDArray::plus::scalar::type convert")
+ let x = mx.NDArray([1, 2, 3])
+ y = x .+ 0.5
+ @test copy(y) == copy(x)
+
+ y = x .+ 2.9
+ @test copy(y) == [3, 4, 5]
+ end
+
+ info("NDArray::broadcast_add")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = x .+ y
+ @test copy(z) == A .+ B
+
+ # TODO
+ # @inplace x .+= y
+ # @test copy(x) == A .+ B
+ end
+end
+
+function test_minus()
+ dims = rand_dims()
+ t1, a1 = rand_tensors(dims)
+ t2, a2 = rand_tensors(dims)
+
+ info("NDArray::minus::dims = $dims")
+
+ @test t1 - t2 ≈ copy(a1 - a2)
+ @test t1 .- t2 ≈ copy(a1 .- a2)
+
+ @test -t1 ≈ copy(-a1)
+
+ # make sure the negation is not in-place, so a1 is not changed after previous
+ # statement is executed
+ @test t1 ≈ copy(a1)
+
+ # test inplace -= operation
+ a0 = a1 # keep a reference to a1
+ @mx.inplace a1 -= a2 # perform inplace -=
+ @test a0 == a1 # make sure they are still the same object
+ @test a0.handle == a1.handle
+ @test copy(a0) ≈ copy(a1)
+ @test copy(a1) ≈ t1 - t2
+
+ # test scalar
+ scalar = rand()
+ @test t2 - scalar ≈ copy(a2 - scalar)
+
+ # test small and large scalar
+ t4 = zeros(Float32, dims)
+ a4 = copy(t4, mx.cpu())
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t4 - scalar_small ≈ copy(a4 .- scalar_small)
+ @test t4 - scalar_large ≈ copy(a4 .- scalar_large)
+
+ t5 = zeros(Float64, dims)
+ a5 = copy(t5, mx.cpu())
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t5 - scalar_small ≈ copy(a5 .- scalar_small)
+ @test t5 - scalar_large ≈ copy(a5 .- scalar_large)
+
+ t6 = zeros(Float16, dims)
+ a6 = copy(t6, mx.cpu())
+ scalar_small = Float16(1e-5)
+ scalar_large = Float16(1e4)
+ @test t6 - scalar_small ≈ copy(a6 .- scalar_small)
+ @test t6 - scalar_large ≈ copy(a6 .- scalar_large)
+
+ info("NDArray::minus::scalar::type convert")
+ let x = mx.NDArray([1, 2, 3])
+ @test copy(x .- π) ≈ [-2, -1, 0]
+ end
+
+ info("NDArray::broadcast_minus")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = x .- y
+ @test copy(z) == A .- B
+
+ # TODO
+ # @inplace x .-= y
+ # @test copy(x) == A .- B
+ end
+end
+
+function test_mul()
+ dims = rand_dims()
+ t1, a1 = rand_tensors(dims)
+ t2, a2 = rand_tensors(dims)
+ t3, a3 = rand_tensors(dims)
+
+ info("NDArray::mul::dims = $dims")
+
+ @test t1 .* t2 ≈ copy(a1.*a2)
+
+ # test inplace .*= operation
+ a0 = a1 # keep a reference to a1
+ @mx.inplace a1 .*= a2 # perform inplace .*=
+ @test a0 == a1 # make sure they are still the same object
+ @test a0.handle == a1.handle
+ @test copy(a0) ≈ copy(a1)
+ @test copy(a1) ≈ t1 .* t2
+
+ # test scalar
+ scalar = mx.MX_float(rand())
+ @test t3 * scalar ≈ copy(a3 .* scalar)
+
+ # test small and large scalar
+ t4, a4 = rand_tensors(Float32, dims)
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t4 * scalar_small ≈ copy(a4 .* scalar_small)
+ @test t4 * scalar_large ≈ copy(a4 .* scalar_large)
+
+ t5, a5 = rand_tensors(Float64, dims)
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t5 * scalar_small ≈ copy(a5 .* scalar_small)
+ @test t5 * scalar_large ≈ copy(a5 .* scalar_large)
+
+ t6, a6 = rand_tensors(Float16, dims)
+ scalar_small = Float16(1e-5)
+ @test t6 * scalar_small ≈ copy(a6 .* scalar_small)
+
+ info("NDArray::mul::matrix multiplication")
+ let x = mx.NDArray([1. 2])
+ y = x' * x
+ @test copy(y) == [1. 2; 2 4]
+ end
+
+ info("NDArray::mul::elementwise::issue 253")
+ let x = mx.NDArray([1. 2])
+ y = x .* x
+ @test copy(y) == [1. 4.]
+ end
+
+ info("NDArray::mul::scalar::type convert")
+ let x = mx.NDArray([1, 2, 3])
+ y = x .* π
+ @test eltype(x) == Int
+ @test copy(y) == [3, 6, 9]
+ end
+
+ info("NDArray::broadcast_mul")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = x .* y
+ @test copy(z) == A .* B
+
+ # TODO
+ # @inplace x .*= y
+ # @test copy(x) == A .* B
+ end
+end
+
+function test_div()
+ dims = rand_dims()
+ t1, a1 = rand_tensors(dims)
+ t2, a2 = rand_tensors(dims)
+
+ info("NDArray::div::dims = $dims")
+ t2 .+= 2 # avoid numerical instability
+ @mx.inplace a2 .+= 2
+
+ @test t1 ./ t2 ≈ copy(a1 ./ a2)
+
+ # test inplace -= operation
+ a0 = a1 # keep a reference to a2
+ @mx.inplace a1 ./= a2 # perform inplace ./=
+ @test a0 == a1 # make sure they are still the same object
+ @test a0.handle == a1.handle
+ @test copy(a0) ≈ copy(a1)
+ @test copy(a1) ≈ t1 ./ t2
+
+ # test scalar
+ scalar = rand() + 2
+ @test t2 ./ scalar ≈ copy(a2 ./ scalar)
+
+ # test small and large scalar
+ t4, a4 = rand_tensors(Float32, dims)
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t4 ./ scalar_small ≈ copy(a4 ./ scalar_small)
+ @test t4 ./ scalar_large ≈ copy(a4 ./ scalar_large)
+
+ t5, a5 = rand_tensors(Float64, dims)
+ scalar_small = 1e-8
+ scalar_large = 1e8
+ @test t5 ./ scalar_small ≈ copy(a5 ./ scalar_small)
+ @test t5 ./ scalar_large ≈ copy(a5 ./ scalar_large)
+
+ t6, a6 = rand_tensors(Float16, dims)
+ scalar_large = 1e4
+ @test t6 ./ scalar_large ≈ copy(a6 ./ scalar_large)
+
+ info("NDArray::div::scalar::type convert")
+ let x = mx.NDArray([1, 2, 3])
+ y = x ./ 1.1
+ @test eltype(y) == Int
+ @test copy(y) == [1, 2, 3]
+
+ y = x ./ 2
+ @test eltype(y) == Int # this differs from julia
+ @test copy(y) == [0, 1, 1]
+
+ @test_throws AssertionError x ./ 0.5
+ end
+
+ info("NDArray::broadcast_div")
+ let
+ A = Float32[1 2 3;
+ 4 5 6]
+ B = Float32[1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = x ./ y
+ @test copy(z) == A ./ B
+
+ # TODO
+ # @inplace x ./= y
+ # @test copy(x) == A ./ B
+ end
+end
+
+
+function test_rdiv()
+ info("NDArray::rdiv")
+
+ info("NDArray::rdiv::Inf16")
+ let x = 1 ./ mx.zeros(Float16, 4)
+ @test copy(x) == [Inf16, Inf16, Inf16, Inf16]
+ end
+
+ info("NDArray::rdiv::Inf32")
+ let x = 1 ./ mx.zeros(Float32, 4)
+ @test copy(x) == [Inf32, Inf32, Inf32, Inf32]
+ end
+
+ info("NDArray::rdiv::Inf64")
+ let x = 1 ./ mx.zeros(Float64, 4)
+ @test copy(x) == [Inf64, Inf64, Inf64, Inf64]
+ end
+
+ info("NDArray::rdiv::Int")
+ let x = 1 ./ mx.NDArray([1 2; 3 4])
+ @test copy(x) == [1 0; 0 0]
+ end
+
+ info("NDArray::rdiv::Float32")
+ let x = 1 ./ mx.NDArray(Float32[1 2; 3 4])
+ y = 1 ./ Float32[1 2; 3 4]
+ @test copy(x) ≈ y
+ end
+
+ info("NDArray::rdiv::type convert")
+ let x = mx.NDArray([1, 2, 3])
+ y = 5.5 ./ x
+ @test eltype(y) == Int # this differs from julia
+ @test copy(y) == [5, 2, 1]
+ end
+end # function test_rdiv
+
+
+function test_mod()
+ info("NDArray::mod")
+ const A = [1 2; 3 4]
+ const B = [1 1; 3 3]
+
+ let x = NDArray(A), y = NDArray(B)
+ C = A .% B
+ D = B .% A
+
+ w = x .% y
+ z = y .% x
+
+ @test copy(w) ≈ C
+ @test copy(z) ≈ D
+ end
+
+ info("NDArray::mod::scalar")
+ let x = NDArray(A)
+ C = A .% 2
+ y = x .% 2
+ @test copy(y) ≈ C
+ end
+
+ info("NDArray::rmod")
+ let x = NDArray(A)
+ C = 11 .% A
+ y = 11 .% x
+ @test copy(y) ≈ C
+ end
+
+ info("NDArray::mod_from!")
+ let
+ x = NDArray(A)
+ y = NDArray(B)
+ C = A .% B
+ mx.mod_from!(x, y)
+ @test copy(x) ≈ C
+ end
+
+ let
+ x = NDArray(A)
+ y = NDArray(B)
+ C = B .% A
+ mx.mod_from!(y, x)
+
+ @test copy(y) ≈ C
+ end
+
+ info("NDArray::mod_from!::scalar")
+ let
+ x = NDArray(A)
+ C = A .% 2
+ mx.mod_from!(x, 2)
+ @test copy(x) ≈ C
+ end
+
+ info("NDArray::rmod_from!")
+ let
+ x = NDArray(A)
+ C = 11 .% A
+ mx.rmod_from!(11, x)
+ @test copy(x) ≈ C
+ end
+
+ info("NDArray::mod_from!::writable")
+ let
+ x = NDArray(A)
+ y = NDArray(B)
+ x.writable = false
+ y.writable = false
+ @test_throws AssertionError mx.mod_from!(x, y)
+ @test_throws AssertionError mx.mod_from!(y, x)
+ @test_throws AssertionError mx.mod_from!(x, 2)
+ @test_throws AssertionError mx.rmod_from!(2, x)
+ end
+
+ info("NDArray::mod::inplace")
+ let
+ x = NDArray(A)
+ y = NDArray(B)
+ C = A .% B
+ @inplace x .%= y
+ @test copy(x) ≈ C
+ end
+
+ info("NDArray::broadcast_mod")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = x .% y
+ @test copy(z) == A .% B
+
+ # TODO
+ # @inplace x .%= y
+ # @test copy(x) == A .% B
+ end
+end # function test_mod
+
+
+function test_gd()
+ dims = rand_dims()
+ tw, aw = rand_tensors(dims)
+ tg, ag = rand_tensors(dims)
+
+ info("NDArray::gd::dims = $dims")
+
+ lr = rand()
+ wd = rand()
+
+ @mx.inplace aw += -lr * (ag + wd * aw)
+ tw += -lr * (tg + wd * tw)
+ @test copy(aw) ≈ tw
+end
+
+
+function test_saveload()
+ n_arrays = 5
+ info("NDArray::saveload::n_arrays = $n_arrays")
+ fname = tempname()
+
+ # save and load a single array
+ dims = rand_dims()
+ j_array, nd_array = rand_tensors(dims)
+ mx.save(fname, nd_array)
+ data = mx.load(fname, mx.NDArray)
+ @test data isa Vector{<:mx.NDArray}
+ @test length(data) == 1
+ @test copy(data[1]) ≈ j_array
+
+ # save and load N arrays of different shape
+ arrays = [rand_tensors(rand_dims()) for i = 1:n_arrays]
+ nd_arrays = mx.NDArray[x[2] for x in arrays]
+ mx.save(fname, nd_arrays)
+ data = mx.load(fname, mx.NDArray)
+ @test data isa Vector{<:mx.NDArray}
+ @test length(data) == n_arrays
+ for i = 1:n_arrays
+ @test copy(data[i]) ≈ arrays[i][1]
+ end
+
+ # save and load dictionary of ndarrays
+ names = [Symbol("array$i") for i = 1:n_arrays]
+ dict = Dict([(n, v) for (n,v) in zip(names, nd_arrays)])
+ mx.save(fname, dict)
+ data = mx.load(fname, mx.NDArray)
+ @test data isa Dict{Symbol,<:mx.NDArray}
+ @test length(data) == n_arrays
+ for i = 1:n_arrays
+ @test copy(data[names[i]]) ≈ arrays[i][1]
+ end
+
+ rm(fname)
+end
+
+function test_clip()
+ dims = rand_dims()
+ info("NDArray::clip::dims = $dims")
+
+ j_array, nd_array = rand_tensors(dims)
+ clip_up = maximum(abs.(j_array)) / 2
+ clip_down = 0
+ clipped = clip(nd_array, clip_down, clip_up)
+
+ # make sure the original array is not modified
+ @test copy(nd_array) ≈ j_array
+
+ @test all(clip_down .<= copy(clipped) .<= clip_up)
+
+ info("NDArray::clip!")
+ let
+ x = NDArray(1.0:20)
+ clip!(x, 5, 15)
+ @test all(5 .<= copy(x) .<= 15)
+ end
+end
+
+function test_power()
+ info("NDArray::power")
+
+ info("NDArray::power::Int::x.^n")
+ let x = mx.NDArray([1 2; 3 4])
+ @test eltype(x) == Int
+ @test copy(x.^-1) == [1 0; 0 0]
+ @test copy(x.^0) == [1 1; 1 1]
+ @test copy(x.^1) == [1 2; 3 4]
+ @test copy(x.^1.1) == [1 2; 3 4]
+ @test copy(x.^2) == [1 4; 9 16]
+ @test copy(x.^2.9) == [1 4; 9 16]
+ @test copy(x.^3) == [1 8; 27 64]
+ end
+
+ info("NDArray::power::Int::n.^x")
+ let x = mx.NDArray([1 2; 3 4])
+ @test eltype(x) == Int
+ @test copy(0.^x) == [0 0; 0 0]
+ @test copy(1.^x) == [1 1; 1 1]
+ @test copy(1.1.^x) == [1 1; 1 1]
+ @test copy(2.^x) == [2 4; 8 16]
+ @test copy(2.9.^x) == [2 4; 8 16]
+ @test copy(3.^x) == [3 9; 27 81]
+ end
+
+ info("NDArray::power::Int::x.^y")
+ let x = mx.NDArray([1 2; 3 4]), y = mx.NDArray([2 2; 2 2])
+ @test eltype(x) == Int
+ @test eltype(y) == Int
+ @test copy(x.^y) == [1 4; 9 16]
+ @test copy(y.^x) == [2 4; 8 16]
+ end
+
+ info("NDArray::power::Float32::x.^n")
+ let x = mx.NDArray(Float32[1 2; 3 4]), A = Float32[1 2; 3 4]
+ @test eltype(x) == Float32
+ @test copy(x.^0) == Float32[1 1; 1 1]
+ @test copy(x.^1) == Float32[1 2; 3 4]
+ @test copy(x.^2) == Float32[1 4; 9 16]
+ @test copy(x.^3) == Float32[1 8; 27 64]
+
+ @test copy(x.^-1) ≈ A.^-1
+ @test copy(x.^1.1) ≈ A.^1.1
+ @test copy(x.^2.9) ≈ A.^2.9
+ end
+
+ info("NDArray::power::Float32::n.^x")
+ let x = mx.NDArray(Float32[1 2; 3 4]), A = Float32[1 2; 3 4]
+ @test eltype(x) == Float32
+ @test copy(0.^x) == Float32[0 0; 0 0]
+ @test copy(1.^x) == Float32[1 1; 1 1]
+ @test copy(2.^x) == Float32[2 4; 8 16]
+ @test copy(3.^x) == Float32[3 9; 27 81]
+
+ @test copy(1.1.^x) ≈ 1.1.^A
+ @test copy(2.9.^x) ≈ 2.9.^A
+ end
+
+ info("NDArray::power::Float32::x.^y")
+ let x = mx.NDArray(Float32[1 2; 3 4]), y = mx.NDArray(Float32[2 2; 2 2])
+ @test eltype(x) == Float32
+ @test eltype(y) == Float32
+ @test copy(x.^y) == Float32[1 4; 9 16]
+ @test copy(y.^x) == Float32[2 4; 8 16]
+ end
+
+ info("NDArray::power::e.^x::x.^e")
+ let x = mx.zeros(2, 3), A = [1 1 1; 1 1 1]
+ @test copy(e.^x) ≈ A
+ end
+
+ let A = Float32[1 2; 3 4], x = mx.NDArray(A)
+ @test copy(e.^x) ≈ e.^A
+ @test copy(x.^e) ≈ A.^e
+ end
+
+ info("NDArray::power::π.^x::x.^π")
+ let A = Float32[1 2; 3 4], x = mx.NDArray(A)
+ @test copy(π.^x) ≈ π.^A
+ @test copy(x.^π) ≈ A.^π
+ end
+
+ # TODO: Float64: wait for https://github.com/apache/incubator-mxnet/pull/8012
+
+ info("NDArray::broadcast_power")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = x.^y
+ @test copy(z) == A.^B
+
+ # TODO
+ # @inplace x .^= y
+ # @test copy(x) == A.^B
+ end
+end # function test_power
+
+function test_sqrt()
+ dims = rand_dims()
+ info("NDArray::sqrt::dims = $dims")
+
+ j_array, nd_array = rand_tensors(dims)
+ sqrt_ed = sqrt(nd_array)
+ @test copy(sqrt_ed) ≈ sqrt.(j_array)
+end
+
+function test_nd_as_jl()
+ dims = (2, 3)
+ info("NDArray::nd_as_jl::dims = $dims")
+
+ x = mx.zeros(dims) + 5
+ y = mx.ones(dims)
+ z = mx.zeros(dims)
+ @mx.nd_as_jl ro=x rw=(y, z) begin
+ for i = 1:length(z)
+ z[i] = x[i]
+ end
+
+ z[:, 1] = y[:, 1]
+ y[:] = 0
+ end
+
+ @test sum(copy(y)) == 0
+ @test sum(copy(z)[:, 1]) == 2
+ @test copy(z)[:, 2:end] ≈ copy(x)[:, 2:end]
+end
+
+function test_dot()
+ dims1 = (2, 3)
+ dims2 = (3, 8)
+ info("NDArray::dot")
+
+ x = mx.zeros(dims1)
+ y = mx.zeros(dims2)
+ z = mx.dot(x, y)
+ @test size(z) == (2, 8)
+
+ x = mx.zeros(1, 2)
+ y = mx.zeros(1, 2, 3)
+ @test_throws mx.MXError dot(x, y) # dimension mismatch
+
+ info("NDArray::matrix mul")
+ let
+ A = [1. 2 3; 4 5 6]
+ B = [-1., -2, -3]
+ x = NDArray(A)
+ y = NDArray(B)
+ z = x * y
+ @test copy(z) == A * B
+ @test size(z) == (2,)
+ end
+
+ let
+ A = [1. 2 3; 4 5 6]
+ B = [-1. -2; -3 -4; -5 -6]
+ x = NDArray(A)
+ y = NDArray(B)
+ z = x * y
+ @test copy(z) == A * B
+ @test size(z) == (2, 2)
+ end
+end
+
+function test_eltype()
+ info("NDArray::eltype")
+ dims1 = (3,3)
+
+ x = mx.empty(dims1)
+ @test eltype(x) == mx.DEFAULT_DTYPE
+
+ for TF in instances(mx.TypeFlag)
+ T = mx.fromTypeFlag(TF)
+ x = mx.empty(T, dims1)
+ @test eltype(x) == T
+ end
+end
+
+function test_reshape()
+ info("NDArray::reshape")
+ A = rand(2, 3, 4)
+
+ B = reshape(NDArray(A), 4, 3, 2)
+ @test size(B) == (4, 3, 2)
+ @test copy(B)[3, 1, 1] == A[1, 2, 1]
+
+ C = reshape(NDArray(A), (4, 3, 2))
+ @test size(C) == (4, 3, 2)
+ @test copy(C)[3, 1, 1] == A[1, 2, 1]
+
+ info("NDArray::reshape::reverse")
+ A = mx.zeros(10, 5, 4)
+
+ B = reshape(A, -1, 0)
+ @test size(B) == (40, 5)
+
+ C = reshape(A, -1, 0, reverse=true)
+ @test size(C) == (50, 4)
+end
+
+function test_expand_dims()
+ info("NDArray::expand_dims")
+ let A = [1, 2, 3, 4], x = NDArray(A)
+ @test size(x) == (4,)
+
+ y = expand_dims(x, 1)
+ @test size(y) == (1, 4)
+
+ y = expand_dims(x, 2)
+ @test size(y) == (4, 1)
+ end
+
+ let A = [1 2; 3 4; 5 6], x = NDArray(A)
+ @test size(x) == (3, 2)
+
+ y = expand_dims(x, 1)
+ @test size(y) == (1, 3, 2)
+
+ y = expand_dims(x, 2)
+ @test size(y) == (3, 1, 2)
+
+ y = expand_dims(x, 3)
+ @test size(y) == (3, 2, 1)
+ end
+end # test_expand_dims
+
+function test_sum()
+ info("NDArray::sum")
+
+ let A = reshape(1.0:8, 2, 2, 2), X = mx.NDArray(A)
+ @test copy(sum(X))[] == sum(A)
+ @test copy(sum(X, 1)) == sum(A, 1)
+ @test copy(sum(X, 2)) == sum(A, 2)
+ @test copy(sum(X, 3)) == sum(A, 3)
+ @test copy(sum(X, [1, 2])) == sum(A, [1, 2])
+ @test copy(sum(X, (1, 2))) == sum(A, (1, 2))
+ end
+end
+
+function test_mean()
+ info("NDArray::mean")
+
+ let A = reshape(1.0:8, 2, 2, 2), X = mx.NDArray(A)
+ @test copy(mean(X))[] == mean(A)
+ @test copy(mean(X, 1)) == mean(A, 1)
+ @test copy(mean(X, 2)) == mean(A, 2)
+ @test copy(mean(X, 3)) == mean(A, 3)
+ @test copy(mean(X, [1, 2])) == mean(A, [1, 2])
+ @test copy(mean(X, (1, 2))) == mean(A, (1, 2))
+ end
+end
+
+function test_maximum()
+ info("NDArray::maximum")
+
+ let A = reshape(1.0:8, 2, 2, 2), X = mx.NDArray(A)
+ @test copy(maximum(X))[] == maximum(A)
+ @test copy(maximum(X, 1)) == maximum(A, 1)
+ @test copy(maximum(X, 2)) == maximum(A, 2)
+ @test copy(maximum(X, 3)) == maximum(A, 3)
+ @test copy(maximum(X, [1, 2])) == maximum(A, [1, 2])
+ @test copy(maximum(X, (1, 2))) == maximum(A, (1, 2))
+ end
+
+ info("NDArray::broadcast_maximum")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = max.(x, y)
+ @test copy(z) == max.(A, B)
+ end
+end
+
+function test_minimum()
+ info("NDArray::minimum")
+
+ let A = reshape(1.0:8, 2, 2, 2), X = mx.NDArray(A)
+ @test copy(minimum(X))[] == minimum(A)
+ @test copy(minimum(X, 1)) == minimum(A, 1)
+ @test copy(minimum(X, 2)) == minimum(A, 2)
+ @test copy(minimum(X, 3)) == minimum(A, 3)
+ @test copy(minimum(X, [1, 2])) == minimum(A, [1, 2])
+ @test copy(minimum(X, (1, 2))) == minimum(A, (1, 2))
+ end
+
+ info("NDArray::broadcast_minimum")
+ let
+ A = [1 2 3;
+ 4 5 6]
+ B = [1,
+ 2]
+ x = NDArray(A)
+ y = NDArray(B)
+
+ z = min.(x, y)
+ @test copy(z) == min.(A, B)
+ end
+end
+
+function test_prod()
+ info("NDArray::prod")
+
+ let A = reshape(1.0:8, 2, 2, 2), X = mx.NDArray(A)
+ @test copy(prod(X))[] == prod(A)
+ @test copy(prod(X, 1)) == prod(A, 1)
+ @test copy(prod(X, 2)) == prod(A, 2)
+ @test copy(prod(X, 3)) == prod(A, 3)
+ @test copy(prod(X, [1, 2])) == prod(A, [1, 2])
+ @test copy(prod(X, (1, 2))) == prod(A, (1, 2))
+ end
+end
+
+function test_fill()
+ info("NDArray::fill")
+
+ let x = mx.fill(42, 2, 3, 4)
+ @test eltype(x) == Int
+ @test size(x) == (2, 3, 4)
+ @test copy(x) == fill(42, 2, 3, 4)
+ end
+
+ let x = mx.fill(Float32(42), 2, 3, 4)
+ @test eltype(x) == Float32
+ @test size(x) == (2, 3, 4)
+ @test copy(x) ≈ fill(Float32(42), 2, 3, 4)
+ end
+
+ let x = mx.fill(42, (2, 3, 4))
+ @test eltype(x) == Int
+ @test size(x) == (2, 3, 4)
+ @test copy(x) == fill(42, 2, 3, 4)
+ end
+
+ let x = mx.fill(Float32(42), (2, 3, 4))
+ @test eltype(x) == Float32
+ @test size(x) == (2, 3, 4)
+ @test copy(x) ≈ fill(Float32(42), 2, 3, 4)
+ end
+
+ info("NDArray::fill!::arr")
+ let x = fill!(mx.zeros(2, 3, 4), 42)
+ @test eltype(x) == Float32
+ @test size(x) == (2, 3, 4)
+ @test copy(x) ≈ fill(Float32(42), 2, 3, 4)
+ end
+end # function test_fill
+
+function test_transpose()
+ info("NDArray::transpose::1D")
+ let A = rand(Float32, 4), x = NDArray(A)
+ @test size(x) == (4,)
+ @test size(x') == (1, 4)
+ end
+
+ info("NDArray::transpose::2D")
+ let A = rand(Float32, 2, 3), x = mx.NDArray(A)
+ @test size(x) == (2, 3)
+ @test size(x') == (3, 2)
+ end
+
+ info("NDArray::permutedims")
+ let A = collect(Float32, reshape(1.0:24, 2, 3, 4)), x = mx.NDArray(A)
+ A′ = permutedims(A, [2, 1, 3])
+ x′ = permutedims(x, [2, 1, 3])
+ @test size(A′) == size(x′)
+ @test A′ == copy(x′)
+ end
+end
+
+function test_show()
+ info("NDArray::show::REPL")
+ let str = sprint(show, MIME"text/plain"(), mx.NDArray([1 2 3 4]))
+ @test contains(str, "1×4")
+ @test contains(str, "mx.NDArray")
+ @test contains(str, "Int64")
+ @test contains(str, "CPU")
+ @test match(r"1\s+2\s+3\s+4", str) != nothing
+ end
+
+ info("NDArray::show")
+ let str = sprint(show, mx.NDArray([1 2 3 4]))
+ @test str == "NDArray [1 2 3 4]"
+ end
+
+ let str = sprint(show, mx.zeros(4))
+ @test str == "NDArray Float32[0.0, 0.0, 0.0, 0.0]"
+ end
+end
+
+function test_size()
+ info("NDArray::size")
+ let A = [1 2; 3 4; 5 6], x = mx.NDArray(A)
+ @test size(A) == size(x)
+ @test size(A, 1, 2, 3, 4, 5) == size(x, 1, 2, 3, 4, 5)
+ @inferred size(x, 1, 2, 3, 4, 5)
+ end
+end # function test_size()
+
+function check_trigonometric(f)
+ info("NDArray::$f")
+ let A = [.1 .2; .3 .4], x = mx.NDArray(A)
+ B = f.(A)
+ y = f.(x)
+ @test copy(y) ≈ B
+ end
+
+ let A = Float32[.1 .2; .3 .4], x = mx.NDArray(A)
+ B = f.(A)
+ y = f.(x)
+ @test copy(y) ≈ B
+ end
+end # function check_trigonometric
+
+function test_trigonometric()
+ for f ∈ [sin, cos, tan, asin, acos, atan]
+ check_trigonometric(f)
+ end
+end # function test_trigonometric
+
+function check_hyperbolic(f, A)
+ info("NDArray::$f")
+ let x = NDArray(A)
+ B = f.(A)
+ y = f.(x)
+ @test copy(y) ≈ B
+ end
+
+ let A = Float32.(A), x = NDArray(A)
+ B = f.(A)
+ y = f.(x)
+ @test copy(y) ≈ B
+ end
+end # function check_hyperbolic
+
+function test_hyperbolic()
+ for f ∈ [sinh, cosh, tanh, asinh, acosh, atanh]
+ A = if f == acosh
+ [1.1, 1.2, 1.3, 1.4]
+ else
+ [.1, .2, .3, .4]
+ end
+ check_hyperbolic(f, A)
+ end
+end # function test_hyperbolic
+
+function test_act_funcs()
+ info("NDArray::σ/sigmoid")
+ let
+ A = Float32[.1, .2, -.3, -.4]
+ B = @. 1 / (1 + e^(-A))
+ x = NDArray(A)
+ y = σ.(x)
+ @test copy(y) ≈ B
+
+ z = sigmoid.(x)
+ @test copy(z) ≈ B
+ end
+
+ info("NDArray::relu")
+ let
+ A = [1, 2, -3, -4]
+ B = max.(A, 0)
+ x = NDArray(A)
+ y = relu.(x)
+ @test copy(y) ≈ B
+ end
+
+ info("NDArray::softmax::1D")
+ let
+ A = Float32[1, 2, 3, 4]
+ B = exp.(A) ./ sum(exp.(A))
+ x = NDArray(A)
+ y = softmax.(x)
+ @test copy(y) ≈ B
+ end
+
+ info("NDArray::softmax::2D")
+ let
+ A = Float32[1 2; 3 4]
+ B = exp.(A) ./ sum(exp.(A), 1)
+ x = NDArray(A)
+ y = softmax.(x, 1)
+ @test copy(y) ≈ B
+
+ C = exp.(A) ./ sum(exp.(A), 2)
+ z = softmax.(x, 2)
+ @test copy(z) ≈ C
+ end
+
+ info("NDArray::log_softmax::1D")
+ let
+ A = Float32[1, 2, 3, 4]
+ B = log.(exp.(A) ./ sum(exp.(A)))
+ x = NDArray(A)
+ y = log_softmax.(x)
+ @test copy(y) ≈ B
+ end
+
+ info("NDArray::log_softmax::2D")
+ let
+ A = Float32[1 2; 3 4]
+ B = log.(exp.(A) ./ sum(exp.(A), 1))
+ x = NDArray(A)
+ y = log_softmax.(x, 1)
+ @test copy(y) ≈ B
+
+ C = log.(exp.(A) ./ sum(exp.(A), 2))
+ z = log_softmax.(x, 2)
+ @test copy(z) ≈ C
+ end
+end # function test_act_funcs
+
+macro check_equal(op)
+ quote
+ A = [1 2 3
+ 4 5 6]
+ B = [1,
+ 6]
+ x = NDArray(A)
+ y = NDArray(B)
+ a = broadcast($op, x, y)
+ @test copy(a) == broadcast($op, A, B)
+
+ C = [3 2 1
+ 6 5 4]
+ z = NDArray(C)
+ b = broadcast($op, x, z)
+ @test copy(b) == broadcast($op, A, C)
+ end
+end
+
+function test_equal()
+ info("NDArray::broadcast_equal")
+ @check_equal ==
+
+ info("NDArray::broadcast_not_equal")
+ @check_equal !=
+
+ info("NDArray::broadcast_greater")
+ @check_equal >
+
+ info("NDArray::broadcast_greater_equal")
+ @check_equal >=
+
+ info("NDArray::broadcast_lesser")
+ @check_equal <
+
+ info("NDArray::broadcast_lesser_equal")
+ @check_equal <=
+end # function test_equal
+
+function test_broadcast_to()
+ info("NDArray::broadcast_to")
+ A = [1 2 3]
+ x = NDArray(A)
+ @test mx.broadcast_to(x, (1, 3)) |> copy == A
+ @test mx.broadcast_to(x, (5, 3)) |> copy == repeat(A, outer = (5, 1))
+
+ @test mx.broadcast_to(x, 1, 3) |> copy == A
+ @test mx.broadcast_to(x, 5, 3) |> copy == repeat(A, outer = (5, 1))
+end # function test_broadcast_to
+
+function test_broadcast_axis()
+ info("NDArray::broadcast_axis")
+ A = reshape([1, 2, 3], 1, 3, 1)
+ x = NDArray(A)
+
+ @test mx.broadcast_axis(x, 1, 4) |> copy == [A; A; A; A]
+ @test mx.broadcast_axis(x, 3, 2) |> copy == cat(3, A, A)
+
+ info("NDArray::broadcast_axes")
+ @test mx.broadcast_axes(x, 1, 4) |> copy == [A; A; A; A]
+ @test mx.broadcast_axes(x, 3, 2) |> copy == cat(3, A, A)
+end # function test_broadcast_axis
+
+function test_hypot()
+ info("NDArray::hypot")
+ A = [3 3 3]
+ B = [4, 4]
+ C = hypot.(A, B)
+
+ x = NDArray(A)
+ y = NDArray(B)
+ z = hypot.(x, y)
+
+ @test copy(z) == C
+end # function test_hypot
+
+################################################################################
+# Run tests
+################################################################################
+@testset "NDArray Test" begin
+ test_constructor()
+ test_ones_zeros_like()
+ test_assign()
+ test_copy()
+ test_slice()
+ test_linear_idx()
+ test_first()
+ test_endof()
+ test_cat()
+ test_plus()
+ test_minus()
+ test_mul()
+ test_div()
+ test_rdiv()
+ test_mod()
+ test_gd()
+ test_saveload()
+ test_clip()
+ test_power()
+ test_sqrt()
+ test_eltype()
+ test_nd_as_jl()
+ test_dot()
+ test_reshape()
+ test_expand_dims()
+ test_sum()
+ test_mean()
+ test_maximum()
+ test_minimum()
+ test_prod()
+ test_fill()
+ test_transpose()
+ test_show()
+ test_size()
+ test_trigonometric()
+ test_hyperbolic()
+ test_act_funcs()
+ test_equal()
+ test_broadcast_to()
+ test_broadcast_axis()
+ test_hypot()
+end
+
+end
diff --git a/julia/test/unittest/operator.jl b/julia/test/unittest/operator.jl
new file mode 100644
index 000000000000..ed8312d91cc9
--- /dev/null
+++ b/julia/test/unittest/operator.jl
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestOperator
+
+using MXNet
+using Base.Test
+
+using ..Main: rand_dims
+
+function test_scalar_op()
+ data = mx.Variable(:data)
+ shape = rand_dims()
+ info("Operator::scalar_op::dims = $shape")
+
+ data_jl = 5ones(Float32, shape)
+ arr_data = mx.copy(data_jl, mx.cpu())
+ arr_grad = mx.zeros(shape)
+
+ test = 2 ./ (4 - ((1+data+1)*2/5) - 0.2)
+ exec_test = mx.bind(test, mx.cpu(), [arr_data], args_grad=[arr_grad])
+ mx.forward(exec_test)
+ out = copy(exec_test.outputs[1])
+ jl_out1 = (4 - ((1+data_jl+1)*2/5) - 0.2)
+ jl_out = 2 ./ jl_out1
+ @test copy(out) ≈ jl_out
+
+ out_grad = 2mx.ones(shape)
+ jl_grad = 2copy(out_grad) / 5
+ jl_grad = 2jl_grad ./ (jl_out1 .^ 2)
+ mx.backward(exec_test, out_grad)
+ @test copy(arr_grad) ≈ jl_grad
+end
+
+################################################################################
+# Run tests
+################################################################################
+
+@testset "Operator Test" begin
+ test_scalar_op()
+end
+
+end
diff --git a/julia/test/unittest/optimizer.jl b/julia/test/unittest/optimizer.jl
new file mode 100644
index 000000000000..b068f12fffd7
--- /dev/null
+++ b/julia/test/unittest/optimizer.jl
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestOptimizer
+
+using Base.Test
+
+using MXNet
+using MXNet.mx.LearningRate
+using MXNet.mx.Momentum
+
+
+function test_fixed_η()
+ info("Optimizer::LearningRate::Fixed")
+ x = LearningRate.Fixed(.42)
+ @test get(x) == .42
+ update!(x)
+ @test get(x) == .42
+end # function test_fixed_η
+
+
+function check_η_decay(x)
+ info("Optimizer::LearningRate::$x")
+
+ η = get(x)
+ @test η == 1
+
+ for i ∈ 1:5
+ update!(x)
+ η′ = get(x)
+ @test η′ < η
+ η = η′
+ end
+end # function check_η_decay
+
+
+test_exp_η() = LearningRate.Exp(1) |> check_η_decay
+
+
+test_inv_η() = LearningRate.Inv(1) |> check_η_decay
+
+
+function test_μ_null()
+ info("Optimizer::Momentum::Null")
+ x = Momentum.Null()
+ @test iszero(get(x))
+end
+
+
+function test_μ_fixed()
+ info("Optimizer::Momentum::Fixed")
+ x = Momentum.Fixed(42)
+ @test get(x) == 42
+end
+
+
+@testset "Optimizer Test" begin
+ @testset "LearningRate Test" begin
+ test_fixed_η()
+ test_exp_η()
+ test_inv_η()
+ end
+
+ @testset "Momentum Test" begin
+ test_μ_null()
+ test_μ_fixed()
+ end
+end
+
+
+end # module TestOptimizer
diff --git a/julia/test/unittest/random.jl b/julia/test/unittest/random.jl
new file mode 100644
index 000000000000..973a4bc32faa
--- /dev/null
+++ b/julia/test/unittest/random.jl
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestRandom
+using MXNet
+using Base.Test
+
+function test_uniform()
+ dims = (100, 100, 2)
+ info("random::uniform::dims = $dims")
+
+ low = -10; high = 10
+ seed = 123
+ mx.srand(seed)
+ ret1 = mx.rand(dims..., low = low, high = high)
+
+ mx.srand(seed)
+ ret2 = mx.empty(dims)
+ mx.rand!(ret2, low = low, high = high)
+
+ @test copy(ret1) == copy(ret2)
+ @test abs(mean(copy(ret1)) - (high+low)/2) < 0.1
+end
+
+function test_gaussian()
+ dims = (80, 80, 4)
+ info("random::gaussian::dims = $dims")
+
+ μ = 10; σ = 2
+ seed = 456
+ mx.srand(seed)
+ ret1 = mx.randn(dims..., μ = μ, σ = σ)
+
+ mx.srand(seed)
+ ret2 = mx.empty(dims)
+ mx.randn!(ret2, μ = μ, σ = σ)
+
+ @test copy(ret1) == copy(ret2)
+ @test abs(mean(copy(ret1)) - μ) < 0.1
+ @test abs(std(copy(ret1)) - σ) < 0.1
+end
+
+@testset "Random Test" begin
+ test_uniform()
+ test_gaussian()
+end
+
+end
diff --git a/julia/test/unittest/symbolic-node.jl b/julia/test/unittest/symbolic-node.jl
new file mode 100644
index 000000000000..507af17332f8
--- /dev/null
+++ b/julia/test/unittest/symbolic-node.jl
@@ -0,0 +1,565 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestSymbolicNode
+
+using MXNet
+using Base.Test
+
+using ..Main: mlp2, mlpchain, exec
+
+################################################################################
+# Test Implementations
+################################################################################
+function test_basic()
+ info("SymbolicNode::basic")
+
+ model = mlp2()
+ @test mx.list_arguments(model) == [:data,:fc1_weight,:fc1_bias,:fc2_weight,:fc2_bias]
+ @test mx.list_outputs(model) == [:fc2_output]
+ @test mx.list_auxiliary_states(model) == Symbol[]
+end
+
+function test_chain()
+ info("SymbolicNode::chain")
+
+ model = mlpchain()
+ @test mx.list_arguments(model) == [:data,:fc1_weight,:fc1_bias,:fc2_weight,:fc2_bias]
+ @test mx.list_outputs(model) == [:fc2_output]
+ @test mx.list_auxiliary_states(model) == Symbol[]
+
+ let layerconfig = [20, 10, 6]
+ model = @mx.chain mx.Variable(:data) =>
+ mx.MLP(layerconfig, prefix=:magic_) =>
+ mx.LinearRegressionOutput(mx.Variable(:label))
+
+ @test mx.list_arguments(model) == [
+ :data,
+ :magic_fc1_weight, :magic_fc1_bias,
+ :magic_fc2_weight, :magic_fc2_bias,
+ :magic_fc3_weight, :magic_fc3_bias,
+ :label]
+ end
+end
+
+function test_internal()
+ info("SymbolicNode::internal")
+
+ data = mx.Variable(:data)
+ oldfc = mx.FullyConnected(data, name=:fc1, num_hidden=10)
+ net1 = mx.FullyConnected(oldfc, name=:fc2, num_hidden=100)
+
+ @test mx.list_arguments(net1) == [:data,:fc1_weight,:fc1_bias,:fc2_weight,:fc2_bias]
+
+ internal = mx.get_internals(net1)
+ fc1 = internal[:fc1_output]
+ @test mx.list_arguments(fc1) == mx.list_arguments(oldfc)
+end
+
+function test_get_children()
+ info("SymbolicNode::get_children")
+
+ let x = mx.Variable(:x), y = mx.Variable(:y)
+ z = x + y
+ @test length(mx.list_outputs(z)) == 1
+ @test length(mx.list_outputs(mx.get_children(z))) == 2
+ @test mx.list_outputs(mx.get_children(z)) == [:x, :y]
+ end
+
+ info("SymbolicNode::get_children::on leaf")
+ let x = mx.Variable(:x)
+ @test mx.get_children(x) == nothing
+ end
+end # test_get_children
+
+
+function test_compose()
+ info("SymbolicNode::compose")
+
+ data = mx.Variable(:data)
+ net1 = mx.FullyConnected(data, name=:fc1, num_hidden=10)
+ net1 = mx.FullyConnected(net1, name=:fc2, num_hidden=100)
+
+ net2 = mx.FullyConnected(mx.SymbolicNode, name=:fc3, num_hidden=10)
+ net2 = mx.Activation(net2, act_type=:relu)
+ net2 = mx.FullyConnected(net2, name=:fc4, num_hidden=20)
+
+ composed = net2(fc3_data=net1, name=:composed)
+ multi_out = mx.Group(composed, net1)
+ @test mx.list_outputs(multi_out) == [:composed_output, :fc2_output]
+end
+
+function test_infer_shape()
+ info("SymbolicNode::infer_shape::mlp2")
+
+ model = mlp2()
+ data_shape = (100, 100)
+ arg_shapes, out_shapes, aux_shapes = mx.infer_shape(model, data=data_shape)
+ arg_shape_dict = Dict{Symbol,Tuple}(zip(mx.list_arguments(model), arg_shapes))
+ @test arg_shape_dict == Dict{Symbol,Tuple}(:fc2_bias => (10,),:fc2_weight => (1000,10),
+ :fc1_bias => (1000,), :fc1_weight => (100, 1000),
+ :data => data_shape)
+ @test length(out_shapes) == 1
+ @test out_shapes[1] == (10, 100)
+end
+
+function test_infer_shape_error()
+ info("SymbolicNode::infer_shape::throws")
+
+ model = mlp2()
+ weight_shape = (100, 1)
+ data_shape = (100, 100)
+ @test_throws mx.MXError mx.infer_shape(model, data=data_shape, fc1_weight=weight_shape)
+end
+
+function test_saveload()
+ info("SymbolicNode::saveload::mlp2")
+
+ model = mlp2()
+ fname = tempname()
+ mx.save(fname, model)
+ model2 = mx.load(fname, mx.SymbolicNode)
+ @test mx.to_json(model) == mx.to_json(model2)
+
+ rm(fname)
+end
+
+function test_attrs()
+ info("SymbolicNode::Attributes")
+
+ data = mx.Variable(:data)
+
+ @test mx.get_name(data) == :data
+ result = mx.get_attr(data, :test)
+ @test isnull(result)
+ mx.set_attr(data, :test, "1.0")
+ result = mx.get_attr(data, :test)
+ @test !isnull(result)
+ @test get(result) == "1.0"
+
+ data2 = mx.Variable(:data2, attrs = Dict(:test => "hallo!"))
+ @test get(mx.get_attr(data2, :test)) == "hallo!"
+
+ conv = mx.Convolution(data2, kernel = (1,1), num_filter = 1)
+ @test isnull(mx.get_attr(conv, :b))
+ @test isa(mx.get_name(conv), Symbol)
+
+ @test_throws MethodError mx.Variable(:data3, attrs = Dict(:test => "1.0", :test2 => 1.0))
+ @test_throws MethodError mx.Convolution(data2, kernel = (1,1), num_filter = 1, attrs = Dict(:test => "1.0", :test2 => 1.0))
+end
+
+function test_functions()
+ info("SymbolicNode::Functions")
+ data = mx.Variable(:data)
+ typeof(mx.sum(data)) == mx.SymbolicNode
+end
+
+function test_reshape()
+ info("SymbolicNode::reshape(sym, dim...)")
+
+ A = mx.NDArray(collect(1:24))
+ x = mx.Variable(:x)
+ y = mx.reshape(x, 2, 3, 4)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (2, 3, 4)
+ @test copy(out) == reshape(1:24, 2, 3, 4)
+
+ info("SymbolicNode::reshape(sym, dim)")
+
+ A = mx.NDArray(collect(1:24))
+ x = mx.Variable(:x)
+ y = mx.reshape(x, (2, 3, 4))
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (2, 3, 4)
+ @test copy(out) == reshape(1:24, 2, 3, 4)
+
+ info("SymbolicNode::reshape::reverse")
+
+ A = mx.zeros(10, 5, 4)
+ x = mx.Variable(:x)
+ y = mx.reshape(x, -1, 0, reverse=true)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (50, 4)
+
+ info("SymbolicNode::reshape::0")
+
+ A = mx.zeros(2, 3, 4)
+ x = mx.Variable(:x)
+ y = mx.reshape(x, 4, 0, 2)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (4, 3, 2)
+
+ info("SymbolicNode::reshape::-1")
+
+ A = mx.zeros(2, 3, 4)
+ x = mx.Variable(:x)
+ y = mx.reshape(x, 6, 1, -1)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (6, 1, 4)
+
+ info("SymbolicNode::reshape::-2")
+
+ A = mx.zeros(2, 3, 4, 2)
+ x = mx.Variable(:x)
+ y = mx.reshape(x, 3, 2, -2)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (3, 2, 4, 2)
+
+ info("SymbolicNode::reshape::-3")
+
+ A = mx.zeros(2, 3, 4, 5)
+ x = mx.Variable(:x)
+ y = mx.reshape(x, -3, -3)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (6, 20)
+
+ info("SymbolicNode::reshape::-4")
+
+ A = mx.zeros(2, 3, 4)
+ x = mx.Variable(:x)
+ y = mx.reshape(x, 0, 0, -4, 2, 2)
+ e = mx.bind(y, mx.cpu(), Dict(:x => A))
+ mx.forward(e)
+ out = e.outputs[1]
+
+ @test size(out) == (2, 3, 2, 2)
+end
+
+function test_dot()
+ info("SymbolicNode::dot")
+ x = mx.Variable(:x)
+ y = mx.Variable(:y)
+ z = mx.dot(x, y)
+ z_exec = mx.bind(z, context=mx.cpu(),
+ args=Dict(:x => mx.ones((100, 2)), :y => mx.ones((2, 200))))
+ mx.forward(z_exec)
+
+ ret = copy(z_exec.outputs[1])
+ @test size(ret) == (100, 200)
+ @test ret ≈ 2*ones(100, 200)
+end
+
+function test_print()
+ info("SymbolicNode::print")
+ io = IOBuffer()
+ print(io, mx.Variable(:x))
+ @test !isempty(String(take!(io)))
+end
+
+function test_misc()
+ info("SymbolicNode::Miscellaneous")
+ # Test for #189
+ a = mx.Variable("a")
+ b = mx.Variable("b")
+ symb = mx.ElementWiseSum(a, b)
+end
+
+function test_add()
+ info("SymbolicNode::elementwise add")
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = exec(x .+ 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == A .+ 42
+ end
+
+ let y = exec(42 .+ x; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == 42 .+ A
+ end
+
+ let y = exec(-1 .+ x .+ 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == -1 .+ A .+ 42
+ end
+ end
+
+ let A = Float32[1 2; 3 4], B = Float32[2 4; 6 8]
+ x = mx.Variable(:x)
+ y = mx.Variable(:y)
+
+ let z = x .+ y
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) == A .+ B
+ end
+
+ let z = y .+ x
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) == B .+ A
+ end
+ end
+end # function test_add
+
+function test_minus()
+ info("SymbolicNode::elementwise minus")
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = exec(x .- 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == A .- 42
+ end
+
+ let y = exec(42 .- x; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == 42 .- A
+ end
+
+ let y = exec(-1 .- x .- 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == -1 .- A .- 42
+ end
+
+ let y = exec(-x; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == -A
+ end
+ end
+
+ let A = Float32[1 2; 3 4], B = Float32[2 4; 6 8]
+ x = mx.Variable(:x)
+ y = mx.Variable(:y)
+
+ let z = x .- y
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) == A .- B
+ end
+
+ let z = y .- x
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) == B .- A
+ end
+ end
+end # function test_minus
+
+function test_mul()
+ info("SymbolicNode::elementwise mul")
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = exec(x .* 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == A .* 42
+ end
+
+ let y = exec(42 .* x; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == 42 .* A
+ end
+
+ let y = exec(-1 .* x .* 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) == -1 .* A .* 42
+ end
+ end
+
+ let A = Float32[1 2; 3 4], B = Float32[2 4; 6 8]
+ x = mx.Variable(:x)
+ y = mx.Variable(:y)
+
+ let z = x .* y
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) == A .* B
+ end
+
+ let z = y .* x
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) == B .* A
+ end
+ end
+end # function test_mul
+
+function test_div()
+ info("SymbolicNode::elementwise div")
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = exec(x ./ 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) ≈ A ./ 42
+ end
+
+ let y = exec(42 ./ x; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) ≈ 42 ./ A
+ end
+
+ let y = exec(-1 ./ x ./ 42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) ≈ -1 ./ A ./ 42
+ end
+ end
+
+ let A = Float32[1 2; 3 4], B = Float32[2 4; 6 8]
+ x = mx.Variable(:x)
+ y = mx.Variable(:y)
+
+ let z = x ./ y
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) ≈ A ./ B
+ end
+
+ let z = y ./ x
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) ≈ B ./ A
+ end
+ end
+end # function test_div
+
+function test_power()
+ info("SymbolicNode::elementwise power")
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = exec(x.^42; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) ≈ A.^42
+ end
+
+ let y = exec(42.^x; :x => A)[]
+ @test size(y) == size(A)
+ @test copy(y) ≈ 42.^A
+ end
+ end
+
+ let A = Float32[1 2; 3 4], B = Float32[2 4; 6 8]
+ x = mx.Variable(:x)
+ y = mx.Variable(:y)
+
+ let z = x.^y
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) ≈ A.^B
+ end
+
+ let z = y.^x
+ z = exec(z; :x => A, :y => B)[]
+
+ @test size(z) == size(A)
+ @test copy(z) ≈ B.^A
+ end
+ end
+
+ info("SymbolicNode::power::e.^x::x.^e")
+ let x = mx.Variable(:x), A = [0 0 0; 0 0 0]
+ y = exec(e.^x; :x => A)[]
+ @test copy(y) ≈ ones(A)
+ end
+
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = e.^x
+ z = exec(y; :x => A)[]
+ @test copy(z) ≈ e.^A
+ end
+
+ let y = x.^e
+ z = exec(y; :x => A)[]
+ @test copy(z) ≈ A.^e
+ end
+ end
+
+ info("SymbolicNode::power::π.^x::x.^π")
+ let x = mx.Variable(:x), A = Float32[1 2; 3 4]
+ let y = π.^x
+ z = exec(y; :x => A)[]
+ @test copy(z) ≈ π.^A
+ end
+
+ let y = x.^π
+ z = exec(y; :x => A)[]
+ @test copy(z) ≈ A.^π
+ end
+ end
+end # function test_power
+
+function test_get_name()
+ info("SymbolicNode::get_name::with get_internals")
+ name = mx.get_name(mx.get_internals(mlp2())) # no error
+ @test contains(name, "Ptr")
+end # function test_get_name
+
+function test_var()
+ info("SymbolicNode::var")
+ x = @mx.var x
+ @test x isa mx.SymbolicNode
+
+ x′ = @mx.var x
+ @test x.handle != x′.handle
+
+ x, y, z = @mx.var x y z
+ @test x isa mx.SymbolicNode
+ @test y isa mx.SymbolicNode
+ @test z isa mx.SymbolicNode
+end # test_var
+
+
+################################################################################
+# Run tests
+################################################################################
+@testset "SymbolicNode Test" begin
+ test_basic()
+ test_chain()
+ test_internal()
+ test_compose()
+ test_infer_shape()
+ test_infer_shape_error()
+ test_saveload()
+ test_attrs()
+ test_functions()
+ test_reshape()
+ test_dot()
+ test_print()
+ test_misc()
+ test_add()
+ test_minus()
+ test_mul()
+ test_div()
+ test_power()
+ test_get_name()
+ test_var()
+end
+
+end
diff --git a/julia/test/unittest/util.jl b/julia/test/unittest/util.jl
new file mode 100644
index 000000000000..ddd613ca48ea
--- /dev/null
+++ b/julia/test/unittest/util.jl
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestUtil
+
+using Base.Test
+
+using MXNet
+
+
+function test_getdocdefine()
+ info("Util::_getdocdefine")
+ @test contains(mx._getdocdefine("sgd_update"), "Defined in")
+end # function test_getdocdefine
+
+
+function test_firstarg()
+ info("Util::_firstarg")
+ @test mx._firstarg(:(f(x, y))) == :x
+ @test mx._firstarg(:(f(x::mx.NDArray, y))) == :x
+ @test mx._firstarg(:(f(x::mx.NDArray, y::mx.NDArray))) == :x
+ @test mx._firstarg(:(f(x::Int, y::mx.NDArray))) == :x
+ @test mx._firstarg(:(f(x::Int, y::mx.NDArray; other = 42))) == :x
+ @test mx._firstarg(:(f(x::mx.NDArray{T}, y) where {T})) == :x
+ @test mx._firstarg(:(f(x::mx.NDArray{T,N}, y) where {T,N})) == :x
+ @test mx._firstarg(:(f(x::mx.NDArray{T,N} where {T,N}, y))) == :x
+ @test mx._firstarg(:(broadcast_(::typeof(asin), x::mx.NDArray))) == :x
+ @test mx._firstarg(:(broadcast_(::typeof(asin), x::mx.NDArray, y::mx.NDArray))) == :x
+end # function test_firstarg
+
+
+@testset "Util Test" begin
+ test_firstarg()
+ test_getdocdefine()
+end # @testset "Util"
+
+end # module TestUtil
diff --git a/julia/test/unittest/visualize.jl b/julia/test/unittest/visualize.jl
new file mode 100644
index 000000000000..58d111b0fe14
--- /dev/null
+++ b/julia/test/unittest/visualize.jl
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module TestVisualize
+using MXNet
+using Base.Test
+
+using ..Main: mlp2
+
+################################################################################
+# Test Implementations
+################################################################################
+
+function test_basic()
+ info("Visualize::basic")
+
+ mlp = mlp2()
+
+ # Order of elements or default color values can change, but length of the output should be more or less stable
+ @test length(mx.to_graphviz(mlp)) == length(
+"""
+digraph "Network Visualization" {
+node [fontsize=10];
+edge [fontsize=10];
+"fc1" [label="fc1\\nFullyConnected\\nnum-hidden=1000",style="rounded,filled",fixedsize=true,width=1.3,fillcolor="#fb8072",shape=box,penwidth=2,height=0.8034,color="#941305"];
+"activation0" [label="activation0\\nActivation\\nact-type=relu",style="rounded,filled",fixedsize=true,width=1.3,fillcolor="#ffffb3",shape=box,penwidth=2,height=0.8034,color="#999900"];
+"fc2" [label="fc2\\nFullyConnected\\nnum-hidden=10",style="rounded,filled",fixedsize=true,width=1.3,fillcolor="#fb8072",shape=box,penwidth=2,height=0.8034,color="#941305"];
+"activation0" -> "fc1" [arrowtail=open,color="#737373",dir=back];
+"fc2" -> "activation0" [arrowtail=open,color="#737373",dir=back];
+}
+""")
+end
+################################################################################
+# Run tests
+################################################################################
+
+@testset "Visualize Test" begin
+ test_basic()
+end
+end
diff --git a/make/config.mk b/make/config.mk
index 2fd8f6ec22f6..03f438db89d5 100644
--- a/make/config.mk
+++ b/make/config.mk
@@ -182,12 +182,18 @@ USE_GPERFTOOLS = 1
# path to gperftools (tcmalloc) library in case of a non-standard installation
USE_GPERFTOOLS_PATH =
+# Link gperftools statically
+USE_GPERFTOOLS_STATIC =
+
# Use JEMalloc if found, and not using gperftools
USE_JEMALLOC = 1
# path to jemalloc library in case of a non-standard installation
USE_JEMALLOC_PATH =
+# Link jemalloc statically
+USE_JEMALLOC_STATIC =
+
#----------------------------
# additional operators
#----------------------------
diff --git a/make/osx.mk b/make/osx.mk
index 47f395bccf97..3e2e592323f4 100644
--- a/make/osx.mk
+++ b/make/osx.mk
@@ -77,10 +77,11 @@ USE_CUDNN = 0
USE_OPENCV = 1
# use openmp for parallelization
+# apple-clang by default does not have openmp built-in
USE_OPENMP = 0
# choose the version of blas you want to use
-# can be: mkl, blas, atlas, openblas
+# can be: mkl, blas, atlas, openblas, apple
USE_BLAS = apple
# whether use lapack during compilation
diff --git a/mkldnn.mk b/mkldnn.mk
index 1be0704dcde1..d79bbe7d2a0e 100644
--- a/mkldnn.mk
+++ b/mkldnn.mk
@@ -47,7 +47,7 @@ $(MKLDNN_LIBFILE):
mkldnn_clean:
$(RM) -r 3rdparty/mkldnn/build
- $(RM) -r 3rdparty/mkldnn/install/*
+ $(RM) -r $(MKLDNNROOT)
ifeq ($(USE_MKLDNN), 1)
mkldnn: mkldnn_build
diff --git a/perl-package/AI-MXNet-Gluon-Contrib/Changes b/perl-package/AI-MXNet-Gluon-Contrib/Changes
index 81e55aa753ab..f91ea2045edc 100644
--- a/perl-package/AI-MXNet-Gluon-Contrib/Changes
+++ b/perl-package/AI-MXNet-Gluon-Contrib/Changes
@@ -1,5 +1,8 @@
Revision history for Perl extension AI::MXNet::Gluon::Contrib
+1.33 Thu Oct 4 13:25:56 PDT 2018
+ - Fixed kwalitee issues.
+
1.32 Sun Jul 15 12:12:15 PDT 2018
- Missing POD fixes.
diff --git a/perl-package/AI-MXNet-Gluon-Contrib/META.json b/perl-package/AI-MXNet-Gluon-Contrib/META.json
index ec65bb01348e..910c7d4d9a15 100644
--- a/perl-package/AI-MXNet-Gluon-Contrib/META.json
+++ b/perl-package/AI-MXNet-Gluon-Contrib/META.json
@@ -30,7 +30,7 @@
},
"runtime" : {
"requires" : {
- "AI::MXNet" : "1.31",
+ "AI::MXNet" : "1.33"
}
},
"test" : {
@@ -38,5 +38,5 @@
}
},
"release_status" : "stable",
- "version" : "1.32"
+ "version" : "1.33"
}
diff --git a/perl-package/AI-MXNet-Gluon-Contrib/META.yml b/perl-package/AI-MXNet-Gluon-Contrib/META.yml
index aaa194debae9..d175c2bd1413 100644
--- a/perl-package/AI-MXNet-Gluon-Contrib/META.yml
+++ b/perl-package/AI-MXNet-Gluon-Contrib/META.yml
@@ -18,4 +18,4 @@ no_index:
- inc
requires:
AI::MXNet: '1.31'
-version: '1.32'
+version: '1.33'
diff --git a/perl-package/AI-MXNet-Gluon-Contrib/Makefile.PL b/perl-package/AI-MXNet-Gluon-Contrib/Makefile.PL
index 6c58d6ea8669..a6ff95e8bcc6 100644
--- a/perl-package/AI-MXNet-Gluon-Contrib/Makefile.PL
+++ b/perl-package/AI-MXNet-Gluon-Contrib/Makefile.PL
@@ -39,7 +39,7 @@ my %WriteMakefileArgs = (
"AI::MXNet" => "1.31",
},
"TEST_REQUIRES" => {},
- "VERSION" => "1.32",
+ "VERSION" => "1.33",
"test" => {
"TESTS" => "t/*.t"
}
diff --git a/perl-package/AI-MXNet-Gluon-Contrib/README b/perl-package/AI-MXNet-Gluon-Contrib/README
index 6c0efcc3c897..f0301d168f75 100644
--- a/perl-package/AI-MXNet-Gluon-Contrib/README
+++ b/perl-package/AI-MXNet-Gluon-Contrib/README
@@ -1,5 +1,5 @@
This archive contains the distribution AI-MXNet-Gluon-Contrib,
-version 1.32:
+version 1.33:
Perl interface to MXNet Gluon Contib modules, a collection of supplemental Gluon blocks.
diff --git a/perl-package/AI-MXNet-Gluon-Contrib/lib/AI/MXNet/Gluon/Contrib.pm b/perl-package/AI-MXNet-Gluon-Contrib/lib/AI/MXNet/Gluon/Contrib.pm
index 029bc4b65a68..807dfc87200d 100644
--- a/perl-package/AI-MXNet-Gluon-Contrib/lib/AI/MXNet/Gluon/Contrib.pm
+++ b/perl-package/AI-MXNet-Gluon-Contrib/lib/AI/MXNet/Gluon/Contrib.pm
@@ -20,10 +20,20 @@ use strict;
use warnings;
use AI::MXNet;
use AI::MXNet::Gluon::Contrib::NN::BasicLayers;
-our $VERSION = '1.32';
+our $VERSION = '1.33';
=head1 NAME
AI::MXNet::Gluon::Contrib - A collection of supplemental Gluon blocks.
=cut
-1;
\ No newline at end of file
+1;
+
+=head1 AUTHOR
+
+ Sergey Kolychev,
+
+=head1 COPYRIGHT & LICENSE
+
+ This library is licensed under Apache 2.0 license L
+
+=cut
diff --git a/perl-package/AI-MXNet-Gluon-ModelZoo/Changes b/perl-package/AI-MXNet-Gluon-ModelZoo/Changes
index 377dff5be8de..61018181c9a5 100644
--- a/perl-package/AI-MXNet-Gluon-ModelZoo/Changes
+++ b/perl-package/AI-MXNet-Gluon-ModelZoo/Changes
@@ -1,5 +1,8 @@
Revision history for Perl extension AI::MXNet::Gluon::ModelZoo
+1.33 Thu Oct 4 13:25:56 PDT 2018
+ - Fixed kwalitee issues.
+
1.32 Sun Aug 5 14:25:31 PDT 2018
- Updated vgg16/19 models
diff --git a/perl-package/AI-MXNet-Gluon-ModelZoo/META.json b/perl-package/AI-MXNet-Gluon-ModelZoo/META.json
index 9ea969e9f5fb..2ce7dddba36c 100644
--- a/perl-package/AI-MXNet-Gluon-ModelZoo/META.json
+++ b/perl-package/AI-MXNet-Gluon-ModelZoo/META.json
@@ -31,7 +31,8 @@
"runtime" : {
"requires" : {
"AI::MXNet" : "1.31",
- "AI::MXNet::Gluon::Contrib" : "1.3"
+ "AI::MXNet::Gluon::Contrib" : "1.3",
+ "IO::Uncompress::Unzip" : "0"
}
},
"test" : {
@@ -39,5 +40,5 @@
}
},
"release_status" : "stable",
- "version" : "1.32"
+ "version" : "1.33"
}
diff --git a/perl-package/AI-MXNet-Gluon-ModelZoo/META.yml b/perl-package/AI-MXNet-Gluon-ModelZoo/META.yml
index a04484a898a9..d6d9652a6dd5 100644
--- a/perl-package/AI-MXNet-Gluon-ModelZoo/META.yml
+++ b/perl-package/AI-MXNet-Gluon-ModelZoo/META.yml
@@ -19,4 +19,5 @@ no_index:
requires:
AI::MXNet: '1.31'
AI::MXNet::Gluon::Contrib: '1.3'
-version: '1.32'
+ IO::Uncompress::Unzip: '0'
+version: '1.33'
diff --git a/perl-package/AI-MXNet-Gluon-ModelZoo/Makefile.PL b/perl-package/AI-MXNet-Gluon-ModelZoo/Makefile.PL
index d15dfce99b8e..de8b1acc5e2f 100644
--- a/perl-package/AI-MXNet-Gluon-ModelZoo/Makefile.PL
+++ b/perl-package/AI-MXNet-Gluon-ModelZoo/Makefile.PL
@@ -37,10 +37,11 @@ my %WriteMakefileArgs = (
"NAME" => "AI::MXNet::Gluon::ModelZoo",
"PREREQ_PM" => {
"AI::MXNet" => "1.31",
- "AI::MXNet::Gluon::Contrib" => "1.3"
+ "AI::MXNet::Gluon::Contrib" => "1.3",
+ "IO::Uncompress::Unzip" => "0"
},
"TEST_REQUIRES" => {},
- "VERSION" => "1.32",
+ "VERSION" => "1.33",
"test" => {
"TESTS" => "t/*.t"
}
@@ -49,7 +50,8 @@ my %WriteMakefileArgs = (
my %FallbackPrereqs = (
"AI::MXNet" => "1.31",
- "AI::MXNet::Gluon::Contrib" => "1.3"
+ "AI::MXNet::Gluon::Contrib" => "1.3",
+ "IO::Uncompress::Unzip" => "0"
);
diff --git a/perl-package/AI-MXNet-Gluon-ModelZoo/README b/perl-package/AI-MXNet-Gluon-ModelZoo/README
index 6b8e04b971ec..e39ae4b69be0 100644
--- a/perl-package/AI-MXNet-Gluon-ModelZoo/README
+++ b/perl-package/AI-MXNet-Gluon-ModelZoo/README
@@ -1,5 +1,5 @@
This archive contains the distribution AI-MXNet-Gluon-ModelZoo,
-version 1.32:
+version 1.33:
Perl interface to MXNet Gluon ModelZoo, a collection of pretrained machine learning models for computer vision.
diff --git a/perl-package/AI-MXNet-Gluon-ModelZoo/lib/AI/MXNet/Gluon/ModelZoo.pm b/perl-package/AI-MXNet-Gluon-ModelZoo/lib/AI/MXNet/Gluon/ModelZoo.pm
index c9e6e7753045..e9cbec02a445 100644
--- a/perl-package/AI-MXNet-Gluon-ModelZoo/lib/AI/MXNet/Gluon/ModelZoo.pm
+++ b/perl-package/AI-MXNet-Gluon-ModelZoo/lib/AI/MXNet/Gluon/ModelZoo.pm
@@ -26,7 +26,7 @@ use AI::MXNet::Gluon::ModelZoo::Vision;
use Exporter;
use base qw(Exporter);
@AI::MXNet::Gluon::ModelZoo::EXPORT_OK = qw(get_model);
-our $VERSION = '1.32';
+our $VERSION = '1.33';
=head1 NAME
@@ -130,3 +130,13 @@ sub get_model
sub vision { 'AI::MXNet::Gluon::ModelZoo::Vision' }
1;
+
+=head1 AUTHOR
+
+ Sergey Kolychev,
+
+=head1 COPYRIGHT & LICENSE
+
+ This library is licensed under Apache 2.0 license L
+
+=cut
diff --git a/perl-package/AI-MXNet/Changes b/perl-package/AI-MXNet/Changes
index 8b9463ee84e8..8bd43f3be205 100644
--- a/perl-package/AI-MXNet/Changes
+++ b/perl-package/AI-MXNet/Changes
@@ -1,5 +1,17 @@
Revision history for Perl extension AI::MXNet
+1.33 Thu Oct 4 13:25:56 PDT 2018
+ - Added randn function.
+ - Internal SELU function on C++ layer.
+ - Predict now accepts ndarray as well.
+ - Gluon: Only warn when the blocks are unregistered.
+ - Gluon: Better sparse support.
+ - Gluon: Improved block summary.
+ - Added validation docs for MXNet installation for Perl.
+ - Flexible perl env for examples.
+ - Gluon: Custom dtypes for the symbol block
+ - Separate eval metric for the epoch level.
+
1.32 Sun Aug 5 14:25:31 PDT 2018
- Several new metric classes
- Expanded documentation
diff --git a/perl-package/AI-MXNet/META.json b/perl-package/AI-MXNet/META.json
index 7d0ab96c0593..bbbea734ccf8 100644
--- a/perl-package/AI-MXNet/META.json
+++ b/perl-package/AI-MXNet/META.json
@@ -30,7 +30,7 @@
},
"runtime" : {
"requires" : {
- "AI::MXNetCAPI" : "1.32",
+ "AI::MXNetCAPI" : "1.33",
"AI::NNVMCAPI" : "1.3",
"Function::Parameters" : "1.0705",
"Hash::Ordered" : "0.012",
@@ -45,5 +45,5 @@
}
},
"release_status" : "stable",
- "version" : "1.32"
+ "version" : "1.33"
}
diff --git a/perl-package/AI-MXNet/META.yml b/perl-package/AI-MXNet/META.yml
index ee5d677a8139..e604b7cd0da8 100644
--- a/perl-package/AI-MXNet/META.yml
+++ b/perl-package/AI-MXNet/META.yml
@@ -17,7 +17,7 @@ no_index:
- t
- inc
requires:
- AI::MXNetCAPI: '1.32'
+ AI::MXNetCAPI: '1.33'
AI::NNVMCAPI: '1.3'
Function::Parameters: '1.0705'
Hash::Ordered: '0.012'
@@ -25,4 +25,4 @@ requires:
Mouse: v2.1.0
PDL: '2.007'
PDL::CCS: '1.23.4'
-version: '1.32'
+version: '1.33'
diff --git a/perl-package/AI-MXNet/Makefile.PL b/perl-package/AI-MXNet/Makefile.PL
index 59036d905f82..6d70b21344c2 100644
--- a/perl-package/AI-MXNet/Makefile.PL
+++ b/perl-package/AI-MXNet/Makefile.PL
@@ -36,7 +36,7 @@ my %WriteMakefileArgs = (
"LICENSE" => "apache_2_0",
"NAME" => "AI::MXNet",
"PREREQ_PM" => {
- "AI::MXNetCAPI" => "1.3",
+ "AI::MXNetCAPI" => "1.33",
"AI::NNVMCAPI" => "1.3",
"Function::Parameters" => "1.0705",
"Hash::Ordered" => "0.012",
@@ -46,7 +46,7 @@ my %WriteMakefileArgs = (
"GraphViz" => "2.14"
},
"TEST_REQUIRES" => {},
- "VERSION" => "1.32",
+ "VERSION" => "1.33",
"test" => {
"TESTS" => "t/*.t"
}
@@ -54,7 +54,7 @@ my %WriteMakefileArgs = (
my %FallbackPrereqs = (
- "AI::MXNetCAPI" => "1.3",
+ "AI::MXNetCAPI" => "1.33",
"AI::NNVMCAPI" => "1.3",
"Function::Parameters" => "1.0705",
"Hash::Ordered" => "0.012",
diff --git a/perl-package/AI-MXNet/README b/perl-package/AI-MXNet/README
index 2f1010a43f9a..f370db3804e9 100644
--- a/perl-package/AI-MXNet/README
+++ b/perl-package/AI-MXNet/README
@@ -1,5 +1,5 @@
This archive contains the distribution AI-MXNet,
-version 1.32:
+version 1.33:
Perl interface to MXNet machine learning library
diff --git a/perl-package/AI-MXNet/examples/calculator.pl b/perl-package/AI-MXNet/examples/calculator.pl
index aadc7cd2641e..0350536c730f 100755
--- a/perl-package/AI-MXNet/examples/calculator.pl
+++ b/perl-package/AI-MXNet/examples/calculator.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/examples/char_lstm.pl b/perl-package/AI-MXNet/examples/char_lstm.pl
index 1e9c385c95f1..a8bf72599797 100755
--- a/perl-package/AI-MXNet/examples/char_lstm.pl
+++ b/perl-package/AI-MXNet/examples/char_lstm.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/examples/cudnn_lstm_bucketing.pl b/perl-package/AI-MXNet/examples/cudnn_lstm_bucketing.pl
index 326e57c5a6cb..53200f3095c0 100755
--- a/perl-package/AI-MXNet/examples/cudnn_lstm_bucketing.pl
+++ b/perl-package/AI-MXNet/examples/cudnn_lstm_bucketing.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/examples/gluon/dcgan.pl b/perl-package/AI-MXNet/examples/gluon/dcgan.pl
index 2bdc56149d7e..dd5294763cb2 100755
--- a/perl-package/AI-MXNet/examples/gluon/dcgan.pl
+++ b/perl-package/AI-MXNet/examples/gluon/dcgan.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
diff --git a/perl-package/AI-MXNet/examples/gluon/mnist.pl b/perl-package/AI-MXNet/examples/gluon/mnist.pl
index 5492e7e98f0e..1fb2d897250f 100755
--- a/perl-package/AI-MXNet/examples/gluon/mnist.pl
+++ b/perl-package/AI-MXNet/examples/gluon/mnist.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/examples/lstm_bucketing.pl b/perl-package/AI-MXNet/examples/lstm_bucketing.pl
index 3618a62d1fb2..168c7c2be30f 100755
--- a/perl-package/AI-MXNet/examples/lstm_bucketing.pl
+++ b/perl-package/AI-MXNet/examples/lstm_bucketing.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/examples/mnist.pl b/perl-package/AI-MXNet/examples/mnist.pl
index ca452cd95444..3786b6be98eb 100755
--- a/perl-package/AI-MXNet/examples/mnist.pl
+++ b/perl-package/AI-MXNet/examples/mnist.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/examples/plot_network.pl b/perl-package/AI-MXNet/examples/plot_network.pl
index fc38ef2baaab..bf39988e7105 100755
--- a/perl-package/AI-MXNet/examples/plot_network.pl
+++ b/perl-package/AI-MXNet/examples/plot_network.pl
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet.pm b/perl-package/AI-MXNet/lib/AI/MXNet.pm
index 651ca92ad69a..6a559a394a9f 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet.pm
@@ -51,7 +51,7 @@ use AI::MXNet::Gluon;
use AI::MXNet::NDArray::Sparse;
use AI::MXNet::Symbol::Sparse;
use AI::MXNet::Engine;
-our $VERSION = '1.32';
+our $VERSION = '1.33';
sub import
{
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon.pm
index 92c8386c0d14..fde2f6ac5a63 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon.pm
@@ -82,7 +82,7 @@ sub model_zoo { require AI::MXNet::Gluon::ModelZoo; 'AI::MXNet::Gluon::ModelZoo'
but rather brings the training algorithm and model closer together to provide flexibility in the development process.
Dynamic Graphs: Gluon enables developers to define neural network models that are dynamic,
- meaning they can be built on the fly, with any structure, and using any of Perl’s native control flow.
+ meaning they can be built on the fly, with any structure, and using any of Perl's native control flow.
High Performance: Gluon provides all of the above benefits without impacting the training speed that the underlying engine provides.
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Block.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Block.pm
index 1b35e7864c12..599c3c3bef6e 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Block.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Block.pm
@@ -93,6 +93,7 @@ method __exit__()
package AI::MXNet::Gluon::Block;
use AI::MXNet::Gluon::Mouse;
+use Scalar::Util qw(refaddr);
=head2 NAME
@@ -288,14 +289,15 @@ method __setattr__($name, $current, $prev=)
method _check_container_with_block()
{
- my $_find_block_in_container;
- $_find_block_in_container = sub { my ($data) = @_;
+ my $_find_unregistered_block_in_container;
+ my %children = map { refaddr($_) => 1 } $self->_children->values;
+ $_find_unregistered_block_in_container = sub { my ($data) = @_;
# Find whether a nested container structure contains Blocks
if(ref $data eq 'ARRAY')
{
for my $ele (@{ $data })
{
- if($_find_block_in_container->($ele))
+ if($_find_unregistered_block_in_container->($ele))
{
return 1
}
@@ -306,7 +308,7 @@ method _check_container_with_block()
{
for my $v (values %$data)
{
- if($_find_block_in_container->($v))
+ if($_find_unregistered_block_in_container->($v))
{
return 1;
}
@@ -315,7 +317,7 @@ method _check_container_with_block()
}
elsif(blessed $data and $data->isa('AI::MXNet::Gluon::Block'))
{
- return 1;
+ return not exists $children{ refaddr($data) };
}
else
{
@@ -327,10 +329,10 @@ method _check_container_with_block()
{
if((ref $v eq 'HASH' or ref $v eq 'ARRAY') and not $k =~ /^__/)
{
- if($_find_block_in_container->($v))
+ if($_find_unregistered_block_in_container->($v))
{
AI::MXNet::Logging->warning(
- '"%s" is a container with Blocks. '.
+ '"%s" is a unregsitered container with Blocks. '.
'Note that Blocks inside the list, tuple or dict will not be '.
'registered automatically. Make sure to register them using '.
'register_child() or switching to '.
@@ -837,10 +839,11 @@ method summary(@inputs)
$shared_params += $summary->get($layer)->get('shared');
}
print (('=')x80, "\n");
- print "Total params: $total_params\n";
- print "Trainable params: $trainable_params\n";
- print "Non-trainable params: ", $total_params - $trainable_params, "\n";
- print "Shared params: $shared_params\n";
+ print "Parameters in forward computation graph, duplicate included\n";
+ print " Total params: $total_params\n";
+ print " Non-trainable params: ", $total_params - $trainable_params, "\n";
+ print "Shared params in forward computation graph: $shared_params\n";
+ print "Unique parameters in model: ", $total_params - $shared_params, "\n";
print (('-')x80, "\n");
};
$_->detach for @hooks;
@@ -1361,19 +1364,25 @@ sub BUILD
}
}
- for my $i (@{ $out->list_arguments })
+ my $arg_params = $out->list_arguments;
+ my $aux_params = $out->list_auxiliary_states;
+ my ($arg_types, $aux_types) = _infer_param_types($syms, $out, $arg_params, $aux_params);
+
+ for(enumerate($arg_params))
{
- if(not exists $input_names{$i})
+ my ($i, $arg) = @$_;
+ if(not exists $input_names{ $arg })
{
- $self->params->get($i, allow_deferred_init => 1);
+ $self->params->get($arg, allow_deferred_init => 1, dtype => $arg_types->[$i]);
}
}
- for my $i (@{ $out->list_auxiliary_states })
+ for(enumerate($aux_params))
{
- if(not exists $input_names{$i})
+ my ($i, $arg) = @$_;
+ if(not exists $input_names{ $arg })
{
- $self->params->get($i, grad_req => 'null', allow_deferred_init => 1);
+ $self->params->get($arg, grad_req => 'null', allow_deferred_init => 1, dtype => $aux_types->[$i]);
}
}
@@ -1388,6 +1397,71 @@ sub BUILD
$self->_prefix($prefix);
}
+
+func _infer_param_types($in_params, $out_params, $arg_params, $aux_params, $default_dtype='float32')
+{
+ # Utility function that helps in inferring DType of args and auxs params
+ # from given input param.
+ # Parameters
+ # ----------
+ # in_params: array ref of AI::MXNet::Symbol objects
+ # List of input symbol variables.
+ # out_params: AI::MXNet::Symbol
+ # Output symbol variable.
+ # arg_params: array ref of Str
+ # List of names of argument parametrs.
+ # aux_params: array ref of Str
+ # List of names of auxiliary parameters.
+ # default_dtype: Dtype, default 'float32'
+ # Default data type for arg_params and aux_params, if unable to infer the type.
+ # Returns
+ # -------
+ # arg_types: Array ref of Dtype
+ # List of arg_params type. Order is same as arg_params.
+ # Defaults to 'float32', if unable to infer type.
+ # aux_types: Array ref of Dtype
+ # List of aux_params type. Order is same as aux_params.
+ # Defaults to 'float32', if unable to infer type.
+
+ my $arg_types;
+ my $aux_types;
+ # Get Input symbol details. This will be used to infer types of
+ # other parameters.
+ my @input_sym_names = map { $_->name } @{ $in_params };
+ # Try to infer input types. If not successful, we will set default dtype.
+ # If successful, we will try to infer other params in the graph.
+ my @input_sym_arg_types;
+ my $can_infer_input_type = 1;
+ for my $in_param(@{ $in_params })
+ {
+ my $input_sym_arg_type = ($in_param->infer_type)[0];
+ if(not $input_sym_arg_type or @$input_sym_arg_type < 1)
+ {
+ $can_infer_input_type = 0;
+ last;
+ }
+ else
+ {
+ push @input_sym_arg_types, $input_sym_arg_type->[0];
+ }
+ }
+ # Try to infer types of other parameters.
+ if($can_infer_input_type)
+ {
+ my %params = map { $_->[0] => $_->[1] } zip(\@input_sym_names, \@input_sym_arg_types);
+ ($arg_types, undef, $aux_types) = $out_params->infer_type(%params);
+ if(not defined $arg_types or @$arg_types != @$arg_params)
+ {
+ $arg_types = [($default_dtype)x@$arg_params];
+ }
+ if(not defined $aux_types or @$aux_types != @$aux_params)
+ {
+ $aux_types = [($default_dtype)x@$aux_params];
+ }
+ }
+ return ($arg_types, $aux_types);
+}
+
func _common_prefix(@names)
{
if(not @names)
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/NN/Activation.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/NN/Activation.pm
index 092893a924aa..1d6342f4955b 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/NN/Activation.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/NN/Activation.pm
@@ -201,16 +201,9 @@ package AI::MXNet::Gluon::NN::SELU;
use AI::MXNet::Gluon::Mouse;
extends 'AI::MXNet::Gluon::HybridBlock';
-sub BUILD
-{
- my $self = shift;
- $self->scale(1.0507009873554804934193349852946);
- $self->alpha(1.6732632423543772848170429916717);
-}
-
method hybrid_forward(GluonClass $F, GluonInput $x)
{
- return $self->scale * $F->where($x > 0, $x, $self->alpha * ($F->exp($x) - 1));
+ $F->LeakyReLU($x, act_type=>'selu', name=>'fwd');
}
__PACKAGE__->register('AI::MXNet::Gluon::NN');
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Parameter.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Parameter.pm
index 475c2a93647e..89cd0cac6229 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Parameter.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Parameter.pm
@@ -421,7 +421,7 @@ method _reduce()
{
my $all_row_ids = AI::MXNet::NDArray->arange(stop => $self->shape->[0], dtype=>'int64', ctx=>$ctx);
$data = AI::MXNet::NDArray->zeros($self->shape, stype=>'row_sparse', ctx=>$ctx);
- $self->_trainer->_row_sparse_pull($self, $data, $all_row_ids);
+ $self->_trainer->_row_sparse_pull($self, $data, $all_row_ids, 1);
}
return $data;
}
@@ -1047,6 +1047,10 @@ method get(Str $name, %kwargs)
next;
}
}
+ elsif($k eq 'dtype' and ($v//'') eq ($existing//''))
+ {
+ next;
+ }
assert(
(not defined $v or Dumper($v) eq Dumper($param->$k)),
"Cannot retrieve Parameter $name because desired attribute ".
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Trainer.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Trainer.pm
index 1b3b49f3652c..6117777eed8f 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Trainer.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Gluon/Trainer.pm
@@ -279,16 +279,24 @@ method _init_kvstore()
$self->_kv_initialized(1);
}
-method _row_sparse_pull($parameter, $out, $row_id)
+# Internal method to invoke pull operations on KVStore. If $full_idx is set to 1,
+# $kv->pull is preferred instead of $kv->row_sparse_pull.
+
+method _row_sparse_pull($parameter, $out, $row_id, $full_idx=0)
{
# initialize kv and params if not already
$self->_init_kvstore() unless $self->_kv_initialized;
$self->_init_params() if scalar(@{ $self->_params_to_init });
- $self->kvstore->row_sparse_pull(
- $self->_param2idx->{ $parameter->name },
- out => $out,
- row_ids => $row_id
- );
+ my $idx = $self->_param2idx->{ $parameter->name };
+ if($full_idx and not $self->kvstore->type =~ /dist/)
+ {
+ assert($row_id->size == $out->shape->[0]);
+ $self->kvstore->pull($idx, out => $out, priority => -$idx, ignore_sparse => 0);
+ }
+ else
+ {
+ $self->kvstore->row_sparse_pull($idx, out => $out, row_ids => $row_id, priority => -$idx);
+ }
}
=head2 step
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Initializer.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Initializer.pm
index fe8dce32e2d8..0359cc3640d4 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Initializer.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Initializer.pm
@@ -86,7 +86,7 @@ has '_print_func' => (is => 'rw', isa => 'CodeRef', lazy => 1,
mx->init->One Initializes weights to one.
mx->init->Constant Initializes the weights to a given value.
mx->init->Orthogonal Initialize weight as orthogonal matrix.
- mx->init->Xavier Returns an initializer performing “Xavier” initialization for weights.
+ mx->init->Xavier Returns an initializer performing Xavier initialization for weights.
mx->init->MSRAPrelu Initialize the weight according to a MSRA paper.
mx->init->Bilinear Initialize weight for upsampling layers.
mx->init->FusedRNN Initialize parameters for fused rnn layers.
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/KVStore.pm b/perl-package/AI-MXNet/lib/AI/MXNet/KVStore.pm
index bb6631f459a9..15aad76c7b4a 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/KVStore.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/KVStore.pm
@@ -215,13 +215,14 @@ method push(
method pull(
Str|ArrayRef[Str] $key,
AI::MXNet::NDArray|ArrayRef[AI::MXNet::NDArray]|ArrayRef[ArrayRef[AI::MXNet::NDArray]] :$out,
- Int :$priority=0
+ Int :$priority=0,
+ Bool :$ignore_sparse=1
)
{
my ($keys, $vals) = _key_value($key, $out);
check_call(
- AI::MXNetCAPI::KVStorePullEx(
- $self->handle, scalar(@{ $keys }), $keys, $vals, $priority
+ AI::MXNetCAPI::KVStorePullWithSparseEx(
+ $self->handle, scalar(@{ $keys }), $keys, $vals, $priority, $ignore_sparse
)
);
}
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Metric.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Metric.pm
index b6e91aeaf729..0941316960a9 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Metric.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Metric.pm
@@ -240,7 +240,7 @@ method get()
The accuracy score is defined as
- accuracy(y, y^) = (1/n) * sum(i=0..n−1) { y^(i)==y(i) }
+ accuracy(y, y^) = (1/n) * sum(i=0..n-1) { y^(i)==y(i) }
Parameters:
axis (Int, default=1) – The axis that represents classes.
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Module/Base.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Module/Base.pm
index b9d5011008d7..542cf498f495 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Module/Base.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Module/Base.pm
@@ -27,6 +27,7 @@ package AI::MXNet::Module::Base;
use Mouse;
use AI::MXNet::Base;
use Time::HiRes qw(time);
+use Storable qw(dclone);
=head1 NAME
@@ -350,7 +351,7 @@ method iter_predict(AI::MXNet::DataIter $eval_data, Maybe[Int] :$num_batch=, Boo
Parameters
----------
- $eval_data : AI::MXNet::DataIter
+ $eval_data : AI::MXNet::DataIter|AcceptableInput (PDL|NDArray)
:$num_batch= : Maybe[Int]
Default is undef, indicating running all the batches in the data iterator.
:$merge_batches=1 : Bool
@@ -363,6 +364,8 @@ method iter_predict(AI::MXNet::DataIter $eval_data, Maybe[Int] :$num_batch=, Boo
Returns
-------
+ If the input is AI::MXNet::NDArray|PDL then the return value is AI::MXNet::NDArray.
+
When $merge_batches is 1 (by default), the return value will be an array ref
[$out1, $out2, $out3] where each element is concatenation of the outputs for
all the mini-batches. If $always_output_list` also is 0 (by default),
@@ -378,13 +381,21 @@ method iter_predict(AI::MXNet::DataIter $eval_data, Maybe[Int] :$num_batch=, Boo
=cut
method predict(
- AI::MXNet::DataIter $eval_data,
+ AI::MXNet::DataIter|AcceptableInput $eval_data,
Maybe[Int] :$num_batch=, Bool :$merge_batches=1, Bool :$reset=1, Bool :$always_output_list=0
)
{
assert($self->binded and $self->params_initialized);
+ if(not blessed $eval_data or not $eval_data->isa('AI::MXNet::DataIter'))
+ {
+ if(not blessed $eval_data or not $eval_data->isa('AI::MXNet::NDArray'))
+ {
+ $eval_data = AI::MXNet::NDArray->array($eval_data);
+ }
+ $self->forward(AI::MXNet::DataBatch->new(data => [$eval_data]));
+ return $self->get_outputs->[0];
+ }
$eval_data->reset() if $reset;
-
my @output_list;
my $nbatch = 0;
while(my $eval_batch = <$eval_data>)
@@ -533,6 +544,7 @@ method fit(
}
$eval_metric = AI::MXNet::Metric->create($eval_metric)
unless blessed $eval_metric;
+ my $epoch_eval_metric = dclone($eval_metric);
################################################################################
# training loop
@@ -541,6 +553,7 @@ method fit(
{
my $tic = time;
$eval_metric->reset;
+ $epoch_eval_metric->reset;
my $nbatch = 0;
my $end_of_batch = 0;
my $next_data_batch = <$train_data>;
@@ -559,10 +572,11 @@ method fit(
{
$end_of_batch = 1;
}
- $self->update_metric($eval_metric, $data_batch->label);
+ $self->update_metric($epoch_eval_metric, $data_batch->label);
$monitor->toc_print if $monitor;
if(defined $batch_end_callback)
{
+ $self->update_metric($eval_metric, $data_batch->label);
my $batch_end_params = AI::MXNet::BatchEndParam->new(
epoch => $epoch,
nbatch => $nbatch,
@@ -576,7 +590,7 @@ method fit(
$nbatch++;
}
# one epoch of training is finished
- my $name_value = $eval_metric->get_name_value;
+ my $name_value = $epoch_eval_metric->get_name_value;
while(my ($name, $val) = each %{ $name_value })
{
$self->logger->info('Epoch[%d] Train-%s=%f', $epoch, $name, $val);
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/NDArray.pm b/perl-package/AI-MXNet/lib/AI/MXNet/NDArray.pm
index 873953192933..3a7b6bab2e2c 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/NDArray.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/NDArray.pm
@@ -27,14 +27,14 @@ package AI::MXNet::NDArray;
AI::MXNet::NDArray - Imperative tensor operations on CPU/GPU
In AI::MXNet, NDArray is the core data structure for all mathematical computations.
An NDArray represents a multidimensional, fixed-size homogenous array.
- If you’re familiar with the PDL, you might notice some similarities.
+ If you're familiar with the PDL, you might notice some similarities.
However, NDArray is row-major, unlike the PDL that is column-major.
Like the PDL, MXNet’s NDArray enables imperative computation.
Some NDArray advandages compared to PDL:
- MXNet’s NDArray supports fast execution on a wide range of hardware configurations, including CPU, GPU, and multi-GPU machines.
+ MXNet's NDArray supports fast execution on a wide range of hardware configurations, including CPU, GPU, and multi-GPU machines.
MXNet also scales to distributed systems in the cloud.
- MXNet’s NDArray executes code lazily, allowing it to automatically parallelize multiple operations across the available hardware.
+ MXNet's NDArray executes code lazily, allowing it to automatically parallelize multiple operations across the available hardware.
An NDArray is a multidimensional array of numbers with the same type.
We could represent the coordinates of a point in 3D space, e.g. [2, 1, 6] as a 1D array with shape (3).
@@ -43,9 +43,9 @@ package AI::MXNet::NDArray;
[[0, 1, 2]
[3, 4, 5]]
- Note that here the use of “dimension” is overloaded. When we say a 2D array, we mean an array with 2 axes, not an array with two components.
+ Note that here the use of 'dimension' is overloaded. When we say a 2D array, we mean an array with 2 axes, not an array with two components.
- Each NDArray supports some important attributes that you’ll often want to query:
+ Each NDArray supports some important attributes that you'll often want to query:
$ndarray->shape: The dimensions of the array.
It is an array ref of integers indicating the length of the array along each axis.
diff --git a/perl-package/AI-MXNet/lib/AI/MXNet/Random.pm b/perl-package/AI-MXNet/lib/AI/MXNet/Random.pm
index 8a47a1210e1a..7a99b1dc7ea9 100644
--- a/perl-package/AI-MXNet/lib/AI/MXNet/Random.pm
+++ b/perl-package/AI-MXNet/lib/AI/MXNet/Random.pm
@@ -90,6 +90,20 @@ sub AUTOLOAD {
);
my @args;
my @tmp = @_;
+ if($sub eq 'randn')
+ {
+ $sub = 'normal';
+ my @shape;
+ while(defined $tmp[0] and $tmp[0] =~ /^\d+$/)
+ {
+ push @shape, shift(@tmp);
+ }
+ if(@shape)
+ {
+ push @tmp, (shape => \@shape);
+ }
+ %defaults = (%defaults, loc => 0, scale => 1);
+ }
if(ref $tmp[-1] eq 'HASH')
{
my @kwargs = %{ pop(@tmp) };
diff --git a/perl-package/AI-MXNet/t/test_gluon_trainer.t b/perl-package/AI-MXNet/t/test_gluon_trainer.t
index 8b3b52b1ce97..81113af28c20 100644
--- a/perl-package/AI-MXNet/t/test_gluon_trainer.t
+++ b/perl-package/AI-MXNet/t/test_gluon_trainer.t
@@ -24,6 +24,7 @@ use AI::MXNet::Gluon::NN qw(nn);
use AI::MXNet::TestUtils qw(almost_equal dies_ok);
use Scalar::Util qw(refaddr);
use AI::MXNet::Base;
+$ENV{MXNET_STORAGE_FALLBACK_LOG_VERBOSE} = 0;
sub test_multi_trainer
{
@@ -127,15 +128,16 @@ sub test_trainer
test_trainer();
-sub test_trainer_save_load
+sub test_trainer_sparse_save_load
{
- my $x = gluon->Parameter('x', shape=>[10], lr_mult=>1.0);
- $x->initialize(ctx=>[mx->cpu(0), mx->cpu(1)], init=>'zeros');
+ my $x = gluon->Parameter('x', shape=>[10, 1], lr_mult=>1.0, stype=>'row_sparse');
+ $x->initialize(ctx=>[mx->cpu(0)], init=>'zeros');
my $trainer = gluon->Trainer([$x], 'sgd', {learning_rate => 0.1});
+ my $all_rows = mx->nd->arange(start => 0, stop => 10, ctx => mx->cpu(0));
mx->autograd->record(sub {
- for my $w (@{ $x->list_data })
+ for my $w (@{ $x->list_row_sparse_data($all_rows) })
{
- my $y = $w + 1;
+ my $y = $w * 1;
$y->backward();
}
});
@@ -148,7 +150,7 @@ sub test_trainer_save_load
ok($trainer->kvstore->_updater->optimizer->_get_lr(0) == 0.2);
}
-test_trainer_save_load();
+test_trainer_sparse_save_load();
sub test_trainer_multi_layer_init
{
diff --git a/perl-package/AI-MXNet/t/test_module.t b/perl-package/AI-MXNet/t/test_module.t
index c29c459bd1bb..3bbd8fdc4ea4 100644
--- a/perl-package/AI-MXNet/t/test_module.t
+++ b/perl-package/AI-MXNet/t/test_module.t
@@ -17,7 +17,7 @@
use strict;
use warnings;
-use Test::More tests => 426;
+use Test::More tests => 428;
use AI::MXNet qw(mx);
use AI::MXNet::Base;
use AI::MXNet::TestUtils qw(almost_equal enumerate same_array dies_like rand_ndarray);
@@ -789,6 +789,17 @@ sub test_forward_reshape
}
+sub test_forward_acceptable_input
+{
+ my $data = mx->sym->Variable('data');
+ my $out = $data * 2;
+ my $mod = mx->mod->Module(symbol => $out);
+ $mod->bind(data_shapes => [['data', [1, 10]]]);
+ $mod->init_params();
+ is_deeply($mod->predict(mx->nd->ones([1, 10]))->shape, [1, 10]);
+ is_deeply($mod->predict(mx->nd->ones([1, 10])->aspdl)->shape, [1, 10]);
+}
+
test_module_input_grads();
test_module_dtype();
test_monitor();
@@ -802,3 +813,4 @@ test_module_set_params();
test_forward_reshape();
test_module_initializer();
test_factorization_machine_module();
+test_forward_acceptable_input();
diff --git a/perl-package/AI-MXNet/t/test_random.t b/perl-package/AI-MXNet/t/test_random.t
index 542f79c5e71d..f049679cbdbd 100644
--- a/perl-package/AI-MXNet/t/test_random.t
+++ b/perl-package/AI-MXNet/t/test_random.t
@@ -17,7 +17,7 @@
use strict;
use warnings;
-use Test::More tests => 506;
+use Test::More tests => 515;
use AI::MXNet qw(mx);
use AI::MXNet::TestUtils qw(same enumerate);
@@ -37,6 +37,15 @@ sub check_with_device
[std => sub { my ($x, $params) = @_; ($x->astype('float64')->aspdl->stats)[6] - $params->{scale} }, $tol]
]
},
+ {
+ name => 'randn',
+ ndop => sub { mx->nd->random->randn(@_) },
+ params => { loc => 10.0, scale => 0.5 },
+ checks => [
+ [mean => sub { my ($x, $params) = @_; $x->astype('float64')->aspdl->avg - $params->{loc} }, $tol],
+ [std => sub { my ($x, $params) = @_; ($x->astype('float64')->aspdl->stats)[6] - $params->{scale} }, $tol]
+ ]
+ },
{
name => 'uniform',
symbol => sub { mx->sym->random->uniform(@_) },
@@ -126,6 +135,7 @@ sub check_with_device
}
# check multi-distribution sampling, only supports cpu for now
+ next unless $symbdic->{inputs};
%params = (shape=>$shape, dtype=>$dtype, ctx=>$device);
%params = (%params, map { $_->[0] => mx->nd->array($_->[1], ctx=>$device, dtype=>$dtype) } @{ $symbdic->{inputs} });
mx->random->seed(128);
@@ -149,6 +159,7 @@ sub check_with_device
# check symbolic
my $symbol = $symbdic->{symbol};
+ next if not $symbol;
my $X = mx->sym->Variable("X");
%params = %{ $symbdic->{params} };
%params = (%params, shape=>$shape, dtype=>$dtype);
diff --git a/perl-package/AI-MXNetCAPI/Changes b/perl-package/AI-MXNetCAPI/Changes
index 938b8e268f1d..08ad085abce9 100644
--- a/perl-package/AI-MXNetCAPI/Changes
+++ b/perl-package/AI-MXNetCAPI/Changes
@@ -1,5 +1,9 @@
Revision history for Perl extension AI::MXNetCAPI
+1.33 Thu Oct 4 13:25:56 PDT 2018
+ - Gluon: Better sparse support for KVStore.
+ - Gpu memory info via mxnet api call.
+
1.32 Sun Aug 5 14:25:31 PDT 2018
- Bugfixes.
diff --git a/perl-package/AI-MXNetCAPI/META.json b/perl-package/AI-MXNetCAPI/META.json
index 854023559c62..1849e6b3bc18 100644
--- a/perl-package/AI-MXNetCAPI/META.json
+++ b/perl-package/AI-MXNetCAPI/META.json
@@ -37,5 +37,5 @@
}
},
"release_status" : "stable",
- "version" : "1.32"
+ "version" : "1.33"
}
diff --git a/perl-package/AI-MXNetCAPI/META.yml b/perl-package/AI-MXNetCAPI/META.yml
index 1db34c501d8c..eb5d9aae8018 100644
--- a/perl-package/AI-MXNetCAPI/META.yml
+++ b/perl-package/AI-MXNetCAPI/META.yml
@@ -19,4 +19,4 @@ no_index:
- inc
requires:
Test::More: '0'
-version: '1.32'
+version: '1.33'
diff --git a/perl-package/AI-MXNetCAPI/README b/perl-package/AI-MXNetCAPI/README
index f5881ff2db07..67b77ccd1614 100644
--- a/perl-package/AI-MXNetCAPI/README
+++ b/perl-package/AI-MXNetCAPI/README
@@ -1,4 +1,4 @@
-AI-MXNetCAPI version 1.32
+AI-MXNetCAPI version 1.33
=====================
Swig interface to MXNet c api.
diff --git a/perl-package/AI-MXNetCAPI/lib/AI/MXNetCAPI.pm b/perl-package/AI-MXNetCAPI/lib/AI/MXNetCAPI.pm
index e371219b0ae6..bc7676047d76 100644
--- a/perl-package/AI-MXNetCAPI/lib/AI/MXNetCAPI.pm
+++ b/perl-package/AI-MXNetCAPI/lib/AI/MXNetCAPI.pm
@@ -18,7 +18,7 @@
package AI::MXNetCAPI;
use base qw(DynaLoader);
bootstrap AI::MXNetCAPI;
-our $VERSION = '1.32';
+our $VERSION = '1.33';
1;
__END__
diff --git a/perl-package/AI-MXNetCAPI/mxnet.i b/perl-package/AI-MXNetCAPI/mxnet.i
index 2540e1bc63bb..b1907f5cd7ec 100644
--- a/perl-package/AI-MXNetCAPI/mxnet.i
+++ b/perl-package/AI-MXNetCAPI/mxnet.i
@@ -342,6 +342,25 @@ int MXEngineSetBulkSize(int bulk_size, int* out);
*/
int MXGetGPUCount(int* out);
+/*!
+ * \brief get the free and total available memory on a GPU
+ * Note: deprecated, use MXGetGPUMemoryInformation64().
+ * \param dev the GPU number to query
+ * \param free_mem pointer to the integer holding free GPU memory
+ * \param total_mem pointer to the integer holding total GPU memory
+ * \return 0 when success, -1 when failure happens
+ */
+int MXGetGPUMemoryInformation(int dev, int *out, int *out);
+
+/*!
+ * \brief get the free and total available memory on a GPU
+ * \param dev the GPU number to query
+ * \param free_mem pointer to the uint64_t holding free GPU memory
+ * \param total_mem pointer to the uint64_t holding total GPU memory
+ * \return 0 when success, -1 when failure happens
+ */
+int MXGetGPUMemoryInformation64(int dev, uint64_t *out, uint64_t *out);
+
//-------------------------------------
// Part 1: NDArray creation and deletion
@@ -1816,6 +1835,24 @@ int MXKVStorePullRowSparseEx(KVStoreHandle handle,
NDArrayHandle* in,
NDArrayHandle* in,
int priority);
+
+/*!
+ * \brief pull a list of (key, value) pairs from the kvstore, where each key is a string
+ * \param handle handle to the kvstore
+ * \param num the number of key-value pairs
+ * \param keys the list of keys
+ * \param vals the list of values
+ * \param priority the priority of the action
+ * \param ignore_sparse whether to ignore sparse arrays in the request
+ * \return 0 when success, -1 when failure happens
+ */
+int MXKVStorePullWithSparseEx(KVStoreHandle handle,
+ mx_uint num,
+ const char** in,
+ NDArrayHandle* in,
+ int priority,
+ bool ignore_sparse);
+
/*!
* \brief user-defined updater for the kvstore
* It's this updater's responsibility to delete \a recv and \a local
diff --git a/python/mxnet/autograd.py b/python/mxnet/autograd.py
index b3acee27caab..afc67e2569bd 100644
--- a/python/mxnet/autograd.py
+++ b/python/mxnet/autograd.py
@@ -273,8 +273,10 @@ def grad(heads, variables, head_grads=None, retain_graph=None, create_graph=Fals
returned as new NDArrays instead of stored into `variable.grad`.
Supports recording gradient graph for computing higher order gradients.
- .. Note: Currently only a very limited set of operators support higher order
- gradients.
+ .. note::
+
+ Currently only a very limited set of operators support higher order \
+ gradients.
Parameters
----------
diff --git a/python/mxnet/base.py b/python/mxnet/base.py
index 84b9e5831c69..feb4d70b6533 100644
--- a/python/mxnet/base.py
+++ b/python/mxnet/base.py
@@ -165,8 +165,7 @@ def setter(self, func):
class _MXClassPropertyMetaClass(type):
def __setattr__(cls, key, value):
- if key in cls.__dict__:
- obj = cls.__dict__.get(key)
+ obj = cls.__dict__.get(key)
if obj and isinstance(obj, _MXClassPropertyDescriptor):
return obj.__set__(cls, value)
@@ -561,7 +560,7 @@ def _as_list(obj):
return [obj]
-_OP_NAME_PREFIX_LIST = ['_contrib_', '_linalg_', '_sparse_', '_image_']
+_OP_NAME_PREFIX_LIST = ['_contrib_', '_linalg_', '_sparse_', '_image_', '_random_']
def _get_op_name_prefix(op_name):
@@ -617,9 +616,13 @@ def _init_op_module(root_namespace, module_name, make_op_func):
op_name_prefix = _get_op_name_prefix(name)
module_name_local = module_name
if len(op_name_prefix) > 0:
- func_name = name[len(op_name_prefix):]
- cur_module = submodule_dict[op_name_prefix]
- module_name_local = "%s.%s.%s" % (root_namespace, module_name, op_name_prefix[1:-1])
+ if op_name_prefix != '_random_' or name.endswith('_like'):
+ func_name = name[len(op_name_prefix):]
+ cur_module = submodule_dict[op_name_prefix]
+ module_name_local = "%s.%s.%s" % (root_namespace, module_name, op_name_prefix[1:-1])
+ else:
+ func_name = name
+ cur_module = module_internal
elif name.startswith('_'):
func_name = name
cur_module = module_internal
diff --git a/python/mxnet/context.py b/python/mxnet/context.py
index 61b70532dd74..15ea9905de03 100644
--- a/python/mxnet/context.py
+++ b/python/mxnet/context.py
@@ -258,6 +258,30 @@ def num_gpus():
check_call(_LIB.MXGetGPUCount(ctypes.byref(count)))
return count.value
+def gpu_memory_info(device_id=0):
+ """Query CUDA for the free and total bytes of GPU global memory.
+
+ Parameters
+ ----------
+ device_id : int, optional
+ The device id of the GPU device.
+
+ Raises
+ ------
+ Will raise an exception on any CUDA error.
+
+ Returns
+ -------
+ (free, total) : (int, int)
+ The number of GPUs.
+
+ """
+ free = ctypes.c_uint64()
+ total = ctypes.c_uint64()
+ dev_id = ctypes.c_int(device_id)
+ check_call(_LIB.MXGetGPUMemoryInformation64(dev_id, ctypes.byref(free), ctypes.byref(total)))
+ return (free.value, total.value)
+
def current_context():
"""Returns the current context.
diff --git a/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py b/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py
index 3ffac96a14e1..7e84cea5ead5 100644
--- a/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py
+++ b/python/mxnet/contrib/onnx/mx2onnx/_op_translations.py
@@ -60,21 +60,16 @@
import logging
import numpy as np
from .export_onnx import MXNetGraph as mx_op
-
-def import_onnx_modules():
- """ To make sure ONNX is runtime dependency, it is imported used only when needed"""
- try:
- from onnx import helper, numpy_helper, mapping
- except ImportError:
- raise ImportError("Onnx and protobuf need to be installed. "
- + "Instructions to install - https://github.com/onnx/onnx")
- return helper, numpy_helper, mapping
+try:
+ import onnx
+except ImportError:
+ onnx = None
def parse_helper(attrs, attrs_name, alt_value=None):
"""Helper function to parse operator attributes in required format."""
tuple_re = re.compile('\([0-9L|,| ]+\)')
- if attrs is None:
+ if not attrs:
return alt_value
attrs_str = None if attrs.get(attrs_name) is None else str(attrs.get(attrs_name))
if attrs_str is None:
@@ -127,25 +122,59 @@ def convert_string_to_list(string_val):
return result_list
+def get_boolean_attribute_value(attrs, attr_name):
+ """ Helper function to convert a string version
+ of Boolean attributes to integer for ONNX.
+ Takes attribute dictionary and attr_name as
+ parameters.
+ """
+ return 1 if attrs.get(attr_name, 0) in ["True", "1"] else 0
+
+def get_inputs(node, kwargs):
+ """Helper function to get inputs"""
+ name = node["name"]
+ proc_nodes = kwargs["proc_nodes"]
+ index_lookup = kwargs["index_lookup"]
+ inputs = node["inputs"]
+ attrs = node.get("attrs", {})
+
+ input_nodes = []
+ for ip in inputs:
+ input_node_id = index_lookup[ip[0]]
+ input_nodes.append(proc_nodes[input_node_id].name)
+
+ return name, input_nodes, attrs
+
+def create_basic_op_node(op_name, node, kwargs):
+ """Helper function to create a basic operator
+ node that doesn't contain op specific attrs"""
+ name, input_nodes, _ = get_inputs(node, kwargs)
+
+ node = onnx.helper.make_node(
+ op_name,
+ input_nodes,
+ [name],
+ name=name
+ )
+ return [node]
+
@mx_op.register("null")
def convert_weights_and_inputs(node, **kwargs):
"""Helper function to convert weights and inputs.
"""
-
- helper, _, mapping = import_onnx_modules()
- name = node["name"]
+ name, _, _ = get_inputs(node, kwargs)
if kwargs["is_input"] is False:
weights = kwargs["weights"]
initializer = kwargs["initializer"]
np_arr = weights[name]
- data_type = mapping.NP_TYPE_TO_TENSOR_TYPE[np_arr.dtype]
+ data_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[np_arr.dtype]
dims = np.shape(np_arr)
- tensor_node = helper.make_tensor_value_info(name, data_type, dims)
+ tensor_node = onnx.helper.make_tensor_value_info(name, data_type, dims)
initializer.append(
- helper.make_tensor(
+ onnx.helper.make_tensor(
name=name,
data_type=data_type,
dims=dims,
@@ -156,7 +185,7 @@ def convert_weights_and_inputs(node, **kwargs):
return [tensor_node]
else:
- tval_node = helper.make_tensor_value_info(name, kwargs["in_type"], kwargs["in_shape"])
+ tval_node = onnx.helper.make_tensor_value_info(name, kwargs["in_type"], kwargs["in_shape"])
return [tval_node]
@@ -165,20 +194,7 @@ def convert_convolution(node, **kwargs):
"""Map MXNet's convolution operator attributes to onnx's Conv operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
-
- num_inputs = len(inputs)
-
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[kwargs["index_lookup"][inputs[0][0]]].name
- weights_node = proc_nodes[kwargs["index_lookup"][inputs[1][0]]].name
-
- if num_inputs > 2:
- bias_node = proc_nodes[kwargs["index_lookup"][inputs[2][0]]].name
-
- attrs = node.get("attrs")
+ name, input_nodes, attrs = get_inputs(node, kwargs)
kernel_dims = list(parse_helper(attrs, "kernel"))
stride_dims = list(parse_helper(attrs, "stride", [1, 1]))
@@ -188,11 +204,7 @@ def convert_convolution(node, **kwargs):
pad_dims = pad_dims + pad_dims
- input_nodes = [input_node, weights_node]
- if num_inputs > 2:
- input_nodes.append(bias_node)
-
- conv_node = helper.make_node(
+ conv_node = onnx.helper.make_node(
"Conv",
inputs=input_nodes,
outputs=[name],
@@ -212,24 +224,33 @@ def convert_fully_connected(node, **kwargs):
"""Map MXNet's FullyConnected operator attributes to onnx's Gemm operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- weight_node_id = kwargs["index_lookup"][inputs[1][0]]
- bias_node_id = kwargs["index_lookup"][inputs[2][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_id]
- weights_node = proc_nodes[weight_node_id]
- bias_node = proc_nodes[bias_node_id]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
+ initializer = kwargs["initializer"]
- input_name = input_node.name
- weights_name = weights_node.name
- bias_name = bias_node.name
+ no_bias = get_boolean_attribute_value(attrs, "no_bias")
- node = helper.make_node(
+ fcnode = []
+
+ if no_bias:
+ data_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[np.dtype('int64')]
+ bias_name = "bias" + str(kwargs["idx"])
+ tensor_node = onnx.helper.make_tensor_value_info(bias_name, data_type, (1,))
+ initializer.append(
+ onnx.helper.make_tensor(
+ name=bias_name,
+ data_type=data_type,
+ dims=(1,),
+ vals=[0],
+ raw=False,
+ )
+ )
+ input_nodes.append(bias_name)
+ fcnode.append(tensor_node)
+
+ node = onnx.helper.make_node(
"Gemm",
- [input_name, weights_name, bias_name], # input (A, B, C) - C can be in place
+ input_nodes, # input (A, B, C) - C can be in place
[name], # output
alpha=1.0,
beta=1.0,
@@ -238,7 +259,9 @@ def convert_fully_connected(node, **kwargs):
name=name
)
- return [node]
+ fcnode.append(node)
+
+ return fcnode
@mx_op.register("BatchNorm")
@@ -246,37 +269,14 @@ def convert_batchnorm(node, **kwargs):
"""Map MXNet's BatchNorm operator attributes to onnx's BatchNormalization operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- attrs = node["attrs"]
- momentum = float(node.get("attrs", {}).get("momentum", 0.9))
+ momentum = float(attrs.get("momentum", 0.9))
eps = float(attrs.get("eps", 0.001))
- data_idx = kwargs["index_lookup"][inputs[0][0]]
- gamma_idx = kwargs["index_lookup"][inputs[1][0]]
- beta_idx = kwargs["index_lookup"][inputs[2][0]]
- moving_mean_idx = kwargs["index_lookup"][inputs[3][0]]
- moving_var_idx = kwargs["index_lookup"][inputs[4][0]]
-
- data_node = proc_nodes[data_idx].name
- gamma_node = proc_nodes[gamma_idx].name
- beta_node = proc_nodes[beta_idx].name
-
- mov_mean_node = proc_nodes[moving_mean_idx]
- mov_mean_node = mov_mean_node.name
- mov_var_node = proc_nodes[moving_var_idx].name
-
- bn_node = helper.make_node(
+ bn_node = onnx.helper.make_node(
"BatchNormalization",
- [data_node,
- gamma_node, # scale
- beta_node, # bias
- mov_mean_node,
- mov_var_node
- ],
+ input_nodes,
[name],
name=name,
epsilon=eps,
@@ -293,140 +293,49 @@ def convert_tanh(node, **kwargs):
"""Map MXNet's tanh operator attributes to onnx's Tanh operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Tanh',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Tanh', node, kwargs)
@mx_op.register("cos")
def convert_cos(node, **kwargs):
"""Map MXNet's cos operator attributes to onnx's Cos operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Cos',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Cos', node, kwargs)
@mx_op.register("sin")
def convert_sin(node, **kwargs):
"""Map MXNet's sin operator attributes to onnx's Sin operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Sin',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Sin', node, kwargs)
@mx_op.register("tan")
def convert_tan(node, **kwargs):
"""Map MXNet's tan operator attributes to onnx's tan operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Tan',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Tan', node, kwargs)
@mx_op.register("arccos")
def convert_acos(node, **kwargs):
"""Map MXNet's acos operator attributes to onnx's acos operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Acos',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Acos', node, kwargs)
@mx_op.register("arcsin")
def convert_asin(node, **kwargs):
"""Map MXNet's asin operator attributes to onnx's asin operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Asin',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Asin', node, kwargs)
@mx_op.register("arctan")
def convert_atan(node, **kwargs):
"""Map MXNet's atan operator attributes to onnx's atan operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Atan',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Atan', node, kwargs)
#Basic neural network functions
@mx_op.register("sigmoid")
@@ -434,58 +343,24 @@ def convert_sigmoid(node, **kwargs):
"""Map MXNet's sigmoid operator attributes to onnx's Sigmoid operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Sigmoid',
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Sigmoid', node, kwargs)
@mx_op.register("relu")
def convert_relu(node, **kwargs):
"""Map MXNet's relu operator attributes to onnx's Relu operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_node_idx].name
-
- node = helper.make_node(
- 'Relu',
- [input_node],
- [name],
- name=name
- )
-
- return [node]
+ return create_basic_op_node('Relu', node, kwargs)
@mx_op.register("Activation")
def convert_activation(node, **kwargs):
"""Map MXNet's Activation operator attributes to onnx's Tanh/Relu operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- proc_nodes = kwargs["proc_nodes"]
- attrs = node["attrs"]
act_type = attrs["act_type"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_idx].output[0]
-
# Creating a dictionary here, but if this titlecase pattern
# mxnet_name.title()
act_types = {
@@ -498,9 +373,9 @@ def convert_activation(node, **kwargs):
act_name = act_types.get(act_type)
if act_name:
- node = helper.make_node(
+ node = onnx.helper.make_node(
act_name,
- [input_node],
+ input_nodes,
[name],
name=name
)
@@ -517,13 +392,7 @@ def convert_pad(node, **kwargs):
"""Map MXNet's pad operator attributes to onnx's Pad operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- attrs = node["attrs"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_idx].name
+ name, input_nodes, attrs = get_inputs(node, kwargs)
mxnet_pad_width = convert_string_to_list(attrs.get("pad_width"))
onnx_pad_width = transform_padding(mxnet_pad_width)
@@ -533,9 +402,9 @@ def convert_pad(node, **kwargs):
if pad_mode == "constant":
pad_value = float(attrs.get("constant_value")) \
if "constant_value" in attrs else 0.0
- node = helper.make_node(
+ node = onnx.helper.make_node(
'Pad',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
mode='constant',
value=pad_value,
@@ -543,9 +412,9 @@ def convert_pad(node, **kwargs):
name=name
)
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'Pad',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
mode=pad_mode,
pads=onnx_pad_width,
@@ -557,10 +426,8 @@ def convert_pad(node, **kwargs):
def create_helper_trans_node(op_name, input_node, node_name):
"""create extra transpose node for dot operator"""
- helper, _, _ = import_onnx_modules()
-
node_name = op_name + "_" + node_name
- trans_node = helper.make_node(
+ trans_node = onnx.helper.make_node(
'Transpose',
inputs=[input_node],
outputs=[node_name],
@@ -574,37 +441,24 @@ def convert_dot(node, **kwargs):
"""Map MXNet's dot operator attributes to onnx's
MatMul and Transpose operators based on the values set for
transpose_a, transpose_b attributes."""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- node_inputs = node["inputs"]
- name = node["name"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_a_idx = kwargs["index_lookup"][node_inputs[0][0]]
- input_node_a = proc_nodes[input_a_idx].name
- input_b_idx = kwargs["index_lookup"][node_inputs[1][0]]
- input_node_b = proc_nodes[input_b_idx].name
-
- attrs = node.get('attrs', {})
trans_a_node = None
trans_b_node = None
- trans_a = 1 if ("transpose_a" in attrs) and \
- attrs.get("transpose_a") in ["True", "1"] else 0
- trans_b = 1 if ("transpose_b" in attrs) and \
- attrs.get("transpose_b") in ["True", "1"] else 0
+ trans_a = get_boolean_attribute_value(attrs, "transpose_a")
+ trans_b = get_boolean_attribute_value(attrs, "transpose_b")
op_name = "transpose" + str(kwargs["idx"])
- create_helper_trans_node(op_name, input_node_a, 'a')
- create_helper_trans_node(op_name, input_node_b, 'b')
if trans_a:
- trans_a_node = create_helper_trans_node(op_name, input_node_a, 'a')
+ trans_a_node = create_helper_trans_node(op_name, input_nodes[0], 'a')
input_node_a = op_name+"_a"
if trans_b:
- trans_b_node = create_helper_trans_node(op_name, input_node_b, 'b')
+ trans_b_node = create_helper_trans_node(op_name, input_nodes[1], 'b')
input_node_b = op_name+"_b"
- matmul_node = helper.make_node(
+ matmul_node = onnx.helper.make_node(
'MatMul',
inputs=[input_node_a, input_node_b],
outputs=[name],
@@ -628,33 +482,19 @@ def convert_linalg_gemm2(node, **kwargs):
transpose_a, transpose_b attributes.
Return multiple nodes created.
"""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- node_inputs = node["inputs"]
- name = node["name"]
-
- input_a_idx = kwargs["index_lookup"][node_inputs[0][0]]
- input_node_a = proc_nodes[input_a_idx].name
- input_b_idx = kwargs["index_lookup"][node_inputs[1][0]]
- input_node_b = proc_nodes[input_b_idx].name
+ name, input_nodes, attrs = get_inputs(node, kwargs)
# Getting the attributes and assigning default values.
- if "attrs" in node:
- attrs = node["attrs"]
- alpha = float(attrs["alpha"])
- trans_a = int(attrs["transpose_a"])
- trans_b = int(attrs["transpose_b"])
- else:
- alpha = 1.0
- trans_a = 0
- trans_b = 0
+ alpha = float(attrs.get("alpha", 1.0))
+ trans_a = get_boolean_attribute_value(attrs, "transpose_a")
+ trans_b = get_boolean_attribute_value(attrs, "transpose_b")
op_name = "transpose" + str(kwargs["idx"])
if alpha == 1.0 and trans_a == 0 and trans_b == 0:
- matmul_node = helper.make_node(
+ matmul_node = onnx.helper.make_node(
'MatMul',
- inputs=[input_node_a, input_node_b],
+ inputs=input_nodes,
outputs=[name],
name=name
)
@@ -662,16 +502,16 @@ def convert_linalg_gemm2(node, **kwargs):
elif trans_a == 1 and trans_b == 0:
op_name = "transpose" + str(kwargs["idx"])
node_name = op_name+"_a"
- trans_a_node = helper.make_node(
+ trans_a_node = onnx.helper.make_node(
'Transpose',
- inputs=[input_node_a],
+ inputs=[input_nodes[0]],
outputs=[op_name+"_a"],
name=node_name
)
- matmul_node = helper.make_node(
+ matmul_node = onnx.helper.make_node(
'MatMul',
- inputs=[node_name, input_node_b],
+ inputs=[node_name, input_nodes[1]],
outputs=[name],
name=name
)
@@ -679,16 +519,16 @@ def convert_linalg_gemm2(node, **kwargs):
elif trans_a == 0 and trans_b == 1:
node_name = op_name + "_b"
- trans_b_node = helper.make_node(
+ trans_b_node = onnx.helper.make_node(
'Transpose',
- inputs=[input_node_b],
+ inputs=[input_nodes[1]],
outputs=[op_name+"_b"],
name=node_name
)
- matmul_node = helper.make_node(
+ matmul_node = onnx.helper.make_node(
'MatMul',
- inputs=[input_node_a, node_name],
+ inputs=[input_nodes[0], node_name],
outputs=[name],
name=name
)
@@ -696,24 +536,24 @@ def convert_linalg_gemm2(node, **kwargs):
return [trans_b_node, matmul_node]
else:
node_name_a = op_name+"_a"
- trans_a_node = helper.make_node(
+ trans_a_node = onnx.helper.make_node(
'Transpose',
- inputs=[input_node_a],
+ inputs=[input_nodes[0]],
outputs=[op_name+"_a"],
name=node_name_a
)
node_name_b = op_name + "_b"
- trans_b_node = helper.make_node(
+ trans_b_node = onnx.helper.make_node(
'Transpose',
- inputs=[input_node_b],
+ inputs=[input_nodes[1]],
outputs=[op_name+"_b"],
name=node_name_b
)
- matmul_node = helper.make_node(
+ matmul_node = onnx.helper.make_node(
'MatMul',
- inputs=[node_name_a, node_name_b],
+ inputs=input_nodes,
outputs=[name],
name=name
)
@@ -727,18 +567,12 @@ def convert_pooling(node, **kwargs):
MaxPool/AveragePool/GlobalMaxPool/GlobalAveragePool operators
based on the input node's attributes and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
kernel = eval(attrs["kernel"])
pool_type = attrs["pool_type"]
stride = eval(attrs["stride"]) if attrs.get("stride") else None
- global_pool = True if "global_pool" in attrs and\
- attrs.get("global_pool") == "True" else False
- node_inputs = node["inputs"]
- input_node_idx = kwargs["index_lookup"][node_inputs[0][0]]
- input_node = proc_nodes[input_node_idx]
- name = node["name"]
+ global_pool = get_boolean_attribute_value(attrs, "global_pool")
pooling_convention = attrs.get('pooling_convention', 'valid')
@@ -755,16 +589,16 @@ def convert_pooling(node, **kwargs):
global_pool_types = {"max": "GlobalMaxPool", "avg": "GlobalAveragePool"}
if global_pool:
- node = helper.make_node(
+ node = onnx.helper.make_node(
global_pool_types[pool_type],
- [input_node.name], # input
+ input_nodes, # input
[name],
name=name
)
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
pool_types[pool_type],
- [input_node.name], # input
+ input_nodes, # input
[name],
kernel_shape=kernel,
pads=pad_dims,
@@ -780,43 +614,14 @@ def convert_exp(node, **kwargs):
"""Map MXNet's exp operator attributes to onnx's Exp operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
-
- node = helper.make_node(
- "Exp",
- [input_node],
- [name],
- name=name,
- )
- return [node]
-
+ return create_basic_op_node('Exp', node, kwargs)
@mx_op.register("_copy")
def convert_identity(node, **kwargs):
"""Map MXNet's _copy operator attributes to onnx's Identity operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
-
- node = helper.make_node(
- "Identity",
- [input_node],
- [name],
- name=name,
- )
- return [node]
+ return create_basic_op_node('Identity', node, kwargs)
@mx_op.register("LeakyReLU")
@@ -824,32 +629,24 @@ def convert_leakyrelu(node, **kwargs):
"""Map MXNet's LeakyReLU operator attributes to onnx's Elu/LeakyRelu/PRelu operators
based on the input node's attributes and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
act_type = attrs.get("act_type", "leaky")
alpha = float(attrs.get("slope", 0.25))
- act_name = {"elu": "Elu", "leaky": "LeakyRelu", "prelu": "PRelu"}
+ act_name = {"elu": "Elu", "leaky": "LeakyRelu", "prelu": "PRelu",
+ "selu": "Selu"}
- if act_type == "prelu":
- alpha_node_index = kwargs["index_lookup"][inputs[1][0]]
- alpha_node_name = proc_nodes[alpha_node_index].name
-
- node = helper.make_node(
+ if act_type == "prelu" or act_type == "selu":
+ node = onnx.helper.make_node(
act_name[act_type],
- inputs=[input_node, alpha_node_name],
+ inputs=input_nodes,
outputs=[name],
name=name)
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
act_name[act_type],
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
name=name,
alpha=alpha)
@@ -862,18 +659,13 @@ def convert_softmax(node, **kwargs):
"""Map MXNet's softmax operator attributes to onnx's Softmax operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- inputs = node["inputs"]
- input_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_idx]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- name = node["name"]
- axis = int(node.get("attrs", {}).get("axis", -1))
+ axis = int(attrs.get("axis", -1))
- softmax_node = helper.make_node(
+ softmax_node = onnx.helper.make_node(
"Softmax",
- [input_node.name],
+ input_nodes,
[name],
axis=axis,
name=name
@@ -889,14 +681,12 @@ def convert_softmax_output(node, **kwargs):
"""Map MXNet's SoftmaxOutput operator attributes to onnx's Softmax operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- inputs = node["inputs"]
- input1_idx = kwargs["index_lookup"][inputs[0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input1 = proc_nodes[input1_idx]
name = node["name"]
- softmax_node = helper.make_node(
+ input1_idx = kwargs["index_lookup"][node["inputs"][0][0]]
+ input1 = kwargs["proc_nodes"][input1_idx]
+
+ softmax_node = onnx.helper.make_node(
"Softmax",
[input1.output[0]],
[name],
@@ -912,15 +702,12 @@ def convert_concat(node, **kwargs):
"""Map MXNet's Concat operator attributes to onnx's Concat operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- inputs = node["inputs"]
- proc_nodes = kwargs["proc_nodes"]
- input_names = [proc_nodes[kwargs["index_lookup"][i[0]]].name for i in inputs]
- axis = int(node.get("attrs", {}).get("dim", 1))
- concat_node = helper.make_node(
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
+ axis = int(attrs.get("dim", 1))
+ concat_node = onnx.helper.make_node(
"Concat",
- input_names,
+ input_nodes,
[name],
axis=axis,
name=name
@@ -933,26 +720,23 @@ def convert_transpose(node, **kwargs):
"""Map MXNet's transpose operator attributes to onnx's Transpose operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- input_idx = kwargs["index_lookup"][node["inputs"][0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_idx].name
- axes = node.get("attrs", {}).get("axes", ())
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
+ axes = attrs.get("axes", ())
if axes:
axes = tuple(map(int, re.findall(r'\d+', axes)))
- transpose_node = helper.make_node(
+ transpose_node = onnx.helper.make_node(
"Transpose",
- [input_node],
+ input_nodes,
[name],
perm=axes,
name=name
)
else:
- transpose_node = helper.make_node(
+ transpose_node = onnx.helper.make_node(
"Transpose",
- [input_node],
+ input_nodes,
[name],
name=name
)
@@ -965,21 +749,16 @@ def convert_lrn(node, **kwargs):
"""Map MXNet's LRN operator attributes to onnx's LRN operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- input_idx = kwargs["index_lookup"][node["inputs"][0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_idx].name
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- attrs = node["attrs"]
- alpha = float(attrs["alpha"]) if "alpha" in attrs else 0.0001
- beta = float(attrs["beta"]) if "beta" in attrs else 0.75
- bias = float(attrs["knorm"]) if "knorm" in attrs else 1.0
- size = int(attrs["nsize"])
+ alpha = float(attrs.get("alpha", 0.0001))
+ beta = float(attrs.get("beta", 0.75))
+ bias = float(attrs.get("knorm", 1.0))
+ size = int(attrs.get("nsize"))
- lrn_node = helper.make_node(
+ lrn_node = onnx.helper.make_node(
"LRN",
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
name=name,
alpha=alpha,
@@ -996,19 +775,16 @@ def convert_l2normalization(node, **kwargs):
"""Map MXNet's L2Normalization operator attributes to onnx's LpNormalization operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- input_id = kwargs["index_lookup"][node["inputs"][0][0]]
- input_name = kwargs["proc_nodes"][input_id].name
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
mode = attrs.get("mode", "instance")
if mode != "channel":
raise AttributeError("ONNX currently supports channel mode only")
- l2norm_node = helper.make_node(
+ l2norm_node = onnx.helper.make_node(
"LpNormalization",
- [input_name],
+ input_nodes,
[name],
axis=1, # channel only
name=name
@@ -1021,16 +797,13 @@ def convert_dropout(node, **kwargs):
"""Map MXNet's Dropout operator attributes to onnx's Dropout operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- input_id = kwargs["index_lookup"][node["inputs"][0][0]]
- input_name = kwargs["proc_nodes"][input_id].name
- attrs = node["attrs"]
- probability = float(attrs["p"])
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- dropout_node = helper.make_node(
+ probability = float(attrs.get("p", 0.5))
+
+ dropout_node = onnx.helper.make_node(
"Dropout",
- [input_name],
+ input_nodes,
[name],
ratio=probability,
name=name
@@ -1043,37 +816,21 @@ def convert_flatten(node, **kwargs):
"""Map MXNet's Flatten operator attributes to onnx's Flatten operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- input_idx = kwargs["index_lookup"][node["inputs"][0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_idx].name # .output[0]
-
- flatten_node = helper.make_node(
- "Flatten",
- [input_node],
- [name],
- name=name
- )
- return [flatten_node]
+ return create_basic_op_node('Flatten', node, kwargs)
@mx_op.register("clip")
def convert_clip(node, **kwargs):
"""Map MXNet's Clip operator attributes to onnx's Clip operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- input_idx = kwargs["index_lookup"][node["inputs"][0][0]]
- proc_nodes = kwargs["proc_nodes"]
- input_node = proc_nodes[input_idx].name
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
a_min = np.float(attrs.get('a_min', -np.inf))
a_max = np.float(attrs.get('a_max', np.inf))
- clip_node = helper.make_node(
+ clip_node = onnx.helper.make_node(
"Clip",
- [input_node],
+ input_nodes,
[name],
name=name,
min=a_min,
@@ -1084,68 +841,63 @@ def convert_clip(node, **kwargs):
def scalar_op_helper(node, op_name, **kwargs):
"""Helper function for scalar arithmetic operations"""
- helper, numpy_helper, mapping = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- scalar_value = [float(node.get("attrs", {}).get("scalar", 1))]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_name_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_name_id].name
+ input_type = kwargs["in_type"]
+ scalar_value = np.array([attrs.get("scalar", 1)],
+ dtype=onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[input_type])
initializer = kwargs["initializer"]
flag = True
# If the input value is in initializer, just multiply with scalar input
# and create a new initializer
for i in initializer:
- if i.name == input_node:
+ if i.name == input_nodes[0]:
if op_name == 'Mul':
- new_initializer = numpy_helper.to_array(i) * scalar_value[0]
+ new_initializer = onnx.numpy_helper.to_array(i) * scalar_value[0]
elif op_name == 'Sub':
- new_initializer = numpy_helper.to_array(i) - scalar_value[0]
+ new_initializer = onnx.numpy_helper.to_array(i) - scalar_value[0]
elif op_name == 'Add':
- new_initializer = numpy_helper.to_array(i) + scalar_value[0]
+ new_initializer = onnx.numpy_helper.to_array(i) + scalar_value[0]
elif op_name == 'Div':
- new_initializer = numpy_helper.to_array(i) / scalar_value[0]
+ new_initializer = onnx.numpy_helper.to_array(i) / scalar_value[0]
flag = False
break
# else create a new tensor of the scalar value, add it in initializer
if flag is True:
- np_arr = np.array(scalar_value)
- data_type = mapping.NP_TYPE_TO_TENSOR_TYPE[np_arr.dtype]
- dims = np.shape(np_arr)
+ dims = np.shape(scalar_value)
scalar_op_name = "scalar_op" + str(kwargs["idx"])
- tensor_node = helper.make_tensor_value_info(scalar_op_name, data_type, dims)
+ tensor_node = onnx.helper.make_tensor_value_info(scalar_op_name, input_type, dims)
initializer.append(
- helper.make_tensor(
+ onnx.helper.make_tensor(
name=scalar_op_name,
- data_type=data_type,
+ data_type=input_type,
dims=dims,
vals=scalar_value,
raw=False,
)
)
- mul_node = helper.make_node(
+ mul_node = onnx.helper.make_node(
op_name,
- [input_node, scalar_op_name],
+ [input_nodes[0], scalar_op_name],
[name],
name=name
)
return [tensor_node, mul_node]
else:
- data_type = mapping.NP_TYPE_TO_TENSOR_TYPE[new_initializer.dtype]
+ data_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[new_initializer.dtype]
dims = np.shape(new_initializer)
- new_a_node = input_node + str(kwargs["idx"])
- tensor_node = helper.make_tensor_value_info(new_a_node, data_type, dims)
+ new_a_node = input_nodes[0] + str(kwargs["idx"])
+ tensor_node = onnx.helper.make_tensor_value_info(new_a_node, data_type, dims)
initializer.append(
- helper.make_tensor(
+ onnx.helper.make_tensor(
name=new_a_node,
data_type=data_type,
dims=dims,
@@ -1200,21 +952,14 @@ def convert_argmax(node, **kwargs):
"""Map MXNet's argmax operator attributes to onnx's ArgMax operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- node_inputs = node["inputs"]
-
- input_node_idx = kwargs["index_lookup"][node_inputs[0][0]]
- input_node = proc_nodes[input_node_idx].name
- name = node["name"]
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
axis = int(attrs.get("axis"))
keepdims = int(attrs.get("keepdims")) if "keepdims" in attrs else 1
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ArgMax',
- inputs=[input_node],
+ inputs=input_nodes,
axis=axis,
keepdims=keepdims,
outputs=[name],
@@ -1227,21 +972,14 @@ def convert_argmin(node, **kwargs):
"""Map MXNet's argmin operator attributes to onnx's ArgMin operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- node_inputs = node["inputs"]
-
- input_node_idx = kwargs["index_lookup"][node_inputs[0][0]]
- input_node = proc_nodes[input_node_idx].name
- name = node["name"]
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
axis = int(attrs.get("axis"))
keepdims = int(attrs.get("keepdims")) if "keepdims" in attrs else 1
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ArgMin',
- inputs=[input_node],
+ inputs=input_nodes,
axis=axis,
keepdims=keepdims,
outputs=[name],
@@ -1254,25 +992,7 @@ def convert_maximum(node, **kwargs):
"""Map MXNet's _maximum operator attributes to onnx's Max operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- node_inputs = node["inputs"]
-
- input_node_list = []
- for node_input in node_inputs:
- node_id = kwargs["index_lookup"][node_input[0]]
- input_node_list.append(proc_nodes[node_id].name)
-
- name = node["name"]
-
- node = helper.make_node(
- 'Max',
- inputs=input_node_list,
- outputs=[name],
- name=name,
- )
-
- return [node]
+ return create_basic_op_node('Max', node, kwargs)
@mx_op.register("_minimum")
@@ -1280,49 +1000,24 @@ def convert_minimum(node, **kwargs):
"""Map MXNet's _minimum operator attributes to onnx's Min operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- proc_nodes = kwargs["proc_nodes"]
- node_inputs = node["inputs"]
-
- input_node_list = []
- for node_input in node_inputs:
- node_id = kwargs["index_lookup"][node_input[0]]
- input_node_list.append(proc_nodes[node_id].name)
-
- name = node["name"]
-
- node = helper.make_node(
- 'Min',
- inputs=input_node_list,
- outputs=[name],
- name=name,
- )
-
- return [node]
-
+ return create_basic_op_node('Min', node, kwargs)
@mx_op.register("min")
def convert_min(node, **kwargs):
"""Map MXNet's min operator attributes to onnx's ReduceMin operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- mx_axis = node.get("attrs", {}).get("axis", None)
+ mx_axis = attrs.get("axis", None)
axes = convert_string_to_list(str(mx_axis)) if mx_axis is not None else None
- keepdims = int(node.get("attrs", {}).get("keepdims", 0))
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ keepdims = int(attrs.get("keepdims", 0))
if axes is not None:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceMin',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
axes=axes,
keepdims=keepdims,
@@ -1331,9 +1026,9 @@ def convert_min(node, **kwargs):
return [node]
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceMin',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
keepdims=keepdims,
name=name
@@ -1347,23 +1042,17 @@ def convert_max(node, **kwargs):
"""Map MXNet's max operator attributes to onnx's ReduceMax operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- mx_axis = node.get("attrs", {}).get("axis", None)
+ mx_axis = attrs.get("axis", None)
axes = convert_string_to_list(str(mx_axis)) if mx_axis is not None else None
- keepdims = int(node.get("attrs", {}).get("keepdims", 0))
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ keepdims = int(attrs.get("keepdims", 0))
if axes is not None:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceMax',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
axes=axes,
keepdims=keepdims,
@@ -1372,9 +1061,9 @@ def convert_max(node, **kwargs):
return [node]
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceMax',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
keepdims=keepdims,
name=name
@@ -1388,23 +1077,17 @@ def convert_mean(node, **kwargs):
"""Map MXNet's mean operator attributes to onnx's ReduceMean operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- mx_axis = node.get("attrs", {}).get("axis", None)
+ mx_axis = attrs.get("axis", None)
axes = convert_string_to_list(str(mx_axis)) if mx_axis is not None else None
- keepdims = int(node.get("attrs", {}).get("keepdims", 0))
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ keepdims = int(attrs.get("keepdims", 0))
if axes is not None:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceMean',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
axes=axes,
keepdims=keepdims,
@@ -1413,9 +1096,9 @@ def convert_mean(node, **kwargs):
return [node]
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceMean',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
keepdims=keepdims,
name=name
@@ -1429,23 +1112,17 @@ def convert_prod(node, **kwargs):
"""Map MXNet's prod operator attributes to onnx's ReduceProd operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- mx_axis = node.get("attrs", {}).get("axis", None)
+ mx_axis = attrs.get("axis", None)
axes = convert_string_to_list(str(mx_axis)) if mx_axis is not None else None
- keepdims = int(node.get("attrs", {}).get("keepdims", 0))
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ keepdims = int(attrs.get("keepdims", 0))
if axes is not None:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceProd',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
axes=axes,
keepdims=keepdims,
@@ -1454,9 +1131,9 @@ def convert_prod(node, **kwargs):
return [node]
else:
- node = helper.make_node(
+ node = onnx.helper.make_node(
'ReduceProd',
- inputs=[input_node],
+ inputs=input_nodes,
outputs=[name],
keepdims=keepdims,
name=name
@@ -1471,25 +1148,7 @@ def convert_elementwise_add(node, **kwargs):
"""Map MXNet's elemwise_add operator attributes to onnx's Add operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- add_node = helper.make_node(
- "Add",
- [input_node_a, input_node_b],
- [name],
- name=name,
- )
-
- return [add_node]
+ return create_basic_op_node('Add', node, kwargs)
@mx_op.register("broadcast_add")
@@ -1497,25 +1156,7 @@ def covert_broadcast_add(node, **kwargs):
"""Map MXNet's broadcast_add operator attributes to onnx's Add operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- add_node = helper.make_node(
- "Add",
- [input_node_a, input_node_b],
- [name],
- name=name,
- )
-
- return [add_node]
+ return create_basic_op_node('Add', node, kwargs)
@mx_op.register("elemwise_sub")
@@ -1523,224 +1164,63 @@ def convert_elementwise_sub(node, **kwargs):
"""Map MXNet's elemwise_sub operator attributes to onnx's Sub operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- sub_node = helper.make_node(
- "Sub",
- [input_node_a, input_node_b],
- [name],
- name=name,
- )
-
- return [sub_node]
+ return create_basic_op_node('Sub', node, kwargs)
@mx_op.register("broadcast_sub")
def covert_broadcast_sub(node, **kwargs):
"""Map MXNet's broadcast_sub operator attributes to onnx's Sub operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- sub_node = helper.make_node(
- "Sub",
- [input_node_a, input_node_b],
- [name],
- name=name,
- )
-
- return [sub_node]
-
+ return create_basic_op_node('Sub', node, kwargs)
@mx_op.register("elemwise_mul")
def convert_elemwise_mul(node, **kwargs):
"""Map MXNet's elemwise_mul operator attributes to onnx's Mul operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- mul_node = helper.make_node(
- "Mul",
- [input_node_a, input_node_b],
- [name],
- name=name,
- )
-
- return [mul_node]
+ return create_basic_op_node('Mul', node, kwargs)
@mx_op.register("broadcast_mul")
def convert_broadcast_mul(node, **kwargs):
"""Map MXNet's broadcast_mul operator attributes to onnx's Mul operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- mul_node = helper.make_node(
- "Mul",
- [input_node_a, input_node_b],
- [name],
- name=name
- )
-
- return [mul_node]
-
+ return create_basic_op_node('Mul', node, kwargs)
@mx_op.register("elemwise_div")
def convert_elemwise_div(node, **kwargs):
"""Map MXNet's elemwise_div operator attributes to onnx's Div operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- div_node = helper.make_node(
- "Div",
- [input_node_a, input_node_b],
- [name],
- name=name
- )
-
- return [div_node]
-
+ return create_basic_op_node('Div', node, kwargs)
@mx_op.register("broadcast_div")
def convert_broadcast_div(node, **kwargs):
"""Map MXNet's broadcast_div operator attributes to onnx's Div operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
-
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
-
- div_node = helper.make_node(
- "Div",
- [input_node_a, input_node_b],
- [name],
- name=name
- )
-
- return [div_node]
-
+ return create_basic_op_node('Div', node, kwargs)
@mx_op.register("negative")
def convert_negative(node, **kwargs):
"""Map MXNet's negative operator attributes to onnx's Neg operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
-
- input_node = proc_nodes[input_node_id].name
-
- neg_node = helper.make_node(
- "Neg",
- [input_node],
- [name],
- name=name,
- )
-
- return [neg_node]
-
+ return create_basic_op_node('Neg', node, kwargs)
@mx_op.register("abs")
def convert_abs(node, **kwargs):
"""Map MXNet's abs operator attributes to onnx's Abs operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
-
- input_node = proc_nodes[input_node_id].name
-
- abs_node = helper.make_node(
- "Abs",
- [input_node],
- [name],
- name=name
- )
-
- return [abs_node]
-
+ return create_basic_op_node('Abs', node, kwargs)
@mx_op.register("add_n")
def convert_addn(node, **kwargs):
"""Map MXNet's add_n operator attributes to onnx's Sum operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_list = []
- for input_val in inputs:
- input_list.append(proc_nodes[kwargs["index_lookup"][input_val[0]]].name)
-
- sum_node = helper.make_node(
- "Sum",
- input_list,
- [name],
- name=name
- )
- return [sum_node]
+ return create_basic_op_node('Sum', node, kwargs)
# Rounding
@mx_op.register("ceil")
@@ -1748,42 +1228,14 @@ def convert_ceil(node, **kwargs):
"""Map MXNet's ceil operator attributes to onnx's Ceil operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
-
- node = helper.make_node(
- "Ceil",
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Ceil', node, kwargs)
@mx_op.register("floor")
def convert_floor(node, **kwargs):
"""Map MXNet's floor operator attributes to onnx's Floor operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
-
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
-
- node = helper.make_node(
- "Floor",
- [input_node],
- [name],
- name=name
- )
- return [node]
+ return create_basic_op_node('Floor', node, kwargs)
# Changing shape and type.
@mx_op.register("Reshape")
@@ -1792,24 +1244,20 @@ def convert_reshape(node, **kwargs):
Converts output shape attribute to output shape tensor
and return multiple created nodes.
"""
- helper, _, mapping = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- attrs = node["attrs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
output_shape_list = convert_string_to_list(attrs["shape"])
initializer = kwargs["initializer"]
- output_shape_np = np.array(output_shape_list)
- data_type = mapping.NP_TYPE_TO_TENSOR_TYPE[output_shape_np.dtype]
+ output_shape_np = np.array(output_shape_list, dtype='int64')
+ data_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[output_shape_np.dtype]
dims = np.shape(output_shape_np)
output_shape_name = "reshape_attr_tensor" + str(kwargs["idx"])
- tensor_node = helper.make_tensor_value_info(output_shape_name, data_type, dims)
+ tensor_node = onnx.helper.make_tensor_value_info(output_shape_name, data_type, dims)
initializer.append(
- helper.make_tensor(
+ onnx.helper.make_tensor(
name=output_shape_name,
data_type=data_type,
dims=dims,
@@ -1818,8 +1266,7 @@ def convert_reshape(node, **kwargs):
)
)
- input_node_idx = kwargs["index_lookup"][inputs[0][0]]
- input_node_name = proc_nodes[input_node_idx].name
+ input_nodes.append(output_shape_name)
not_supported_shape = [-2, -3, -4]
@@ -1827,9 +1274,9 @@ def convert_reshape(node, **kwargs):
if val in not_supported_shape:
raise AttributeError("Shape value not supported in ONNX", val)
- reshape_node = helper.make_node(
+ reshape_node = onnx.helper.make_node(
"Reshape",
- [input_node_name, output_shape_name],
+ input_nodes,
[name],
name=name
)
@@ -1841,20 +1288,23 @@ def convert_cast(node, **kwargs):
"""Map MXNet's Cast operator attributes to onnx's Cast operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- dtype = node["attrs"]["dtype"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ dtype = attrs["dtype"]
- node = helper.make_node(
+ # dtype can be mapped only with types from TensorProto
+ # float32 is mapped to float and float64 to double in onnx
+ # following tensorproto mapping https://github.com/onnx/onnx/blob/master/onnx/mapping.py
+ if dtype == 'float32':
+ dtype = 'float'
+ elif dtype == 'float64':
+ dtype = 'double'
+
+ node = onnx.helper.make_node(
"Cast",
- [input_node],
+ input_nodes,
[name],
- to=dtype,
+ to=getattr(onnx.TensorProto, dtype.upper()),
name=name,
)
return [node]
@@ -1865,23 +1315,17 @@ def convert_slice_axis(node, **kwargs):
"""Map MXNet's slice_axis operator attributes to onnx's Slice operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- axes = int(node["attrs"]["axis"])
- starts = int(node["attrs"]["begin"])
- if node["attrs"]["end"] == 'None':
- raise ValueError("Slice: ONNX doesnt't support 'None' in 'end' attribute")
- else:
- ends = int(node["attrs"]["end"])
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ axes = int(attrs.get("axis"))
+ starts = int(attrs.get("begin"))
+ ends = int(attrs.get("end", None))
+ if not ends:
+ raise ValueError("Slice: ONNX doesnt't support 'None' in 'end' attribute")
- node = helper.make_node(
+ node = onnx.helper.make_node(
"Slice",
- [input_node],
+ input_nodes,
[name],
axes=[axes],
starts=[starts],
@@ -1897,30 +1341,25 @@ def convert_slice_channel(node, **kwargs):
operator based on squeeze_axis attribute
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- num_outputs = int(node.get("attrs", {})["num_outputs"])
- axis = int(node.get("attrs", {}).get("axis", 1))
- squeeze_axis = int(node.get("attrs", {}).get("squeeze_axis", 0))
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ num_outputs = int(attrs.get("num_outputs"))
+ axis = int(attrs.get("axis", 1))
+ squeeze_axis = int(attrs.get("squeeze_axis", 0))
if squeeze_axis == 1 and num_outputs == 1:
- node = helper.make_node(
+ node = onnx.helper.make_node(
"Squeeze",
- [input_node],
+ input_nodes,
[name],
axes=[axis],
name=name,
)
return [node]
elif squeeze_axis == 0 and num_outputs > 1:
- node = helper.make_node(
+ node = onnx.helper.make_node(
"Split",
- [input_node],
+ input_nodes,
[name],
axis=axis,
split=[num_outputs],
@@ -1937,18 +1376,13 @@ def convert_expand_dims(node, **kwargs):
"""Map MXNet's expand_dims operator attributes to onnx's Unsqueeze operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- axis = int(node["attrs"]["axis"])
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ axis = int(attrs.get("axis"))
- node = helper.make_node(
+ node = onnx.helper.make_node(
"Unsqueeze",
- [input_node],
+ input_nodes,
[name],
axes=[axis],
name=name,
@@ -1960,22 +1394,17 @@ def convert_squeeze(node, **kwargs):
"""Map MXNet's squeeze operator attributes to onnx's squeeze operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
- if "axis" in node["attrs"]:
- axis = convert_string_to_list(node["attrs"]["axis"])
- else:
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
+ axis = attrs.get("axis", None)
+ if not axis:
raise AttributeError("Missing axis attribute: ONNX currently requires axis to "
"be specified for squeeze operator")
+ axis = convert_string_to_list(axis)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
-
- node = helper.make_node(
+ node = onnx.helper.make_node(
"Squeeze",
- [input_node],
+ input_nodes,
[name],
axes=axis,
name=name,
@@ -1988,86 +1417,131 @@ def convert_log(node, **kwargs):
"""Map MXNet's log operator attributes to onnx's Log operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ return create_basic_op_node('Log', node, kwargs)
+
+@mx_op.register("reciprocal")
+def convert_reciprocal(node, **kwargs):
+ """Map MXNet's reciprocal operator attributes to onnx's Reciprocal operator
+ and return the created node.
+ """
+ return create_basic_op_node('Reciprocal', node, kwargs)
+
+@mx_op.register("_power")
+def convert_power(node, **kwargs):
+ """Map MXNet's _power operator attributes to onnx's Pow operator
+ and return the created node.
+ """
+ return create_basic_op_node('Pow', node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+@mx_op.register("broadcast_power")
+def convert_broadcast_power(node, **kwargs):
+ """Map MXNet's _power operator attributes to onnx's Pow operator
+ and return the created node.
+ """
+ return create_basic_op_node('Pow', node, kwargs)
- node = helper.make_node(
- "Log",
- [input_node],
+@mx_op.register("sqrt")
+def convert_sqrt(node, **kwargs):
+ """Map MXNet's sqrt operator attributes to onnx's Sqrt operator
+ and return the created node.
+ """
+ return create_basic_op_node('Sqrt', node, kwargs)
+
+@mx_op.register("depth_to_space")
+def convert_depthtospace(node, **kwargs):
+ """Map MXNet's depth_to_space operator attributes to onnx's
+ DepthToSpace operator and return the created node.
+ """
+ name, input_nodes, attrs = get_inputs(node, kwargs)
+
+ blksize = int(attrs.get("block_size", 0))
+
+ node = onnx.helper.make_node(
+ "DepthToSpace",
+ input_nodes,
[name],
+ blocksize=blksize,
name=name,
)
return [node]
-
-@mx_op.register("reciprocal")
-def convert_reciprocal(node, **kwargs):
- """Map MXNet's reciprocal operator attributes to onnx's Reciprocal operator
- and return the created node.
+@mx_op.register("space_to_depth")
+def convert_spacetodepth(node, **kwargs):
+ """Map MXNet's space_to_depth operator attributes to onnx's
+ SpaceToDepth operator and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ blksize = int(attrs.get("block_size", 0))
- node = helper.make_node(
- "Reciprocal",
- [input_node],
+ node = onnx.helper.make_node(
+ "SpaceToDepth",
+ input_nodes,
[name],
+ blocksize=blksize,
name=name,
)
return [node]
-
-@mx_op.register("_power")
-def convert_power(node, **kwargs):
- """Map MXNet's _power operator attributes to onnx's Pow operator
+@mx_op.register("square")
+def convert_square(node, **kwargs):
+ """Map MXNet's square operator attributes to onnx's Pow operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, _ = get_inputs(node, kwargs)
- input_node_a_id = kwargs["index_lookup"][inputs[0][0]]
- input_node_b_id = kwargs["index_lookup"][inputs[1][0]]
+ initializer = kwargs["initializer"]
+ data_type = onnx.mapping.NP_TYPE_TO_TENSOR_TYPE[np.dtype('int64')]
- input_node_a = proc_nodes[input_node_a_id].name
- input_node_b = proc_nodes[input_node_b_id].name
+ power2_name = "square_tensor" + str(kwargs["idx"])
+ tensor_node = onnx.helper.make_tensor_value_info(power2_name, data_type, (1,))
+ initializer.append(
+ onnx.helper.make_tensor(
+ name=power2_name,
+ data_type=data_type,
+ dims=(1,),
+ vals=[2],
+ raw=False,
+ )
+ )
- node = helper.make_node(
+ input_nodes.append(power2_name)
+
+ node = onnx.helper.make_node(
"Pow",
- [input_node_a, input_node_b],
+ input_nodes,
[name],
- name=None
+ name=name
)
- return [node]
+ return [tensor_node, node]
-@mx_op.register("sqrt")
-def convert_sqrt(node, **kwargs):
- """Map MXNet's sqrt operator attributes to onnx's Sqrt operator
+@mx_op.register("sum")
+def convert_sum(node, **kwargs):
+ """Map MXNet's sum operator attributes to onnx's ReduceSum operator
and return the created node.
"""
- helper, _, _ = import_onnx_modules()
- name = node["name"]
- proc_nodes = kwargs["proc_nodes"]
- inputs = node["inputs"]
+ name, input_nodes, attrs = get_inputs(node, kwargs)
- input_node_id = kwargs["index_lookup"][inputs[0][0]]
- input_node = proc_nodes[input_node_id].name
+ mx_axis = attrs.get("axis", None)
+ axes = convert_string_to_list(str(mx_axis)) if mx_axis is not None else None
- node = helper.make_node(
- "Sqrt",
- [input_node],
- [name],
- name=name,
- )
+ keepdims = get_boolean_attribute_value(attrs, "keepdims")
+
+ if axes:
+ node = onnx.helper.make_node(
+ 'ReduceSum',
+ inputs=input_nodes,
+ outputs=[name],
+ axes=axes,
+ keepdims=keepdims,
+ name=name
+ )
+ else:
+ node = onnx.helper.make_node(
+ 'ReduceSum',
+ inputs=input_nodes,
+ outputs=[name],
+ keepdims=keepdims,
+ name=name
+ )
return [node]
diff --git a/python/mxnet/contrib/onnx/mx2onnx/export_model.py b/python/mxnet/contrib/onnx/mx2onnx/export_model.py
index e5158051d6f4..f5e4c3b69e15 100644
--- a/python/mxnet/contrib/onnx/mx2onnx/export_model.py
+++ b/python/mxnet/contrib/onnx/mx2onnx/export_model.py
@@ -58,6 +58,11 @@ def export_model(sym, params, input_shape, input_type=np.float32,
-------
onnx_file_path : str
Onnx file path
+
+ Notes
+ -----
+ This method is available when you ``import mxnet.contrib.onnx``
+
"""
try:
diff --git a/python/mxnet/contrib/onnx/mx2onnx/export_onnx.py b/python/mxnet/contrib/onnx/mx2onnx/export_onnx.py
index 11847381ab24..b02d970f9c2d 100644
--- a/python/mxnet/contrib/onnx/mx2onnx/export_onnx.py
+++ b/python/mxnet/contrib/onnx/mx2onnx/export_onnx.py
@@ -77,7 +77,11 @@ def register(op_name):
"""Register operators"""
def wrapper(func):
"""Helper function to map functions"""
- MXNetGraph.registry_[op_name] = func
+ try:
+ import onnx as _
+ MXNetGraph.registry_[op_name] = func
+ except ImportError:
+ pass
return func
return wrapper
diff --git a/python/mxnet/contrib/onnx/onnx2mx/_import_helper.py b/python/mxnet/contrib/onnx/onnx2mx/_import_helper.py
index c44403d49927..f61910f838ea 100644
--- a/python/mxnet/contrib/onnx/onnx2mx/_import_helper.py
+++ b/python/mxnet/contrib/onnx/onnx2mx/_import_helper.py
@@ -24,7 +24,7 @@
from ._op_translations import softplus, shape, gather, lp_pooling
from ._op_translations import ceil, floor, hardsigmoid, global_lppooling
from ._op_translations import concat
-from ._op_translations import leaky_relu, _elu, _prelu, softmax, fully_connected
+from ._op_translations import leaky_relu, _elu, _prelu, _selu, softmax, fully_connected
from ._op_translations import global_avgpooling, global_maxpooling, linalg_gemm
from ._op_translations import sigmoid, pad, relu, matrix_multiplication, batch_norm
from ._op_translations import dropout, local_response_norm, conv, deconv
@@ -37,7 +37,7 @@
from ._op_translations import reduce_sum_square, reduce_l1, reduce_l2, max_roi_pooling
from ._op_translations import log_softmax, softsign, lesser, greater, equal
from ._op_translations import logical_and, logical_or, logical_xor, logical_not
-from ._op_translations import mean
+from ._op_translations import mean, depthtospace, spacetodepth
# convert_map defines maps of ONNX operator names to converter functor(callable)
# defined in the op_translations module.
@@ -75,6 +75,7 @@
'LeakyRelu' : leaky_relu,
'Elu' : _elu,
'PRelu' : _prelu,
+ 'Selu' : _selu,
'Softmax' : softmax,
'FC' : fully_connected,
'GlobalAveragePool' : global_avgpooling,
@@ -140,5 +141,7 @@
'Shape' : shape,
'Gather' : gather,
'HardSigmoid' : hardsigmoid,
- 'LpPool' : lp_pooling
+ 'LpPool' : lp_pooling,
+ 'DepthToSpace' : depthtospace,
+ 'SpaceToDepth' : spacetodepth
}
diff --git a/python/mxnet/contrib/onnx/onnx2mx/_op_translations.py b/python/mxnet/contrib/onnx/onnx2mx/_op_translations.py
index 4d1e9561230a..368b98d662b1 100644
--- a/python/mxnet/contrib/onnx/onnx2mx/_op_translations.py
+++ b/python/mxnet/contrib/onnx/onnx2mx/_op_translations.py
@@ -118,11 +118,21 @@ def add_n(attrs, inputs, proto_obj):
# Sorting and Searching
def argmax(attrs, inputs, proto_obj):
"""Returns indices of the maximum values along an axis"""
- return 'argmax', attrs, inputs
+ axis = attrs.get('axis', 0)
+ keepdims = attrs.get('keepdims', 1)
+ argmax_op = symbol.argmax(inputs[0], axis=axis, keepdims=keepdims)
+ # onnx argmax operator always expects int64 as output type
+ cast_attrs = {'dtype': 'int64'}
+ return 'cast', cast_attrs, argmax_op
def argmin(attrs, inputs, proto_obj):
"""Returns indices of the minimum values along an axis."""
- return 'argmin', attrs, inputs
+ axis = attrs.get('axis', 0)
+ keepdims = attrs.get('keepdims', 1)
+ argmin_op = symbol.argmin(inputs[0], axis=axis, keepdims=keepdims)
+ # onnx argmax operator always expects int64 as output type
+ cast_attrs = {'dtype': 'int64'}
+ return 'cast', cast_attrs, argmin_op
def maximum(attrs, inputs, proto_obj):
"""
@@ -231,6 +241,7 @@ def batch_norm(attrs, inputs, proto_obj):
def instance_norm(attrs, inputs, proto_obj):
"""Instance Normalization."""
new_attrs = translation_utils._fix_attribute_names(attrs, {'epsilon' : 'eps'})
+ new_attrs['eps'] = attrs.get('epsilon', 1e-5)
return 'InstanceNorm', new_attrs, inputs
def leaky_relu(attrs, inputs, proto_obj):
@@ -255,6 +266,11 @@ def _prelu(attrs, inputs, proto_obj):
new_attrs = translation_utils._add_extra_attributes(attrs, {'act_type': 'prelu'})
return 'LeakyReLU', new_attrs, inputs
+def _selu(attrs, inputs, proto_obj):
+ """Selu function"""
+ new_attrs = translation_utils._add_extra_attributes(attrs, {'act_type': 'selu'})
+ return 'LeakyReLU', new_attrs, inputs
+
def softmax(attrs, inputs, proto_obj):
"""Softmax function."""
if 'axis' not in attrs:
@@ -422,8 +438,13 @@ def reshape(attrs, inputs, proto_obj):
def cast(attrs, inputs, proto_obj):
""" Cast input to a given dtype"""
+ try:
+ from onnx.mapping import TENSOR_TYPE_TO_NP_TYPE
+ except ImportError:
+ raise ImportError("Onnx and protobuf need to be installed. "
+ + "Instructions to install - https://github.com/onnx/onnx")
new_attrs = translation_utils._fix_attribute_names(attrs, {'to' : 'dtype'})
- new_attrs['dtype'] = new_attrs['dtype'].lower()
+ new_attrs['dtype'] = TENSOR_TYPE_TO_NP_TYPE[int(new_attrs['dtype'])]
return 'cast', new_attrs, inputs
def split(attrs, inputs, proto_obj):
@@ -518,10 +539,15 @@ def squareroot(attrs, inputs, proto_obj):
def power(attrs, inputs, proto_obj):
"""Returns element-wise result of base element raised to powers from exp element."""
new_attrs = translation_utils._fix_attribute_names(attrs, {'exponent':'exp'})
- if 'broadcast' in attrs and attrs['broadcast'] == 1:
+ if 'broadcast' in attrs:
new_attrs = translation_utils._remove_attributes(new_attrs, ['broadcast'])
- return 'broadcast_power', new_attrs, inputs
- return 'pow', new_attrs, inputs
+ if attrs['broadcast'] == 1:
+ return 'broadcast_power', new_attrs, inputs
+ else:
+ mxnet_op = symbol.pow(inputs[0], inputs[1])
+ return mxnet_op, new_attrs, inputs
+ mxnet_op = symbol.broadcast_power(inputs[0], inputs[1])
+ return mxnet_op, new_attrs, inputs
def exponent(attrs, inputs, proto_obj):
"""Elementwise exponent of input array."""
@@ -672,3 +698,15 @@ def max_roi_pooling(attrs, inputs, proto_obj):
'spatial_scale': 'spatial_scale'
})
return 'ROIPooling', new_attrs, inputs
+
+def depthtospace(attrs, inputs, proto_obj):
+ """Rearranges data from depth into blocks of spatial data."""
+ new_attrs = translation_utils._fix_attribute_names(attrs, {'blocksize':'block_size'})
+
+ return "depth_to_space", new_attrs, inputs
+
+def spacetodepth(attrs, inputs, proto_obj):
+ """Rearranges blocks of spatial data into depth."""
+ new_attrs = translation_utils._fix_attribute_names(attrs, {'blocksize':'block_size'})
+
+ return "space_to_depth", new_attrs, inputs
diff --git a/python/mxnet/contrib/onnx/onnx2mx/import_model.py b/python/mxnet/contrib/onnx/onnx2mx/import_model.py
index b8d3bf28ee2f..0048c748e918 100644
--- a/python/mxnet/contrib/onnx/onnx2mx/import_model.py
+++ b/python/mxnet/contrib/onnx/onnx2mx/import_model.py
@@ -41,6 +41,11 @@ def import_model(model_file):
aux_params : dict of ``str`` to :class:`~mxnet.ndarray.NDArray`
Dict of converted parameters stored in ``mxnet.ndarray.NDArray`` format
+
+ Notes
+ -----
+ This method is available when you ``import mxnet.contrib.onnx``
+
"""
graph = GraphProto()
@@ -50,7 +55,7 @@ def import_model(model_file):
raise ImportError("Onnx and protobuf need to be installed. "
+ "Instructions to install - https://github.com/onnx/onnx")
# loads model file and returns ONNX protobuf object
- model_proto = onnx.load(model_file)
+ model_proto = onnx.load_model(model_file)
sym, arg_params, aux_params = graph.from_onnx(model_proto.graph)
return sym, arg_params, aux_params
@@ -58,6 +63,10 @@ def get_model_metadata(model_file):
"""
Returns the name and shape information of input and output tensors of the given ONNX model file.
+ Notes
+ -----
+ This method is available when you ``import mxnet.contrib.onnx``
+
Parameters
----------
model_file : str
@@ -67,20 +76,18 @@ def get_model_metadata(model_file):
-------
model_metadata : dict
A dictionary object mapping various metadata to its corresponding value.
- The dictionary will have the following template.
- {
- 'input_tensor_data' : ,
- 'output_tensor_data' :
- }
+ The dictionary will have the following template::
+ 'input_tensor_data' : list of tuples representing the shape of the input paramters
+ 'output_tensor_data' : list of tuples representing the shape of the output of the model
"""
graph = GraphProto()
+
try:
import onnx
except ImportError:
raise ImportError("Onnx and protobuf need to be installed. "
+ "Instructions to install - https://github.com/onnx/onnx")
- model_proto = onnx.load(model_file)
+ model_proto = onnx.load_model(model_file)
metadata = graph.get_graph_metadata(model_proto.graph)
return metadata
diff --git a/python/mxnet/contrib/onnx/onnx2mx/import_to_gluon.py b/python/mxnet/contrib/onnx/onnx2mx/import_to_gluon.py
index 5df41c3f327d..13ad5b9f8fa1 100644
--- a/python/mxnet/contrib/onnx/onnx2mx/import_to_gluon.py
+++ b/python/mxnet/contrib/onnx/onnx2mx/import_to_gluon.py
@@ -36,6 +36,11 @@ def import_to_gluon(model_file, ctx):
-------
sym_block : :class:`~mxnet.gluon.SymbolBlock`
A SymbolBlock object representing the given model file.
+
+ Notes
+ -----
+ This method is available when you ``import mxnet.contrib.onnx``
+
"""
graph = GraphProto()
try:
@@ -43,6 +48,6 @@ def import_to_gluon(model_file, ctx):
except ImportError:
raise ImportError("Onnx and protobuf need to be installed. Instructions to"
+ " install - https://github.com/onnx/onnx#installation")
- model_proto = onnx.load(model_file)
+ model_proto = onnx.load_model(model_file)
net = graph.graph_to_gluon(model_proto.graph, ctx)
return net
diff --git a/python/mxnet/contrib/quantization.py b/python/mxnet/contrib/quantization.py
index 8df923908fec..3b04016351ad 100644
--- a/python/mxnet/contrib/quantization.py
+++ b/python/mxnet/contrib/quantization.py
@@ -40,7 +40,7 @@
from ..module import Module
-def _quantize_params(qsym, params):
+def _quantize_params(qsym, params, th_dict):
"""Given a quantized symbol and a dict of params that have not been quantized,
generate quantized params. Currently only supports quantizing the arg_params
with names of `weight` or `bias`, not aux_params. If `qsym` contains symbols
@@ -53,6 +53,7 @@ def _quantize_params(qsym, params):
qsym : Symbol
Quantized symbol from FP32 symbol.
params : dict of str->NDArray
+ th_dict: dict of min/max pairs of layers' output
"""
inputs_name = qsym.list_arguments()
quantized_params = {}
@@ -69,11 +70,18 @@ def _quantize_params(qsym, params):
quantized_params[name+'_max'] = vmax
elif name in params:
quantized_params[name] = params[name]
+ elif name.endswith(('_min')):
+ output = name[: - len('_min')] + "_output"
+ if output in th_dict:
+ quantized_params[name] = ndarray.array([th_dict[output][0]])
+ elif name.endswith(('_max')):
+ output = name[: - len('_min')] + "_output"
+ if output in th_dict:
+ quantized_params[name] = ndarray.array([th_dict[output][1]])
return quantized_params
-
def _quantize_symbol(sym, excluded_symbols=None, offline_params=None,
- quantized_dtype='int8'):
+ quantized_dtype='int8', calib_quantize_op=False):
"""Given a symbol object representing a neural network of data type FP32,
quantize it into a INT8 network.
@@ -81,22 +89,24 @@ def _quantize_symbol(sym, excluded_symbols=None, offline_params=None,
----------
sym : Symbol
FP32 neural network symbol.
- excluded_symbols : list of symbols
- Nodes in the network that users do not want to replace with a symbol of INT8 data type.
+ excluded_sym_names : list of strings
+ A list of strings representing the names of the symbols that users want to excluding
+ from being quantized.
offline_params : list of strs
Names of the parameters that users want to quantize offline. It's always recommended to
quantize parameters offline so that quantizing parameters during the inference can be
avoided.
quantized_dtype: str
The quantized destination type for input data.
+ calib_quantize_op : bool
+ Whether perform offline calibration for quantize op.
"""
num_excluded_symbols = 0
- excluded_handles = []
if excluded_symbols is not None:
assert isinstance(excluded_symbols, list)
num_excluded_symbols = len(excluded_symbols)
- for s in excluded_symbols:
- excluded_handles.append(s.handle)
+ else:
+ excluded_symbols = []
num_offline = 0
offline = []
@@ -109,10 +119,11 @@ def _quantize_symbol(sym, excluded_symbols=None, offline_params=None,
check_call(_LIB.MXQuantizeSymbol(sym.handle,
ctypes.byref(out),
mx_uint(num_excluded_symbols),
- c_array(SymbolHandle, excluded_handles),
+ c_str_array(excluded_symbols),
mx_uint(num_offline),
c_array(ctypes.c_char_p, offline),
- c_str(quantized_dtype)))
+ c_str(quantized_dtype),
+ ctypes.c_bool(calib_quantize_op)))
return Symbol(out)
@@ -254,9 +265,6 @@ def _smooth_distribution(p, eps=0.0001):
# pylint: disable=line-too-long
def _get_optimal_threshold(arr, num_bins=8001, num_quantized_bins=255):
"""Given a dataset, find the optimal threshold for quantizing it.
- The reference distribution is `q`, and the candidate distribution is `p`.
- `q` is a truncated version of the original distribution.
-
Ref: http://on-demand.gputechconf.com/gtc/2017/presentation/s7310-8-bit-inference-with-tensorrt.pdf
"""
if isinstance(arr, NDArray):
@@ -307,10 +315,10 @@ def _get_optimal_threshold(arr, num_bins=8001, num_quantized_bins=255):
right_outlier_count = np.sum(hist[p_bin_idx_stop:])
p[-1] += right_outlier_count
# is_nonzeros[k] indicates whether hist[k] is nonzero
- is_nonzeros = (p != 0).astype(np.int32)
+ is_nonzeros = (sliced_nd_hist != 0).astype(np.int32)
# calculate how many bins should be merged to generate quantized distribution q
- num_merged_bins = sliced_nd_hist.size // num_quantized_bins
+ num_merged_bins = p.size // num_quantized_bins
# merge hist into num_quantized_bins bins
for j in range(num_quantized_bins):
start = j * num_merged_bins
@@ -318,17 +326,17 @@ def _get_optimal_threshold(arr, num_bins=8001, num_quantized_bins=255):
quantized_bins[j] = sliced_nd_hist[start:stop].sum()
quantized_bins[-1] += sliced_nd_hist[num_quantized_bins * num_merged_bins:].sum()
# expand quantized_bins into p.size bins
- q = np.zeros(sliced_nd_hist.size, dtype=np.float32)
+ q = np.zeros(p.size, dtype=np.float32)
for j in range(num_quantized_bins):
start = j * num_merged_bins
if j == num_quantized_bins - 1:
- stop = len(is_nonzeros)
+ stop = -1
else:
stop = start + num_merged_bins
norm = is_nonzeros[start:stop].sum()
if norm != 0:
q[start:stop] = float(quantized_bins[j]) / float(norm)
- q[p == 0] = 0
+ q[sliced_nd_hist == 0] = 0
p = _smooth_distribution(p)
# There is a chance that q is an invalid probability distribution.
try:
@@ -336,6 +344,7 @@ def _get_optimal_threshold(arr, num_bins=8001, num_quantized_bins=255):
except ValueError:
divergence[i - num_half_quantized_bins] = float("inf")
divergence[i - num_half_quantized_bins] = stats.entropy(p, q)
+ quantized_bins[:] = 0
min_divergence_idx = np.argmin(divergence)
min_divergence = divergence[min_divergence_idx]
@@ -363,7 +372,10 @@ def _get_optimal_thresholds(nd_dict, num_bins=8001, num_quantized_bins=255, logg
_get_optimal_threshold(nd_dict[name], num_bins=num_bins,
num_quantized_bins=num_quantized_bins)
del nd_dict[name] # release the memory of ndarray
- th_dict[name] = (-opt_th, opt_th)
+ if min_val < 0:
+ th_dict[name] = (-opt_th, opt_th)
+ else:
+ th_dict[name] = (0, opt_th)
if logger is not None:
logger.info('layer=%s, min_val=%f, max_val=%f, min_divergence=%f, optimal_threshold=%f'
% (name, min_val, max_val, min_divergence, opt_th))
@@ -408,12 +420,11 @@ def _load_params(params, logger=logging):
raise ValueError('Unsupported params provided. Must be either a path to the param file or'
' a pair of dictionaries representing arg_params and aux_params')
-
def quantize_model(sym, arg_params, aux_params,
data_names=('data',), label_names=('softmax_label',),
ctx=cpu(), excluded_sym_names=None, calib_mode='entropy',
calib_data=None, num_calib_examples=None, calib_layer=None,
- quantized_dtype='int8', logger=logging):
+ quantized_dtype='int8', calib_quantize_op=False, logger=logging):
"""User-level API for generating a quantized model from a FP32 model w/ or w/o calibration.
The backend quantized operators are only enabled for Linux systems. Please do not run
inference using the quantized models on Windows for now.
@@ -466,6 +477,8 @@ def quantize_model(sym, arg_params, aux_params,
quantized_dtype : str
The quantized destination type for input data. Currently support 'int8'
and 'uint8', default value is 'int8'.
+ calib_quantize_op: bool
+ Whether calibrate quantize op with its input calibration data. The quantize op's input should be in calib_layer
logger : Object
A logging object for printing information during the process of quantization.
@@ -481,24 +494,17 @@ def quantize_model(sym, arg_params, aux_params,
raise ValueError('excluded_sym_names must be a list of strings representing'
' the names of the symbols that will not be quantized,'
' while received type %s' % str(type(excluded_sym_names)))
- excluded_syms = []
- if excluded_sym_names is not None:
- for sym_name in excluded_sym_names:
- nodes = sym.get_internals()
- idx = nodes.list_outputs().index(sym_name + '_output')
- excluded_syms.append(nodes[idx])
- logger.info('Quantizing symbol')
+ logger.info('Quantizing symbol')
if quantized_dtype not in ('int8', 'uint8'):
raise ValueError('unknown quantized_dtype %s received,'
' expected `int8` or `uint8`' % quantized_dtype)
- qsym = _quantize_symbol(sym, excluded_symbols=excluded_syms,
+ qsym = _quantize_symbol(sym, excluded_symbols=excluded_sym_names,
offline_params=list(arg_params.keys()),
- quantized_dtype=quantized_dtype)
-
- logger.info('Quantizing parameters')
- qarg_params = _quantize_params(qsym, arg_params)
+ quantized_dtype=quantized_dtype,
+ calib_quantize_op=calib_quantize_op)
+ th_dict = {}
if calib_mode is not None and calib_mode != 'none':
if not isinstance(ctx, Context):
raise ValueError('currently only supports single ctx, while received %s' % str(ctx))
@@ -537,4 +543,7 @@ def quantize_model(sym, arg_params, aux_params,
logger.info('Calibrating quantized symbol')
qsym = _calibrate_quantized_sym(qsym, th_dict)
+ logger.info('Quantizing parameters')
+ qarg_params = _quantize_params(qsym, arg_params, th_dict)
+
return qsym, qarg_params, aux_params
diff --git a/python/mxnet/contrib/svrg_optimization/svrg_module.py b/python/mxnet/contrib/svrg_optimization/svrg_module.py
index 5d6b5dd5720c..47d0e57b45e7 100644
--- a/python/mxnet/contrib/svrg_optimization/svrg_module.py
+++ b/python/mxnet/contrib/svrg_optimization/svrg_module.py
@@ -401,6 +401,7 @@ def fit(self, train_data, eval_data=None, eval_metric='acc',
force_rebind=False, force_init=False, begin_epoch=0, num_epoch=None,
validation_metric=None, monitor=None, sparse_row_id_fn=None):
"""Trains the module parameters.
+
Parameters
----------
train_data : DataIter
diff --git a/python/mxnet/contrib/text/embedding.py b/python/mxnet/contrib/text/embedding.py
index 277f78222922..e2a05c841afd 100644
--- a/python/mxnet/contrib/text/embedding.py
+++ b/python/mxnet/contrib/text/embedding.py
@@ -161,7 +161,7 @@ class _TokenEmbedding(vocab.Vocabulary):
pre-trained token embedding file, are taken as the indexed tokens of the embedding.
- Properties
+ Attributes
----------
token_to_idx : dict mapping str to int
A dict mapping each token to its index integer.
@@ -506,25 +506,6 @@ class GloVe(_TokenEmbedding):
embedding vectors, such as loaded from a pre-trained token embedding file. If None, all the
tokens from the loaded embedding vectors, such as loaded from a pre-trained token embedding
file, will be indexed.
-
-
- Properties
- ----------
- token_to_idx : dict mapping str to int
- A dict mapping each token to its index integer.
- idx_to_token : list of strs
- A list of indexed tokens where the list indices and the token indices are aligned.
- unknown_token : hashable object
- The representation for any unknown token. In other words, any unknown token will be indexed
- as the same representation.
- reserved_tokens : list of strs or None
- A list of reserved tokens that will always be indexed.
- vec_len : int
- The length of the embedding vector for each token.
- idx_to_vec : mxnet.ndarray.NDArray
- For all the indexed tokens in this embedding, this NDArray maps each token's index to an
- embedding vector. The largest valid index maps to the initialized embedding vector for every
- reserved token, such as an unknown_token token and a padding token.
"""
# Map a pre-trained token embedding archive file and its SHA-1 hash.
@@ -610,25 +591,6 @@ class FastText(_TokenEmbedding):
embedding vectors, such as loaded from a pre-trained token embedding file. If None, all the
tokens from the loaded embedding vectors, such as loaded from a pre-trained token embedding
file, will be indexed.
-
-
- Properties
- ----------
- token_to_idx : dict mapping str to int
- A dict mapping each token to its index integer.
- idx_to_token : list of strs
- A list of indexed tokens where the list indices and the token indices are aligned.
- unknown_token : hashable object
- The representation for any unknown token. In other words, any unknown token will be indexed
- as the same representation.
- reserved_tokens : list of strs or None
- A list of reserved tokens that will always be indexed.
- vec_len : int
- The length of the embedding vector for each token.
- idx_to_vec : mxnet.ndarray.NDArray
- For all the indexed tokens in this embedding, this NDArray maps each token's index to an
- embedding vector. The largest valid index maps to the initialized embedding vector for every
- reserved token, such as an unknown_token token and a padding token.
"""
# Map a pre-trained token embedding archive file and its SHA-1 hash.
@@ -687,25 +649,6 @@ class CustomEmbedding(_TokenEmbedding):
embedding vectors, such as loaded from a pre-trained token embedding file. If None, all the
tokens from the loaded embedding vectors, such as loaded from a pre-trained token embedding
file, will be indexed.
-
-
- Properties
- ----------
- token_to_idx : dict mapping str to int
- A dict mapping each token to its index integer.
- idx_to_token : list of strs
- A list of indexed tokens where the list indices and the token indices are aligned.
- unknown_token : hashable object
- The representation for any unknown token. In other words, any unknown token will be indexed
- as the same representation.
- reserved_tokens : list of strs or None
- A list of reserved tokens that will always be indexed.
- vec_len : int
- The length of the embedding vector for each token.
- idx_to_vec : mxnet.ndarray.NDArray
- For all the indexed tokens in this embedding, this NDArray maps each token's index to an
- embedding vector. The largest valid index maps to the initialized embedding vector for every
- reserved token, such as an unknown_token token and a padding token.
"""
def __init__(self, pretrained_file_path, elem_delim=' ', encoding='utf8',
@@ -735,25 +678,6 @@ class CompositeEmbedding(_TokenEmbedding):
token_embeddings : instance or list of `mxnet.contrib.text.embedding._TokenEmbedding`
One or multiple pre-trained token embeddings to load. If it is a list of multiple
embeddings, these embedding vectors will be concatenated for each token.
-
-
- Properties
- ----------
- token_to_idx : dict mapping str to int
- A dict mapping each token to its index integer.
- idx_to_token : list of strs
- A list of indexed tokens where the list indices and the token indices are aligned.
- unknown_token : hashable object
- The representation for any unknown token. In other words, any unknown token will be indexed
- as the same representation.
- reserved_tokens : list of strs or None
- A list of reserved tokens that will always be indexed.
- vec_len : int
- The length of the embedding vector for each token.
- idx_to_vec : mxnet.ndarray.NDArray
- For all the indexed tokens in this embedding, this NDArray maps each token's index to an
- embedding vector. The largest valid index maps to the initialized embedding vector for every
- reserved token, such as an unknown_token token and a padding token.
"""
def __init__(self, vocabulary, token_embeddings):
diff --git a/python/mxnet/contrib/text/vocab.py b/python/mxnet/contrib/text/vocab.py
index 9e44acb10199..ede2ca535712 100644
--- a/python/mxnet/contrib/text/vocab.py
+++ b/python/mxnet/contrib/text/vocab.py
@@ -63,12 +63,8 @@ class Vocabulary(object):
`reserved_tokens` must be of the same hashable type. Examples: str, int, and tuple.
- Properties
+ Attributes
----------
- token_to_idx : dict mapping str to int
- A dict mapping each token to its index integer.
- idx_to_token : list of strs
- A list of indexed tokens where the list indices and the token indices are aligned.
unknown_token : hashable object
The representation for any unknown token. In other words, any unknown token will be indexed
as the same representation.
@@ -143,10 +139,16 @@ def __len__(self):
@property
def token_to_idx(self):
+ """
+ dict mapping str to int: A dict mapping each token to its index integer.
+ """
return self._token_to_idx
@property
def idx_to_token(self):
+ """
+ list of strs: A list of indexed tokens where the list indices and the token indices are aligned.
+ """
return self._idx_to_token
@property
diff --git a/python/mxnet/gluon/block.py b/python/mxnet/gluon/block.py
index 6cb9fc690b5a..7047364966af 100644
--- a/python/mxnet/gluon/block.py
+++ b/python/mxnet/gluon/block.py
@@ -326,8 +326,7 @@ def save_parameters(self, filename):
References
----------
- `Saving and Loading Gluon Models
-
+ `Saving and Loading Gluon Models \
`_
"""
params = self._collect_params_with_prefix()
@@ -372,8 +371,7 @@ def load_parameters(self, filename, ctx=None, allow_missing=False,
References
----------
- `Saving and Loading Gluon Models
-
+ `Saving and Loading Gluon Models \
`_
"""
loaded = ndarray.load(filename)
diff --git a/python/mxnet/gluon/contrib/nn/basic_layers.py b/python/mxnet/gluon/contrib/nn/basic_layers.py
index c656cd2d4e1d..28fea1592da4 100644
--- a/python/mxnet/gluon/contrib/nn/basic_layers.py
+++ b/python/mxnet/gluon/contrib/nn/basic_layers.py
@@ -27,7 +27,7 @@
from ...nn import Sequential, HybridSequential, BatchNorm
class Concurrent(Sequential):
- """Lays `Block`s concurrently.
+ """Lays `Block` s concurrently.
This block feeds its input to all children blocks, and
produce the output by concatenating all the children blocks' outputs
@@ -60,7 +60,7 @@ def forward(self, x):
class HybridConcurrent(HybridSequential):
- """Lays `HybridBlock`s concurrently.
+ """Lays `HybridBlock` s concurrently.
This block feeds its input to all children blocks, and
produce the output by concatenating all the children blocks' outputs
diff --git a/python/mxnet/gluon/contrib/rnn/conv_rnn_cell.py b/python/mxnet/gluon/contrib/rnn/conv_rnn_cell.py
index 09db5470ef93..b7a19f78b2f9 100644
--- a/python/mxnet/gluon/contrib/rnn/conv_rnn_cell.py
+++ b/python/mxnet/gluon/contrib/rnn/conv_rnn_cell.py
@@ -255,7 +255,7 @@ class Conv1DRNNCell(_ConvRNNCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_rnn_'
+ prefix : str, default ``'conv_rnn_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -322,7 +322,7 @@ class Conv2DRNNCell(_ConvRNNCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_rnn_'
+ prefix : str, default ``'conv_rnn_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -389,7 +389,7 @@ class Conv3DRNNCell(_ConvRNNCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_rnn_'
+ prefix : str, default ``'conv_rnn_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -519,7 +519,7 @@ class Conv1DLSTMCell(_ConvLSTMCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_lstm_'
+ prefix : str, default ``'conv_lstm_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -596,7 +596,7 @@ class Conv2DLSTMCell(_ConvLSTMCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_lstm_'
+ prefix : str, default ``'conv_lstm_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -673,7 +673,7 @@ class Conv3DLSTMCell(_ConvLSTMCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_lstm_'
+ prefix : str, default ``'conv_lstm_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -803,7 +803,7 @@ class Conv1DGRUCell(_ConvGRUCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_gru_'
+ prefix : str, default ``'conv_gru_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -875,7 +875,7 @@ class Conv2DGRUCell(_ConvGRUCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_gru_'
+ prefix : str, default ``'conv_gru_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -947,7 +947,7 @@ class Conv3DGRUCell(_ConvGRUCell):
If argument type is string, it's equivalent to nn.Activation(act_type=str). See
:func:`~mxnet.ndarray.Activation` for available choices.
Alternatively, other activation blocks such as nn.LeakyReLU can be used.
- prefix : str, default 'conv_gru_'
+ prefix : str, default ``'conv_gru_``'
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
diff --git a/python/mxnet/gluon/contrib/rnn/rnn_cell.py b/python/mxnet/gluon/contrib/rnn/rnn_cell.py
index 1b9afee14bf2..0cbc9eaac375 100644
--- a/python/mxnet/gluon/contrib/rnn/rnn_cell.py
+++ b/python/mxnet/gluon/contrib/rnn/rnn_cell.py
@@ -26,7 +26,7 @@
class VariationalDropoutCell(ModifierCell):
"""
Applies Variational Dropout on base cell.
- (https://arxiv.org/pdf/1512.05287.pdf,
+ (https://arxiv.org/pdf/1512.05287.pdf, \
https://www.stat.berkeley.edu/~tsmoon/files/Conference/asru2015.pdf).
Variational dropout uses the same dropout mask across time-steps. It can be applied to RNN
@@ -197,24 +197,29 @@ def unroll(self, length, inputs, begin_state=None, layout='NTC', merge_outputs=N
class LSTMPCell(HybridRecurrentCell):
r"""Long-Short Term Memory Projected (LSTMP) network cell.
(https://arxiv.org/abs/1402.1128)
+
Each call computes the following function:
+
.. math::
\begin{array}{ll}
i_t = sigmoid(W_{ii} x_t + b_{ii} + W_{ri} r_{(t-1)} + b_{ri}) \\
f_t = sigmoid(W_{if} x_t + b_{if} + W_{rf} r_{(t-1)} + b_{rf}) \\
- g_t = \tanh(W_{ig} x_t + b_{ig} + W_{rc} r_{(t-1)} + b_{rg}}) \\
+ g_t = \tanh(W_{ig} x_t + b_{ig} + W_{rc} r_{(t-1)} + b_{rg}) \\
o_t = sigmoid(W_{io} x_t + b_{io} + W_{ro} r_{(t-1)} + b_{ro}) \\
c_t = f_t * c_{(t-1)} + i_t * g_t \\
h_t = o_t * \tanh(c_t) \\
r_t = W_{hr} h_t
\end{array}
+
where :math:`r_t` is the projected recurrent activation at time `t`,
- math:`h_t` is the hidden state at time `t`, :math:`c_t` is the
+ :math:`h_t` is the hidden state at time `t`, :math:`c_t` is the
cell state at time `t`, :math:`x_t` is the input at time `t`, and :math:`i_t`,
:math:`f_t`, :math:`g_t`, :math:`o_t` are the input, forget, cell, and
out gates, respectively.
+
Parameters
----------
+
hidden_size : int
Number of units in cell state symbol.
projection_size : int
@@ -234,7 +239,7 @@ class LSTMPCell(HybridRecurrentCell):
to zero.
h2h_bias_initializer : str or Initializer
Initializer for the bias vector.
- prefix : str, default 'lstmp_'
+ prefix : str, default ``'lstmp_``'
Prefix for name of `Block`s
(and name of weight if params is `None`).
params : Parameter or None
diff --git a/python/mxnet/gluon/data/dataloader.py b/python/mxnet/gluon/data/dataloader.py
index 1c54158a2ba4..50e2ad9f784d 100644
--- a/python/mxnet/gluon/data/dataloader.py
+++ b/python/mxnet/gluon/data/dataloader.py
@@ -175,7 +175,12 @@ def _recursive_fork_recordio(obj, depth, max_depth=1000):
def worker_loop(dataset, key_queue, data_queue, batchify_fn):
"""Worker loop for multiprocessing DataLoader."""
# re-fork a new recordio handler in new process if applicable
- _recursive_fork_recordio(dataset, 0, 1000)
+ # for a dataset with transform function, the depth of MXRecordIO is 1
+ # for a lazy transformer, the depth is 2
+ # for a user defined transformer, the depth is unknown, try a reasonable depth
+ limit = sys.getrecursionlimit()
+ max_recursion_depth = min(limit - 5, max(10, limit // 2))
+ _recursive_fork_recordio(dataset, 0, max_recursion_depth)
while True:
idx, samples = key_queue.get()
diff --git a/python/mxnet/gluon/data/vision/datasets.py b/python/mxnet/gluon/data/vision/datasets.py
index 2c98000389ad..0e380c06efa0 100644
--- a/python/mxnet/gluon/data/vision/datasets.py
+++ b/python/mxnet/gluon/data/vision/datasets.py
@@ -45,8 +45,7 @@ class MNIST(dataset._DownloadedDataset):
train : bool, default True
Whether to load the training or testing set.
transform : function, default None
- A user defined callback that transforms each sample. For example:
- ::
+ A user defined callback that transforms each sample. For example::
transform=lambda data, label: (data.astype(np.float32)/255, label)
@@ -106,8 +105,7 @@ class FashionMNIST(MNIST):
train : bool, default True
Whether to load the training or testing set.
transform : function, default None
- A user defined callback that transforms each sample. For example:
- ::
+ A user defined callback that transforms each sample. For example::
transform=lambda data, label: (data.astype(np.float32)/255, label)
@@ -139,8 +137,7 @@ class CIFAR10(dataset._DownloadedDataset):
train : bool, default True
Whether to load the training or testing set.
transform : function, default None
- A user defined callback that transforms each sample. For example:
- ::
+ A user defined callback that transforms each sample. For example::
transform=lambda data, label: (data.astype(np.float32)/255, label)
@@ -204,8 +201,7 @@ class CIFAR100(CIFAR10):
train : bool, default True
Whether to load the training or testing set.
transform : function, default None
- A user defined callback that transforms each sample. For example:
- ::
+ A user defined callback that transforms each sample. For example::
transform=lambda data, label: (data.astype(np.float32)/255, label)
@@ -242,8 +238,7 @@ class ImageRecordDataset(dataset.RecordFileDataset):
If 1, always convert images to colored (RGB).
transform : function, default None
- A user defined callback that transforms each sample. For example:
- ::
+ A user defined callback that transforms each sample. For example::
transform=lambda data, label: (data.astype(np.float32)/255, label)
@@ -279,8 +274,7 @@ class ImageFolderDataset(dataset.Dataset):
If 0, always convert loaded images to greyscale (1 channel).
If 1, always convert loaded images to colored (3 channels).
transform : callable, default None
- A function that takes data and label and transforms them:
- ::
+ A function that takes data and label and transforms them::
transform = lambda data, label: (data.astype(np.float32)/255, label)
diff --git a/python/mxnet/gluon/loss.py b/python/mxnet/gluon/loss.py
index 2be43981a64c..7b5832e1ace6 100644
--- a/python/mxnet/gluon/loss.py
+++ b/python/mxnet/gluon/loss.py
@@ -23,8 +23,9 @@
'SigmoidBinaryCrossEntropyLoss', 'SigmoidBCELoss',
'SoftmaxCrossEntropyLoss', 'SoftmaxCELoss',
'KLDivLoss', 'CTCLoss', 'HuberLoss', 'HingeLoss',
- 'SquaredHingeLoss', 'LogisticLoss', 'TripletLoss']
+ 'SquaredHingeLoss', 'LogisticLoss', 'TripletLoss', 'PoissonNLLLoss', 'CosineEmbeddingLoss']
+import numpy as np
from .. import ndarray
from ..base import numeric_types
from .block import HybridBlock
@@ -468,10 +469,10 @@ def hybrid_forward(self, F, pred, label,
pred = F.swapaxes(pred, 0, 1)
if self._batch_axis == 1:
label = F.swapaxes(label, 0, 1)
- loss = F.contrib.CTCLoss(pred, label, pred_lengths, label_lengths,
- use_data_lengths=pred_lengths is not None,
- use_label_lengths=label_lengths is not None,
- blank_label='last')
+ loss = F.CTCLoss(pred, label, pred_lengths, label_lengths,
+ use_data_lengths=pred_lengths is not None,
+ use_label_lengths=label_lengths is not None,
+ blank_label='last')
return _apply_weighting(F, loss, self._weight, sample_weight)
@@ -620,7 +621,7 @@ class LogisticLoss(Loss):
where `pred` is the classifier prediction and `label` is the target tensor
containing values -1 or 1 (0 or 1 if `label_format` is binary).
- `pred` and `label` can have arbitrary shape as long as they have the same number of elements.
+ `pred` and `label` can have arbitrary shape as long as they have the same number of elements.
Parameters
----------
@@ -706,3 +707,131 @@ def hybrid_forward(self, F, pred, positive, negative):
axis=self._batch_axis, exclude=True)
loss = F.relu(loss + self._margin)
return _apply_weighting(F, loss, self._weight, None)
+
+
+class PoissonNLLLoss(Loss):
+ r"""For a target (Random Variable) in a Poisson distribution, the function calculates the Negative
+ Log likelihood loss.
+ PoissonNLLLoss measures the loss accrued from a poisson regression prediction made by the model.
+
+ .. math::
+ L = \text{pred} - \text{target} * \log(\text{pred}) +\log(\text{target!})
+
+ `pred`, `target` can have arbitrary shape as long as they have the same number of elements.
+
+ Parameters
+ ----------
+ from_logits : boolean, default True
+ indicating whether log(predicted) value has already been computed. If True, the loss is computed as
+ :math:`\exp(\text{pred}) - \text{target} * \text{pred}`, and if False, then loss is computed as
+ :math:`\text{pred} - \text{target} * \log(\text{pred}+\text{epsilon})`.The default value
+ weight : float or None
+ Global scalar weight for loss.
+ batch_axis : int, default 0
+ The axis that represents mini-batch.
+ compute_full: boolean, default False
+ Indicates whether to add an approximation(Stirling factor) for the Factorial term in the formula for the loss.
+ The Stirling factor is:
+ :math:`\text{target} * \log(\text{target}) - \text{target} + 0.5 * \log(2 * \pi * \text{target})`
+ epsilon: float, default 1e-08
+ This is to avoid calculating log(0) which is not defined.
+
+
+ Inputs:
+ - **pred**: Predicted value
+ - **target**: Random variable(count or number) which belongs to a Poisson distribution.
+ - **sample_weight**: element-wise weighting tensor. Must be broadcastable
+ to the same shape as pred. For example, if pred has shape (64, 10)
+ and you want to weigh each sample in the batch separately,
+ sample_weight should have shape (64, 1).
+
+ Outputs:
+ - **loss**: Average loss (shape=(1,1)) of the loss tensor with shape (batch_size,).
+ """
+ def __init__(self, weight=None, from_logits=True, batch_axis=0, compute_full=False, **kwargs):
+ super(PoissonNLLLoss, self).__init__(weight, batch_axis, **kwargs)
+ self._from_logits = from_logits
+ self._compute_full = compute_full
+
+ def hybrid_forward(self, F, pred, target, sample_weight=None, epsilon=1e-08):
+ target = _reshape_like(F, target, pred)
+ if self._from_logits:
+ loss = F.exp(pred) - target * pred
+ else:
+ loss = pred - target * F.log(pred + epsilon)
+ if self._compute_full:
+ # Using numpy's pi value
+ stirling_factor = target * F.log(target)- target + 0.5 * F.log(2 * target * np.pi)
+ target_gt_1 = target > 1
+ stirling_factor *= target_gt_1
+ loss += stirling_factor
+ loss = _apply_weighting(F, loss, self._weight, sample_weight)
+ return F.mean(loss)
+
+
+class CosineEmbeddingLoss(Loss):
+ r"""For a target label 1 or -1, vectors input1 and input2, the function computes the cosine distance
+ between the vectors. This can be interpreted as how similar/dissimilar two input vectors are.
+
+ .. math::
+
+ L = \sum_i \begin{cases} 1 - {cos\_sim({input1}_i, {input2}_i)} & \text{ if } {label}_i = 1\\
+ {cos\_sim({input1}_i, {input2}_i)} & \text{ if } {label}_i = -1 \end{cases}\\
+ cos\_sim(input1, input2) = \frac{{input1}_i.{input2}_i}{||{input1}_i||.||{input2}_i||}
+
+ `input1`, `input2` can have arbitrary shape as long as they have the same number of elements.
+
+ Parameters
+ ----------
+ weight : float or None
+ Global scalar weight for loss.
+ batch_axis : int, default 0
+ The axis that represents mini-batch.
+ margin : float
+ Margin of separation between correct and incorrect pair.
+
+
+ Inputs:
+ - **input1**: a tensor with arbitrary shape
+ - **input2**: another tensor with same shape as pred to which input1 is
+ compared for similarity and loss calculation
+ - **label**: A 1-D tensor indicating for each pair input1 and input2, target label is 1 or -1
+ - **sample_weight**: element-wise weighting tensor. Must be broadcastable
+ to the same shape as input1. For example, if input1 has shape (64, 10)
+ and you want to weigh each sample in the batch separately,
+ sample_weight should have shape (64, 1).
+
+ Outputs:
+ - **loss**: The loss tensor with shape (batch_size,).
+ """
+ def __init__(self, weight=None, batch_axis=0, margin=0, **kwargs):
+ super(CosineEmbeddingLoss, self).__init__(weight, batch_axis, **kwargs)
+ self._margin = margin
+
+ def hybrid_forward(self, F, input1, input2, label, sample_weight=None):
+ input1 = _reshape_like(F, input1, input2)
+ label = label.reshape((-1, 1))
+ cos_sim = self._cosine_similarity(F, input1, input2)
+ y_1 = label == 1
+ y_minus_1 = label == -1
+ cos_sim_a = (1 - cos_sim) * y_1
+
+ if F is ndarray:
+ z_array = F.array([0])
+ else:
+ z_array = F.zeros((1, 1))
+ cos_sim_b = F.broadcast_maximum(z_array, y_minus_1 * (cos_sim - self._margin), axis=1)
+ loss = cos_sim_a + cos_sim_b
+ loss = _apply_weighting(F, loss, self._weight, sample_weight)
+ return loss
+
+ def _cosine_similarity(self, F, x, y, axis=-1):
+ # Calculates the cosine similarity between 2 vectors
+ x_norm = F.norm(x, axis=axis).reshape(-1, 1)
+ y_norm = F.norm(y, axis=axis).reshape(-1, 1)
+ x_dot_y = F.sum(x*y, axis=axis).reshape(-1, 1)
+ if F is ndarray:
+ eps_arr = F.array([1e-12])
+ else:
+ eps_arr = F.full((1, 1), 1e-12)
+ return (x_dot_y / F.broadcast_maximum(x_norm * y_norm, eps_arr))
diff --git a/python/mxnet/gluon/model_zoo/vision/mobilenet.py b/python/mxnet/gluon/model_zoo/vision/mobilenet.py
index 1a84e05af208..88610571252e 100644
--- a/python/mxnet/gluon/model_zoo/vision/mobilenet.py
+++ b/python/mxnet/gluon/model_zoo/vision/mobilenet.py
@@ -62,7 +62,7 @@ def _add_conv_dw(out, dw_channels, channels, stride, relu6=False):
class LinearBottleneck(nn.HybridBlock):
r"""LinearBottleneck used in MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
@@ -138,7 +138,7 @@ def hybrid_forward(self, F, x):
class MobileNetV2(nn.HybridBlock):
r"""MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
@@ -223,7 +223,7 @@ def get_mobilenet_v2(multiplier, pretrained=False, ctx=cpu(),
root=os.path.join(base.data_dir(), 'models'), **kwargs):
r"""MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
@@ -269,7 +269,7 @@ def mobilenet1_0(**kwargs):
def mobilenet_v2_1_0(**kwargs):
r"""MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
@@ -300,7 +300,7 @@ def mobilenet0_75(**kwargs):
def mobilenet_v2_0_75(**kwargs):
r"""MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
@@ -331,7 +331,7 @@ def mobilenet0_5(**kwargs):
def mobilenet_v2_0_5(**kwargs):
r"""MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
@@ -362,7 +362,7 @@ def mobilenet0_25(**kwargs):
def mobilenet_v2_0_25(**kwargs):
r"""MobileNetV2 model from the
`"Inverted Residuals and Linear Bottlenecks:
- Mobile Networks for Classification, Detection and Segmentation"
+ Mobile Networks for Classification, Detection and Segmentation"
`_ paper.
Parameters
diff --git a/python/mxnet/gluon/nn/basic_layers.py b/python/mxnet/gluon/nn/basic_layers.py
index d26841977ac2..c69b980935fc 100644
--- a/python/mxnet/gluon/nn/basic_layers.py
+++ b/python/mxnet/gluon/nn/basic_layers.py
@@ -75,7 +75,7 @@ def __len__(self):
return len(self._children)
def hybridize(self, active=True, **kwargs):
- """Activates or deactivates `HybridBlock`s recursively. Has no effect on
+ """Activates or deactivates `HybridBlock` s recursively. Has no effect on
non-hybrid children.
Parameters
@@ -160,9 +160,9 @@ class Dense(HybridBlock):
Activation function to use. See help on `Activation` layer.
If you don't specify anything, no activation is applied
(ie. "linear" activation: `a(x) = x`).
- use_bias : bool
+ use_bias : bool, default True
Whether the layer uses a bias vector.
- flatten: bool
+ flatten: bool, default True
Whether the input tensor should be flattened.
If true, all but the first axis of input data are collapsed together.
If false, all but the last axis of input data are kept the same, and the transformation
@@ -625,7 +625,7 @@ class Lambda(Block):
block = Lambda('tanh')
- 2) a function that conforms to "def function(*args)". For example::
+ 2) a function that conforms to ``def function(*args)``. For example::
block = Lambda(lambda x: nd.LeakyReLU(x, slope=0.1))
@@ -663,20 +663,21 @@ class HybridLambda(HybridBlock):
----------
function : str or function
Function used in lambda must be one of the following:
- 1) the name of an operator that is available in both symbol and ndarray. For example::
+ 1) The name of an operator that is available in both symbol and ndarray. For example::
block = HybridLambda('tanh')
- 2) a function that conforms to "def function(F, data, *args)". For example::
+ 2) A function that conforms to ``def function(F, data, *args)``. For example::
block = HybridLambda(lambda F, x: F.LeakyReLU(x, slope=0.1))
Inputs:
- - ** *args **: one or more input data. First argument must be symbol or ndarray.
- Their shapes depend on the function.
+ - ** *args **: one or more input data. First argument must be symbol or ndarray. Their \
+ shapes depend on the function.
Output:
- ** *outputs **: one or more output data. Their shapes depend on the function.
+
"""
def __init__(self, function, prefix=None):
super(HybridLambda, self).__init__(prefix=prefix)
diff --git a/python/mxnet/gluon/parameter.py b/python/mxnet/gluon/parameter.py
index f53eeb00694a..b3d8f80318ba 100644
--- a/python/mxnet/gluon/parameter.py
+++ b/python/mxnet/gluon/parameter.py
@@ -580,7 +580,7 @@ class Constant(Parameter):
will not change during training. But you can still update their values
manually with the `set_data` method.
- `Constant`s can be created with either::
+ `Constant` s can be created with either::
const = mx.gluon.Constant('const', [[1,2],[3,4]])
@@ -740,9 +740,9 @@ def get(self, name, **kwargs):
return param
def get_constant(self, name, value=None):
- """Retrieves a :py:class:`Constant` with name ``self.prefix+name``. If not found,
+ """Retrieves a :py:class:`.Constant` with name ``self.prefix+name``. If not found,
:py:func:`get` will first try to retrieve it from "shared" dict. If still not
- found, :py:func:`get` will create a new :py:class:`Constant` with key-word
+ found, :py:func:`get` will create a new :py:class:`.Constant` with key-word
arguments and insert it to self.
Parameters
@@ -756,7 +756,7 @@ def get_constant(self, name, value=None):
Returns
-------
Constant
- The created or retrieved :py:class:`Constant`.
+ The created or retrieved :py:class:`.Constant`.
"""
name = self.prefix + name
param = self._get_impl(name)
diff --git a/python/mxnet/gluon/rnn/rnn_cell.py b/python/mxnet/gluon/rnn/rnn_cell.py
index 557837c3fa51..98e96fc6da17 100644
--- a/python/mxnet/gluon/rnn/rnn_cell.py
+++ b/python/mxnet/gluon/rnn/rnn_cell.py
@@ -333,7 +333,7 @@ class RNNCell(HybridRecurrentCell):
Initializer for the bias vector.
h2h_bias_initializer : str or Initializer, default 'zeros'
Initializer for the bias vector.
- prefix : str, default 'rnn_'
+ prefix : str, default ``'rnn_'``
Prefix for name of `Block`s
(and name of weight if params is `None`).
params : Parameter or None
@@ -398,7 +398,8 @@ def hybrid_forward(self, F, inputs, states, i2h_weight,
h2h = F.FullyConnected(data=states[0], weight=h2h_weight, bias=h2h_bias,
num_hidden=self._hidden_size,
name=prefix+'h2h')
- output = self._get_activation(F, i2h + h2h, self._activation,
+ i2h_plus_h2h = F.elemwise_add(i2h, h2h, name=prefix+'plus0')
+ output = self._get_activation(F, i2h_plus_h2h, self._activation,
name=prefix+'out')
return output, [output]
@@ -439,7 +440,7 @@ class LSTMCell(HybridRecurrentCell):
Initializer for the bias vector.
h2h_bias_initializer : str or Initializer, default 'zeros'
Initializer for the bias vector.
- prefix : str, default 'lstm_'
+ prefix : str, default ``'lstm_'``
Prefix for name of `Block`s
(and name of weight if params is `None`).
params : Parameter or None, default None
@@ -511,7 +512,7 @@ def hybrid_forward(self, F, inputs, states, i2h_weight,
num_hidden=self._hidden_size*4, name=prefix+'i2h')
h2h = F.FullyConnected(data=states[0], weight=h2h_weight, bias=h2h_bias,
num_hidden=self._hidden_size*4, name=prefix+'h2h')
- gates = i2h + h2h
+ gates = F.elemwise_add(i2h, h2h, name=prefix+'plus0')
slice_gates = F.SliceChannel(gates, num_outputs=4, name=prefix+'slice')
in_gate = self._get_activation(
F, slice_gates[0], self._recurrent_activation, name=prefix+'i')
@@ -521,9 +522,10 @@ def hybrid_forward(self, F, inputs, states, i2h_weight,
F, slice_gates[2], self._activation, name=prefix+'c')
out_gate = self._get_activation(
F, slice_gates[3], self._recurrent_activation, name=prefix+'o')
- next_c = F._internal._plus(forget_gate * states[1], in_gate * in_transform,
+ next_c = F._internal._plus(F.elemwise_mul(forget_gate, states[1], name=prefix+'mul0'),
+ F.elemwise_mul(in_gate, in_transform, name=prefix+'mul1'),
name=prefix+'state')
- next_h = F._internal._mul(out_gate, F.Activation(next_c, act_type=self._activation),
+ next_h = F._internal._mul(out_gate, F.Activation(next_c, act_type=self._activation, name=prefix+'activation0'),
name=prefix+'out')
return next_h, [next_h, next_c]
@@ -532,15 +534,16 @@ def hybrid_forward(self, F, inputs, states, i2h_weight,
class GRUCell(HybridRecurrentCell):
r"""Gated Rectified Unit (GRU) network cell.
Note: this is an implementation of the cuDNN version of GRUs
- (slight modification compared to Cho et al. 2014).
+ (slight modification compared to Cho et al. 2014; the reset gate :math:`r_t`
+ is applied after matrix multiplication).
Each call computes the following function:
.. math::
\begin{array}{ll}
r_t = sigmoid(W_{ir} x_t + b_{ir} + W_{hr} h_{(t-1)} + b_{hr}) \\
- i_t = sigmoid(W_{ii} x_t + b_{ii} + W_hi h_{(t-1)} + b_{hi}) \\
- n_t = \tanh(W_{in} x_t + b_{in} + r_t * (W_{hn} h_{(t-1)}+ b_{hn})) \\
+ i_t = sigmoid(W_{ii} x_t + b_{ii} + W_{hi} h_{(t-1)} + b_{hi}) \\
+ n_t = \tanh(W_{in} x_t + b_{in} + r_t * (W_{hn} h_{(t-1)} + b_{hn})) \\
h_t = (1 - i_t) * n_t + i_t * h_{(t-1)} \\
\end{array}
@@ -562,7 +565,7 @@ class GRUCell(HybridRecurrentCell):
Initializer for the bias vector.
h2h_bias_initializer : str or Initializer, default 'zeros'
Initializer for the bias vector.
- prefix : str, default 'gru_'
+ prefix : str, default ``'gru_'``
prefix for name of `Block`s
(and name of weight if params is `None`).
params : Parameter or None, default None
@@ -635,15 +638,22 @@ def hybrid_forward(self, F, inputs, states, i2h_weight,
h2h_r, h2h_z, h2h = F.SliceChannel(h2h, num_outputs=3,
name=prefix+'h2h_slice')
- reset_gate = F.Activation(i2h_r + h2h_r, act_type="sigmoid",
+ reset_gate = F.Activation(F.elemwise_add(i2h_r, h2h_r, name=prefix+'plus0'), act_type="sigmoid",
name=prefix+'r_act')
- update_gate = F.Activation(i2h_z + h2h_z, act_type="sigmoid",
+ update_gate = F.Activation(F.elemwise_add(i2h_z, h2h_z, name=prefix+'plus1'), act_type="sigmoid",
name=prefix+'z_act')
- next_h_tmp = F.Activation(i2h + reset_gate * h2h, act_type="tanh",
+ next_h_tmp = F.Activation(F.elemwise_add(i2h,
+ F.elemwise_mul(reset_gate, h2h, name=prefix+'mul0'),
+ name=prefix+'plus2'),
+ act_type="tanh",
name=prefix+'h_act')
- next_h = F._internal._plus((1. - update_gate) * next_h_tmp, update_gate * prev_state_h,
+ ones = F.ones_like(update_gate, name=prefix+"ones_like0")
+ next_h = F._internal._plus(F.elemwise_mul(F.elemwise_sub(ones, update_gate, name=prefix+'minus0'),
+ next_h_tmp,
+ name=prefix+'mul1'),
+ F.elemwise_mul(update_gate, prev_state_h, name=prefix+'mul20'),
name=prefix+'out')
return next_h, [next_h]
diff --git a/python/mxnet/gluon/rnn/rnn_layer.py b/python/mxnet/gluon/rnn/rnn_layer.py
index daf8ecbf5631..c43dc8527fd4 100644
--- a/python/mxnet/gluon/rnn/rnn_layer.py
+++ b/python/mxnet/gluon/rnn/rnn_layer.py
@@ -35,11 +35,14 @@ def __init__(self, hidden_size, num_layers, layout,
dropout, bidirectional, input_size,
i2h_weight_initializer, h2h_weight_initializer,
i2h_bias_initializer, h2h_bias_initializer,
- mode, **kwargs):
+ mode, projection_size, h2r_weight_initializer,
+ lstm_state_clip_min, lstm_state_clip_max, lstm_state_clip_nan,
+ **kwargs):
super(_RNNLayer, self).__init__(**kwargs)
assert layout in ('TNC', 'NTC'), \
"Invalid layout %s; must be one of ['TNC' or 'NTC']"%layout
self._hidden_size = hidden_size
+ self._projection_size = projection_size if projection_size else None
self._num_layers = num_layers
self._mode = mode
self._layout = layout
@@ -50,25 +53,50 @@ def __init__(self, hidden_size, num_layers, layout,
self._h2h_weight_initializer = h2h_weight_initializer
self._i2h_bias_initializer = i2h_bias_initializer
self._h2h_bias_initializer = h2h_bias_initializer
+ self._h2r_weight_initializer = h2r_weight_initializer
+ self._lstm_state_clip_min = lstm_state_clip_min
+ self._lstm_state_clip_max = lstm_state_clip_max
+ self._lstm_state_clip_nan = lstm_state_clip_nan
self._gates = {'rnn_relu': 1, 'rnn_tanh': 1, 'lstm': 4, 'gru': 3}[mode]
ng, ni, nh = self._gates, input_size, hidden_size
- for i in range(num_layers):
- for j in ['l', 'r'][:self._dir]:
- self._register_param('{}{}_i2h_weight'.format(j, i),
- shape=(ng*nh, ni),
- init=i2h_weight_initializer)
- self._register_param('{}{}_h2h_weight'.format(j, i),
- shape=(ng*nh, nh),
- init=h2h_weight_initializer)
- self._register_param('{}{}_i2h_bias'.format(j, i),
- shape=(ng*nh,),
- init=i2h_bias_initializer)
- self._register_param('{}{}_h2h_bias'.format(j, i),
- shape=(ng*nh,),
- init=h2h_bias_initializer)
- ni = nh * self._dir
+ if not projection_size:
+ for i in range(num_layers):
+ for j in ['l', 'r'][:self._dir]:
+ self._register_param('{}{}_i2h_weight'.format(j, i),
+ shape=(ng*nh, ni),
+ init=i2h_weight_initializer)
+ self._register_param('{}{}_h2h_weight'.format(j, i),
+ shape=(ng*nh, nh),
+ init=h2h_weight_initializer)
+ self._register_param('{}{}_i2h_bias'.format(j, i),
+ shape=(ng*nh,),
+ init=i2h_bias_initializer)
+ self._register_param('{}{}_h2h_bias'.format(j, i),
+ shape=(ng*nh,),
+ init=h2h_bias_initializer)
+ ni = nh * self._dir
+ else:
+ np = self._projection_size
+ for i in range(num_layers):
+ for j in ['l', 'r'][:self._dir]:
+ self._register_param('{}{}_i2h_weight'.format(j, i),
+ shape=(ng*nh, ni),
+ init=i2h_weight_initializer)
+ self._register_param('{}{}_h2h_weight'.format(j, i),
+ shape=(ng*nh, np),
+ init=h2h_weight_initializer)
+ self._register_param('{}{}_i2h_bias'.format(j, i),
+ shape=(ng*nh,),
+ init=i2h_bias_initializer)
+ self._register_param('{}{}_h2h_bias'.format(j, i),
+ shape=(ng*nh,),
+ init=h2h_bias_initializer)
+ self._register_param('{}{}_h2r_weight'.format(j, i),
+ shape=(np, nh),
+ init=h2r_weight_initializer)
+ ni = np * self._dir
def _register_param(self, name, shape, init):
p = self.params.get(name, shape=shape, init=init,
@@ -114,6 +142,9 @@ def state_info(self, batch_size=0):
def _unfuse(self):
"""Unfuses the fused RNN in to a stack of rnn cells."""
+ assert not self._projection_size, "_unfuse does not support projection layer yet!"
+ assert not self._lstm_state_clip_min and not self._lstm_state_clip_max, \
+ "_unfuse does not support state clipping yet!"
get_cell = {'rnn_relu': lambda **kwargs: rnn_cell.RNNCell(self._hidden_size,
activation='relu',
**kwargs),
@@ -189,7 +220,7 @@ def hybrid_forward(self, F, inputs, states=None, **kwargs):
skip_states = states is None
if skip_states:
if F is ndarray:
- states = self.begin_state(batch_size, ctx=inputs.context)
+ states = self.begin_state(batch_size, ctx=inputs.context, dtype=inputs.dtype)
else:
states = self.begin_state(0, func=symbol.zeros)
if isinstance(states, tensor_types):
@@ -209,16 +240,29 @@ def _forward_kernel(self, F, inputs, states, **kwargs):
""" forward using CUDNN or CPU kenrel"""
if self._layout == 'NTC':
inputs = F.swapaxes(inputs, dim1=0, dim2=1)
- params = (kwargs['{}{}_{}_{}'.format(d, l, g, t)].reshape(-1)
- for t in ['weight', 'bias']
- for l in range(self._num_layers)
- for d in ['l', 'r'][:self._dir]
- for g in ['i2h', 'h2h'])
+ if self._projection_size is None:
+ params = (kwargs['{}{}_{}_{}'.format(d, l, g, t)].reshape(-1)
+ for t in ['weight', 'bias']
+ for l in range(self._num_layers)
+ for d in ['l', 'r'][:self._dir]
+ for g in ['i2h', 'h2h'])
+ else:
+ params = (kwargs['{}{}_{}_{}'.format(d, l, g, t)].reshape(-1)
+ for t in ['weight', 'bias']
+ for l in range(self._num_layers)
+ for d in ['l', 'r'][:self._dir]
+ for g in ['i2h', 'h2h', 'h2r']
+ if g != 'h2r' or t != 'bias')
+
params = F._internal._rnn_param_concat(*params, dim=0)
rnn = F.RNN(inputs, params, *states, state_size=self._hidden_size,
+ projection_size=self._projection_size,
num_layers=self._num_layers, bidirectional=self._dir == 2,
- p=self._dropout, state_outputs=True, mode=self._mode)
+ p=self._dropout, state_outputs=True, mode=self._mode,
+ lstm_state_clip_min=self._lstm_state_clip_min,
+ lstm_state_clip_max=self._lstm_state_clip_max,
+ lstm_state_clip_nan=self._lstm_state_clip_nan)
if self._mode == 'lstm':
outputs, states = rnn[0], [rnn[1], rnn[2]]
@@ -318,7 +362,8 @@ def __init__(self, hidden_size, num_layers=1, activation='relu',
dropout, bidirectional, input_size,
i2h_weight_initializer, h2h_weight_initializer,
i2h_bias_initializer, h2h_bias_initializer,
- 'rnn_'+activation, **kwargs)
+ 'rnn_'+activation, None, None, None, None, False,
+ **kwargs)
def state_info(self, batch_size=0):
return [{'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
@@ -373,6 +418,20 @@ class LSTM(_RNNLayer):
to zero.
h2h_bias_initializer : str or Initializer
Initializer for the bias vector.
+ projection_size: int, default None
+ The number of features after projection.
+ h2r_weight_initializer : str or Initializer, default None
+ Initializer for the projected recurrent weights matrix, used for the linear
+ transformation of the recurrent state to the projected space.
+ state_clip_min : float or None, default None
+ Minimum clip value of LSTM states. This option must be used together with
+ state_clip_max. If None, clipping is not applied.
+ state_clip_max : float or None, default None
+ Maximum clip value of LSTM states. This option must be used together with
+ state_clip_min. If None, clipping is not applied.
+ state_clip_nan : boolean, default False
+ Whether to stop NaN from propagating in state by clipping it to min/max.
+ If the clipping range is not specified, this option is ignored.
input_size: int, default 0
The number of expected features in the input x.
If not specified, it will be inferred from input.
@@ -416,22 +475,35 @@ def __init__(self, hidden_size, num_layers=1, layout='TNC',
dropout=0, bidirectional=False, input_size=0,
i2h_weight_initializer=None, h2h_weight_initializer=None,
i2h_bias_initializer='zeros', h2h_bias_initializer='zeros',
+ projection_size=None, h2r_weight_initializer=None,
+ state_clip_min=None, state_clip_max=None, state_clip_nan=False,
**kwargs):
super(LSTM, self).__init__(hidden_size, num_layers, layout,
dropout, bidirectional, input_size,
i2h_weight_initializer, h2h_weight_initializer,
i2h_bias_initializer, h2h_bias_initializer,
- 'lstm', **kwargs)
+ 'lstm', projection_size, h2r_weight_initializer,
+ state_clip_min, state_clip_max, state_clip_nan,
+ **kwargs)
def state_info(self, batch_size=0):
- return [{'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
- '__layout__': 'LNC'},
- {'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
- '__layout__': 'LNC'}]
+ if self._projection_size is None:
+ return [{'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
+ '__layout__': 'LNC'},
+ {'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
+ '__layout__': 'LNC'}]
+ else:
+ return [{'shape': (self._num_layers * self._dir, batch_size, self._projection_size),
+ '__layout__': 'LNC'},
+ {'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
+ '__layout__': 'LNC'}]
class GRU(_RNNLayer):
r"""Applies a multi-layer gated recurrent unit (GRU) RNN to an input sequence.
+ Note: this is an implementation of the cuDNN version of GRUs
+ (slight modification compared to Cho et al. 2014; the reset gate :math:`r_t`
+ is applied after matrix multiplication).
For each element in the input sequence, each layer computes the following
function:
@@ -439,8 +511,8 @@ class GRU(_RNNLayer):
.. math::
\begin{array}{ll}
r_t = sigmoid(W_{ir} x_t + b_{ir} + W_{hr} h_{(t-1)} + b_{hr}) \\
- i_t = sigmoid(W_{ii} x_t + b_{ii} + W_hi h_{(t-1)} + b_{hi}) \\
- n_t = \tanh(W_{in} x_t + b_{in} + r_t * (W_{hn} h_{(t-1)}+ b_{hn})) \\
+ i_t = sigmoid(W_{ii} x_t + b_{ii} + W_{hi} h_{(t-1)} + b_{hi}) \\
+ n_t = \tanh(W_{in} x_t + b_{in} + r_t * (W_{hn} h_{(t-1)} + b_{hn})) \\
h_t = (1 - i_t) * n_t + i_t * h_{(t-1)} \\
\end{array}
@@ -519,7 +591,8 @@ def __init__(self, hidden_size, num_layers=1, layout='TNC',
dropout, bidirectional, input_size,
i2h_weight_initializer, h2h_weight_initializer,
i2h_bias_initializer, h2h_bias_initializer,
- 'gru', **kwargs)
+ 'gru', None, None, None, None, False,
+ **kwargs)
def state_info(self, batch_size=0):
return [{'shape': (self._num_layers * self._dir, batch_size, self._hidden_size),
diff --git a/python/mxnet/gluon/trainer.py b/python/mxnet/gluon/trainer.py
index 028e66075100..c4d49e82c908 100644
--- a/python/mxnet/gluon/trainer.py
+++ b/python/mxnet/gluon/trainer.py
@@ -194,14 +194,18 @@ def _init_kvstore(self):
if config['update_on_kvstore'] is not None:
update_on_kvstore = config['update_on_kvstore']
+
if kvstore:
if self._compression_params:
kvstore.set_gradient_compression(self._compression_params)
self._distributed = 'dist' in kvstore.type
if self._distributed:
# kv.pull(row_sparse_grad) is not supported for dist kvstore
+ # Captures condition for dist_async, dist_device_sync or based on config for
+ # update_on_kvstore
update_on_kvstore = self._contains_sparse_weight or self._contains_sparse_grad \
- or 'async' in kvstore.type
+ or 'device' in kvstore.type or 'async' in kvstore.type \
+ or config['update_on_kvstore']
if update_on_kvstore:
# optimizer preferably needs to be set before init for multiprecision
kvstore.set_optimizer(self._optimizer)
@@ -269,13 +273,20 @@ def step(self, batch_size, ignore_stale_grad=False):
If true, ignores Parameters with stale gradient (gradient that has not
been updated by `backward` after last step) and skip update.
"""
+ rescale_grad = self._scale / batch_size
+ if self._update_on_kvstore and self._distributed and \
+ self._optimizer.rescale_grad != rescale_grad:
+ raise UserWarning('Possible change in the `batch_size` from previous `step` detected.' \
+ 'Optimizer gradient normalizing factor will not change w.r.t new batch_size when ' \
+ 'update_on_kvstore=True and when distributed `kvstore` is used.')
+
+ self._optimizer.rescale_grad = rescale_grad
+
if not self._kv_initialized:
self._init_kvstore()
if self._params_to_init:
self._init_params()
- self._optimizer.rescale_grad = self._scale / batch_size
-
self._allreduce_grads()
self._update(ignore_stale_grad)
diff --git a/python/mxnet/gluon/utils.py b/python/mxnet/gluon/utils.py
index d5a14a6859a7..78324986760a 100644
--- a/python/mxnet/gluon/utils.py
+++ b/python/mxnet/gluon/utils.py
@@ -22,7 +22,9 @@
'check_sha1', 'download']
import os
+import sys
import hashlib
+import uuid
import warnings
import collections
import weakref
@@ -195,6 +197,62 @@ def check_sha1(filename, sha1_hash):
return sha1.hexdigest() == sha1_hash
+if not sys.platform.startswith('win32'):
+ # refer to https://github.com/untitaker/python-atomicwrites
+ def _replace_atomic(src, dst):
+ """Implement atomic os.replace with linux and OSX. Internal use only"""
+ try:
+ os.rename(src, dst)
+ except OSError:
+ try:
+ os.remove(src)
+ except OSError:
+ pass
+ finally:
+ raise OSError(
+ 'Moving downloaded temp file - {}, to {} failed. \
+ Please retry the download.'.format(src, dst))
+else:
+ import ctypes
+
+ _MOVEFILE_REPLACE_EXISTING = 0x1
+ # Setting this value guarantees that a move performed as a copy
+ # and delete operation is flushed to disk before the function returns.
+ # The flush occurs at the end of the copy operation.
+ _MOVEFILE_WRITE_THROUGH = 0x8
+ _windows_default_flags = _MOVEFILE_WRITE_THROUGH
+
+ text_type = unicode if sys.version_info[0] == 2 else str # noqa
+
+ def _str_to_unicode(x):
+ """Handle text decoding. Internal use only"""
+ if not isinstance(x, text_type):
+ return x.decode(sys.getfilesystemencoding())
+ return x
+
+ def _handle_errors(rv, src):
+ """Handle WinError. Internal use only"""
+ if not rv:
+ msg = ctypes.FormatError(ctypes.GetLastError())
+ # if the MoveFileExW fails(e.g. fail to acquire file lock), removes the tempfile
+ try:
+ os.remove(src)
+ except OSError:
+ pass
+ finally:
+ raise OSError(msg)
+
+ def _replace_atomic(src, dst):
+ """Implement atomic os.replace with windows.
+ refer to https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-movefileexw
+ The function fails when one of the process(copy, flush, delete) fails.
+ Internal use only"""
+ _handle_errors(ctypes.windll.kernel32.MoveFileExW(
+ _str_to_unicode(src), _str_to_unicode(dst),
+ _windows_default_flags | _MOVEFILE_REPLACE_EXISTING
+ ), src)
+
+
def download(url, path=None, overwrite=False, sha1_hash=None, retries=5, verify_ssl=True):
"""Download an given URL
@@ -231,7 +289,8 @@ def download(url, path=None, overwrite=False, sha1_hash=None, retries=5, verify_
fname = os.path.join(path, url.split('/')[-1])
else:
fname = path
- assert retries >= 0, "Number of retries should be at least 0"
+ assert retries >= 0, "Number of retries should be at least 0, currently it's {}".format(
+ retries)
if not verify_ssl:
warnings.warn(
@@ -242,31 +301,48 @@ def download(url, path=None, overwrite=False, sha1_hash=None, retries=5, verify_
dirname = os.path.dirname(os.path.abspath(os.path.expanduser(fname)))
if not os.path.exists(dirname):
os.makedirs(dirname)
- while retries+1 > 0:
+ while retries + 1 > 0:
# Disable pyling too broad Exception
# pylint: disable=W0703
try:
- print('Downloading %s from %s...'%(fname, url))
+ print('Downloading {} from {}...'.format(fname, url))
r = requests.get(url, stream=True, verify=verify_ssl)
if r.status_code != 200:
- raise RuntimeError("Failed downloading url %s"%url)
- with open(fname, 'wb') as f:
+ raise RuntimeError('Failed downloading url {}'.format(url))
+ # create uuid for temporary files
+ random_uuid = str(uuid.uuid4())
+ with open('{}.{}'.format(fname, random_uuid), 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
+ # if the target file exists(created by other processes)
+ # and have the same hash with target file
+ # delete the temporary file
+ if not os.path.exists(fname) or (sha1_hash and not check_sha1(fname, sha1_hash)):
+ # atmoic operation in the same file system
+ _replace_atomic('{}.{}'.format(fname, random_uuid), fname)
+ else:
+ try:
+ os.remove('{}.{}'.format(fname, random_uuid))
+ except OSError:
+ pass
+ finally:
+ warnings.warn(
+ 'File {} exists in file system so the downloaded file is deleted'.format(fname))
if sha1_hash and not check_sha1(fname, sha1_hash):
- raise UserWarning('File {} is downloaded but the content hash does not match.'\
- ' The repo may be outdated or download may be incomplete. '\
- 'If the "repo_url" is overridden, consider switching to '\
- 'the default repo.'.format(fname))
+ raise UserWarning(
+ 'File {} is downloaded but the content hash does not match.'
+ ' The repo may be outdated or download may be incomplete. '
+ 'If the "repo_url" is overridden, consider switching to '
+ 'the default repo.'.format(fname))
break
except Exception as e:
retries -= 1
if retries <= 0:
raise e
else:
- print("download failed, retrying, {} attempt{} left"
- .format(retries, 's' if retries > 1 else ''))
+ print('download failed due to {}, retrying, {} attempt{} left'
+ .format(repr(e), retries, 's' if retries > 1 else ''))
return fname
diff --git a/python/mxnet/image/image.py b/python/mxnet/image/image.py
index eee2ccf14a8e..5c4f01e2fd24 100644
--- a/python/mxnet/image/image.py
+++ b/python/mxnet/image/image.py
@@ -93,7 +93,7 @@ def imdecode(buf, *args, **kwargs):
Parameters
----------
- buf : str/bytes or numpy.ndarray
+ buf : str/bytes/bytearray or numpy.ndarray
Binary image data as string or numpy ndarray.
flag : int, optional, default=1
1 for three channel color output. 0 for grayscale output.
@@ -135,10 +135,11 @@ def imdecode(buf, *args, **kwargs):
"""
if not isinstance(buf, nd.NDArray):
- if sys.version_info[0] == 3 and not isinstance(buf, (bytes, np.ndarray)):
- raise ValueError('buf must be of type bytes or numpy.ndarray,'
+ if sys.version_info[0] == 3 and not isinstance(buf, (bytes, bytearray, np.ndarray)):
+ raise ValueError('buf must be of type bytes, bytearray or numpy.ndarray,'
'if you would like to input type str, please convert to bytes')
buf = nd.array(np.frombuffer(buf, dtype=np.uint8), dtype=np.uint8)
+
return _internal._cvimdecode(buf, *args, **kwargs)
diff --git a/python/mxnet/initializer.py b/python/mxnet/initializer.py
index 8ae729f3ccf9..b67ab624ac70 100755
--- a/python/mxnet/initializer.py
+++ b/python/mxnet/initializer.py
@@ -32,10 +32,11 @@
# inherit str for backward compatibility
class InitDesc(str):
- """Descriptor for the initialization pattern.
+ """
+ Descriptor for the initialization pattern.
- Parameter
- ---------
+ Parameters
+ ----------
name : str
Name of variable.
attrs : dict of str to str
@@ -67,7 +68,7 @@ def set_verbosity(self, verbose=False, print_func=None):
print_func : function
A function that computes statistics of initialized arrays.
Takes an `NDArray` and returns an `str`. Defaults to mean
- absolute value str((|x|/size(x)).asscalar()).
+ absolute value str((abs(x)/size(x)).asscalar()).
"""
self._verbose = verbose
if print_func is None:
@@ -152,6 +153,12 @@ def __call__(self, desc, arr):
elif desc.endswith('beta'):
self._init_beta(desc, arr)
self._verbose_print(desc, 'beta', arr)
+ elif desc.endswith('min'):
+ self._init_zero(desc, arr)
+ self._verbose_print(desc, 'min', arr)
+ elif desc.endswith('max'):
+ self._init_one(desc, arr)
+ self._verbose_print(desc, 'max', arr)
else:
self._init_default(desc, arr)
@@ -196,6 +203,10 @@ def _legacy_init(self, name, arr):
self._init_zero(name, arr)
elif name.endswith("moving_avg"):
self._init_zero(name, arr)
+ elif name.endswith('min'):
+ self._init_zero(name, arr)
+ elif name.endswith('max'):
+ self._init_one(name, arr)
else:
self._init_default(name, arr)
diff --git a/python/mxnet/io/__init__.py b/python/mxnet/io/__init__.py
new file mode 100644
index 000000000000..5c5e2e68d84a
--- /dev/null
+++ b/python/mxnet/io/__init__.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# coding: utf-8
+# pylint: disable=wildcard-import
+""" Data iterators for common data formats and utility functions."""
+from __future__ import absolute_import
+
+from . import io
+from .io import *
+
+from . import utils
+from .utils import *
diff --git a/python/mxnet/io.py b/python/mxnet/io/io.py
similarity index 81%
rename from python/mxnet/io.py
rename to python/mxnet/io/io.py
index 884e9294741a..2bd1d6115ac3 100644
--- a/python/mxnet/io.py
+++ b/python/mxnet/io/io.py
@@ -17,30 +17,26 @@
"""Data iterators for common data formats."""
from __future__ import absolute_import
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
import sys
import ctypes
import logging
import threading
-try:
- import h5py
-except ImportError:
- h5py = None
import numpy as np
-from .base import _LIB
-from .base import c_str_array, mx_uint, py_str
-from .base import DataIterHandle, NDArrayHandle
-from .base import mx_real_t
-from .base import check_call, build_param_doc as _build_param_doc
-from .ndarray import NDArray
-from .ndarray.sparse import CSRNDArray
-from .ndarray.sparse import array as sparse_array
-from .ndarray import _ndarray_cls
-from .ndarray import array
-from .ndarray import concatenate
-from .ndarray import arange
-from .ndarray.random import shuffle as random_shuffle
+
+from ..base import _LIB
+from ..base import c_str_array, mx_uint, py_str
+from ..base import DataIterHandle, NDArrayHandle
+from ..base import mx_real_t
+from ..base import check_call, build_param_doc as _build_param_doc
+from ..ndarray import NDArray
+from ..ndarray.sparse import CSRNDArray
+from ..ndarray import _ndarray_cls
+from ..ndarray import array
+from ..ndarray import concat
+
+from .utils import _init_data, _has_instance, _getdata_by_idx
class DataDesc(namedtuple('DataDesc', ['name', 'shape'])):
"""DataDesc is used to store name, shape, type and layout
@@ -106,8 +102,8 @@ def get_list(shapes, types):
Parameters
----------
- shapes : a tuple of (name, shape)
- types : a tuple of (name, type)
+ shapes : a tuple of (name_, shape_)
+ types : a tuple of (name_, np.dtype)
"""
if types is not None:
type_dict = dict(types)
@@ -489,66 +485,13 @@ def getindex(self):
def getpad(self):
return self.current_batch.pad
-def _init_data(data, allow_empty, default_name):
- """Convert data into canonical form."""
- assert (data is not None) or allow_empty
- if data is None:
- data = []
-
- if isinstance(data, (np.ndarray, NDArray, h5py.Dataset)
- if h5py else (np.ndarray, NDArray)):
- data = [data]
- if isinstance(data, list):
- if not allow_empty:
- assert(len(data) > 0)
- if len(data) == 1:
- data = OrderedDict([(default_name, data[0])]) # pylint: disable=redefined-variable-type
- else:
- data = OrderedDict( # pylint: disable=redefined-variable-type
- [('_%d_%s' % (i, default_name), d) for i, d in enumerate(data)])
- if not isinstance(data, dict):
- raise TypeError("Input must be NDArray, numpy.ndarray, h5py.Dataset " + \
- "a list of them or dict with them as values")
- for k, v in data.items():
- if not isinstance(v, (NDArray, h5py.Dataset) if h5py else NDArray):
- try:
- data[k] = array(v)
- except:
- raise TypeError(("Invalid type '%s' for %s, " % (type(v), k)) + \
- "should be NDArray, numpy.ndarray or h5py.Dataset")
-
- return list(sorted(data.items()))
-
-def _has_instance(data, dtype):
- """Return True if ``data`` has instance of ``dtype``.
- This function is called after _init_data.
- ``data`` is a list of (str, NDArray)"""
- for item in data:
- _, arr = item
- if isinstance(arr, dtype):
- return True
- return False
-
-def _shuffle(data, idx):
- """Shuffle the data."""
- shuffle_data = []
-
- for k, v in data:
- if (isinstance(v, h5py.Dataset) if h5py else False):
- shuffle_data.append((k, v))
- elif isinstance(v, CSRNDArray):
- shuffle_data.append((k, sparse_array(v.asscipy()[idx], v.context)))
- else:
- shuffle_data.append((k, array(v.asnumpy()[idx], v.context)))
-
- return shuffle_data
class NDArrayIter(DataIter):
"""Returns an iterator for ``mx.nd.NDArray``, ``numpy.ndarray``, ``h5py.Dataset``
``mx.nd.sparse.CSRNDArray`` or ``scipy.sparse.csr_matrix``.
- Example usage:
- ----------
+ Examples
+ --------
>>> data = np.arange(40).reshape((10,2,2))
>>> labels = np.ones([10, 1])
>>> dataiter = mx.io.NDArrayIter(data, labels, 3, True, last_batch_handle='discard')
@@ -601,6 +544,22 @@ class NDArrayIter(DataIter):
...
>>> batchidx # Remaining examples are discarded. So, 10/3 batches are created.
3
+ >>> dataiter = mx.io.NDArrayIter(data, labels, 3, False, last_batch_handle='roll_over')
+ >>> batchidx = 0
+ >>> for batch in dataiter:
+ ... batchidx += 1
+ ...
+ >>> batchidx # Remaining examples are rolled over to the next iteration.
+ 3
+ >>> dataiter.reset()
+ >>> dataiter.next().data[0].asnumpy()
+ [[[ 36. 37.]
+ [ 38. 39.]]
+ [[ 0. 1.]
+ [ 2. 3.]]
+ [[ 4. 5.]
+ [ 6. 7.]]]
+ (3L, 2L, 2L)
`NDArrayIter` also supports multiple input and labels.
@@ -633,8 +592,11 @@ class NDArrayIter(DataIter):
Only supported if no h5py.Dataset inputs are used.
last_batch_handle : str, optional
How to handle the last batch. This parameter can be 'pad', 'discard' or
- 'roll_over'. 'roll_over' is intended for training and can cause problems
- if used for prediction.
+ 'roll_over'.
+ If 'pad', the last batch will be padded with data starting from the begining
+ If 'discard', the last batch will be discarded
+ If 'roll_over', the remaining elements will be rolled over to the next iteration and
+ note that it is intended for training and can cause problems if used for prediction.
data_name : str, optional
The data name.
label_name : str, optional
@@ -648,33 +610,26 @@ def __init__(self, data, label=None, batch_size=1, shuffle=False,
self.data = _init_data(data, allow_empty=False, default_name=data_name)
self.label = _init_data(label, allow_empty=True, default_name=label_name)
- if ((_has_instance(self.data, CSRNDArray) or _has_instance(self.label, CSRNDArray)) and
+ if ((_has_instance(self.data, CSRNDArray) or
+ _has_instance(self.label, CSRNDArray)) and
(last_batch_handle != 'discard')):
raise NotImplementedError("`NDArrayIter` only supports ``CSRNDArray``" \
" with `last_batch_handle` set to `discard`.")
- # shuffle data
- if shuffle:
- tmp_idx = arange(self.data[0][1].shape[0], dtype=np.int32)
- self.idx = random_shuffle(tmp_idx, out=tmp_idx).asnumpy()
- self.data = _shuffle(self.data, self.idx)
- self.label = _shuffle(self.label, self.idx)
- else:
- self.idx = np.arange(self.data[0][1].shape[0])
-
- # batching
- if last_batch_handle == 'discard':
- new_n = self.data[0][1].shape[0] - self.data[0][1].shape[0] % batch_size
- self.idx = self.idx[:new_n]
+ self.idx = np.arange(self.data[0][1].shape[0])
+ self.shuffle = shuffle
+ self.last_batch_handle = last_batch_handle
+ self.batch_size = batch_size
+ self.cursor = -self.batch_size
+ self.num_data = self.idx.shape[0]
+ # shuffle
+ self.reset()
self.data_list = [x[1] for x in self.data] + [x[1] for x in self.label]
self.num_source = len(self.data_list)
- self.num_data = self.idx.shape[0]
- assert self.num_data >= batch_size, \
- "batch_size needs to be smaller than data size."
- self.cursor = -batch_size
- self.batch_size = batch_size
- self.last_batch_handle = last_batch_handle
+ # used for 'roll_over'
+ self._cache_data = None
+ self._cache_label = None
@property
def provide_data(self):
@@ -694,74 +649,141 @@ def provide_label(self):
def hard_reset(self):
"""Ignore roll over data and set to start."""
+ if self.shuffle:
+ self._shuffle_data()
self.cursor = -self.batch_size
+ self._cache_data = None
+ self._cache_label = None
def reset(self):
- if self.last_batch_handle == 'roll_over' and self.cursor > self.num_data:
- self.cursor = -self.batch_size + (self.cursor%self.num_data)%self.batch_size
+ """Resets the iterator to the beginning of the data."""
+ if self.shuffle:
+ self._shuffle_data()
+ # the range below indicate the last batch
+ if self.last_batch_handle == 'roll_over' and \
+ self.num_data - self.batch_size < self.cursor < self.num_data:
+ # (self.cursor - self.num_data) represents the data we have for the last batch
+ self.cursor = self.cursor - self.num_data - self.batch_size
else:
self.cursor = -self.batch_size
def iter_next(self):
+ """Increments the coursor by batch_size for next batch
+ and check current cursor if it exceed the number of data points."""
self.cursor += self.batch_size
return self.cursor < self.num_data
def next(self):
- if self.iter_next():
- return DataBatch(data=self.getdata(), label=self.getlabel(), \
- pad=self.getpad(), index=None)
- else:
+ """Returns the next batch of data."""
+ if not self.iter_next():
+ raise StopIteration
+ data = self.getdata()
+ label = self.getlabel()
+ # iter should stop when last batch is not complete
+ if data[0].shape[0] != self.batch_size:
+ # in this case, cache it for next epoch
+ self._cache_data = data
+ self._cache_label = label
raise StopIteration
+ return DataBatch(data=data, label=label, \
+ pad=self.getpad(), index=None)
+
+ def _getdata(self, data_source, start=None, end=None):
+ """Load data from underlying arrays."""
+ assert start is not None or end is not None, 'should at least specify start or end'
+ start = start if start is not None else 0
+ if end is None:
+ end = data_source[0][1].shape[0] if data_source else 0
+ s = slice(start, end)
+ return [
+ x[1][s]
+ if isinstance(x[1], (np.ndarray, NDArray)) else
+ # h5py (only supports indices in increasing order)
+ array(x[1][sorted(self.idx[s])][[
+ list(self.idx[s]).index(i)
+ for i in sorted(self.idx[s])
+ ]]) for x in data_source
+ ]
- def _getdata(self, data_source):
- """Load data from underlying arrays, internal use only."""
- assert(self.cursor < self.num_data), "DataIter needs reset."
- if self.cursor + self.batch_size <= self.num_data:
+ def _concat(self, first_data, second_data):
+ """Helper function to concat two NDArrays."""
+ assert len(first_data) == len(
+ second_data), 'data source should contain the same size'
+ if first_data and second_data:
return [
- # np.ndarray or NDArray case
- x[1][self.cursor:self.cursor + self.batch_size]
- if isinstance(x[1], (np.ndarray, NDArray)) else
- # h5py (only supports indices in increasing order)
- array(x[1][sorted(self.idx[
- self.cursor:self.cursor + self.batch_size])][[
- list(self.idx[self.cursor:
- self.cursor + self.batch_size]).index(i)
- for i in sorted(self.idx[
- self.cursor:self.cursor + self.batch_size])
- ]]) for x in data_source
+ concat(
+ first_data[x],
+ second_data[x],
+ dim=0
+ ) for x in range(len(first_data))
]
+ elif (not first_data) and (not second_data):
+ return []
else:
- pad = self.batch_size - self.num_data + self.cursor
return [
- # np.ndarray or NDArray case
- concatenate([x[1][self.cursor:], x[1][:pad]])
- if isinstance(x[1], (np.ndarray, NDArray)) else
- # h5py (only supports indices in increasing order)
- concatenate([
- array(x[1][sorted(self.idx[self.cursor:])][[
- list(self.idx[self.cursor:]).index(i)
- for i in sorted(self.idx[self.cursor:])
- ]]),
- array(x[1][sorted(self.idx[:pad])][[
- list(self.idx[:pad]).index(i)
- for i in sorted(self.idx[:pad])
- ]])
- ]) for x in data_source
+ first_data[0] if first_data else second_data[0]
+ for x in range(len(first_data))
]
+ def _batchify(self, data_source):
+ """Load data from underlying arrays, internal use only."""
+ assert self.cursor < self.num_data, 'DataIter needs reset.'
+ # first batch of next epoch with 'roll_over'
+ if self.last_batch_handle == 'roll_over' and \
+ -self.batch_size < self.cursor < 0:
+ assert self._cache_data is not None or self._cache_label is not None, \
+ 'next epoch should have cached data'
+ cache_data = self._cache_data if self._cache_data is not None else self._cache_label
+ second_data = self._getdata(
+ data_source, end=self.cursor + self.batch_size)
+ if self._cache_data is not None:
+ self._cache_data = None
+ else:
+ self._cache_label = None
+ return self._concat(cache_data, second_data)
+ # last batch with 'pad'
+ elif self.last_batch_handle == 'pad' and \
+ self.cursor + self.batch_size > self.num_data:
+ pad = self.batch_size - self.num_data + self.cursor
+ first_data = self._getdata(data_source, start=self.cursor)
+ second_data = self._getdata(data_source, end=pad)
+ return self._concat(first_data, second_data)
+ # normal case
+ else:
+ if self.cursor + self.batch_size < self.num_data:
+ end_idx = self.cursor + self.batch_size
+ # get incomplete last batch
+ else:
+ end_idx = self.num_data
+ return self._getdata(data_source, self.cursor, end_idx)
+
def getdata(self):
- return self._getdata(self.data)
+ """Get data."""
+ return self._batchify(self.data)
def getlabel(self):
- return self._getdata(self.label)
+ """Get label."""
+ return self._batchify(self.label)
def getpad(self):
+ """Get pad value of DataBatch."""
if self.last_batch_handle == 'pad' and \
self.cursor + self.batch_size > self.num_data:
return self.cursor + self.batch_size - self.num_data
+ # check the first batch
+ elif self.last_batch_handle == 'roll_over' and \
+ -self.batch_size < self.cursor < 0:
+ return -self.cursor
else:
return 0
+ def _shuffle_data(self):
+ """Shuffle the data."""
+ # shuffle index
+ np.random.shuffle(self.idx)
+ # get the data by corresponding index
+ self.data = _getdata_by_idx(self.data, self.idx)
+ self.label = _getdata_by_idx(self.label, self.idx)
class MXDataIter(DataIter):
"""A python wrapper a C++ data iterator.
diff --git a/python/mxnet/io/utils.py b/python/mxnet/io/utils.py
new file mode 100644
index 000000000000..55ba34aea426
--- /dev/null
+++ b/python/mxnet/io/utils.py
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""utility functions for io.py"""
+from collections import OrderedDict
+
+import numpy as np
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
+from ..ndarray.sparse import CSRNDArray
+from ..ndarray.sparse import array as sparse_array
+from ..ndarray import NDArray
+from ..ndarray import array
+
+def _init_data(data, allow_empty, default_name):
+ """Convert data into canonical form."""
+ assert (data is not None) or allow_empty
+ if data is None:
+ data = []
+
+ if isinstance(data, (np.ndarray, NDArray, h5py.Dataset)
+ if h5py else (np.ndarray, NDArray)):
+ data = [data]
+ if isinstance(data, list):
+ if not allow_empty:
+ assert(len(data) > 0)
+ if len(data) == 1:
+ data = OrderedDict([(default_name, data[0])]) # pylint: disable=redefined-variable-type
+ else:
+ data = OrderedDict( # pylint: disable=redefined-variable-type
+ [('_%d_%s' % (i, default_name), d) for i, d in enumerate(data)])
+ if not isinstance(data, dict):
+ raise TypeError("Input must be NDArray, numpy.ndarray, h5py.Dataset " +
+ "a list of them or dict with them as values")
+ for k, v in data.items():
+ if not isinstance(v, (NDArray, h5py.Dataset) if h5py else NDArray):
+ try:
+ data[k] = array(v)
+ except:
+ raise TypeError(("Invalid type '%s' for %s, " % (type(v), k)) +
+ "should be NDArray, numpy.ndarray or h5py.Dataset")
+
+ return list(sorted(data.items()))
+
+
+def _has_instance(data, dtype):
+ """Return True if ``data`` has instance of ``dtype``.
+ This function is called after _init_data.
+ ``data`` is a list of (str, NDArray)"""
+ for item in data:
+ _, arr = item
+ if isinstance(arr, dtype):
+ return True
+ return False
+
+
+def _getdata_by_idx(data, idx):
+ """Shuffle the data."""
+ shuffle_data = []
+
+ for k, v in data:
+ if (isinstance(v, h5py.Dataset) if h5py else False):
+ shuffle_data.append((k, v))
+ elif isinstance(v, CSRNDArray):
+ shuffle_data.append((k, sparse_array(v.asscipy()[idx], v.context)))
+ else:
+ shuffle_data.append((k, array(v.asnumpy()[idx], v.context)))
+
+ return shuffle_data
diff --git a/python/mxnet/metric.py b/python/mxnet/metric.py
index 80ea26b5f368..6d9972074b67 100644
--- a/python/mxnet/metric.py
+++ b/python/mxnet/metric.py
@@ -98,7 +98,7 @@ def __str__(self):
def get_config(self):
"""Save configurations of metric. Can be recreated
- from configs with metric.create(**config)
+ from configs with metric.create(``**config``)
"""
config = self._kwargs.copy()
config.update({
diff --git a/python/mxnet/module/base_module.py b/python/mxnet/module/base_module.py
index c534261eacc2..babea53d6e40 100644
--- a/python/mxnet/module/base_module.py
+++ b/python/mxnet/module/base_module.py
@@ -279,8 +279,8 @@ def score(self, eval_data, eval_metric, num_batch=None, batch_end_callback=None,
def iter_predict(self, eval_data, num_batch=None, reset=True, sparse_row_id_fn=None):
"""Iterates over predictions.
- Example Usage:
- ----------
+ Examples
+ --------
>>> for pred, i_batch, batch in module.iter_predict(eval_data):
... # pred is a list of outputs from the module
... # i_batch is a integer
diff --git a/python/mxnet/module/sequential_module.py b/python/mxnet/module/sequential_module.py
index 8d563a4def7e..507f3f730f2e 100644
--- a/python/mxnet/module/sequential_module.py
+++ b/python/mxnet/module/sequential_module.py
@@ -50,34 +50,35 @@ def __init__(self, logger=logging):
if x.startswith('META_')])
def add(self, module, **kwargs):
- """Adds a module to the chain.
+ """Add a module to the chain.
Parameters
----------
module : BaseModule
The new module to add.
- kwargs : **keywords
+ kwargs : ``**keywords``
All the keyword arguments are saved as meta information
for the added module. The currently known meta includes
- `take_labels`: indicating whether the module expect to
- take labels when doing computation. Note any module in
- the chain can take labels (not necessarily only the top
- most one), and they all take the same labels passed
- from the original data batch for the `SequentialModule`.
+ take labels when doing computation. Note any module in
+ the chain can take labels (not necessarily only the top
+ most one), and they all take the same labels passed
+ from the original data batch for the `SequentialModule`.
+
Returns
-------
self
This function returns `self` to allow us to easily chain a
series of `add` calls.
-
Examples
--------
>>> # An example of addinging two modules to a chain.
>>> seq_mod = mx.mod.SequentialModule()
>>> seq_mod.add(mod1)
>>> seq_mod.add(mod2)
+
"""
self._modules.append(module)
diff --git a/python/mxnet/ndarray/contrib.py b/python/mxnet/ndarray/contrib.py
index 67ee7e4d68b7..6bbee8a6fbc3 100644
--- a/python/mxnet/ndarray/contrib.py
+++ b/python/mxnet/ndarray/contrib.py
@@ -19,6 +19,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import,redefined-outer-name
"""Contrib NDArray API of MXNet."""
import math
+import numpy as np
from ..context import current_context
from ..random import uniform
from ..base import _as_list
@@ -28,7 +29,7 @@
except ImportError:
pass
-__all__ = ["rand_zipfian", "foreach", "while_loop", "cond"]
+__all__ = ["rand_zipfian", "foreach", "while_loop", "cond", "isinf", "isfinite", "isnan"]
# pylint: disable=line-too-long
def rand_zipfian(true_classes, num_sampled, range_max, ctx=None):
@@ -139,9 +140,9 @@ def foreach(body, data, init_states):
NDArrays.
body takes two arguments as input and outputs a tuple of two elements,
- as illustrated below:
+ as illustrated below::
- out, states = body(data1, states)
+ out, states = body(data1, states)
data1 can be either an NDArray or a list of NDArrays. If data is an NDArray,
data1 is an NDArray. Otherwise, data1 is a list of NDArrays and has the same
@@ -151,15 +152,15 @@ def foreach(body, data, init_states):
are the second output of foreach.
The computation done by this operator is equivalent to the pseudo code below
- when the input data is NDArray:
+ when the input data is NDArray::
- states = init_states
- outs = []
- for i in data.shape[0]:
- s = data[i]
- out, states = body(s, states)
- outs.append(out)
- outs = stack(*outs)
+ states = init_states
+ outs = []
+ for i in data.shape[0]:
+ s = data[i]
+ out, states = body(s, states)
+ outs.append(out)
+ outs = stack(*outs)
Parameters
@@ -436,8 +437,8 @@ def cond(pred, then_func, else_func):
--------
>>> a, b = mx.nd.array([1]), mx.nd.array([2])
>>> pred = a * b < 5
- >>> then_func = lambda a, b: (a + 5) * (b + 5)
- >>> else_func = lambda a, b: (a - 5) * (b - 5)
+ >>> then_func = lambda: (a + 5) * (b + 5)
+ >>> else_func = lambda: (a - 5) * (b - 5)
>>> outputs = mx.nd.contrib.cond(pred, then_func, else_func)
>>> outputs[0]
[42.]
@@ -460,3 +461,84 @@ def _to_python_scalar(inputs, type_, name):
return then_func()
else:
return else_func()
+
+def isinf(data):
+ """Performs an element-wise check to determine if the NDArray contains an infinite element
+ or not.
+
+
+ Parameters
+ ----------
+ input : NDArray
+ An N-D NDArray.
+
+ Returns
+ -------
+ output: NDArray
+ The output NDarray, with same shape as input, where 1 indicates the array element is
+ equal to positive or negative infinity and 0 otherwise.
+
+ Examples
+ --------
+ >>> data = mx.nd.array([np.inf, -np.inf, np.NINF, -1])
+ >>> output = mx.nd.contrib.isinf(data)
+ >>> output
+ [1. 1. 1. 0.]
+
+ """
+ return data.abs() == np.inf
+
+def isfinite(data):
+ """Performs an element-wise check to determine if the NDArray contains an infinite element
+ or not.
+
+
+ Parameters
+ ----------
+ input : NDArray
+ An N-D NDArray.
+
+ Returns
+ -------
+ output: NDArray
+ The output NDarray, with same shape as input, where 1 indicates the array element is
+ finite i.e. not equal to positive or negative infinity and 0 in places where it is
+ positive or negative infinity.
+
+ Examples
+ --------
+ >>> data = mx.nd.array([np.inf, -np.inf, np.NINF, -1])
+ >>> output = mx.nd.contrib.isfinite(data)
+ >>> output
+ [0. 0. 0. 1.]
+
+ """
+ is_data_not_nan = data == data
+ is_data_not_infinite = data.abs() != np.inf
+ return ndarray.logical_and(is_data_not_infinite, is_data_not_nan)
+
+def isnan(data):
+ """Performs an element-wise check to determine if the NDArray contains a NaN element
+ or not.
+
+
+ Parameters
+ ----------
+ input : NDArray
+ An N-D NDArray.
+
+ Returns
+ -------
+ output: NDArray
+ The output NDarray, with same shape as input, where 1 indicates the array element is
+ NaN i.e. Not a Number and 0 otherwise.
+
+ Examples
+ --------
+ >>> data = mx.nd.array([np.nan, -1])
+ >>> output = mx.nd.contrib.isnan(data)
+ >>> output
+ [1. 0.]
+
+ """
+ return data != data
diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py
index de2ad692adfc..78ec0b91f88d 100644
--- a/python/mxnet/ndarray/ndarray.py
+++ b/python/mxnet/ndarray/ndarray.py
@@ -399,7 +399,7 @@ def __setitem__(self, key, value):
Parameters
----------
- key : int, slice, list, np.ndarray, NDArray, or tuple of all previous types
+ key : int, mxnet.ndarray.slice, list, np.ndarray, NDArray, or tuple of all previous types
The indexing key.
value : scalar or array-like object that can be broadcast to the shape of self[key]
The value to set.
@@ -467,7 +467,7 @@ def __getitem__(self, key):
Parameters
----------
- key : int, slice, list, np.ndarray, NDArray, or tuple of all previous types
+ key : int, mxnet.ndarray.slice, list, np.ndarray, NDArray, or tuple of all previous types
Indexing key.
Examples
@@ -866,10 +866,10 @@ def _sync_copyfrom(self, source_array):
except:
raise TypeError('array must consist of array-like data,' +
'type %s is not supported' % str(type(array)))
- source_array = np.ascontiguousarray(source_array, dtype=self.dtype)
+ source_array = np.asarray(source_array, dtype=self.dtype, order='C')
if source_array.shape != self.shape:
raise ValueError('Shape inconsistent: expected %s vs got %s'%(
- str(self.shape), str(source_array.shape)))
+ str(source_array.shape), str(self.shape)))
check_call(_LIB.MXNDArraySyncCopyFromCPU(
self.handle,
source_array.ctypes.data_as(ctypes.c_void_p),
@@ -998,7 +998,7 @@ def reshape(self, *shape, **kwargs):
Example::
- - without reverse=1, for input shape = (10,5,4), shape = (-1,0), output shape would be
+ - without reverse=1, for input shape = (10,5,4), shape = (-1,0), output shape would be \
(40,5).
- with reverse=1, output shape will be (50,4).
@@ -2547,6 +2547,9 @@ def arange(start, stop=None, step=1.0, repeat=1, infer_range=False, ctx=None, dt
Spacing between values. The default step size is 1.
repeat : int, optional
Number of times to repeat each element. The default repeat count is 1.
+ infer_range : boolean, optional
+ When set to True, infer the stop position from the start, step,
+ repeat, and output tensor size.
ctx : Context, optional
Device context. Default context is the current default context.
dtype : str or numpy.dtype, optional
@@ -2639,9 +2642,9 @@ def add(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be added.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be added.
If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -2701,9 +2704,9 @@ def subtract(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be subtracted.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be subtracted.
If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -2762,9 +2765,9 @@ def multiply(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be multiplied.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be multiplied.
If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -2823,9 +2826,9 @@ def divide(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array in division.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array in division.
The arrays to be divided. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -2880,9 +2883,9 @@ def modulo(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array in modulo.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array in modulo.
The arrays to be taken modulo. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -2999,9 +3002,9 @@ def maximum(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3056,9 +3059,9 @@ def minimum(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3117,9 +3120,9 @@ def equal(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3181,9 +3184,9 @@ def not_equal(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3248,9 +3251,9 @@ def greater(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3312,9 +3315,9 @@ def greater_equal(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3376,9 +3379,9 @@ def lesser(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3440,9 +3443,9 @@ def lesser_equal(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First array to be compared.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second array to be compared. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3503,9 +3506,9 @@ def logical_and(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First input of the function.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second input of the function. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3563,9 +3566,9 @@ def logical_or(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First input of the function.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second input of the function. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -3623,9 +3626,9 @@ def logical_xor(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.array
First input of the function.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.array
Second input of the function. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
diff --git a/python/mxnet/ndarray/sparse.py b/python/mxnet/ndarray/sparse.py
index 7b4cc90648c2..928079749db5 100644
--- a/python/mxnet/ndarray/sparse.py
+++ b/python/mxnet/ndarray/sparse.py
@@ -195,7 +195,8 @@ def asnumpy(self):
return self.tostype('default').asnumpy()
def astype(self, dtype, copy=True):
- """Returns a copy of the array after casting to a specified type.
+ """Return a copy of the array after casting to a specified type.
+
Parameters
----------
dtype : numpy.dtype or str
@@ -205,6 +206,7 @@ def astype(self, dtype, copy=True):
allocated ndarray on the same context. If this is set to
`False`, and the dtype requested is the same as the ndarray's
dtype, the ndarray is returned instead of a copy.
+
Examples
--------
>>> x = mx.nd.sparse.zeros('row_sparse', (2,3), dtype='float32')
@@ -339,7 +341,7 @@ def __getitem__(self, key):
Parameters
----------
- key : int or slice
+ key : int or mxnet.ndarray.NDArray.slice
Indexing key.
Examples
@@ -387,7 +389,7 @@ def __setitem__(self, key, value):
Parameters
----------
- key : slice
+ key : mxnet.ndarray.NDArray.slice
The indexing key.
value : NDArray or CSRNDArray or numpy.ndarray
The value to set.
@@ -418,7 +420,7 @@ def __setitem__(self, key, value):
if isinstance(key, py_slice):
if key.step is not None or key.start is not None or key.stop is not None:
raise ValueError('Assignment with slice for CSRNDArray is not ' \
- 'implmented yet.')
+ 'implemented yet.')
if isinstance(value, NDArray):
# avoid copying to itself
if value.handle is not self.handle:
@@ -624,7 +626,7 @@ def __getitem__(self, key):
Parameters
----------
- key : slice
+ key : mxnet.ndarray.NDArray.slice
Indexing key.
Examples
@@ -652,7 +654,7 @@ def __setitem__(self, key, value):
Parameters
----------
- key : slice
+ key : mxnet.ndarray.NDArray.slice
The indexing key.
value : NDArray or numpy.ndarray
The value to set.
@@ -1023,28 +1025,28 @@ def row_sparse_array(arg1, shape=None, ctx=None, dtype=None):
- row_sparse_array(D):
to construct a RowSparseNDArray with a dense ndarray ``D``
- - **D** (*array_like*) - An object exposing the array interface, an object whose \
- `__array__` method returns an array, or any (nested) sequence.
- - **ctx** (*Context, optional*) - Device context \
- (default is the current default context).
- - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
- The default dtype is ``D.dtype`` if ``D`` is an NDArray or numpy.ndarray, \
- float32 otherwise.
+ - **D** (*array_like*) - An object exposing the array interface, an object whose \
+ `__array__` method returns an array, or any (nested) sequence.
+ - **ctx** (*Context, optional*) - Device context \
+ (default is the current default context).
+ - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
+ The default dtype is ``D.dtype`` if ``D`` is an NDArray or numpy.ndarray, \
+ float32 otherwise.
- row_sparse_array(S)
to construct a RowSparseNDArray with a sparse ndarray ``S``
- - **S** (*RowSparseNDArray*) - A sparse ndarray.
- - **ctx** (*Context, optional*) - Device context \
- (default is the current default context).
- - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
- The default dtype is ``S.dtype``.
+ - **S** (*RowSparseNDArray*) - A sparse ndarray.
+ - **ctx** (*Context, optional*) - Device context \
+ (default is the current default context).
+ - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
+ The default dtype is ``S.dtype``.
- row_sparse_array((D0, D1 .. Dn))
to construct an empty RowSparseNDArray with shape ``(D0, D1, ... Dn)``
- - **D0, D1 .. Dn** (*int*) - The shape of the ndarray
- - **ctx** (*Context, optional*) - Device context \
- (default is the current default context).
- - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
+ - **D0, D1 .. Dn** (*int*) - The shape of the ndarray
+ - **ctx** (*Context, optional*) - Device context \
+ (default is the current default context).
+ - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
The default dtype is float32.
- row_sparse_array((data, indices))
@@ -1055,35 +1057,35 @@ def row_sparse_array(arg1, shape=None, ctx=None, dtype=None):
represented by RowSparseNDArray ``rsp`` has \
``dense[rsp.indices[i], :, :, :, ...] = rsp.data[i, :, :, :, ...]``
The row indices for are expected to be **sorted in ascending order.** \
- - **data** (*array_like*) - An object exposing the array interface, which \
- holds all the non-zero row slices of the array.
- - **indices** (*array_like*) - An object exposing the array interface, which \
- stores the row index for each row slice with non-zero elements.
- - **shape** (*tuple of int, optional*) - The shape of the array. The default \
- shape is inferred from the indices and indptr arrays.
- - **ctx** (*Context, optional*) - Device context \
- (default is the current default context).
- - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
- The default dtype is float32.
+ - **data** (*array_like*) - An object exposing the array interface, which \
+ holds all the non-zero row slices of the array.
+ - **indices** (*array_like*) - An object exposing the array interface, which \
+ stores the row index for each row slice with non-zero elements.
+ - **shape** (*tuple of int, optional*) - The shape of the array. The default \
+ shape is inferred from the indices and indptr arrays.
+ - **ctx** (*Context, optional*) - Device context \
+ (default is the current default context).
+ - **dtype** (*str or numpy.dtype, optional*) - The data type of the output array. \
+ The default dtype is float32.
Parameters
----------
- arg1: NDArray, numpy.ndarray, RowSparseNDArray, tuple of int or tuple of array_like
+ arg1 : NDArray, numpy.ndarray, RowSparseNDArray, tuple of int or tuple of array_like
The argument to help instantiate the row sparse ndarray. See above for further details.
shape : tuple of int, optional
- The shape of the row sparse ndarray.
+ The shape of the row sparse ndarray. (Default value = None)
ctx : Context, optional
Device context (default is the current default context).
dtype : str or numpy.dtype, optional
- The data type of the output array.
+ The data type of the output array. (Default value = None)
Returns
-------
RowSparseNDArray
An `RowSparseNDArray` with the `row_sparse` storage representation.
- Example
- -------
+ Examples
+ --------
>>> a = mx.nd.sparse.row_sparse_array(([[1, 2], [3, 4]], [1, 4]), shape=(6, 2))
>>> a.asnumpy()
array([[ 0., 0.],
@@ -1203,9 +1205,9 @@ def add(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.sparse.array
First array to be added.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.sparse.array
Second array to be added.
If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -1275,9 +1277,9 @@ def subtract(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.sparse.array
First array to be subtracted.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.sparse.array
Second array to be subtracted.
If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.__spec__
@@ -1346,9 +1348,9 @@ def multiply(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.sparse.array
First array to be multiplied.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.sparse.array
Second array to be multiplied.
If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
@@ -1430,9 +1432,9 @@ def divide(lhs, rhs):
Parameters
----------
- lhs : scalar or array
+ lhs : scalar or mxnet.ndarray.sparse.array
First array in division.
- rhs : scalar or array
+ rhs : scalar or mxnet.ndarray.sparse.array
Second array in division.
The arrays to be divided. If ``lhs.shape != rhs.shape``, they must be
broadcastable to a common shape.
diff --git a/python/mxnet/optimizer/__init__.py b/python/mxnet/optimizer/__init__.py
new file mode 100644
index 000000000000..72eb5a741520
--- /dev/null
+++ b/python/mxnet/optimizer/__init__.py
@@ -0,0 +1,24 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Optimizer API of MXNet."""
+
+from . import optimizer, contrib
+# pylint: disable=wildcard-import
+from .optimizer import *
+# pylint: enable=wildcard-import
+
+__all__ = optimizer.__all__ + ['contrib']
diff --git a/python/mxnet/optimizer/contrib.py b/python/mxnet/optimizer/contrib.py
new file mode 100644
index 000000000000..d269aa1bd069
--- /dev/null
+++ b/python/mxnet/optimizer/contrib.py
@@ -0,0 +1,100 @@
+# coding: utf-8
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# pylint: disable=too-many-lines
+"""Contrib optimizers."""
+from ..ndarray import (NDArray, clip, contrib, mean, sqrt, square, zeros)
+from .optimizer import Optimizer
+
+# convenience wrapper for Optimizer.Register
+register = Optimizer.register # pylint: disable=invalid-name
+
+__all__ = ['GroupAdaGrad']
+
+
+@register
+class GroupAdaGrad(Optimizer):
+ """Adagrad optimizer with row-wise learning rates.
+
+ This class implements the AdaGrad optimizer described in *Adaptive
+ Subgradient Methods for Online Learning and Stochastic Optimization*, and
+ available at http://www.jmlr.org/papers/volume12/duchi11a/duchi11a.pdf but
+ uses only a single learning rate for every row of the parameter array.
+
+ This optimizer updates each weight by::
+
+ grad = clip(grad * rescale_grad, clip_gradient)
+ history += mean(square(grad), axis=1, keepdims=True)
+ div = grad / sqrt(history + float_stable_eps)
+ weight -= div * lr
+
+ Weights are updated lazily if the gradient is sparse.
+
+ For details of the update algorithm see
+ :class:`~mxnet.ndarray.contrib.group_adagrad_update`.
+
+ This optimizer accepts the following parameters in addition to those
+ accepted by :class:`.Optimizer`. Weight decay is not supported.
+
+ Parameters
+ ----------
+ eps: float, optional
+ Initial value of the history accumulator. Avoids division by 0.
+
+ """
+
+ def __init__(self, eps=1e-5, **kwargs):
+ super(GroupAdaGrad, self).__init__(**kwargs)
+ self.float_stable_eps = eps
+
+ def create_state(self, index, weight):
+ assert len(weight.shape) == 2
+ history = zeros(
+ (weight.shape[0], 1), weight.context, stype=weight.stype)
+ return history
+
+ def update(self, index, weight, grad, state):
+ assert (isinstance(weight, NDArray))
+ assert (isinstance(grad, NDArray))
+ self._update_count(index)
+ lr = self._get_lr(index)
+ wd = self._get_wd(index)
+ assert wd == 0, 'Weight decay is not supported for GroupAdaGrad'
+
+ is_sparse = grad.stype == 'row_sparse'
+ if is_sparse:
+ kwargs = {
+ 'epsilon': self.float_stable_eps,
+ 'rescale_grad': self.rescale_grad
+ }
+ if self.clip_gradient:
+ kwargs['clip_gradient'] = self.clip_gradient
+ contrib.group_adagrad_update(
+ weight,
+ grad,
+ state,
+ out=weight,
+ lr=lr,
+ **kwargs)
+ else:
+ grad = grad * self.rescale_grad
+ if self.clip_gradient is not None:
+ grad = clip(grad, -self.clip_gradient, self.clip_gradient)
+ state[:] += mean(square(grad), axis=1, keepdims=True)
+ div = lr * grad / sqrt(state + self.float_stable_eps)
+ weight[:] -= div
diff --git a/python/mxnet/optimizer.py b/python/mxnet/optimizer/optimizer.py
similarity index 95%
rename from python/mxnet/optimizer.py
rename to python/mxnet/optimizer/optimizer.py
index b69d0c9af0dc..d632a8c7c640 100644
--- a/python/mxnet/optimizer.py
+++ b/python/mxnet/optimizer/optimizer.py
@@ -23,13 +23,19 @@
import pickle
import warnings
import numpy
-from .base import py_str
-from .ndarray import (NDArray, zeros, clip, sqrt, cast, maximum, abs as NDabs, array, multiply)
-from .ndarray import (sgd_update, sgd_mom_update, adam_update, rmsprop_update, rmspropalex_update,
- mp_sgd_update, mp_sgd_mom_update, square, ftrl_update, ftml_update,
- signsgd_update, signum_update)
-from .ndarray import sparse
-from .random import normal
+from ..base import py_str
+from ..ndarray import (NDArray, zeros, clip, sqrt, cast, maximum, abs as NDabs, array, multiply)
+from ..ndarray import (sgd_update, sgd_mom_update, adam_update, rmsprop_update, rmspropalex_update,
+ mp_sgd_update, mp_sgd_mom_update, square, ftrl_update, ftml_update,
+ signsgd_update, signum_update)
+from ..ndarray import sparse
+from ..random import normal
+
+__all__ = [
+ 'AdaDelta', 'AdaGrad', 'Adam', 'Adamax', 'DCASGD', 'FTML', 'Ftrl', 'LBSGD',
+ 'NAG', 'NDabs', 'Nadam', 'Optimizer', 'RMSProp', 'SGD', 'SGLD', 'Signum',
+ 'Test', 'Updater', 'ccSGD', 'create', 'get_updater', 'register'
+]
class Optimizer(object):
@@ -64,11 +70,12 @@ class Optimizer(object):
The initial number of updates.
multi_precision : bool, optional
- Flag to control the internal precision of the optimizer.
- ``False`` results in using the same precision as the weights (default),
- ``True`` makes internal 32-bit copy of the weights and applies gradients
- in 32-bit precision even if actual weights used in the model have lower precision.
- Turning this on can improve convergence and accuracy when training with float16.
+ Flag to control the internal precision of the optimizer.::
+
+ False: results in using the same precision as the weights (default),
+ True: makes internal 32-bit copy of the weights and applies gradients
+ in 32-bit precision even if actual weights used in the model have lower precision.
+ Turning this on can improve convergence and accuracy when training with float16.
Properties
----------
@@ -475,16 +482,17 @@ class SGD(Optimizer):
Parameters
----------
momentum : float, optional
- The momentum value.
+ The momentum value.
lazy_update : bool, optional
- Default is True. If True, lazy updates are applied \
- if the storage types of weight and grad are both ``row_sparse``.
+ Default is True. If True, lazy updates are applied \
+ if the storage types of weight and grad are both ``row_sparse``.
multi_precision: bool, optional
- Flag to control the internal precision of the optimizer.
- ``False`` results in using the same precision as the weights (default),
- ``True`` makes internal 32-bit copy of the weights and applies gradients \
- in 32-bit precision even if actual weights used in the model have lower precision.\
- Turning this on can improve convergence and accuracy when training with float16.
+ Flag to control the internal precision of the optimizer.::
+
+ False: results in using the same precision as the weights (default),
+ True: makes internal 32-bit copy of the weights and applies gradients
+ in 32-bit precision even if actual weights used in the model have lower precision.
+ Turning this on can improve convergence and accuracy when training with float16.
"""
def __init__(self, momentum=0.0, lazy_update=True, **kwargs):
super(SGD, self).__init__(**kwargs)
@@ -686,20 +694,21 @@ class LBSGD(Optimizer):
Parameters
----------
momentum : float, optional
- The momentum value.
+ The momentum value.
multi_precision: bool, optional
- Flag to control the internal precision of the optimizer.
- ``False`` results in using the same precision as the weights (default),
- ``True`` makes internal 32-bit copy of the weights and applies gradients
- in 32-bit precision even if actual weights used in the model have lower precision.`<
- Turning this on can improve convergence and accuracy when training with float16.
+ Flag to control the internal precision of the optimizer.::
+
+ False: results in using the same precision as the weights (default),
+ True: makes internal 32-bit copy of the weights and applies gradients
+ in 32-bit precision even if actual weights used in the model have lower precision.
+ Turning this on can improve convergence and accuracy when training with float16.
+
warmup_strategy: string ('linear', 'power2', 'sqrt'. , 'lars' default : 'linear')
warmup_epochs: unsigned, default: 5
batch_scale: unsigned, default: 1 (same as batch size*numworkers)
updates_per_epoch: updates_per_epoch (default: 32, Default might not reflect true number batches per epoch. Used for warmup.)
begin_epoch: unsigned, default 0, starting epoch.
"""
-
def __init__(self, momentum=0.0, multi_precision=False, warmup_strategy='linear',
warmup_epochs=5, batch_scale=1, updates_per_epoch=32, begin_epoch=0, num_epochs=60,
**kwargs):
@@ -928,11 +937,12 @@ class NAG(Optimizer):
momentum : float, optional
The momentum value.
multi_precision: bool, optional
- Flag to control the internal precision of the optimizer.
- ``False`` results in using the same precision as the weights (default),
- ``True`` makes internal 32-bit copy of the weights and applies gradients \
- in 32-bit precision even if actual weights used in the model have lower precision.\
- Turning this on can improve convergence and accuracy when training with float16.
+ Flag to control the internal precision of the optimizer.::
+
+ False: results in using the same precision as the weights (default),
+ True: makes internal 32-bit copy of the weights and applies gradients
+ in 32-bit precision even if actual weights used in the model have lower precision.
+ Turning this on can improve convergence and accuracy when training with float16.
"""
def __init__(self, momentum=0.0, **kwargs):
super(NAG, self).__init__(**kwargs)
@@ -1170,9 +1180,11 @@ class RMSProp(Optimizer):
epsilon : float, optional
Small value to avoid division by 0.
centered : bool, optional
- Flag to control which version of RMSProp to use.
- ``True`` will use Graves's version of `RMSProp`,
- ``False`` will use Tieleman & Hinton's version of `RMSProp`.
+ Flag to control which version of RMSProp to use.::
+
+ True: will use Graves's version of `RMSProp`,
+ False: will use Tieleman & Hinton's version of `RMSProp`.
+
clip_weights : float, optional
Clips weights into range ``[-clip_weights, clip_weights]``.
"""
diff --git a/python/mxnet/recordio.py b/python/mxnet/recordio.py
index 2ebe657accbd..2def141c9340 100644
--- a/python/mxnet/recordio.py
+++ b/python/mxnet/recordio.py
@@ -36,8 +36,8 @@
class MXRecordIO(object):
"""Reads/writes `RecordIO` data format, supporting sequential read and write.
- Example usage:
- ----------
+ Examples
+ ---------
>>> record = mx.recordio.MXRecordIO('tmp.rec', 'w')
>>> for i in range(5):
@@ -83,6 +83,32 @@ def open(self):
def __del__(self):
self.close()
+ def __getstate__(self):
+ """Override pickling behavior."""
+ # pickling pointer is not allowed
+ is_open = self.is_open
+ self.close()
+ d = dict(self.__dict__)
+ d['is_open'] = is_open
+ uri = self.uri.value
+ try:
+ uri = uri.decode('utf-8')
+ except AttributeError:
+ pass
+ del d['handle']
+ d['uri'] = uri
+ return d
+
+ def __setstate__(self, d):
+ """Restore from pickled."""
+ self.__dict__ = d
+ is_open = d['is_open']
+ self.is_open = False
+ self.handle = RecordIOHandle()
+ self.uri = c_str(self.uri)
+ if is_open:
+ self.open()
+
def close(self):
"""Closes the record file."""
if not self.is_open:
@@ -98,8 +124,8 @@ def reset(self):
If the record is opened with 'w', this function will truncate the file to empty.
- Example usage:
- ----------
+ Examples
+ ---------
>>> record = mx.recordio.MXRecordIO('tmp.rec', 'r')
>>> for i in range(2):
... item = record.read()
@@ -117,8 +143,8 @@ def reset(self):
def write(self, buf):
"""Inserts a string buffer as a record.
- Example usage:
- ----------
+ Examples
+ ---------
>>> record = mx.recordio.MXRecordIO('tmp.rec', 'w')
>>> for i in range(5):
... record.write('record_%d'%i)
@@ -137,8 +163,8 @@ def write(self, buf):
def read(self):
"""Returns record as a string.
- Example usage:
- ----------
+ Examples
+ ---------
>>> record = mx.recordio.MXRecordIO('tmp.rec', 'r')
>>> for i in range(5):
... item = record.read()
@@ -170,8 +196,8 @@ def read(self):
class MXIndexedRecordIO(MXRecordIO):
"""Reads/writes `RecordIO` data format, supporting random access.
- Example usage:
- ----------
+ Examples
+ ---------
>>> for i in range(5):
... record.write_idx(i, 'record_%d'%i)
>>> record.close()
@@ -217,6 +243,12 @@ def close(self):
super(MXIndexedRecordIO, self).close()
self.fidx.close()
+ def __getstate__(self):
+ """Override pickling behavior."""
+ d = super(MXIndexedRecordIO, self).__getstate__()
+ d['fidx'] = None
+ return d
+
def seek(self, idx):
"""Sets the current read pointer position.
@@ -229,8 +261,8 @@ def seek(self, idx):
def tell(self):
"""Returns the current position of write head.
- Example usage:
- ----------
+ Examples
+ ---------
>>> record = mx.recordio.MXIndexedRecordIO('tmp.idx', 'tmp.rec', 'w')
>>> print(record.tell())
0
@@ -251,8 +283,8 @@ def tell(self):
def read_idx(self, idx):
"""Returns the record at given index.
- Example usage:
- ----------
+ Examples
+ ---------
>>> record = mx.recordio.MXIndexedRecordIO('tmp.idx', 'tmp.rec', 'w')
>>> for i in range(5):
... record.write_idx(i, 'record_%d'%i)
@@ -267,8 +299,8 @@ def read_idx(self, idx):
def write_idx(self, idx, buf):
"""Inserts input record at given index.
- Example usage:
- ----------
+ Examples
+ ---------
>>> for i in range(5):
... record.write_idx(i, 'record_%d'%i)
>>> record.close()
diff --git a/python/mxnet/rnn/rnn.py b/python/mxnet/rnn/rnn.py
index 47307c55b042..0255c55dbef7 100644
--- a/python/mxnet/rnn/rnn.py
+++ b/python/mxnet/rnn/rnn.py
@@ -35,7 +35,7 @@ def save_rnn_checkpoint(cells, prefix, epoch, symbol, arg_params, aux_params):
Parameters
----------
- cells : RNNCell or list of RNNCells
+ cells : mxnet.rnn.RNNCell or list of RNNCells
The RNN cells used by this symbol.
prefix : str
Prefix of model name.
@@ -65,7 +65,7 @@ def load_rnn_checkpoint(cells, prefix, epoch):
Parameters
----------
- cells : RNNCell or list of RNNCells
+ cells : mxnet.rnn.RNNCell or list of RNNCells
The RNN cells used by this symbol.
prefix : str
Prefix of model name.
@@ -100,7 +100,7 @@ def do_rnn_checkpoint(cells, prefix, period=1):
Parameters
----------
- cells : RNNCell or list of RNNCells
+ cells : mxnet.rnn.RNNCell or list of RNNCells
The RNN cells used by this symbol.
prefix : str
The file prefix to checkpoint to
diff --git a/python/mxnet/rnn/rnn_cell.py b/python/mxnet/rnn/rnn_cell.py
index 3301102ba905..3f8a459e40d7 100644
--- a/python/mxnet/rnn/rnn_cell.py
+++ b/python/mxnet/rnn/rnn_cell.py
@@ -368,7 +368,7 @@ class RNNCell(BaseRNNCell):
Number of units in output symbol.
activation : str or Symbol, default 'tanh'
Type of activation function. Options are 'relu' and 'tanh'.
- prefix : str, default 'rnn_'
+ prefix : str, default ``'rnn_'``
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -412,7 +412,7 @@ class LSTMCell(BaseRNNCell):
----------
num_hidden : int
Number of units in output symbol.
- prefix : str, default 'lstm_'
+ prefix : str, default ``'lstm_'``
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -475,7 +475,7 @@ class GRUCell(BaseRNNCell):
----------
num_hidden : int
Number of units in output symbol.
- prefix : str, default 'gru_'
+ prefix : str, default ``'gru_'``
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -554,7 +554,7 @@ class FusedRNNCell(BaseRNNCell):
Whether to return the states that can be used as starting states next time.
forget_bias : bias added to forget gate, default 1.0.
Jozefowicz et al. 2015 recommends setting this to 1.0
- prefix : str, default '$mode_' such as 'lstm_'
+ prefix : str, default ``'$mode_'`` such as ``'lstm_'``
Prefix for names of layers
(this prefix is also used for names of weights if `params` is None
i.e. if `params` are being created and not reused)
@@ -716,7 +716,7 @@ def unfuse(self):
Returns
-------
- cell : SequentialRNNCell
+ cell : mxnet.rnn.SequentialRNNCell
unfused cell that can be used for stepping, and can run on CPU.
"""
stack = SequentialRNNCell()
@@ -832,7 +832,7 @@ class DropoutCell(BaseRNNCell):
dropout : float
Percentage of elements to drop out, which
is 1 - percentage to retain.
- prefix : str, default 'dropout_'
+ prefix : str, default ``'dropout_'``
Prefix for names of layers
(this prefix is also used for names of weights if `params` is None
i.e. if `params` are being created and not reused)
@@ -1007,7 +1007,7 @@ class BidirectionalCell(BaseRNNCell):
params : RNNParams, default None.
Container for weight sharing between cells.
A new RNNParams container is created if `params` is None.
- output_prefix : str, default 'bi_'
+ output_prefix : str, default ``'bi_'``
prefix for name of output
"""
def __init__(self, l_cell, r_cell, params=None, output_prefix='bi_'):
@@ -1207,7 +1207,7 @@ class ConvRNNCell(BaseConvRNNCell):
activation : str or Symbol,
default functools.partial(symbol.LeakyReLU, act_type='leaky', slope=0.2)
Type of activation function.
- prefix : str, default 'ConvRNN_'
+ prefix : str, default ``'ConvRNN_'``
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -1287,7 +1287,7 @@ class ConvLSTMCell(BaseConvRNNCell):
activation : str or Symbol
default functools.partial(symbol.LeakyReLU, act_type='leaky', slope=0.2)
Type of activation function.
- prefix : str, default 'ConvLSTM_'
+ prefix : str, default ``'ConvLSTM_'``
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
@@ -1379,7 +1379,7 @@ class ConvGRUCell(BaseConvRNNCell):
activation : str or Symbol,
default functools.partial(symbol.LeakyReLU, act_type='leaky', slope=0.2)
Type of activation function.
- prefix : str, default 'ConvGRU_'
+ prefix : str, default ``'ConvGRU_'``
Prefix for name of layers (and name of weight if params is None).
params : RNNParams, default None
Container for weight sharing between cells. Created if None.
diff --git a/python/mxnet/symbol/symbol.py b/python/mxnet/symbol/symbol.py
index 554539b424ad..530d72796c00 100644
--- a/python/mxnet/symbol/symbol.py
+++ b/python/mxnet/symbol/symbol.py
@@ -1347,7 +1347,7 @@ def simple_bind(self, ctx, grad_req='write', type_dict=None, stype_dict=None,
shared_buffer : Dict of string to `NDArray`
The dict mapping argument names to the `NDArray` that can be reused for initializing
the current executor. This buffer will be checked for reuse if one argument name
- of the current executor is not found in `shared_arg_names`. The `NDArray`s are
+ of the current executor is not found in `shared_arg_names`. The `NDArray` s are
expected have default storage type.
kwargs : Dict of str->shape
@@ -2439,6 +2439,23 @@ def squeeze(self, *args, **kwargs):
"""
return op.squeeze(self, *args, **kwargs)
+ def get_backend_symbol(self, backend):
+ """Return symbol for target backend.
+
+ Parameters
+ ----------
+ backend : str
+ The backend names.
+
+ Returns
+ -------
+ out : Symbol
+ The created Symbol for target backend.
+ """
+ out = SymbolHandle()
+ check_call(_LIB.MXGenBackendSubgraph(self.handle, c_str(backend), ctypes.byref(out)))
+ return Symbol(out)
+
def wait_to_read(self):
raise NotImplementedForSymbol(self.wait_to_read, None)
@@ -2909,6 +2926,9 @@ def arange(start, stop=None, step=1.0, repeat=1, infer_range=False, name=None, d
repeat : int, optional
"The repeating time of all elements.
E.g repeat=3, the element a will be repeated three times --> a, a, a.
+ infer_range : boolean, optional
+ When set to True, infer the stop position from the start, step,
+ repeat, and output tensor size.
dtype : str or numpy.dtype, optional
The value type of the inner value, default to ``np.float32``.
diff --git a/python/mxnet/symbol_doc.py b/python/mxnet/symbol_doc.py
index 3cb1997584d2..e59437a3ccbb 100644
--- a/python/mxnet/symbol_doc.py
+++ b/python/mxnet/symbol_doc.py
@@ -44,9 +44,6 @@
- *Examples*: simple and short code snippet showing how to use this operator.
It should show typical calling examples and behaviors (e.g. maps an input
of what shape to an output of what shape).
-- *Regression Test*: longer test code for the operators. We normally do not
- expect the users to read those, but they will be executed by `doctest` to
- ensure the behavior of each operator does not change unintentionally.
"""
from __future__ import absolute_import as _abs
import re as _re
@@ -75,8 +72,6 @@ class ActivationDoc(SymbolDoc):
>>> mlp
- Regression Test
- ---------------
ReLU activation
>>> test_suites = [
@@ -107,8 +102,6 @@ class DropoutDoc(SymbolDoc):
>>> data = Variable('data')
>>> data_dp = Dropout(data=data, p=0.2)
- Regression Test
- ---------------
>>> shape = (100, 100) # take larger shapes to be more statistical stable
>>> x = np.ones(shape)
>>> op = Dropout(p=0.5, name='dp')
@@ -141,8 +134,6 @@ class EmbeddingDoc(SymbolDoc):
>>> SymbolDoc.get_output_shape(op, letters=(seq_len, batch_size))
{'embed_output': (10L, 64L, 16L)}
- Regression Test
- ---------------
>>> vocab_size, embed_dim = (26, 16)
>>> batch_size = 12
>>> word_vecs = test_utils.random_arrays((vocab_size, embed_dim))
@@ -167,8 +158,6 @@ class FlattenDoc(SymbolDoc):
>>> SymbolDoc.get_output_shape(flatten, data=(2, 3, 4, 5))
{'flat_output': (2L, 60L)}
- Regression Test
- ---------------
>>> test_dims = [(2, 3, 4, 5), (2, 3), (2,)]
>>> op = Flatten(name='flat')
>>> for dims in test_dims:
@@ -208,8 +197,6 @@ class FullyConnectedDoc(SymbolDoc):
>>> net
- Regression Test
- ---------------
>>> dim_in, dim_out = (3, 4)
>>> x, w, b = test_utils.random_arrays((10, dim_in), (dim_out, dim_in), (dim_out,))
>>> op = FullyConnected(num_hidden=dim_out, name='FC')
diff --git a/python/mxnet/test_utils.py b/python/mxnet/test_utils.py
index 49a1a0f10277..14875601cd25 100644
--- a/python/mxnet/test_utils.py
+++ b/python/mxnet/test_utils.py
@@ -261,10 +261,14 @@ def rand_sparse_ndarray(shape, stype, density=None, dtype=None, distribution=Non
Parameters
----------
shape: list or tuple
- stype: str, valid values: "csr" or "row_sparse"
- density, optional: float, should be between 0 and 1
- distribution, optional: str, valid values: "uniform" or "powerlaw"
- dtype, optional: numpy.dtype, default value is None
+ stype: str
+ valid values: "csr" or "row_sparse"
+ density: float, optional
+ should be between 0 and 1
+ distribution: str, optional
+ valid values: "uniform" or "powerlaw"
+ dtype: numpy.dtype, optional
+ default value is None
Returns
-------
@@ -336,7 +340,7 @@ def rand_sparse_ndarray(shape, stype, density=None, dtype=None, distribution=Non
assert(False), "unknown storage type"
return False
-def rand_ndarray(shape, stype, density=None, dtype=None,
+def rand_ndarray(shape, stype='default', density=None, dtype=None,
modifier_func=None, shuffle_csr_indices=False, distribution=None):
if stype == 'default':
arr = mx.nd.array(random_arrays(shape), dtype=dtype)
@@ -753,7 +757,7 @@ def as_stype(var, stype, dtype):
if stype == 'default':
executor.arg_dict[k][:] = as_stype(v, stype, dtype=dtype)
for k in location:
- location[k] = np.ascontiguousarray(location[k])
+ location[k] = np.asarray(location[k], order='C')
for k, v in location.items():
if v.dtype.kind != 'f':
continue
@@ -801,10 +805,12 @@ def check_numeric_gradient(sym, location, aux_states=None, numeric_eps=1e-3, rto
location : list or tuple or dict
Argument values used as location to compute gradient
- - if type is list of numpy.ndarray
+ - if type is list of numpy.ndarray, \
inner elements should have the same order as mxnet.sym.list_arguments().
- - if type is dict of str -> numpy.ndarray
+
+ - if type is dict of str -> numpy.ndarray, \
maps the name of arguments to the corresponding numpy.ndarray.
+
*In either case, value of all the arguments must be provided.*
aux_states : list or tuple or dict, optional
The auxiliary states required when generating the executor for the symbol.
@@ -825,7 +831,7 @@ def check_numeric_gradient(sym, location, aux_states=None, numeric_eps=1e-3, rto
References
---------
- ..[1] https://github.com/Theano/Theano/blob/master/theano/gradient.py
+ [1] https://github.com/Theano/Theano/blob/master/theano/gradient.py
"""
assert dtype in (np.float16, np.float32, np.float64)
# cannot use finite differences with small eps without high precision
@@ -1379,7 +1385,7 @@ def list_gpus():
for cmd in nvidia_smi:
try:
re = subprocess.check_output([cmd, "-L"], universal_newlines=True)
- except OSError:
+ except (subprocess.CalledProcessError, OSError):
pass
return range(len([i for i in re.split('\n') if 'GPU' in i]))
@@ -1840,21 +1846,23 @@ def var_check(generator, sigma, nsamples=1000000):
def chi_square_check(generator, buckets, probs, nsamples=1000000):
"""Run the chi-square test for the generator. The generator can be both continuous and discrete.
- If the generator is continuous, the buckets should contain tuples of (range_min, range_max) and
- the probs should be the corresponding ideal probability within the specific ranges.
- Otherwise, the buckets should be the possible output of the discrete distribution and the probs
- should be groud-truth probability.
+
+ If the generator is continuous, the buckets should contain tuples of (range_min, range_max) \
+ and the probs should be the corresponding ideal probability within the specific ranges. \
+ Otherwise, the buckets should be the possible output of the discrete distribution and the \
+ probs should be groud-truth probability.
Usually the user is required to specify the probs parameter.
- After obtatining the p value, we could further use the standard p > 0.05 threshold to get
- the final result.
+ After obtatining the p value, we could further use the standard p > 0.05 threshold to get \
+ the final result.
Examples::
- buckets, probs = gen_buckets_probs_with_ppf(lambda x: ss.norm.ppf(x, 0, 1), 5)
- generator = lambda x: np.random.normal(0, 1.0, size=x)
- p = chi_square_check(generator=generator, buckets=buckets, probs=probs)
- assert(p > 0.05)
+
+ buckets, probs = gen_buckets_probs_with_ppf(lambda x: ss.norm.ppf(x, 0, 1), 5)
+ generator = lambda x: np.random.normal(0, 1.0, size=x)
+ p = chi_square_check(generator=generator, buckets=buckets, probs=probs)
+ assert(p > 0.05)
Parameters
----------
@@ -1863,8 +1871,8 @@ def chi_square_check(generator, buckets, probs, nsamples=1000000):
generator(N) should generate N random samples.
buckets: list of tuple or list of number
The buckets to run the chi-square the test. Make sure that the buckets cover
- the whole range of the distribution. Also, the buckets must be in ascending order and have
- no intersection
+ the whole range of the distribution. Also, the buckets must be in ascending order and have
+ no intersection
probs: list or tuple
The ground-truth probability of the random value fall in a specific bucket.
nsamples:int
@@ -1957,3 +1965,61 @@ def verify_generator(generator, buckets, probs, nsamples=1000000, nrepeat=5, suc
% (str(cs_ret_l), str(obs_freq_l), str(expected_freq_l),
str(buckets), str(probs)))
return cs_ret_l
+
+def compare_ndarray_tuple(t1, t2, rtol=None, atol=None):
+ """Compare ndarray tuple."""
+ if t1 is not None and t2 is not None:
+ if isinstance(t1, tuple):
+ for s1, s2 in zip(t1, t2):
+ compare_ndarray_tuple(s1, s2, rtol, atol)
+ else:
+ assert_almost_equal(t1.asnumpy(), t2.asnumpy(), rtol=rtol, atol=atol)
+
+
+def compare_optimizer(opt1, opt2, shape, dtype, w_stype='default', g_stype='default',
+ rtol=1e-4, atol=1e-5, compare_states=True):
+ """Compare opt1 and opt2."""
+ if w_stype == 'default':
+ w2 = mx.random.uniform(shape=shape, ctx=default_context(), dtype=dtype)
+ w1 = w2.copyto(default_context())
+ elif w_stype == 'row_sparse' or w_stype == 'csr':
+ w2 = rand_ndarray(shape, w_stype, density=1, dtype=dtype)
+ w1 = w2.copyto(default_context()).tostype('default')
+ else:
+ raise Exception("type not supported yet")
+ if g_stype == 'default':
+ g2 = mx.random.uniform(shape=shape, ctx=default_context(), dtype=dtype)
+ g1 = g2.copyto(default_context())
+ elif g_stype == 'row_sparse' or g_stype == 'csr':
+ g2 = rand_ndarray(shape, g_stype, dtype=dtype)
+ g1 = g2.copyto(default_context()).tostype('default')
+ else:
+ raise Exception("type not supported yet")
+
+ state1 = opt1.create_state_multi_precision(0, w1)
+ state2 = opt2.create_state_multi_precision(0, w2)
+ if compare_states:
+ compare_ndarray_tuple(state1, state2)
+
+ opt1.update_multi_precision(0, w1, g1, state1)
+ opt2.update_multi_precision(0, w2, g2, state2)
+ if compare_states:
+ compare_ndarray_tuple(state1, state2, rtol=rtol, atol=atol)
+ assert_almost_equal(w1.asnumpy(), w2.asnumpy(), rtol=rtol, atol=atol)
+
+class EnvManager(object):
+ """Environment variable setter and unsetter via with idiom"""
+ def __init__(self, key, val):
+ self._key = key
+ self._next_val = val
+ self._prev_val = None
+
+ def __enter__(self):
+ self._prev_val = os.environ.get(self._key)
+ os.environ[self._key] = self._next_val
+
+ def __exit__(self, ptype, value, trace):
+ if self._prev_val:
+ os.environ[self._key] = self._prev_val
+ else:
+ del os.environ[self._key]
diff --git a/python/mxnet/visualization.py b/python/mxnet/visualization.py
index 82946204847a..1ebdcb54f4ce 100644
--- a/python/mxnet/visualization.py
+++ b/python/mxnet/visualization.py
@@ -57,9 +57,22 @@ def print_summary(symbol, shape=None, line_length=120, positions=[.44, .64, .74,
Rotal length of printed lines
positions: list
Relative or absolute positions of log elements in each line.
+
Returns
------
None
+
+ Notes
+ -----
+ If ``mxnet`` is imported, the visualization module can be used in its short-form.
+ For example, if we ``import mxnet`` as follows::
+
+ import mxnet
+
+ this method in visualization module can be used in its short-form as::
+
+ mxnet.viz.print_summary(...)
+
"""
if not isinstance(symbol, Symbol):
raise TypeError("symbol must be Symbol")
@@ -157,6 +170,8 @@ def print_layer_summary(node, out_shape):
if show_shape:
num_filter = shape_dict[key][1]
cur_param = int(num_filter) * 2
+ elif op == 'Embedding':
+ cur_param = int(node["attrs"]['input_dim']) * int(node["attrs"]['output_dim'])
if not pre_node:
first_connection = ''
else:
@@ -211,12 +226,14 @@ def plot_network(symbol, title="plot", save_format='pdf', shape=None, node_attrs
input symbol names (str) to the corresponding tensor shape (tuple).
node_attrs: dict, optional
Specifies the attributes for nodes in the generated visualization. `node_attrs` is
- a dictionary of Graphviz attribute names and values. For example,
- ``node_attrs={"shape":"oval","fixedsize":"false"}``
- will use oval shape for nodes and allow variable sized nodes in the visualization.
+ a dictionary of Graphviz attribute names and values. For example::
+
+ node_attrs={"shape":"oval","fixedsize":"false"}
+
+ will use oval shape for nodes and allow variable sized nodes in the visualization.
hide_weights: bool, optional
- If True (default), then inputs with names of form *_weight (corresponding to weight
- tensors) or *_bias (corresponding to bias vectors) will be hidden for a cleaner
+ If True (default), then inputs with names of form *_weight* (corresponding to weight
+ tensors) or *_bias* (corresponding to bias vectors) will be hidden for a cleaner
visualization.
Returns
@@ -234,6 +251,18 @@ def plot_network(symbol, title="plot", save_format='pdf', shape=None, node_attrs
>>> digraph = mx.viz.plot_network(net, shape={'data':(100,200)},
... node_attrs={"fixedsize":"false"})
>>> digraph.view()
+
+ Notes
+ -----
+ If ``mxnet`` is imported, the visualization module can be used in its short-form.
+ For example, if we ``import mxnet`` as follows::
+
+ import mxnet
+
+ this method in visualization module can be used in its short-form as::
+
+ mxnet.viz.plot_network(...)
+
"""
# todo add shape support
try:
diff --git a/python/setup.py b/python/setup.py
index 915635398224..7385403b488b 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -30,7 +30,7 @@
else:
from setuptools import setup
from setuptools.extension import Extension
- kwargs = {'install_requires': ['numpy<=1.15.0,>=1.8.2', 'requests<2.19.0,>=2.18.4', 'graphviz<0.9.0,>=0.8.1'], 'zip_safe': False}
+ kwargs = {'install_requires': ['numpy<=1.15.2,>=1.8.2', 'requests<2.19.0,>=2.18.4', 'graphviz<0.9.0,>=0.8.1'], 'zip_safe': False}
with_cython = False
if '--with-cython' in sys.argv:
diff --git a/scala-package/.gitignore b/scala-package/.gitignore
new file mode 100644
index 000000000000..6aa4da6b1cfc
--- /dev/null
+++ b/scala-package/.gitignore
@@ -0,0 +1,8 @@
+.flattened-pom.xml
+core/src/main/scala/org/apache/mxnet/NDArrayAPIBase.scala
+core/src/main/scala/org/apache/mxnet/NDArrayBase.scala
+core/src/main/scala/org/apache/mxnet/javaapi/NDArrayBase.scala
+core/src/main/scala/org/apache/mxnet/SymbolAPIBase.scala
+core/src/main/scala/org/apache/mxnet/SymbolBase.scala
+examples/scripts/infer/images/
+examples/scripts/infer/models/
diff --git a/scala-package/README.md b/scala-package/README.md
index 79caaf309152..20fbee2469b0 100644
--- a/scala-package/README.md
+++ b/scala-package/README.md
@@ -1,7 +1,7 @@
Deep Learning for Scala/Java
=====
-[![Build Status](https://travis-ci.org/dmlc/mxnet.svg?branch=master)](https://travis-ci.org/dmlc/mxnet)
+[![Build Status](http://jenkins.mxnet-ci.amazon-ml.com/job/incubator-mxnet/job/master/badge/icon)](http://jenkins.mxnet-ci.amazon-ml.com/job/incubator-mxnet/job/master/)
[![GitHub license](http://dmlc.github.io/img/apache2.svg)](./LICENSE)
Here you find the MXNet Scala Package!
@@ -15,11 +15,13 @@ It brings flexible and efficient GPU/CPU computing and state-of-art deep learnin
Install
------------
-Technically, all you need is the `mxnet-full_2.10-{arch}-{xpu}-0.1.1.jar` in your classpath.
+Technically, all you need is the `mxnet-full_2.11-{arch}-{xpu}-{version}.jar` in your classpath.
It will automatically extract the native library to a tempfile and load it.
+You can find the pre-built jar file in [here](https://search.maven.org/search?q=g:org.apache.mxnet)
+ and also our nightly build package [here](https://repository.apache.org/#nexus-search;gav~org.apache.mxnet~)
Currently we provide `linux-x86_64-gpu`, `linux-x86_64-cpu` and `osx-x86_64-cpu`. Support for Windows will come soon.
-Use the following dependency in maven, change the artifactId according to your own architecture, e.g., `mxnet-full_2.10-osx-x86_64-cpu` for OSX (and cpu-only).
+Use the following dependency in maven, change the artifactId according to your own architecture, e.g., `mxnet-full_2.11-osx-x86_64-cpu` for OSX (and cpu-only).
```HTML
diff --git a/scala-package/core/pom.xml b/scala-package/core/pom.xml
index 0ee749419655..daf8c389cbeb 100644
--- a/scala-package/core/pom.xml
+++ b/scala-package/core/pom.xml
@@ -10,6 +10,10 @@
../pom.xml
+
+ true
+
+
mxnet-core_2.11
MXNet Scala Package - Core
@@ -20,12 +24,6 @@
false
-
- integrationtest
-
- true
-
-
osx-x86_64-cpu
@@ -82,8 +80,15 @@
- org.scalastyle
- scalastyle-maven-plugin
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.0
+
+
+ -Djava.library.path=${project.parent.basedir}/native/${platform}/target
+
+ ${skipTests}
+
org.scalastyle
@@ -104,10 +109,23 @@
1.3.1-SNAPSHOT
provided
+
+ junit
+ junit
+ 4.11
+ test
+
commons-io
commons-io
2.1
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Context.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Context.scala
index beeb430f62fa..ab44f434b160 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/Context.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Context.scala
@@ -17,6 +17,8 @@
package org.apache.mxnet
+import scala.language.implicitConversions
+
object Context {
val devtype2str = Map(1 -> "cpu", 2 -> "gpu", 3 -> "cpu_pinned")
val devstr2type = Map("cpu" -> 1, "gpu" -> 2, "cpu_pinned" -> 3)
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Executor.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Executor.scala
index fc791d5cd9a3..b342a96097ff 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/Executor.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Executor.scala
@@ -45,7 +45,7 @@ object Executor {
* @see Symbol.bind : to create executor
*/
class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
- private[mxnet] val symbol: Symbol) extends WarnIfNotDisposed {
+ private[mxnet] val symbol: Symbol) extends NativeResource {
private[mxnet] var argArrays: Array[NDArray] = null
private[mxnet] var gradArrays: Array[NDArray] = null
private[mxnet] var auxArrays: Array[NDArray] = null
@@ -59,14 +59,15 @@ class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
private[mxnet] var _group2ctx: Map[String, Context] = null
private val logger: Logger = LoggerFactory.getLogger(classOf[Executor])
- private var disposed = false
- protected def isDisposed = disposed
-
- def dispose(): Unit = {
- if (!disposed) {
- outputs.foreach(_.dispose())
- _LIB.mxExecutorFree(handle)
- disposed = true
+ override def nativeAddress: CPtrAddress = handle
+ override def nativeDeAllocator: (CPtrAddress => Int) = _LIB.mxExecutorFree
+ // cannot determine the off-heap size of this object
+ override val bytesAllocated: Long = 0
+ override val ref: NativeResourceRef = super.register()
+ override def dispose(): Unit = {
+ if (!super.isDisposed) {
+ super.dispose()
+ outputs.foreach(o => o.dispose())
}
}
@@ -223,7 +224,6 @@ class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
/**
* Get dictionary representation of argument arrrays.
* @return The dictionary that maps name of arguments to NDArrays.
- * @throws IllegalArgumentException if there are duplicated names in the arguments.
*/
def argDict: Map[String, NDArray] = {
if (_argDict == null) {
@@ -235,7 +235,6 @@ class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
/**
* Get dictionary representation of gradient arrays.
* @return The dictionary that maps name of arguments to gradient arrays.
- * @throws IllegalArgumentException if there are duplicated names in the grads.
*/
def gradDict: Map[String, NDArray] = {
if (_gradDict == null) {
@@ -247,7 +246,6 @@ class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
/**
* Get dictionary representation of auxiliary states arrays.
* @return The dictionary that maps name of auxiliary states to NDArrays.
- * @throws IllegalArgumentException if there are duplicated names in the auxiliary states.
*/
def auxDict: Map[String, NDArray] = {
if (_auxDict == null) {
@@ -264,8 +262,6 @@ class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
* Whether allow extra parameters that are not needed by symbol
* If this is True, no error will be thrown when arg_params or aux_params
* contain extra parameters that is not needed by the executor.
- * @throws IllegalArgumentException
- * If there is additional parameters in the dict but allow_extra_params=False
*/
def copyParamsFrom(argParams: Map[String, NDArray],
auxParams: Map[String, NDArray],
@@ -305,4 +301,5 @@ class Executor private[mxnet](private[mxnet] val handle: ExecutorHandle,
checkCall(_LIB.mxExecutorPrint(handle, str))
str.value
}
+
}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/FeedForward.scala b/scala-package/core/src/main/scala/org/apache/mxnet/FeedForward.scala
index 00a1450089f7..2ed9d8cfbb84 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/FeedForward.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/FeedForward.scala
@@ -17,9 +17,10 @@
package org.apache.mxnet
+import org.apache.mxnet.Base.CPtrAddress
import org.apache.mxnet.io.NDArrayIter
import org.apache.mxnet.optimizer.SGD
-import org.slf4j.{LoggerFactory, Logger}
+import org.slf4j.{Logger, LoggerFactory}
import scala.collection.mutable.ListBuffer
@@ -55,7 +56,7 @@ class FeedForward private(
argParams: Map[String, NDArray],
auxParams: Map[String, NDArray],
private val allowExtraParams: Boolean,
- val beginEpoch: Int) {
+ val beginEpoch: Int) extends NativeResource {
val logger: Logger = LoggerFactory.getLogger(classOf[FeedForward])
private var argumentChecked = false
@@ -126,6 +127,8 @@ class FeedForward private(
}
// Initialize weight parameters and auxiliary states
+ // The NDArrays associated with the _argParms and _auxParams are not disposed instead
+ // they are passed a outer scope if available.
private def initParams(inputShapes: Map[String, Shape], overwrite: Boolean = false)
: (IndexedSeq[String], IndexedSeq[String], IndexedSeq[String]) = {
val (argShapes, _, auxShapes) = symbol.inferShape(inputShapes)
@@ -137,16 +140,26 @@ class FeedForward private(
val paramNameShapes = (argNames zip argShapes).filter { case (name, _) =>
paramNames.contains(name)
}
- val argParams = paramNameShapes.map { case (name, shape) =>
- (name, NDArray.zeros(shape))
+ val argParams = paramNameShapes.map { case (name, shape) => {
+ val param = NDArray.zeros(shape)
+ val curScope = ResourceScope.getCurrentScope()
+ if (curScope.isDefined) curScope.get.moveToOuterScope(param)
+ (name, param)
+ }
}.toMap
- val auxParams = (auxNames zip auxShapes).map { case (name, shape) =>
- (name, NDArray.zeros(shape))
+
+ val auxParams = (auxNames zip auxShapes).map { case (name, shape) => {
+ val param = NDArray.zeros(shape)
+ val curScope = ResourceScope.getCurrentScope()
+ if (curScope.isDefined) curScope.get.moveToOuterScope(param)
+ (name, param)
+ }
}.toMap
for ((k, v) <- argParams) {
if (_argParams != null && _argParams.contains(k) && (!overwrite)) {
argParams(k).set(_argParams(k))
+
} else {
initializer(k, v)
}
@@ -277,13 +290,15 @@ class FeedForward private(
def fit(trainData: DataIter, evalData: DataIter, evalMetric: EvalMetric, kvStoreType: String,
epochEndCallback: EpochEndCallback, batchEndCallback: BatchEndCallback,
logger: Logger, workLoadList: Seq[Float]): Unit = {
- // init params first to allow kv store use _argParams to decide its type
- initSymbolParams(trainData)
- // create kvstore
- val (kvStore, updateOnKVStore) = Model.createKVStore(kvStoreType, ctx.length, _argParams)
- fit(trainData, evalData, evalMetric, kvStore, updateOnKVStore,
- epochEndCallback, batchEndCallback, logger, workLoadList)
- kvStore.foreach(_.dispose())
+ ResourceScope.using() {
+ // init params first to allow kv store use _argParams to decide its type
+ initSymbolParams(trainData)
+ // create kvstore
+ val (kvStore, updateOnKVStore) = Model.createKVStore(kvStoreType, ctx.length, _argParams)
+ fit(trainData, evalData, evalMetric, kvStore, updateOnKVStore,
+ epochEndCallback, batchEndCallback, logger, workLoadList)
+// kvStore.foreach(_.dispose())
+ }
}
def fit(trainData: DataIter, evalData: DataIter, evalMetric: EvalMetric,
@@ -313,11 +328,13 @@ class FeedForward private(
batchEndCallback: BatchEndCallback, logger: Logger,
workLoadList: Seq[Float]): Unit = {
// init params first to allow kv store use _argParams to decide its type
- initSymbolParams(trainData)
- // create kvstore
- val (kvStore, updateOnKVStore) = Model.createKVStore(kv)
- fit(trainData, evalData, evalMetric, kvStore, updateOnKVStore,
- epochEndCallback, batchEndCallback, logger, workLoadList)
+ ResourceScope.using() {
+ initSymbolParams(trainData)
+ // create kvstore
+ val (kvStore, updateOnKVStore) = Model.createKVStore(kv)
+ fit(trainData, evalData, evalMetric, kvStore, updateOnKVStore,
+ epochEndCallback, batchEndCallback, logger, workLoadList)
+ }
}
def fit(trainData: DataIter, evalData: DataIter, evalMetric: EvalMetric,
@@ -352,44 +369,49 @@ class FeedForward private(
batchEndCallback: BatchEndCallback = null, logger: Logger = FeedForward.logger,
workLoadList: Seq[Float] = null): Unit = {
require(evalMetric != null, "evalMetric cannot be null")
- val (argNames, paramNames, auxNames) = initSymbolParams(trainData)
-
- // init optimizer
- val batchSizeMultiplier = kvStore.map { kv =>
- if (kv.`type` == "dist_sync") {
- kv.numWorkers
- } else {
- 1
- }
- }
- val batchSize = trainData.batchSize * batchSizeMultiplier.getOrElse(1)
- this.optimizer.setArgNames(argNames)
- this.optimizer.setRescaleGrad(1f / batchSize)
- this.optimizer.setSymbol(this.symbol)
- val paramIdx2Name =
- if (updateOnKVStore) {
- paramNames.zipWithIndex.map { case (name, idx) => idx -> name }.toMap
- } else {
- paramNames.zipWithIndex.flatMap { case (name, idx) =>
- (0 until ctx.length).map(k => (idx * ctx.length + k) -> name).toMap
- }.toMap
+ // TODO: https://issues.apache.org/jira/browse/MXNET-1171
+ // this leaks memory, initSymbolParams->initParams is already called which allocates
+ // NDArray in argParams, auxParams and here we are overwriting it by calling again.
+ // PhantomRef should take care of releasing this when GC is called, however we have to
+ // wait for the GC call to happen.
+ val (argNames, paramNames, auxNames) = initSymbolParams(trainData)
+
+ // init optimizer
+ val batchSizeMultiplier = kvStore.map { kv =>
+ if (kv.`type` == "dist_sync") {
+ kv.numWorkers
+ } else {
+ 1
+ }
}
- this.optimizer.setIdx2Name(paramIdx2Name)
-
- logger.debug("Start training on multi-device")
- Model.trainMultiDevice(
- symbol, ctx, argNames, paramNames, auxNames,
- _argParams, _auxParams,
- this.beginEpoch, this.numEpoch,
- this.epochSize, this.optimizer,
- kvStore, updateOnKVStore,
- trainData = trainData, evalData = Option(evalData),
- evalMetric = evalMetric,
- epochEndCallback = Option(epochEndCallback),
- batchEndCallback = Option(batchEndCallback),
- workLoadList = workLoadList,
- monitor = monitor,
- symGen = symGen)
+ val batchSize = trainData.batchSize * batchSizeMultiplier.getOrElse(1)
+ this.optimizer.setArgNames(argNames)
+ this.optimizer.setRescaleGrad(1f / batchSize)
+ this.optimizer.setSymbol(this.symbol)
+ val paramIdx2Name =
+ if (updateOnKVStore) {
+ paramNames.zipWithIndex.map { case (name, idx) => idx -> name }.toMap
+ } else {
+ paramNames.zipWithIndex.flatMap { case (name, idx) =>
+ (0 until ctx.length).map(k => (idx * ctx.length + k) -> name).toMap
+ }.toMap
+ }
+ this.optimizer.setIdx2Name(paramIdx2Name)
+
+ logger.debug("Start training on multi-device")
+ Model.trainMultiDevice(
+ symbol, ctx, argNames, paramNames, auxNames,
+ _argParams, _auxParams,
+ this.beginEpoch, this.numEpoch,
+ this.epochSize, this.optimizer,
+ kvStore, updateOnKVStore,
+ trainData = trainData, evalData = Option(evalData),
+ evalMetric = evalMetric,
+ epochEndCallback = Option(epochEndCallback),
+ batchEndCallback = Option(batchEndCallback),
+ workLoadList = workLoadList,
+ monitor = monitor,
+ symGen = symGen)
}
/**
@@ -416,9 +438,29 @@ class FeedForward private(
def serialize(): Array[Byte] = {
Model.serialize(this.symbol, getArgParams, getAuxParams)
}
+
+ // hack to make the FeedForward.scala work with ResourceScope and
+ // automatically release _argParms and _auxParms
+ override def nativeAddress: CPtrAddress = hashCode()
+
+ override def nativeDeAllocator: CPtrAddress => Int = FeedForward.doNothingDeAllocator
+
+ override val ref: NativeResourceRef = super.register()
+
+ override val bytesAllocated: Long = 0L
+
+ override def dispose(): Unit = {
+ if (!super.isDisposed) {
+ _argParams.foreach { case (_, param) => param.dispose() }
+ _auxParams.foreach { case (_, param) => param.dispose() }
+ }
+ }
}
object FeedForward {
+
+ private def doNothingDeAllocator(dummy: CPtrAddress): Int = 0
+
private val logger: Logger = LoggerFactory.getLogger(classOf[FeedForward])
// Check if name is a data argument.
private def isDataArg(name: String): Boolean = {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/IO.scala b/scala-package/core/src/main/scala/org/apache/mxnet/IO.scala
index e8351422c488..b580ad10a04e 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/IO.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/IO.scala
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory
import scala.annotation.varargs
import scala.collection.immutable.ListMap
import scala.collection.mutable.ListBuffer
+import scala.language.implicitConversions
/**
* IO iterators for loading training & validation data
*/
@@ -340,11 +341,11 @@ abstract class DataIter extends Iterator[DataBatch] {
def getIndex(): IndexedSeq[Long]
// The name and shape of data provided by this iterator
- @deprecated
+ @deprecated("Use provideDataDesc instead", "1.3.0")
def provideData: ListMap[String, Shape]
// The name and shape of label provided by this iterator
- @deprecated
+ @deprecated("Use provideLabelDesc instead", "1.3.0")
def provideLabel: ListMap[String, Shape]
// Provide type:DataDesc of the data
@@ -404,7 +405,7 @@ object DataDesc {
}
}
- @deprecated
+ @deprecated("Please use DataDesc methods instead", "1.3.0")
implicit def ListMap2Descs(shapes: ListMap[String, Shape]): IndexedSeq[DataDesc] = {
if (shapes != null) {
shapes.map { case (k, s) => new DataDesc(k, s) }.toIndexedSeq
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Image.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Image.scala
new file mode 100644
index 000000000000..77881ab940be
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Image.scala
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+// scalastyle:off
+import java.awt.image.BufferedImage
+// scalastyle:on
+import java.io.InputStream
+
+import scala.collection.mutable
+import scala.collection.mutable.{ArrayBuffer, ListBuffer}
+
+/**
+ * Image API of Scala package
+ * enable OpenCV feature
+ */
+object Image {
+
+ /**
+ * Decode image with OpenCV.
+ * Note: return image in RGB by default, instead of OpenCV's default BGR.
+ * @param buf Buffer containing binary encoded image
+ * @param flag Convert decoded image to grayscale (0) or color (1).
+ * @param to_rgb Whether to convert decoded image
+ * to mxnet's default RGB format (instead of opencv's default BGR).
+ * @return NDArray in HWC format
+ */
+ def imDecode(buf: Array[Byte], flag: Int,
+ to_rgb: Boolean,
+ out: Option[NDArray]): NDArray = {
+ val nd = NDArray.array(buf.map( x => (x & 0xFF).toFloat), Shape(buf.length))
+ val byteND = NDArray.api.cast(nd, "uint8")
+ val args : ListBuffer[Any] = ListBuffer()
+ val map : mutable.Map[String, Any] = mutable.Map()
+ args += byteND
+ map("flag") = flag
+ map("to_rgb") = to_rgb
+ if (out.isDefined) map("out") = out.get
+ NDArray.genericNDArrayFunctionInvoke("_cvimdecode", args, map.toMap)
+ }
+
+ /**
+ * Same imageDecode with InputStream
+ * @param inputStream the inputStream of the image
+ * @return NDArray in HWC format
+ */
+ def imDecode(inputStream: InputStream, flag: Int = 1,
+ to_rgb: Boolean = true,
+ out: Option[NDArray] = None): NDArray = {
+ val buffer = new Array[Byte](2048)
+ val arrBuffer = ArrayBuffer[Byte]()
+ var length = 0
+ while (length != -1) {
+ length = inputStream.read(buffer)
+ if (length != -1) arrBuffer ++= buffer.slice(0, length)
+ }
+ imDecode(arrBuffer.toArray, flag, to_rgb, out)
+ }
+
+ /**
+ * Read and decode image with OpenCV.
+ * Note: return image in RGB by default, instead of OpenCV's default BGR.
+ * @param filename Name of the image file to be loaded.
+ * @param flag Convert decoded image to grayscale (0) or color (1).
+ * @param to_rgb Whether to convert decoded image to mxnet's default RGB format
+ * (instead of opencv's default BGR).
+ * @return org.apache.mxnet.NDArray in HWC format
+ */
+ def imRead(filename: String, flag: Option[Int] = None,
+ to_rgb: Option[Boolean] = None,
+ out: Option[NDArray] = None): NDArray = {
+ val args : ListBuffer[Any] = ListBuffer()
+ val map : mutable.Map[String, Any] = mutable.Map()
+ map("filename") = filename
+ if (flag.isDefined) map("flag") = flag.get
+ if (to_rgb.isDefined) map("to_rgb") = to_rgb.get
+ if (out.isDefined) map("out") = out.get
+ NDArray.genericNDArrayFunctionInvoke("_cvimread", args, map.toMap)
+ }
+
+ /**
+ * Resize image with OpenCV.
+ * @param src source image in NDArray
+ * @param w Width of resized image.
+ * @param h Height of resized image.
+ * @param interp Interpolation method (default=cv2.INTER_LINEAR).
+ * @return org.apache.mxnet.NDArray
+ */
+ def imResize(src: org.apache.mxnet.NDArray, w: Int, h: Int,
+ interp: Option[Int] = None,
+ out: Option[NDArray] = None): NDArray = {
+ val args : ListBuffer[Any] = ListBuffer()
+ val map : mutable.Map[String, Any] = mutable.Map()
+ args += src
+ map("w") = w
+ map("h") = h
+ if (interp.isDefined) map("interp") = interp.get
+ if (out.isDefined) map("out") = out.get
+ NDArray.genericNDArrayFunctionInvoke("_cvimresize", args, map.toMap)
+ }
+
+ /**
+ * Pad image border with OpenCV.
+ * @param src source image
+ * @param top Top margin.
+ * @param bot Bottom margin.
+ * @param left Left margin.
+ * @param right Right margin.
+ * @param typeOf Filling type (default=cv2.BORDER_CONSTANT).
+ * @param value (Deprecated! Use ``values`` instead.) Fill with single value.
+ * @param values Fill with value(RGB[A] or gray), up to 4 channels.
+ * @return org.apache.mxnet.NDArray
+ */
+ def copyMakeBorder(src: org.apache.mxnet.NDArray, top: Int, bot: Int,
+ left: Int, right: Int, typeOf: Option[Int] = None,
+ value: Option[Double] = None, values: Option[Any] = None,
+ out: Option[NDArray] = None): NDArray = {
+ val args : ListBuffer[Any] = ListBuffer()
+ val map : mutable.Map[String, Any] = mutable.Map()
+ args += src
+ map("top") = top
+ map("bot") = bot
+ map("left") = left
+ map("right") = right
+ if (typeOf.isDefined) map("type") = typeOf.get
+ if (value.isDefined) map("value") = value.get
+ if (values.isDefined) map("values") = values.get
+ if (out.isDefined) map("out") = out.get
+ NDArray.genericNDArrayFunctionInvoke("_cvcopyMakeBorder", args, map.toMap)
+ }
+
+ /**
+ * Do a fixed crop on the image
+ * @param src Src image in NDArray
+ * @param x0 starting x point
+ * @param y0 starting y point
+ * @param w width of the image
+ * @param h height of the image
+ * @return cropped NDArray
+ */
+ def fixedCrop(src: NDArray, x0: Int, y0: Int, w: Int, h: Int): NDArray = {
+ NDArray.api.crop(src, Shape(y0, x0, 0), Shape(y0 + h, x0 + w, src.shape.get(2)))
+ }
+
+ /**
+ * Convert a NDArray image to a real image
+ * The time cost will increase if the image resolution is big
+ * @param src Source image file in RGB
+ * @return Buffered Image
+ */
+ def toImage(src: NDArray): BufferedImage = {
+ require(src.dtype == DType.UInt8, "The input NDArray must be bytes")
+ require(src.shape.length == 3, "The input should contains height, width and channel")
+ val height = src.shape.get(0)
+ val width = src.shape.get(1)
+ val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
+ (0 until height).par.foreach(r => {
+ (0 until width).par.foreach(c => {
+ val arr = src.at(r).at(c).toArray
+ // NDArray in RGB
+ val red = arr(0).toByte & 0xFF
+ val green = arr(1).toByte & 0xFF
+ val blue = arr(2).toByte & 0xFF
+ val rgb = (red << 16) | (green << 8) | blue
+ img.setRGB(c, r, rgb)
+ })
+ })
+ img
+ }
+
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/KVStore.scala b/scala-package/core/src/main/scala/org/apache/mxnet/KVStore.scala
index 8e89ce76b877..b2d4349b4f64 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/KVStore.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/KVStore.scala
@@ -52,22 +52,17 @@ object KVStore {
}
}
-class KVStore(private[mxnet] val handle: KVStoreHandle) extends WarnIfNotDisposed {
+class KVStore(private[mxnet] val handle: KVStoreHandle) extends NativeResource {
private val logger: Logger = LoggerFactory.getLogger(classOf[KVStore])
private var updaterFunc: MXKVStoreUpdater = null
- private var disposed = false
- protected def isDisposed = disposed
- /**
- * Release the native memory.
- * The object shall never be used after it is disposed.
- */
- def dispose(): Unit = {
- if (!disposed) {
- _LIB.mxKVStoreFree(handle)
- disposed = true
- }
- }
+ override def nativeAddress: CPtrAddress = handle
+
+ override def nativeDeAllocator: CPtrAddress => MXUint = _LIB.mxKVStoreFree
+
+ override val ref: NativeResourceRef = super.register()
+
+ override val bytesAllocated: Long = 0L
/**
* Initialize a single or a sequence of key-value pairs into the store.
@@ -291,7 +286,7 @@ class KVStore(private[mxnet] val handle: KVStoreHandle) extends WarnIfNotDispose
case cachedStates: MXKVStoreCachedStates =>
val bis = new BufferedInputStream (new FileInputStream (fname) )
try {
- val bArray = Stream.continually (bis.read).takeWhile (- 1 !=).map (_.toByte).toArray
+ val bArray = Stream.continually (bis.read).takeWhile (_ != -1).map (_.toByte).toArray
cachedStates.deserializeState(bArray)
} finally {
bis.close ()
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Model.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Model.scala
index 4bb9cdd331a6..b835c4964dd0 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/Model.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Model.scala
@@ -259,7 +259,9 @@ object Model {
workLoadList: Seq[Float] = Nil,
monitor: Option[Monitor] = None,
symGen: SymbolGenerator = null): Unit = {
- val executorManager = new DataParallelExecutorManager(
+ ResourceScope.using() {
+
+ val executorManager = new DataParallelExecutorManager(
symbol = symbol,
symGen = symGen,
ctx = ctx,
@@ -269,17 +271,17 @@ object Model {
auxNames = auxNames,
workLoadList = workLoadList)
- monitor.foreach(executorManager.installMonitor)
- executorManager.setParams(argParams, auxParams)
+ monitor.foreach(executorManager.installMonitor)
+ executorManager.setParams(argParams, auxParams)
- // updater for updateOnKVStore = false
- val updaterLocal = Optimizer.getUpdater(optimizer)
+ // updater for updateOnKVStore = false
+ val updaterLocal = Optimizer.getUpdater(optimizer)
- kvStore.foreach(initializeKVStore(_, executorManager.paramArrays,
- argParams, executorManager.paramNames, updateOnKVStore))
- if (updateOnKVStore) {
- kvStore.foreach(_.setOptimizer(optimizer))
- }
+ kvStore.foreach(initializeKVStore(_, executorManager.paramArrays,
+ argParams, executorManager.paramNames, updateOnKVStore))
+ if (updateOnKVStore) {
+ kvStore.foreach(_.setOptimizer(optimizer))
+ }
// Now start training
for (epoch <- beginEpoch until endEpoch) {
@@ -290,45 +292,46 @@ object Model {
var epochDone = false
// Iterate over training data.
trainData.reset()
- while (!epochDone) {
- var doReset = true
- while (doReset && trainData.hasNext) {
- val dataBatch = trainData.next()
- executorManager.loadDataBatch(dataBatch)
- monitor.foreach(_.tic())
- executorManager.forward(isTrain = true)
- executorManager.backward()
- if (updateOnKVStore) {
- updateParamsOnKVStore(executorManager.paramArrays,
- executorManager.gradArrays,
- kvStore, executorManager.paramNames)
- } else {
- updateParams(executorManager.paramArrays,
- executorManager.gradArrays,
- updaterLocal, ctx.length,
- executorManager.paramNames,
- kvStore)
- }
- monitor.foreach(_.tocPrint())
- // evaluate at end, so out_cpu_array can lazy copy
- executorManager.updateMetric(evalMetric, dataBatch.label)
+ ResourceScope.using() {
+ while (!epochDone) {
+ var doReset = true
+ while (doReset && trainData.hasNext) {
+ val dataBatch = trainData.next()
+ executorManager.loadDataBatch(dataBatch)
+ monitor.foreach(_.tic())
+ executorManager.forward(isTrain = true)
+ executorManager.backward()
+ if (updateOnKVStore) {
+ updateParamsOnKVStore(executorManager.paramArrays,
+ executorManager.gradArrays,
+ kvStore, executorManager.paramNames)
+ } else {
+ updateParams(executorManager.paramArrays,
+ executorManager.gradArrays,
+ updaterLocal, ctx.length,
+ executorManager.paramNames,
+ kvStore)
+ }
+ monitor.foreach(_.tocPrint())
+ // evaluate at end, so out_cpu_array can lazy copy
+ executorManager.updateMetric(evalMetric, dataBatch.label)
- nBatch += 1
- batchEndCallback.foreach(_.invoke(epoch, nBatch, evalMetric))
+ nBatch += 1
+ batchEndCallback.foreach(_.invoke(epoch, nBatch, evalMetric))
- // this epoch is done possibly earlier
- if (epochSize != -1 && nBatch >= epochSize) {
- doReset = false
+ // this epoch is done possibly earlier
+ if (epochSize != -1 && nBatch >= epochSize) {
+ doReset = false
+ }
+ }
+ if (doReset) {
+ trainData.reset()
}
- }
- if (doReset) {
- trainData.reset()
- }
- // this epoch is done
- epochDone = (epochSize == -1 || nBatch >= epochSize)
+ // this epoch is done
+ epochDone = (epochSize == -1 || nBatch >= epochSize)
+ }
}
-
val (name, value) = evalMetric.get
name.zip(value).foreach { case (n, v) =>
logger.info(s"Epoch[$epoch] Train-$n=$v")
@@ -336,20 +339,22 @@ object Model {
val toc = System.currentTimeMillis
logger.info(s"Epoch[$epoch] Time cost=${toc - tic}")
- evalData.foreach { evalDataIter =>
- evalMetric.reset()
- evalDataIter.reset()
- // TODO: make DataIter implement Iterator
- while (evalDataIter.hasNext) {
- val evalBatch = evalDataIter.next()
- executorManager.loadDataBatch(evalBatch)
- executorManager.forward(isTrain = false)
- executorManager.updateMetric(evalMetric, evalBatch.label)
- }
+ ResourceScope.using() {
+ evalData.foreach { evalDataIter =>
+ evalMetric.reset()
+ evalDataIter.reset()
+ // TODO: make DataIter implement Iterator
+ while (evalDataIter.hasNext) {
+ val evalBatch = evalDataIter.next()
+ executorManager.loadDataBatch(evalBatch)
+ executorManager.forward(isTrain = false)
+ executorManager.updateMetric(evalMetric, evalBatch.label)
+ }
- val (name, value) = evalMetric.get
- name.zip(value).foreach { case (n, v) =>
- logger.info(s"Epoch[$epoch] Train-$n=$v")
+ val (name, value) = evalMetric.get
+ name.zip(value).foreach { case (n, v) =>
+ logger.info(s"Epoch[$epoch] Validation-$n=$v")
+ }
}
}
@@ -359,8 +364,7 @@ object Model {
epochEndCallback.foreach(_.invoke(epoch, symbol, argParams, auxParams))
}
- updaterLocal.dispose()
- executorManager.dispose()
+ }
}
// scalastyle:on parameterNum
}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/NDArray.scala b/scala-package/core/src/main/scala/org/apache/mxnet/NDArray.scala
index 9b6a7dc66540..3a0c3c11f16a 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/NDArray.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/NDArray.scala
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory
import scala.collection.mutable
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
+import scala.language.implicitConversions
import scala.ref.WeakReference
/**
@@ -403,6 +404,9 @@ object NDArray extends NDArrayBase {
* @param stop End of interval.
* @param step Spacing between values. The default step size is 1.
* @param repeat Number of times to repeat each element. The default repeat count is 1.
+ * @param infer_range
+ * When set to True, infer the stop position from the start, step,
+ * repeat, and output tensor size.
* @param ctx Device context. Default context is the current default context.
* @param dType The data type of the `NDArray`. The default datatype is `DType.Float32`.
* @return NDArray of evenly spaced values in the specified range.
@@ -562,16 +566,20 @@ object NDArray extends NDArrayBase {
*/
class NDArray private[mxnet](private[mxnet] val handle: NDArrayHandle,
val writable: Boolean = true,
- addToCollector: Boolean = true) extends WarnIfNotDisposed {
+ addToCollector: Boolean = true) extends NativeResource {
if (addToCollector) {
NDArrayCollector.collect(this)
}
+ override def nativeAddress: CPtrAddress = handle
+ override def nativeDeAllocator: (CPtrAddress => Int) = _LIB.mxNDArrayFree
+ override val bytesAllocated: Long = DType.numOfBytes(this.dtype) * this.shape.product
+
+ override val ref: NativeResourceRef = super.register()
+
// record arrays who construct this array instance
// we use weak reference to prevent gc blocking
private[mxnet] val dependencies = mutable.HashMap.empty[Long, WeakReference[NDArray]]
- @volatile private var disposed = false
- def isDisposed: Boolean = disposed
def serialize(): Array[Byte] = {
val buf = ArrayBuffer.empty[Byte]
@@ -584,11 +592,10 @@ class NDArray private[mxnet](private[mxnet] val handle: NDArrayHandle,
* The NDArrays it depends on will NOT be disposed.
* The object shall never be used after it is disposed.
*/
- def dispose(): Unit = {
- if (!disposed) {
- _LIB.mxNDArrayFree(handle)
+ override def dispose(): Unit = {
+ if (!super.isDisposed) {
+ super.dispose()
dependencies.clear()
- disposed = true
}
}
@@ -1034,6 +1041,7 @@ class NDArray private[mxnet](private[mxnet] val handle: NDArrayHandle,
// TODO: naive implementation
shape.hashCode + toArray.hashCode
}
+
}
private[mxnet] object NDArrayConversions {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/NativeResource.scala b/scala-package/core/src/main/scala/org/apache/mxnet/NativeResource.scala
new file mode 100644
index 000000000000..1806b8653376
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/NativeResource.scala
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+
+import org.apache.mxnet.Base.CPtrAddress
+import java.lang.ref.{PhantomReference, ReferenceQueue, WeakReference}
+import java.util.concurrent._
+
+import org.apache.mxnet.Base.checkCall
+import java.util.concurrent.atomic.AtomicLong
+
+
+/**
+ * NativeResource trait is used to manage MXNet Objects
+ * such as NDArray, Symbol, Executor, etc.,
+ * The MXNet Object calls NativeResource.register
+ * and assign the returned NativeResourceRef to PhantomReference
+ * NativeResource also implements AutoCloseable so MXNetObjects
+ * can be used like Resources in try-with-resources paradigm
+ */
+private[mxnet] trait NativeResource
+ extends AutoCloseable with WarnIfNotDisposed {
+
+ /**
+ * native Address associated with this object
+ */
+ def nativeAddress: CPtrAddress
+
+ /**
+ * Function Pointer to the NativeDeAllocator of nativeAddress
+ */
+ def nativeDeAllocator: (CPtrAddress => Int)
+
+ /**
+ * Call NativeResource.register to get the reference
+ */
+ val ref: NativeResourceRef
+
+ /**
+ * Off-Heap Bytes Allocated for this object
+ */
+ // intentionally making it a val, so it gets evaluated when defined
+ val bytesAllocated: Long
+
+ // this is set and unset by [[ResourceScope.add]] and [[ResourceScope.remove]]
+ private[mxnet] var scope: Option[ResourceScope] = None
+
+ @volatile private var disposed = false
+
+ override def isDisposed: Boolean = disposed || isDeAllocated
+
+ /**
+ * Register this object for PhantomReference tracking and in
+ * ResourceScope if used inside ResourceScope.
+ * @return NativeResourceRef that tracks reachability of this object
+ * using PhantomReference
+ */
+ def register(): NativeResourceRef = {
+ val scope = ResourceScope.getCurrentScope()
+ if (scope.isDefined) scope.get.add(this)
+
+ NativeResource.totalBytesAllocated.getAndAdd(bytesAllocated)
+ // register with PhantomRef tracking to release in case the objects go
+ // out of reference within scope but are held for long time
+ NativeResourceRef.register(this, nativeDeAllocator)
+ }
+
+ // Implements [[@link AutoCloseable.close]]
+ override def close(): Unit = {
+ dispose()
+ }
+
+ // Implements [[@link WarnIfNotDisposed.dispose]]
+ def dispose(): Unit = dispose(true)
+
+ /**
+ * This method deAllocates nativeResource and deRegisters
+ * from PhantomRef and removes from Scope if
+ * removeFromScope is set to true.
+ * @param removeFromScope remove from the currentScope if true
+ */
+ // the parameter here controls whether to remove from current scope.
+ // [[ResourceScope.close]] calls NativeResource.dispose
+ // if we remove from the ResourceScope ie., from the container in ResourceScope.
+ // while iterating on the container, calling iterator.next is undefined and not safe.
+ // Note that ResourceScope automatically disposes all the resources within.
+ private[mxnet] def dispose(removeFromScope: Boolean = true): Unit = {
+ if (!disposed) {
+ checkCall(nativeDeAllocator(this.nativeAddress))
+ NativeResourceRef.deRegister(ref) // removes from PhantomRef tracking
+ if (removeFromScope && scope.isDefined) scope.get.remove(this)
+ NativeResource.totalBytesAllocated.getAndAdd(-1*bytesAllocated)
+ disposed = true
+ }
+ }
+
+ /*
+ this is used by the WarnIfNotDisposed finalizer,
+ the object could be disposed by the GC without the need for explicit disposal
+ but the finalizer might not have run, then the WarnIfNotDisposed throws a warning
+ */
+ private[mxnet] def isDeAllocated(): Boolean = NativeResourceRef.isDeAllocated(ref)
+
+}
+
+private[mxnet] object NativeResource {
+ var totalBytesAllocated : AtomicLong = new AtomicLong(0)
+}
+
+// Do not make [[NativeResource.resource]] a member of the class,
+// this will hold reference and GC will not clear the object.
+private[mxnet] class NativeResourceRef(resource: NativeResource,
+ val resourceDeAllocator: CPtrAddress => Int)
+ extends PhantomReference[NativeResource](resource, NativeResourceRef.refQ) {}
+
+private[mxnet] object NativeResourceRef {
+
+ private[mxnet] val refQ: ReferenceQueue[NativeResource]
+ = new ReferenceQueue[NativeResource]
+
+ private[mxnet] val refMap = new ConcurrentHashMap[NativeResourceRef, CPtrAddress]()
+
+ private[mxnet] val cleaner = new ResourceCleanupThread()
+
+ cleaner.start()
+
+ def register(resource: NativeResource, nativeDeAllocator: (CPtrAddress => Int)):
+ NativeResourceRef = {
+ val ref = new NativeResourceRef(resource, nativeDeAllocator)
+ refMap.put(ref, resource.nativeAddress)
+ ref
+ }
+
+ // remove from PhantomRef tracking
+ def deRegister(ref: NativeResourceRef): Unit = refMap.remove(ref)
+
+ /**
+ * This method will check if the cleaner ran and deAllocated the object
+ * As a part of GC, when the object is unreachable GC inserts a phantomRef
+ * to the ReferenceQueue which the cleaner thread will deallocate, however
+ * the finalizer runs much later depending on the GC.
+ * @param resource resource to verify if it has been deAllocated
+ * @return true if already deAllocated
+ */
+ def isDeAllocated(ref: NativeResourceRef): Boolean = {
+ !refMap.containsKey(ref)
+ }
+
+ def cleanup: Unit = {
+ // remove is a blocking call
+ val ref: NativeResourceRef = refQ.remove().asInstanceOf[NativeResourceRef]
+ // phantomRef will be removed from the map when NativeResource.close is called.
+ val resource = refMap.get(ref)
+ if (resource != 0L) { // since CPtrAddress is Scala a Long, it cannot be null
+ ref.resourceDeAllocator(resource)
+ refMap.remove(ref)
+ }
+ }
+
+ protected class ResourceCleanupThread extends Thread {
+ setPriority(Thread.MAX_PRIORITY)
+ setName("NativeResourceDeAllocatorThread")
+ setDaemon(true)
+
+ override def run(): Unit = {
+ while (true) {
+ try {
+ NativeResourceRef.cleanup
+ }
+ catch {
+ case _: InterruptedException => Thread.currentThread().interrupt()
+ }
+ }
+ }
+ }
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Optimizer.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Optimizer.scala
index 758cbc829618..1fb634cebb26 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/Optimizer.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Optimizer.scala
@@ -19,6 +19,8 @@ package org.apache.mxnet
import java.io._
+import org.apache.mxnet.Base.CPtrAddress
+
import scala.collection.mutable
import scala.util.Either
@@ -38,8 +40,10 @@ object Optimizer {
}
override def dispose(): Unit = {
- states.values.foreach(optimizer.disposeState)
- states.clear()
+ if (!super.isDisposed) {
+ states.values.foreach(optimizer.disposeState)
+ states.clear()
+ }
}
override def serializeState(): Array[Byte] = {
@@ -140,7 +144,7 @@ abstract class Optimizer extends Serializable {
def deserializeState(bytes: Array[Byte]): AnyRef
// Set individual learning rate scale for parameters
- @deprecated("Use setLrMult instead.")
+ @deprecated("Use setLrMult instead.", "0.10.0")
def setLrScale(lrScale: Map[Int, Float]): Unit = {
val argsLrScale: Map[Either[Int, String], Float] = lrScale.map { case (k, v) => Left(k) -> v }
setLrMult(argsLrScale)
@@ -285,7 +289,8 @@ abstract class Optimizer extends Serializable {
}
}
-trait MXKVStoreUpdater {
+trait MXKVStoreUpdater extends
+ NativeResource {
/**
* user-defined updater for the kvstore
* It's this updater's responsibility to delete recv and local
@@ -294,9 +299,14 @@ trait MXKVStoreUpdater {
* @param local the value stored on local on this key
*/
def update(key: Int, recv: NDArray, local: NDArray): Unit
- def dispose(): Unit
- // def serializeState(): Array[Byte]
- // def deserializeState(bytes: Array[Byte]): Unit
+
+ // This is a hack to make Optimizers work with ResourceScope
+ // otherwise the user has to manage calling dispose on this object.
+ override def nativeAddress: CPtrAddress = hashCode()
+ override def nativeDeAllocator: CPtrAddress => Int = doNothingDeAllocator
+ private def doNothingDeAllocator(dummy: CPtrAddress): Int = 0
+ override val ref: NativeResourceRef = super.register()
+ override val bytesAllocated: Long = 0L
}
trait MXKVStoreCachedStates {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/ResourceScope.scala b/scala-package/core/src/main/scala/org/apache/mxnet/ResourceScope.scala
new file mode 100644
index 000000000000..bb363c0c396b
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/ResourceScope.scala
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+
+import java.util.HashSet
+
+import org.slf4j.LoggerFactory
+
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+import scala.util.Try
+import scala.util.control.{ControlThrowable, NonFatal}
+
+/**
+ * This class manages automatically releasing of `org.apache.mxnet.NativeResource`s
+ */
+class ResourceScope extends AutoCloseable {
+
+ // HashSet does not take a custom comparator
+ private[mxnet] val resourceQ = new mutable.TreeSet[NativeResource]()(nativeAddressOrdering)
+
+ private object nativeAddressOrdering extends Ordering[NativeResource] {
+ def compare(a: NativeResource, b: NativeResource): Int = {
+ a.nativeAddress compare b.nativeAddress
+ }
+ }
+
+ ResourceScope.addToThreadLocal(this)
+
+ /**
+ * Releases all the `org.apache.mxnet.NativeResource` by calling
+ * the associated`'org.apache.mxnet.NativeResource.close()` method
+ */
+ override def close(): Unit = {
+ ResourceScope.removeFromThreadLocal(this)
+ resourceQ.foreach(resource => if (resource != null) resource.dispose(false) )
+ resourceQ.clear()
+ }
+
+ /**
+ * Add a NativeResource to the scope
+ * @param resource
+ */
+ def add(resource: NativeResource): Unit = {
+ resourceQ.+=(resource)
+ resource.scope = Some(this)
+ }
+
+ /**
+ * Remove NativeResource from the Scope, this uses
+ * object equality to find the resource in the stack.
+ * @param resource
+ */
+ def remove(resource: NativeResource): Unit = {
+ resourceQ.-=(resource)
+ resource.scope = None
+ }
+
+ /**
+ * Removes from current Scope and moves to outer scope if it exists
+ * @param resource Resource to be moved to an outer scope
+ */
+ def moveToOuterScope(resource: NativeResource): Unit = {
+ val prevScope: Option[ResourceScope] = ResourceScope.getPrevScope()
+ if (prevScope.isDefined) {
+ this.remove(resource)
+ prevScope.get.add(resource)
+ } else this.remove(resource)
+ }
+
+}
+
+object ResourceScope {
+
+ private val logger = LoggerFactory.getLogger(classOf[ResourceScope])
+
+ /**
+ * Captures all Native Resources created using the ResourceScope and
+ * at the end of the body, de allocates all the Native resources by calling close on them.
+ * This method will not deAllocate NativeResources returned from the block.
+ * @param scope (Optional). Scope in which to capture the native resources
+ * @param body block of code to execute in this scope
+ * @tparam A return type
+ * @return result of the operation, if the result is of type NativeResource, it is not
+ * de allocated so the user can use it and then de allocate manually by calling
+ * close or enclose in another resourceScope.
+ */
+ // inspired from slide 21 of https://www.slideshare.net/Odersky/fosdem-2009-1013261
+ // and https://github.com/scala/scala/blob/2.13.x/src/library/scala/util/Using.scala
+ // TODO: we should move to the Scala util's Using method when we move to Scala 2.13
+ def using[A](scope: ResourceScope = null)(body: => A): A = {
+
+ val curScope = if (scope != null) scope else new ResourceScope()
+
+ @inline def resourceInGeneric(g: scala.collection.Iterable[_]) = {
+ g.foreach( n =>
+ n match {
+ case nRes: NativeResource => {
+ curScope.moveToOuterScope(nRes)
+ }
+ case kv: scala.Tuple2[_, _] => {
+ if (kv._1.isInstanceOf[NativeResource]) curScope.moveToOuterScope(
+ kv._1.asInstanceOf[NativeResource])
+ if (kv._2.isInstanceOf[NativeResource]) curScope.moveToOuterScope(
+ kv._2.asInstanceOf[NativeResource])
+ }
+ }
+ )
+ }
+
+ @inline def safeAddSuppressed(t: Throwable, suppressed: Throwable): Unit = {
+ if (!t.isInstanceOf[ControlThrowable]) t.addSuppressed(suppressed)
+ }
+
+ var retThrowable: Throwable = null
+
+ try {
+ val ret = body
+ ret match {
+ // don't de-allocate if returning any collection that contains NativeResource.
+ case resInGeneric: scala.collection.Iterable[_] => resourceInGeneric(resInGeneric)
+ case nRes: NativeResource => curScope.moveToOuterScope(nRes)
+ case ndRet: NDArrayFuncReturn => ndRet.arr.foreach( nd => curScope.moveToOuterScope(nd) )
+ case _ => // do nothing
+ }
+ ret
+ } catch {
+ case t: Throwable =>
+ retThrowable = t
+ null.asInstanceOf[A] // we'll throw in finally
+ } finally {
+ var toThrow: Throwable = retThrowable
+ if (retThrowable eq null) curScope.close()
+ else {
+ try {
+ curScope.close
+ } catch {
+ case closeThrowable: Throwable =>
+ if (NonFatal(retThrowable) && !NonFatal(closeThrowable)) toThrow = closeThrowable
+ else safeAddSuppressed(retThrowable, closeThrowable)
+ } finally {
+ throw toThrow
+ }
+ }
+ }
+ }
+
+ // thread local Scopes
+ private[mxnet] val threadLocalScopes = new ThreadLocal[ArrayBuffer[ResourceScope]] {
+ override def initialValue(): ArrayBuffer[ResourceScope] =
+ new ArrayBuffer[ResourceScope]()
+ }
+
+ /**
+ * Add resource to current ThreadLocal DataStructure
+ * @param r ResourceScope to add.
+ */
+ private[mxnet] def addToThreadLocal(r: ResourceScope): Unit = {
+ threadLocalScopes.get() += r
+ }
+
+ /**
+ * Remove resource from current ThreadLocal DataStructure
+ * @param r ResourceScope to remove
+ */
+ private[mxnet] def removeFromThreadLocal(r: ResourceScope): Unit = {
+ threadLocalScopes.get() -= r
+ }
+
+ /**
+ * Get the latest Scope in the stack
+ * @return
+ */
+ private[mxnet] def getCurrentScope(): Option[ResourceScope] = {
+ Try(Some(threadLocalScopes.get().last)).getOrElse(None)
+ }
+
+ /**
+ * Get the Last but one Scope from threadLocal Scopes.
+ * @return n-1th scope or None when not found
+ */
+ private[mxnet] def getPrevScope(): Option[ResourceScope] = {
+ val scopes = threadLocalScopes.get()
+ Try(Some(scopes(scopes.size - 2))).getOrElse(None)
+ }
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Symbol.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Symbol.scala
index b1a3e392f41e..01349a689b6c 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/Symbol.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Symbol.scala
@@ -22,6 +22,7 @@ import org.apache.mxnet.DType.DType
import org.slf4j.{Logger, LoggerFactory}
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
+import scala.language.implicitConversions
/**
* Symbolic configuration API of mxnet.
@@ -29,21 +30,15 @@ import scala.collection.mutable.{ArrayBuffer, ListBuffer}
* WARNING: it is your responsibility to clear this object through dispose().
*
*/
-class Symbol private(private[mxnet] val handle: SymbolHandle) extends WarnIfNotDisposed {
+class Symbol private(private[mxnet] val handle: SymbolHandle) extends NativeResource {
private val logger: Logger = LoggerFactory.getLogger(classOf[Symbol])
- private var disposed = false
- protected def isDisposed = disposed
- /**
- * Release the native memory.
- * The object shall never be used after it is disposed.
- */
- def dispose(): Unit = {
- if (!disposed) {
- _LIB.mxSymbolFree(handle)
- disposed = true
- }
- }
+ // unable to get the byteAllocated for Symbol
+ override val bytesAllocated: Long = 0L
+ override def nativeAddress: CPtrAddress = handle
+ override def nativeDeAllocator: (CPtrAddress => Int) = _LIB.mxSymbolFree
+ override val ref: NativeResourceRef = super.register()
+
def +(other: Symbol): Symbol = Symbol.createFromListedSymbols("_Plus")(Array(this, other))
def +[@specialized(Int, Float, Double) V](other: V): Symbol = {
@@ -793,7 +788,7 @@ class Symbol private(private[mxnet] val handle: SymbolHandle) extends WarnIfNotD
}
val execHandle = new ExecutorHandleRef
- val sharedHadle = if (sharedExec != null) sharedExec.handle else 0L
+ val sharedHandle = if (sharedExec != null) sharedExec.handle else 0L
checkCall(_LIB.mxExecutorBindEX(handle,
ctx.deviceTypeid,
ctx.deviceId,
@@ -806,7 +801,7 @@ class Symbol private(private[mxnet] val handle: SymbolHandle) extends WarnIfNotD
argsGradHandle,
reqsArray,
auxArgsHandle,
- sharedHadle,
+ sharedHandle,
execHandle))
val executor = new Executor(execHandle.value, this.clone())
executor.argArrays = argsNDArray
@@ -832,6 +827,7 @@ class Symbol private(private[mxnet] val handle: SymbolHandle) extends WarnIfNotD
checkCall(_LIB.mxSymbolSaveToJSON(handle, jsonStr))
jsonStr.value
}
+
}
/**
@@ -979,7 +975,9 @@ object Symbol extends SymbolBase {
* @param stop End of interval.
* @param step Spacing between values. The default step size is 1.
* @param repeat Number of times to repeat each element. The default repeat count is 1.
- * @param infer_range Infer the stop value from output shape
+ * @param infer_range
+ * When set to True, infer the stop position from the start, step,
+ * repeat, and output tensor size.
* @param ctx Device context. Default context is the current default context.
* @param dType The data type of the `NDArray`. The default datatype is `DType.Float32`.
* @return NDArray of evenly spaced values in the specified range.
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/Visualization.scala b/scala-package/core/src/main/scala/org/apache/mxnet/Visualization.scala
index 2a7b7a8b31bc..b990137b5a45 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/Visualization.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/Visualization.scala
@@ -21,6 +21,7 @@ import scala.util.parsing.json._
import java.io.File
import java.io.PrintWriter
import scala.collection.mutable.ArrayBuffer
+import scala.language.postfixOps
object Visualization {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/io/MXDataIter.scala b/scala-package/core/src/main/scala/org/apache/mxnet/io/MXDataIter.scala
index f7f858deb82d..a84bd106b763 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/io/MXDataIter.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/io/MXDataIter.scala
@@ -33,7 +33,7 @@ import scala.collection.mutable.ListBuffer
private[mxnet] class MXDataIter(private[mxnet] val handle: DataIterHandle,
dataName: String = "data",
labelName: String = "label")
- extends DataIter with WarnIfNotDisposed {
+ extends DataIter with NativeResource {
private val logger = LoggerFactory.getLogger(classOf[MXDataIter])
@@ -67,20 +67,13 @@ private[mxnet] class MXDataIter(private[mxnet] val handle: DataIterHandle,
}
}
+ override def nativeAddress: CPtrAddress = handle
- private var disposed = false
- protected def isDisposed = disposed
+ override def nativeDeAllocator: CPtrAddress => MXUint = _LIB.mxDataIterFree
- /**
- * Release the native memory.
- * The object shall never be used after it is disposed.
- */
- def dispose(): Unit = {
- if (!disposed) {
- _LIB.mxDataIterFree(handle)
- disposed = true
- }
- }
+ override val ref: NativeResourceRef = super.register()
+
+ override val bytesAllocated: Long = 0L
/**
* reset the iterator
@@ -165,11 +158,11 @@ private[mxnet] class MXDataIter(private[mxnet] val handle: DataIterHandle,
}
// The name and shape of data provided by this iterator
- @deprecated
+ @deprecated("Please use provideDataDesc instead", "1.3.0")
override def provideData: ListMap[String, Shape] = _provideData
// The name and shape of label provided by this iterator
- @deprecated
+ @deprecated("Please use provideLabelDesc instead", "1.3.0")
override def provideLabel: ListMap[String, Shape] = _provideLabel
// Provide type:DataDesc of the data
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/io/NDArrayIter.scala b/scala-package/core/src/main/scala/org/apache/mxnet/io/NDArrayIter.scala
index e6be0ad02f83..0032a54dd802 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/io/NDArrayIter.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/io/NDArrayIter.scala
@@ -237,11 +237,11 @@ class NDArrayIter(data: IndexedSeq[(DataDesc, NDArray)],
// The name and shape of data provided by this iterator
- @deprecated
+ @deprecated("Please use provideDataDesc instead", "1.3.0")
override def provideData: ListMap[String, Shape] = _provideData
// The name and shape of label provided by this iterator
- @deprecated
+ @deprecated("Please use provideLabelDesc instead", "1.3.0")
override def provideLabel: ListMap[String, Shape] = _provideLabel
// Provide type:DataDesc of the data
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/io/PrefetchingIter.scala b/scala-package/core/src/main/scala/org/apache/mxnet/io/PrefetchingIter.scala
index e59e3706317d..d277351b124b 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/io/PrefetchingIter.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/io/PrefetchingIter.scala
@@ -178,11 +178,11 @@ class PrefetchingIter(
override def getPad(): Int = this.currentBatch.pad
// The name and shape of label provided by this iterator
- @deprecated
+ @deprecated("Please use provideDataDesc instead", "1.3.0")
override def provideLabel: ListMap[String, Shape] = this._provideLabel
// The name and shape of data provided by this iterator
- @deprecated
+ @deprecated("Please use provideLabelDesc instead", "1.3.0")
override def provideData: ListMap[String, Shape] = this._provideData
// Provide type:DataDesc of the data
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/io/ResizeIter.scala b/scala-package/core/src/main/scala/org/apache/mxnet/io/ResizeIter.scala
index e840af9395f7..9bc042a7b988 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/io/ResizeIter.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/io/ResizeIter.scala
@@ -134,13 +134,13 @@ class ResizeIter(
}
// The name and shape of data provided by this iterator
- @deprecated
+ @deprecated("Please use provideDataDesc instead", "1.3.0")
override def provideData: ListMap[String, Shape] = {
dataIter.provideData
}
// The name and shape of label provided by this iterator
- @deprecated
+ @deprecated("Please use provideLabelDesc instead", "1.3.0")
override def provideLabel: ListMap[String, Shape] = {
dataIter.provideLabel
}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/Context.scala b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/Context.scala
new file mode 100644
index 000000000000..3d397e3fc496
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/Context.scala
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.mxnet.javaapi
+
+import collection.JavaConverters._
+import scala.language.implicitConversions
+
+/**
+ * Constructing a context which is used to specify the device and device type that will
+ * be utilized by the engine.
+ *
+ * @param deviceTypeName {'cpu', 'gpu'} String representing the device type
+ * @param deviceId The device id of the device, needed for GPU
+ */
+class Context private[mxnet] (val context: org.apache.mxnet.Context) {
+
+ val deviceTypeid: Int = context.deviceTypeid
+
+ def this(deviceTypeName: String, deviceId: Int = 0)
+ = this(new org.apache.mxnet.Context(deviceTypeName, deviceId))
+
+ def withScope[T](body: => T): T = context.withScope(body)
+
+ /**
+ * Return device type of current context.
+ * @return device_type
+ */
+ def deviceType: String = context.deviceType
+
+ override def toString: String = context.toString
+ override def equals(other: Any): Boolean = context.equals(other)
+ override def hashCode: Int = context.hashCode
+}
+
+
+object Context {
+ implicit def fromContext(context: org.apache.mxnet.Context): Context = new Context(context)
+ implicit def toContext(jContext: Context): org.apache.mxnet.Context = jContext.context
+
+ val cpu: Context = org.apache.mxnet.Context.cpu()
+ val gpu: Context = org.apache.mxnet.Context.gpu()
+ val devtype2str = org.apache.mxnet.Context.devstr2type.asJava
+ val devstr2type = org.apache.mxnet.Context.devstr2type.asJava
+ def defaultCtx: Context = org.apache.mxnet.Context.defaultCtx
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/DType.scala b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/DType.scala
new file mode 100644
index 000000000000..e25cdde7ac73
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/DType.scala
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.mxnet.javaapi
+
+object DType extends Enumeration {
+ type DType = org.apache.mxnet.DType.DType
+ val Float32 = org.apache.mxnet.DType.Float32
+ val Float64 = org.apache.mxnet.DType.Float64
+ val Float16 = org.apache.mxnet.DType.Float16
+ val UInt8 = org.apache.mxnet.DType.UInt8
+ val Int32 = org.apache.mxnet.DType.Int32
+ val Unknown = org.apache.mxnet.DType.Unknown
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/IO.scala b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/IO.scala
new file mode 100644
index 000000000000..d0e10815a1e6
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/IO.scala
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi
+import scala.language.implicitConversions
+
+class DataDesc private[mxnet] (val dataDesc: org.apache.mxnet.DataDesc) {
+
+ def this(name: String, shape: Shape, dType: DType.DType, layout: String) =
+ this(new org.apache.mxnet.DataDesc(name, shape, dType, layout))
+
+ override def toString(): String = dataDesc.toString()
+}
+
+object DataDesc{
+ implicit def fromDataDesc(dDesc: org.apache.mxnet.DataDesc): DataDesc = new DataDesc(dDesc)
+
+ implicit def toDataDesc(dataDesc: DataDesc): org.apache.mxnet.DataDesc = dataDesc.dataDesc
+
+ /**
+ * Get the dimension that corresponds to the batch size.
+ * @param layout layout string. For example, "NCHW".
+ * @return An axis indicating the batch_size dimension. When data-parallelism is used,
+ * the data will be automatically split and concatenate along the batch_size dimension.
+ * Axis can be -1, which means the whole array will be copied
+ * for each data-parallelism device.
+ */
+ def getBatchAxis(layout: String): Int = org.apache.mxnet.DataDesc.getBatchAxis(Some(layout))
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/NDArray.scala b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/NDArray.scala
new file mode 100644
index 000000000000..6b4f4bdebda5
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/NDArray.scala
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi
+
+import org.apache.mxnet.javaapi.DType.DType
+
+import collection.JavaConverters._
+
+@AddJNDArrayAPIs(false)
+object NDArray extends NDArrayBase {
+ implicit def fromNDArray(nd: org.apache.mxnet.NDArray): NDArray = new NDArray(nd)
+
+ implicit def toNDArray(jnd: NDArray): org.apache.mxnet.NDArray = jnd.nd
+
+ def waitall(): Unit = org.apache.mxnet.NDArray.waitall()
+
+ /**
+ * One hot encoding indices into matrix out.
+ * @param indices An NDArray containing indices of the categorical features.
+ * @param out The result holder of the encoding.
+ * @return Same as out.
+ */
+ def onehotEncode(indices: NDArray, out: NDArray): NDArray
+ = org.apache.mxnet.NDArray.onehotEncode(indices, out)
+
+ /**
+ * Create an empty uninitialized new NDArray, with specified shape.
+ *
+ * @param shape shape of the NDArray.
+ * @param ctx The context of the NDArray.
+ *
+ * @return The created NDArray.
+ */
+ def empty(shape: Shape, ctx: Context, dtype: DType.DType): NDArray
+ = org.apache.mxnet.NDArray.empty(shape, ctx, dtype)
+ def empty(ctx: Context, shape: Array[Int]): NDArray
+ = org.apache.mxnet.NDArray.empty(new Shape(shape), ctx)
+ def empty(ctx: Context, shape: java.util.List[java.lang.Integer]): NDArray
+ = org.apache.mxnet.NDArray.empty(new Shape(shape), ctx)
+
+ /**
+ * Create a new NDArray filled with 0, with specified shape.
+ *
+ * @param shape shape of the NDArray.
+ * @param ctx The context of the NDArray.
+ *
+ * @return The created NDArray.
+ */
+ def zeros(shape: Shape, ctx: Context, dtype: DType.DType): NDArray
+ = org.apache.mxnet.NDArray.zeros(shape, ctx, dtype)
+ def zeros(ctx: Context, shape: Array[Int]): NDArray
+ = org.apache.mxnet.NDArray.zeros(new Shape(shape), ctx)
+ def zeros(ctx: Context, shape: java.util.List[java.lang.Integer]): NDArray
+ = org.apache.mxnet.NDArray.zeros(new Shape(shape), ctx)
+
+ /**
+ * Create a new NDArray filled with 1, with specified shape.
+ * @param shape shape of the NDArray.
+ * @param ctx The context of the NDArray.
+ * @return The created NDArray.
+ */
+ def ones(shape: Shape, ctx: Context, dtype: DType.DType): NDArray
+ = org.apache.mxnet.NDArray.ones(shape, ctx, dtype)
+ def ones(ctx: Context, shape: Array[Int]): NDArray
+ = org.apache.mxnet.NDArray.ones(new Shape(shape), ctx)
+ def ones(ctx: Context, shape: java.util.List[java.lang.Integer]): NDArray
+ = org.apache.mxnet.NDArray.ones(new Shape(shape), ctx)
+
+ /**
+ * Create a new NDArray filled with given value, with specified shape.
+ * @param shape shape of the NDArray.
+ * @param value value to be filled with
+ * @param ctx The context of the NDArray
+ */
+ def full(shape: Shape, value: Float, ctx: Context): NDArray
+ = org.apache.mxnet.NDArray.full(shape, value, ctx)
+
+ def power(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.power(lhs, rhs)
+ def power(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.power(lhs, rhs)
+ def power(lhs: Float, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.power(lhs, rhs)
+
+ def maximum(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.maximum(lhs, rhs)
+ def maximum(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.maximum(lhs, rhs)
+ def maximum(lhs: Float, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.maximum(lhs, rhs)
+
+ def minimum(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.minimum(lhs, rhs)
+ def minimum(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.minimum(lhs, rhs)
+ def minimum(lhs: Float, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.minimum(lhs, rhs)
+
+
+ /**
+ * Returns the result of element-wise **equal to** (==) comparison operation with broadcasting.
+ * For each element in input arrays, return 1(true) if corresponding elements are same,
+ * otherwise return 0(false).
+ */
+ def equal(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.equal(lhs, rhs)
+ def equal(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.equal(lhs, rhs)
+
+ /**
+ * Returns the result of element-wise **not equal to** (!=) comparison operation
+ * with broadcasting.
+ * For each element in input arrays, return 1(true) if corresponding elements are different,
+ * otherwise return 0(false).
+ */
+ def notEqual(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.notEqual(lhs, rhs)
+ def notEqual(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.notEqual(lhs, rhs)
+
+ /**
+ * Returns the result of element-wise **greater than** (>) comparison operation
+ * with broadcasting.
+ * For each element in input arrays, return 1(true) if lhs elements are greater than rhs,
+ * otherwise return 0(false).
+ */
+ def greater(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.greater(lhs, rhs)
+ def greater(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.greater(lhs, rhs)
+
+ /**
+ * Returns the result of element-wise **greater than or equal to** (>=) comparison
+ * operation with broadcasting.
+ * For each element in input arrays, return 1(true) if lhs elements are greater than equal to rhs
+ * otherwise return 0(false).
+ */
+ def greaterEqual(lhs: NDArray, rhs: NDArray): NDArray
+ = org.apache.mxnet.NDArray.greaterEqual(lhs, rhs)
+ def greaterEqual(lhs: NDArray, rhs: Float): NDArray
+ = org.apache.mxnet.NDArray.greaterEqual(lhs, rhs)
+
+ /**
+ * Returns the result of element-wise **lesser than** (<) comparison operation
+ * with broadcasting.
+ * For each element in input arrays, return 1(true) if lhs elements are less than rhs,
+ * otherwise return 0(false).
+ */
+ def lesser(lhs: NDArray, rhs: NDArray): NDArray = org.apache.mxnet.NDArray.lesser(lhs, rhs)
+ def lesser(lhs: NDArray, rhs: Float): NDArray = org.apache.mxnet.NDArray.lesser(lhs, rhs)
+
+ /**
+ * Returns the result of element-wise **lesser than or equal to** (<=) comparison
+ * operation with broadcasting.
+ * For each element in input arrays, return 1(true) if lhs elements are
+ * lesser than equal to rhs, otherwise return 0(false).
+ */
+ def lesserEqual(lhs: NDArray, rhs: NDArray): NDArray
+ = org.apache.mxnet.NDArray.lesserEqual(lhs, rhs)
+ def lesserEqual(lhs: NDArray, rhs: Float): NDArray
+ = org.apache.mxnet.NDArray.lesserEqual(lhs, rhs)
+
+ /**
+ * Create a new NDArray that copies content from source_array.
+ * @param sourceArr Source data to create NDArray from.
+ * @param shape shape of the NDArray
+ * @param ctx The context of the NDArray, default to current default context.
+ * @return The created NDArray.
+ */
+ def array(sourceArr: java.util.List[java.lang.Float], shape: Shape, ctx: Context = null): NDArray
+ = org.apache.mxnet.NDArray.array(
+ sourceArr.asScala.map(ele => Float.unbox(ele)).toArray, shape, ctx)
+
+ /**
+ * Returns evenly spaced values within a given interval.
+ * Values are generated within the half-open interval [`start`, `stop`). In other
+ * words, the interval includes `start` but excludes `stop`.
+ * @param start Start of interval.
+ * @param stop End of interval.
+ * @param step Spacing between values.
+ * @param repeat Number of times to repeat each element.
+ * @param ctx Device context.
+ * @param dType The data type of the `NDArray`.
+ * @return NDArray of evenly spaced values in the specified range.
+ */
+ def arange(start: Float, stop: Float, step: Float, repeat: Int,
+ ctx: Context, dType: DType.DType): NDArray =
+ org.apache.mxnet.NDArray.arange(start, Some(stop), step, repeat, ctx, dType)
+}
+
+/**
+ * NDArray object in mxnet.
+ * NDArray is basic ndarray/Tensor like data structure in mxnet.
+ *
+ * NOTE: NDArray is stored in native memory. Use NDArray in a try-with-resources() construct
+ * or a [[org.apache.mxnet.ResourceScope]] in a try-with-resource to have them
+ * automatically disposed. You can explicitly control the lifetime of NDArray
+ * by calling dispose manually. Failure to do this will result in leaking native memory.
+ *
+ */
+class NDArray private[mxnet] (val nd: org.apache.mxnet.NDArray ) {
+
+ def this(arr: Array[Float], shape: Shape, ctx: Context) = {
+ this(org.apache.mxnet.NDArray.array(arr, shape, ctx))
+ }
+
+ def this(arr: java.util.List[java.lang.Float], shape: Shape, ctx: Context) = {
+ this(NDArray.array(arr, shape, ctx))
+ }
+
+ def serialize(): Array[Byte] = nd.serialize()
+
+ /**
+ * Release the native memory.
+ * The NDArrays it depends on will NOT be disposed.
+ * The object shall never be used after it is disposed.
+ */
+ def dispose(): Unit = nd.dispose()
+
+ /**
+ * Dispose all NDArrays who help to construct this array.
+ * e.g. (a * b + c).disposeDeps() will dispose a, b, c (including their deps) and a * b
+ * @return this array
+ */
+ def disposeDeps(): NDArray = nd.disposeDepsExcept()
+
+ /**
+ * Dispose all NDArrays who help to construct this array, excepts those in the arguments.
+ * e.g. (a * b + c).disposeDepsExcept(a, b)
+ * will dispose c and a * b.
+ * Note that a, b's dependencies will not be disposed either.
+ * @param arr the Array of NDArray not to dispose
+ * @return this array
+ */
+ def disposeDepsExcept(arr: Array[NDArray]): NDArray =
+ nd.disposeDepsExcept(arr.map(NDArray.toNDArray): _*)
+
+ /**
+ * Return a sliced NDArray that shares memory with current one.
+ * NDArray only support continuous slicing on axis 0
+ *
+ * @param start Starting index of slice.
+ * @param stop Finishing index of slice.
+ *
+ * @return a sliced NDArray that shares memory with current one.
+ */
+ def slice(start: Int, stop: Int): NDArray = nd.slice(start, stop)
+
+ /**
+ * Return a sliced NDArray at the ith position of axis0
+ * @param i
+ * @return a sliced NDArray that shares memory with current one.
+ */
+ def slice (i: Int): NDArray = nd.slice(i)
+
+ /**
+ * Return a sub NDArray that shares memory with current one.
+ * the first axis will be rolled up, which causes its shape different from slice(i, i+1)
+ * @param idx index of sub array.
+ */
+ def at(idx: Int): NDArray = nd.at(idx)
+
+ def T: NDArray = nd.T
+
+ /**
+ * Get data type of current NDArray.
+ * @return class representing type of current ndarray
+ */
+ def dtype: DType = nd.dtype
+
+ /**
+ * Return a copied numpy array of current array with specified type.
+ * @param dtype Desired type of result array.
+ * @return A copy of array content.
+ */
+ def asType(dtype: DType): NDArray = nd.asType(dtype)
+
+ /**
+ * Return a reshaped NDArray that shares memory with current one.
+ * @param dims New shape.
+ *
+ * @return a reshaped NDArray that shares memory with current one.
+ */
+ def reshape(dims: Array[Int]): NDArray = nd.reshape(dims)
+
+ /**
+ * Block until all pending writes operations on current NDArray are finished.
+ * This function will return when all the pending writes to the current
+ * NDArray finishes. There can still be pending read going on when the
+ * function returns.
+ */
+ def waitToRead(): Unit = nd.waitToRead()
+
+ /**
+ * Get context of current NDArray.
+ * @return The context of current NDArray.
+ */
+ def context: Context = nd.context
+
+ /**
+ * Set the values of the NDArray
+ * @param value Value to set
+ * @return Current NDArray
+ */
+ def set(value: Float): NDArray = nd.set(value)
+ def set(other: NDArray): NDArray = nd.set(other)
+ def set(other: Array[Float]): NDArray = nd.set(other)
+
+ def add(other: NDArray): NDArray = this.nd + other.nd
+ def add(other: Float): NDArray = this.nd + other
+ def addInplace(other: NDArray): NDArray = this.nd += other
+ def addInplace(other: Float): NDArray = this.nd += other
+ def subtract(other: NDArray): NDArray = this.nd - other
+ def subtract(other: Float): NDArray = this.nd - other
+ def subtractInplace(other: NDArray): NDArray = this.nd -= other
+ def subtractInplace(other: Float): NDArray = this.nd -= other
+ def multiply(other: NDArray): NDArray = this.nd * other
+ def multiply(other: Float): NDArray = this.nd * other
+ def multiplyInplace(other: NDArray): NDArray = this.nd *= other
+ def multiplyInplace(other: Float): NDArray = this.nd *= other
+ def div(other: NDArray): NDArray = this.nd / other
+ def div(other: Float): NDArray = this.nd / other
+ def divInplace(other: NDArray): NDArray = this.nd /= other
+ def divInplace(other: Float): NDArray = this.nd /= other
+ def pow(other: NDArray): NDArray = this.nd ** other
+ def pow(other: Float): NDArray = this.nd ** other
+ def powInplace(other: NDArray): NDArray = this.nd **= other
+ def powInplace(other: Float): NDArray = this.nd **= other
+ def mod(other: NDArray): NDArray = this.nd % other
+ def mod(other: Float): NDArray = this.nd % other
+ def modInplace(other: NDArray): NDArray = this.nd %= other
+ def modInplace(other: Float): NDArray = this.nd %= other
+ def greater(other: NDArray): NDArray = this.nd > other
+ def greater(other: Float): NDArray = this.nd > other
+ def greaterEqual(other: NDArray): NDArray = this.nd >= other
+ def greaterEqual(other: Float): NDArray = this.nd >= other
+ def lesser(other: NDArray): NDArray = this.nd < other
+ def lesser(other: Float): NDArray = this.nd < other
+ def lesserEqual(other: NDArray): NDArray = this.nd <= other
+ def lesserEqual(other: Float): NDArray = this.nd <= other
+
+ /**
+ * Return a copied flat java array of current array (row-major).
+ * @return A copy of array content.
+ */
+ def toArray: Array[Float] = nd.toArray
+
+ /**
+ * Return a CPU scalar(float) of current ndarray.
+ * This ndarray must have shape (1,)
+ *
+ * @return The scalar representation of the ndarray.
+ */
+ def toScalar: Float = nd.toScalar
+
+ /**
+ * Copy the content of current array to other.
+ *
+ * @param other Target NDArray or context we want to copy data to.
+ * @return The copy target NDArray
+ */
+ def copyTo(other: NDArray): NDArray = nd.copyTo(other)
+
+ /**
+ * Copy the content of current array to a new NDArray in the context.
+ *
+ * @param ctx Target context we want to copy data to.
+ * @return The copy target NDArray
+ */
+ def copyTo(ctx: Context): NDArray = nd.copyTo(ctx)
+
+ /**
+ * Clone the current array
+ * @return the copied NDArray in the same context
+ */
+ def copy(): NDArray = copyTo(this.context)
+
+ /**
+ * Get shape of current NDArray.
+ * @return an array representing shape of current ndarray
+ */
+ def shape: Shape = nd.shape
+
+
+ def size: Int = shape.product
+
+ /**
+ * Return an `NDArray` that lives in the target context. If the array
+ * is already in that context, `self` is returned. Otherwise, a copy is made.
+ * @param context The target context we want the return value to live in.
+ * @return A copy or `self` as an `NDArray` that lives in the target context.
+ */
+ def asInContext(context: Context): NDArray = nd.asInContext(context)
+
+ override def equals(obj: Any): Boolean = nd.equals(obj)
+ override def hashCode(): Int = nd.hashCode
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/Shape.scala b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/Shape.scala
new file mode 100644
index 000000000000..b795fe31f726
--- /dev/null
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/javaapi/Shape.scala
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi
+
+import collection.JavaConverters._
+import scala.language.implicitConversions
+
+/**
+ * Shape of [[NDArray]] or other data
+ */
+
+class Shape private[mxnet] (val shape: org.apache.mxnet.Shape) {
+ def this(dims: java.util.List[java.lang.Integer])
+ = this(new org.apache.mxnet.Shape(dims.asScala.map(Int.unbox)))
+ def this(dims: Array[Int]) = this(new org.apache.mxnet.Shape(dims))
+
+ def apply(dim: Int): Int = shape.apply(dim)
+ def get(dim: Int): Int = apply(dim)
+ def size: Int = shape.size
+ def length: Int = shape.length
+ def drop(dim: Int): Shape = shape.drop(dim)
+ def slice(from: Int, end: Int): Shape = shape.slice(from, end)
+ def product: Int = shape.product
+ def head: Int = shape.head
+
+ def toArray: Array[Int] = shape.toArray
+ def toVector: java.util.List[Int] = shape.toVector.asJava
+
+ override def toString(): String = shape.toString
+ override def equals(o: Any): Boolean = shape.equals(o)
+ override def hashCode(): Int = shape.hashCode()
+}
+
+object Shape {
+ implicit def fromShape(shape: org.apache.mxnet.Shape): Shape = new Shape(shape)
+
+ implicit def toShape(jShape: Shape): org.apache.mxnet.Shape = jShape.shape
+}
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/module/BaseModule.scala b/scala-package/core/src/main/scala/org/apache/mxnet/module/BaseModule.scala
index 30e57c57fbe5..b73f4ad4b112 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/module/BaseModule.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/module/BaseModule.scala
@@ -210,7 +210,7 @@ abstract class BaseModule {
* @param reset Default is `True`, indicating whether we should reset the data iter before start
* doing prediction.
* @return The return value will be a nested list like
- * `[[out1_batch1, out2_batch1, ...], [out1_batch2, out2_batch2, ...]]`
+ * `[ [out1_batch1, out2_batch1, ...], [out1_batch2, out2_batch2, ...] ]`
* This mode is useful because in some cases (e.g. bucketing),
* the module does not necessarily produce the same number of outputs.
*/
@@ -501,7 +501,7 @@ abstract class BaseModule {
* Get outputs of the previous forward computation.
* @return In the case when data-parallelism is used,
* the outputs will be collected from multiple devices.
- * The results will look like `[[out1_dev1, out1_dev2], [out2_dev1, out2_dev2]]`,
+ * The results will look like `[ [out1_dev1, out1_dev2], [out2_dev1, out2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getOutputs(): IndexedSeq[IndexedSeq[NDArray]]
@@ -519,7 +519,7 @@ abstract class BaseModule {
* Get the gradients to the inputs, computed in the previous backward computation.
* @return In the case when data-parallelism is used,
* the grads will be collected from multiple devices.
- * The results will look like `[[grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2]]`,
+ * The results will look like `[ [grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getInputGrads(): IndexedSeq[IndexedSeq[NDArray]]
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/module/BucketingModule.scala b/scala-package/core/src/main/scala/org/apache/mxnet/module/BucketingModule.scala
index 2262f5c6aa39..1ac798e1b617 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/module/BucketingModule.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/module/BucketingModule.scala
@@ -339,7 +339,7 @@ class BucketingModule(symGen: AnyRef => (Symbol, IndexedSeq[String], IndexedSeq[
* Get outputs of the previous forward computation.
* @return In the case when data-parallelism is used,
* the outputs will be collected from multiple devices.
- * The results will look like `[[out1_dev1, out1_dev2], [out2_dev1, out2_dev2]]`,
+ * The results will look like `[ [out1_dev1, out1_dev2], [out2_dev1, out2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
override def getOutputs(): IndexedSeq[IndexedSeq[NDArray]] = {
@@ -363,7 +363,7 @@ class BucketingModule(symGen: AnyRef => (Symbol, IndexedSeq[String], IndexedSeq[
* Get the gradients to the inputs, computed in the previous backward computation.
* @return In the case when data-parallelism is used,
* the grads will be collected from multiple devices.
- * The results will look like `[[grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2]]`,
+ * The results will look like `[ [grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
override def getInputGrads(): IndexedSeq[IndexedSeq[NDArray]] = {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/module/DataParallelExecutorGroup.scala b/scala-package/core/src/main/scala/org/apache/mxnet/module/DataParallelExecutorGroup.scala
index 5c567fe5d53c..df66ea7721fb 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/module/DataParallelExecutorGroup.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/module/DataParallelExecutorGroup.scala
@@ -517,7 +517,7 @@ class DataParallelExecutorGroup private[module](
* Get outputs of the previous forward computation.
* @return In the case when data-parallelism is used,
* the outputs will be collected from multiple devices.
- * The results will look like `[[out1_dev1, out1_dev2], [out2_dev1, out2_dev2]]`,
+ * The results will look like `[ [out1_dev1, out1_dev2], [out2_dev1, out2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getOutputs(): IndexedSeq[IndexedSeq[NDArray]] = {
@@ -539,7 +539,7 @@ class DataParallelExecutorGroup private[module](
* Get the gradients to the inputs, computed in the previous backward computation.
* @return In the case when data-parallelism is used,
* the grads will be collected from multiple devices.
- * The results will look like `[[grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2]]`,
+ * The results will look like `[ [grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getInputGrads(): IndexedSeq[IndexedSeq[NDArray]] = {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/module/Module.scala b/scala-package/core/src/main/scala/org/apache/mxnet/module/Module.scala
index fec1ba0dc355..97df3dcb307d 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/module/Module.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/module/Module.scala
@@ -486,7 +486,7 @@ class Module(symbolVar: Symbol,
* Get outputs of the previous forward computation.
* @return In the case when data-parallelism is used,
* the outputs will be collected from multiple devices.
- * The results will look like `[[out1_dev1, out1_dev2], [out2_dev1, out2_dev2]]`,
+ * The results will look like `[ [out1_dev1, out1_dev2], [out2_dev1, out2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getOutputs(): IndexedSeq[IndexedSeq[NDArray]] = {
@@ -510,7 +510,7 @@ class Module(symbolVar: Symbol,
* Get the gradients to the inputs, computed in the previous backward computation.
* @return In the case when data-parallelism is used,
* the grads will be collected from multiple devices.
- * The results will look like `[[grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2]]`,
+ * The results will look like `[ [grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getInputGrads(): IndexedSeq[IndexedSeq[NDArray]] = {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/module/SequentialModule.scala b/scala-package/core/src/main/scala/org/apache/mxnet/module/SequentialModule.scala
index e75550a1d1f0..2e506c08e548 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/module/SequentialModule.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/module/SequentialModule.scala
@@ -346,7 +346,7 @@ class SequentialModule extends BaseModule {
* Get outputs of the previous forward computation.
* @return In the case when data-parallelism is used,
* the outputs will be collected from multiple devices.
- * The results will look like `[[out1_dev1, out1_dev2], [out2_dev1, out2_dev2]]`,
+ * The results will look like `[ [out1_dev1, out1_dev2], [out2_dev1, out2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getOutputs(): IndexedSeq[IndexedSeq[NDArray]] = {
@@ -370,7 +370,7 @@ class SequentialModule extends BaseModule {
* Get the gradients to the inputs, computed in the previous backward computation.
* @return In the case when data-parallelism is used,
* the grads will be collected from multiple devices.
- * The results will look like `[[grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2]]`,
+ * The results will look like `[ [grad1_dev1, grad1_dev2], [grad2_dev1, grad2_dev2] ]`,
* those `NDArray` might live on different devices.
*/
def getInputGrads(): IndexedSeq[IndexedSeq[NDArray]] = {
diff --git a/scala-package/core/src/main/scala/org/apache/mxnet/optimizer/SGD.scala b/scala-package/core/src/main/scala/org/apache/mxnet/optimizer/SGD.scala
index e20b433ed1ed..d349feac3e93 100644
--- a/scala-package/core/src/main/scala/org/apache/mxnet/optimizer/SGD.scala
+++ b/scala-package/core/src/main/scala/org/apache/mxnet/optimizer/SGD.scala
@@ -17,7 +17,7 @@
package org.apache.mxnet.optimizer
-import org.apache.mxnet.{Optimizer, LRScheduler, NDArray}
+import org.apache.mxnet._
import org.apache.mxnet.NDArrayConversions._
/**
@@ -92,7 +92,13 @@ class SGD(val learningRate: Float = 0.01f, momentum: Float = 0.0f,
if (momentum == 0.0f) {
null
} else {
- NDArray.zeros(weight.shape, weight.context)
+ val s = NDArray.zeros(weight.shape, weight.context)
+ // this is created on the fly and shared between runs,
+ // we don't want it to be dispose from the scope
+ // and should be handled by the dispose
+ val scope = ResourceScope.getCurrentScope()
+ if (scope.isDefined) scope.get.remove(s)
+ s
}
}
diff --git a/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ContextTest.java b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ContextTest.java
new file mode 100644
index 000000000000..abd4b5edb1e6
--- /dev/null
+++ b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ContextTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ContextTest {
+
+ @Test
+ public void testCPU() {
+ Context.cpu();
+ }
+
+ @Test
+ public void testDefault() {
+ Context.defaultCtx();
+ }
+
+ @Test
+ public void testConstructor() {
+ new Context("cpu", 0);
+ }
+}
diff --git a/scala-package/core/src/test/java/org/apache/mxnet/javaapi/DTypeTest.java b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/DTypeTest.java
new file mode 100644
index 000000000000..2e356edf5326
--- /dev/null
+++ b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/DTypeTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi;
+
+import org.junit.Test;
+
+public class DTypeTest {
+
+ @Test
+ public void Float16Test() {
+ DType.Float16();
+ }
+
+ @Test
+ public void Float32Test() {
+ DType.Float32();
+ }
+
+ @Test
+ public void Float64Test() {
+ DType.Float64();
+ }
+
+ @Test
+ public void UnknownTest() {
+ DType.Unknown();
+ }
+
+ @Test
+ public void Int32Test() {
+ DType.Int32();
+ }
+
+ @Test
+ public void UInt8Test() {
+ DType.UInt8();
+ }
+}
diff --git a/scala-package/core/src/test/java/org/apache/mxnet/javaapi/IOTest.java b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/IOTest.java
new file mode 100644
index 000000000000..f53b5c405642
--- /dev/null
+++ b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/IOTest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi;
+
+import org.junit.Test;
+
+public class IOTest {
+
+ @Test
+ public void testConstructor() {
+ Shape inputShape = new Shape(new int[] {1, 3, 512, 512});
+ new DataDesc("data", inputShape, DType.Float32(), "NCHW");
+ }
+
+ @Test
+ public void testgetBatchAxis() {
+ DataDesc.getBatchAxis("NCHW");
+ }
+
+}
diff --git a/scala-package/core/src/test/java/org/apache/mxnet/javaapi/NDArrayTest.java b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/NDArrayTest.java
new file mode 100644
index 000000000000..2659b7848bc6
--- /dev/null
+++ b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/NDArrayTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.mxnet.javaapi.NDArrayBase.*;
+
+import static org.junit.Assert.assertTrue;
+
+public class NDArrayTest {
+ @Test
+ public void testCreateNDArray() {
+ NDArray nd = new NDArray(new float[]{1.0f, 2.0f, 3.0f},
+ new Shape(new int[]{1, 3}),
+ new Context("cpu", 0));
+ int[] arr = new int[]{1, 3};
+ assertTrue(Arrays.equals(nd.shape().toArray(), arr));
+ assertTrue(nd.at(0).at(0).toArray()[0] == 1.0f);
+ List list = Arrays.asList(1.0f, 2.0f, 3.0f);
+ // Second way creating NDArray
+ nd = NDArray.array(list,
+ new Shape(new int[]{1, 3}),
+ new Context("cpu", 0));
+ assertTrue(Arrays.equals(nd.shape().toArray(), arr));
+ }
+
+ @Test
+ public void testZeroOneEmpty(){
+ NDArray ones = NDArray.ones(new Context("cpu", 0), new int[]{100, 100});
+ NDArray zeros = NDArray.zeros(new Context("cpu", 0), new int[]{100, 100});
+ NDArray empty = NDArray.empty(new Context("cpu", 0), new int[]{100, 100});
+ int[] arr = new int[]{100, 100};
+ assertTrue(Arrays.equals(ones.shape().toArray(), arr));
+ assertTrue(Arrays.equals(zeros.shape().toArray(), arr));
+ assertTrue(Arrays.equals(empty.shape().toArray(), arr));
+ }
+
+ @Test
+ public void testComparison(){
+ NDArray nd = new NDArray(new float[]{1.0f, 2.0f, 3.0f}, new Shape(new int[]{3}), new Context("cpu", 0));
+ NDArray nd2 = new NDArray(new float[]{3.0f, 4.0f, 5.0f}, new Shape(new int[]{3}), new Context("cpu", 0));
+ nd = nd.add(nd2);
+ float[] greater = new float[]{1, 1, 1};
+ assertTrue(Arrays.equals(nd.greater(nd2).toArray(), greater));
+ nd = nd.subtract(nd2);
+ nd = nd.subtract(nd2);
+ float[] lesser = new float[]{0, 0, 0};
+ assertTrue(Arrays.equals(nd.greater(nd2).toArray(), lesser));
+ }
+
+ @Test
+ public void testGenerated(){
+ NDArray$ NDArray = NDArray$.MODULE$;
+ float[] arr = new float[]{1.0f, 2.0f, 3.0f};
+ NDArray nd = new NDArray(arr, new Shape(new int[]{3}), new Context("cpu", 0));
+ float result = NDArray.norm(NDArray.new normParam(nd))[0].toArray()[0];
+ float cal = 0.0f;
+ for (float ele : arr) {
+ cal += ele * ele;
+ }
+ cal = (float) Math.sqrt(cal);
+ assertTrue(Math.abs(result - cal) < 1e-5);
+ NDArray dotResult = new NDArray(new float[]{0}, new Shape(new int[]{1}), new Context("cpu", 0));
+ NDArray.dot(NDArray.new dotParam(nd, nd).setOut(dotResult));
+ assertTrue(Arrays.equals(dotResult.toArray(), new float[]{14.0f}));
+ }
+}
diff --git a/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ResourceScopeTestSuite.java b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ResourceScopeTestSuite.java
new file mode 100644
index 000000000000..1c246d870e28
--- /dev/null
+++ b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ResourceScopeTestSuite.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.mxnet.javaapi;
+
+import org.apache.mxnet.NativeResourceRef;
+import org.apache.mxnet.ResourceScope;
+import org.junit.Test;
+
+import java.util.*;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ResourceScopeTestSuite {
+
+ /**
+ * This is a placeholder class to test out whether NDArray References get collected or not when using
+ * try-with-resources in Java.
+ *
+ */
+ class TestNDArray {
+ NDArray selfArray;
+
+ public TestNDArray(Context context, int[] shape) {
+ this.selfArray = NDArray.ones(context, shape);
+ }
+
+ public boolean verifyIsDisposed() {
+ return this.selfArray.nd().isDisposed();
+ }
+
+ public NativeResourceRef getNDArrayReference() {
+ return this.selfArray.nd().ref();
+ }
+ }
+
+ @Test
+ public void testNDArrayAutoRelease() {
+ TestNDArray test = null;
+
+ try (ResourceScope scope = new ResourceScope()) {
+ test = new TestNDArray(Context.cpu(), new int[]{100, 100});
+ }
+
+ assertTrue(test.verifyIsDisposed());
+ }
+
+ @Test
+ public void testObjectReleaseFromList() {
+ List list = new ArrayList<>();
+
+ try (ResourceScope scope = new ResourceScope()) {
+ for (int i = 0;i < 10; i++) {
+ list.add(new TestNDArray(Context.cpu(), new int[] {100, 100}));
+ }
+ }
+
+ assertEquals(list.size() , 10);
+ for (TestNDArray item : list) {
+ assertTrue(item.verifyIsDisposed());
+ }
+ }
+
+ @Test
+ public void testObjectReleaseFromMap() {
+ Map stringToNDArrayMap = new HashMap<>();
+
+ try (ResourceScope scope = new ResourceScope()) {
+ for (int i = 0;i < 10; i++) {
+ stringToNDArrayMap.put(String.valueOf(i),new TestNDArray(Context.cpu(), new int[] {i, i}));
+ }
+ }
+
+ assertEquals(stringToNDArrayMap.size(), 10);
+ for (Map.Entry entry : stringToNDArrayMap.entrySet()) {
+ assertTrue(entry.getValue().verifyIsDisposed());
+ }
+
+ Map ndArrayToStringMap = new HashMap<>();
+
+ try (ResourceScope scope = new ResourceScope()) {
+ for (int i = 0;i < 10; i++) {
+ ndArrayToStringMap.put(new TestNDArray(Context.cpu(), new int[] {i, i}), String.valueOf(i));
+ }
+ }
+
+ assertEquals(ndArrayToStringMap.size(), 10);
+ for (Map.Entry entry : ndArrayToStringMap.entrySet()) {
+ assertTrue(entry.getKey().verifyIsDisposed());
+ }
+
+ }
+}
diff --git a/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ShapeTest.java b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ShapeTest.java
new file mode 100644
index 000000000000..8f045b5687ab
--- /dev/null
+++ b/scala-package/core/src/test/java/org/apache/mxnet/javaapi/ShapeTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class ShapeTest {
+ @Test
+ public void testArrayConstructor()
+ {
+ new Shape(new int[] {3, 4, 5});
+ }
+
+ @Test
+ public void testListConstructor()
+ {
+ ArrayList arrList = new ArrayList();
+ arrList.add(3);
+ arrList.add(4);
+ arrList.add(5);
+ new Shape(arrList);
+ }
+
+ @Test
+ public void testApply()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertEquals(jS.apply(1), 4);
+ }
+
+ @Test
+ public void testGet()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertEquals(jS.get(1), 4);
+ }
+
+ @Test
+ public void testSize()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertEquals(jS.size(), 3);
+ }
+
+ @Test
+ public void testLength()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertEquals(jS.length(), 3);
+ }
+
+ @Test
+ public void testDrop()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ ArrayList l = new ArrayList();
+ l.add(4);
+ l.add(5);
+ assertTrue(jS.drop(1).toVector().equals(l));
+ }
+
+ @Test
+ public void testSlice()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ ArrayList l = new ArrayList();
+ l.add(4);
+ assertTrue(jS.slice(1,2).toVector().equals(l));
+ }
+
+ @Test
+ public void testProduct()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertEquals(jS.product(), 60);
+ }
+
+ @Test
+ public void testHead()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertEquals(jS.head(), 3);
+ }
+
+ @Test
+ public void testToArray()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ assertTrue(Arrays.equals(jS.toArray(), new int[] {3,4,5}));
+ }
+
+ @Test
+ public void testToVector()
+ {
+ Shape jS = new Shape(new int[] {3, 4, 5});
+ ArrayList l = new ArrayList();
+ l.add(3);
+ l.add(4);
+ l.add(5);
+ assertTrue(jS.toVector().equals(l));
+ }
+}
diff --git a/scala-package/core/src/test/scala/org/apache/mxnet/ImageSuite.scala b/scala-package/core/src/test/scala/org/apache/mxnet/ImageSuite.scala
new file mode 100644
index 000000000000..67815ad6c108
--- /dev/null
+++ b/scala-package/core/src/test/scala/org/apache/mxnet/ImageSuite.scala
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+
+import java.io.File
+import java.net.URL
+
+import javax.imageio.ImageIO
+import org.apache.commons.io.FileUtils
+import org.scalatest.{BeforeAndAfterAll, FunSuite}
+import org.slf4j.LoggerFactory
+
+class ImageSuite extends FunSuite with BeforeAndAfterAll {
+ private var imLocation = ""
+ private val logger = LoggerFactory.getLogger(classOf[ImageSuite])
+
+ private def downloadUrl(url: String, filePath: String, maxRetry: Option[Int] = None) : Unit = {
+ val tmpFile = new File(filePath)
+ var retry = maxRetry.getOrElse(3)
+ var success = false
+ if (!tmpFile.exists()) {
+ while (retry > 0 && !success) {
+ try {
+ FileUtils.copyURLToFile(new URL(url), tmpFile)
+ success = true
+ } catch {
+ case e: Exception => retry -= 1
+ }
+ }
+ } else {
+ success = true
+ }
+ if (!success) throw new Exception(s"$url Download failed!")
+ }
+
+ override def beforeAll(): Unit = {
+ val tempDirPath = System.getProperty("java.io.tmpdir")
+ imLocation = tempDirPath + "/inputImages/Pug-Cookie.jpg"
+ downloadUrl("https://s3.amazonaws.com/model-server/inputs/Pug-Cookie.jpg",
+ imLocation)
+ }
+
+ test("Test load image") {
+ val nd = Image.imRead(imLocation)
+ logger.info(s"OpenCV load image with shape: ${nd.shape}")
+ require(nd.shape == Shape(576, 1024, 3), "image shape not Match!")
+ }
+
+ test("Test load image from Socket") {
+ val url = new URL("https://s3.amazonaws.com/model-server/inputs/Pug-Cookie.jpg")
+ val inputStream = url.openStream
+ val nd = Image.imDecode(inputStream)
+ logger.info(s"OpenCV load image with shape: ${nd.shape}")
+ require(nd.shape == Shape(576, 1024, 3), "image shape not Match!")
+ }
+
+ test("Test resize image") {
+ val nd = Image.imRead(imLocation)
+ val resizeIm = Image.imResize(nd, 224, 224)
+ logger.info(s"OpenCV resize image with shape: ${resizeIm.shape}")
+ require(resizeIm.shape == Shape(224, 224, 3), "image shape not Match!")
+ }
+
+ test("Test crop image") {
+ val nd = Image.imRead(imLocation)
+ val nd2 = Image.fixedCrop(nd, 0, 0, 224, 224)
+ require(nd2.shape == Shape(224, 224, 3), "image shape not Match!")
+ }
+
+ test("Test apply border") {
+ val nd = Image.imRead(imLocation)
+ val nd2 = Image.copyMakeBorder(nd, 1, 1, 1, 1)
+ require(nd2.shape == Shape(578, 1026, 3), s"image shape not Match!")
+ }
+
+ test("Test convert to Image") {
+ val nd = Image.imRead(imLocation)
+ val resizeIm = Image.imResize(nd, 224, 224)
+ val tempDirPath = System.getProperty("java.io.tmpdir")
+ val img = Image.toImage(resizeIm)
+ ImageIO.write(img, "png", new File(tempDirPath + "/inputImages/out.png"))
+ logger.info(s"converted image stored in ${tempDirPath + "/inputImages/out.png"}")
+ }
+
+}
diff --git a/scala-package/core/src/test/scala/org/apache/mxnet/NativeResourceSuite.scala b/scala-package/core/src/test/scala/org/apache/mxnet/NativeResourceSuite.scala
new file mode 100644
index 000000000000..81a9f605a887
--- /dev/null
+++ b/scala-package/core/src/test/scala/org/apache/mxnet/NativeResourceSuite.scala
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+
+import java.lang.ref.ReferenceQueue
+import java.util.concurrent.ConcurrentHashMap
+
+import org.apache.mxnet.Base.CPtrAddress
+import org.mockito.Matchers.any
+import org.scalatest.{BeforeAndAfterAll, FunSuite, Matchers, TagAnnotation}
+import org.mockito.Mockito._
+
+@TagAnnotation("resource")
+class NativeResourceSuite extends FunSuite with BeforeAndAfterAll with Matchers {
+
+ object TestRef {
+ def getRefQueue: ReferenceQueue[NativeResource] = { NativeResourceRef.refQ}
+ def getRefMap: ConcurrentHashMap[NativeResourceRef, CPtrAddress]
+ = {NativeResourceRef.refMap}
+ def getCleaner: Thread = { NativeResourceRef.cleaner }
+ }
+
+ class TestRef(resource: NativeResource,
+ resourceDeAllocator: CPtrAddress => Int)
+ extends NativeResourceRef(resource, resourceDeAllocator) {
+ }
+
+ test(testName = "test native resource setup/teardown") {
+ val a = spy(NDArray.ones(Shape(2, 3)))
+ val aRef = a.ref
+ val spyRef = spy(aRef)
+
+ assert(TestRef.getRefMap.containsKey(aRef) == true)
+ a.close()
+ verify(a).dispose()
+ verify(a).nativeDeAllocator
+ // resourceDeAllocator does not get called when explicitly closing
+ verify(spyRef, times(0)).resourceDeAllocator
+
+ assert(TestRef.getRefMap.containsKey(aRef) == false)
+ assert(a.isDisposed == true, "isDisposed should be set to true after calling close")
+ }
+
+ test(testName = "test dispose") {
+ val a: NDArray = spy(NDArray.ones(Shape(3, 4)))
+ val aRef = a.ref
+ val spyRef = spy(aRef)
+ a.dispose()
+ verify(a).nativeDeAllocator
+ assert(TestRef.getRefMap.containsKey(aRef) == false)
+ assert(a.isDisposed == true, "isDisposed should be set to true after calling close")
+ }
+}
+
diff --git a/scala-package/core/src/test/scala/org/apache/mxnet/ResourceScopeSuite.scala b/scala-package/core/src/test/scala/org/apache/mxnet/ResourceScopeSuite.scala
new file mode 100644
index 000000000000..41dfa7d0ead2
--- /dev/null
+++ b/scala-package/core/src/test/scala/org/apache/mxnet/ResourceScopeSuite.scala
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+
+import java.lang.ref.ReferenceQueue
+import java.util.concurrent.ConcurrentHashMap
+
+import org.apache.mxnet.Base.CPtrAddress
+import org.apache.mxnet.ResourceScope.logger
+import org.mockito.Matchers.any
+import org.scalatest.{BeforeAndAfterAll, FunSuite, Matchers}
+import org.mockito.Mockito._
+import scala.collection.mutable.HashMap
+
+class ResourceScopeSuite extends FunSuite with BeforeAndAfterAll with Matchers {
+
+ class TestNativeResource extends NativeResource {
+ /**
+ * native Address associated with this object
+ */
+ override def nativeAddress: CPtrAddress = hashCode()
+
+ /**
+ * Function Pointer to the NativeDeAllocator of nativeAddress
+ */
+ override def nativeDeAllocator: CPtrAddress => Int = TestNativeResource.deAllocator
+
+ /** Call NativeResource.register to get the reference
+ */
+ override val ref: NativeResourceRef = super.register()
+ /**
+ * Off-Heap Bytes Allocated for this object
+ */
+ override val bytesAllocated: Long = 0
+ }
+ object TestNativeResource {
+ def deAllocator(handle: CPtrAddress): Int = 0
+ }
+
+ object TestPhantomRef {
+ def getRefQueue: ReferenceQueue[NativeResource] = { NativeResourceRef.refQ}
+ def getRefMap: ConcurrentHashMap[NativeResourceRef, CPtrAddress]
+ = {NativeResourceRef.refMap}
+ def getCleaner: Thread = { NativeResourceRef.cleaner }
+
+ }
+
+ class TestPhantomRef(resource: NativeResource,
+ resourceDeAllocator: CPtrAddress => Int)
+ extends NativeResourceRef(resource, resourceDeAllocator) {
+ }
+
+ test(testName = "test NDArray Auto Release") {
+ var a: NDArray = null
+ var aRef: NativeResourceRef = null
+ var b: NDArray = null
+
+ ResourceScope.using() {
+ b = ResourceScope.using() {
+ a = NDArray.ones(Shape(3, 4))
+ aRef = a.ref
+ val x = NDArray.ones(Shape(3, 4))
+ x
+ }
+ val bRef: NativeResourceRef = b.ref
+ assert(a.isDisposed == true,
+ "objects created within scope should have isDisposed set to true")
+ assert(b.isDisposed == false,
+ "returned NativeResource should not be released")
+ assert(TestPhantomRef.getRefMap.containsKey(aRef) == false,
+ "reference of resource in Scope should be removed refMap")
+ assert(TestPhantomRef.getRefMap.containsKey(bRef) == true,
+ "reference of resource outside scope should be not removed refMap")
+ }
+ assert(b.isDisposed, "resource returned from inner scope should be released in outer scope")
+ }
+
+ test("test return object release from outer scope") {
+ var a: TestNativeResource = null
+ ResourceScope.using() {
+ a = ResourceScope.using() {
+ new TestNativeResource()
+ }
+ assert(a.isDisposed == false, "returned object should not be disposed within Using")
+ }
+ assert(a.isDisposed == true, "returned object should be disposed in the outer scope")
+ }
+
+ test(testName = "test NativeResources in returned Lists are not disposed") {
+ var ndListRet: IndexedSeq[TestNativeResource] = null
+ ResourceScope.using() {
+ ndListRet = ResourceScope.using() {
+ val ndList: IndexedSeq[TestNativeResource] =
+ IndexedSeq(new TestNativeResource(), new TestNativeResource())
+ ndList
+ }
+ ndListRet.foreach(nd => assert(nd.isDisposed == false,
+ "NativeResources within a returned collection should not be disposed"))
+ }
+ ndListRet.foreach(nd => assert(nd.isDisposed == true,
+ "NativeResources returned from inner scope should be disposed in outer scope"))
+ }
+
+ test("test native resource inside a map") {
+ var nRInKeyOfMap: HashMap[TestNativeResource, String] = null
+ var nRInValOfMap: HashMap[String, TestNativeResource] = HashMap[String, TestNativeResource]()
+
+ ResourceScope.using() {
+ nRInKeyOfMap = ResourceScope.using() {
+ val ret = HashMap[TestNativeResource, String]()
+ ret.put(new TestNativeResource, "hello")
+ ret
+ }
+ assert(!nRInKeyOfMap.isEmpty)
+
+ nRInKeyOfMap.keysIterator.foreach(it => assert(it.isDisposed == false,
+ "NativeResources returned in Traversable should not be disposed"))
+ }
+
+ nRInKeyOfMap.keysIterator.foreach(it => assert(it.isDisposed))
+
+ ResourceScope.using() {
+
+ nRInValOfMap = ResourceScope.using() {
+ val ret = HashMap[String, TestNativeResource]()
+ ret.put("world!", new TestNativeResource)
+ ret
+ }
+ assert(!nRInValOfMap.isEmpty)
+ nRInValOfMap.valuesIterator.foreach(it => assert(it.isDisposed == false,
+ "NativeResources returned in Collection should not be disposed"))
+ }
+ nRInValOfMap.valuesIterator.foreach(it => assert(it.isDisposed))
+ }
+
+}
diff --git a/scala-package/core/src/test/scala/org/apache/mxnet/ShapeSuite.scala b/scala-package/core/src/test/scala/org/apache/mxnet/ShapeSuite.scala
index b4b7486d0bec..b186653e3db9 100644
--- a/scala-package/core/src/test/scala/org/apache/mxnet/ShapeSuite.scala
+++ b/scala-package/core/src/test/scala/org/apache/mxnet/ShapeSuite.scala
@@ -29,4 +29,19 @@ class ShapeSuite extends FunSuite with BeforeAndAfterAll {
assert(Shape(1, 2, 3) === Shape(1, 2, 3))
assert(Shape(1, 2) != Shape(1, 2, 3))
}
+
+ test("drop") {
+ val s = Shape(1, 2, 3)
+ val s2 = s.drop(1)
+ assert(s == Shape(1, 2, 3))
+ assert(s2 == Shape(2, 3))
+ val s3 = s.drop(2)
+ assert(s3 == Shape(3))
+ }
+
+ test("slice") {
+ val s = Shape(1, 2, 3)
+ val s2 = s.slice(0, 1)
+ assert(s2 == Shape(1))
+ }
}
diff --git a/scala-package/examples/pom.xml b/scala-package/examples/pom.xml
index 436f2992768b..72a40dc01f01 100644
--- a/scala-package/examples/pom.xml
+++ b/scala-package/examples/pom.xml
@@ -13,13 +13,11 @@
mxnet-examples_2.11
MXNet Scala Package - Examples
+
+ true
+
+
-
- unittest
-
- true
-
-
integrationtest
diff --git a/scala-package/examples/scripts/benchmark/run_image_inference_bm.sh b/scala-package/examples/scripts/benchmark/run_image_inference_bm.sh
new file mode 100755
index 000000000000..b647bdf8c6bf
--- /dev/null
+++ b/scala-package/examples/scripts/benchmark/run_image_inference_bm.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -e
+
+echo $OSTYPE
+
+hw_type=cpu
+if [[ $1 = gpu ]]
+then
+ hw_type=gpu
+fi
+
+platform=linux-x86_64
+
+if [[ $OSTYPE = [darwin]* ]]
+then
+ platform=osx-x86_64
+fi
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../../.."; pwd)
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/$platform-$hw_type/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*
+
+MODEL_NAME=$2
+
+RUNS=$3
+
+BATCHSIZE=$4
+
+# model dir
+MODEL_PATH_PREFIX=$5
+# input image
+INPUT_IMG=$6
+# which input image dir
+INPUT_DIR=$7
+
+java -Xmx8G -Dmxnet.traceLeakedObjects=true -cp $CLASS_PATH \
+ org.apache.mxnetexamples.benchmark.ScalaInferenceBenchmark \
+ --example $MODEL_NAME \
+ --count $RUNS \
+ --batchSize $BATCHSIZE \
+ --model-path-prefix $MODEL_PATH_PREFIX \
+ --input-image $INPUT_IMG \
+ --input-dir $INPUT_DIR \
+
diff --git a/scala-package/examples/scripts/benchmark/run_java_inference_bm.sh b/scala-package/examples/scripts/benchmark/run_java_inference_bm.sh
new file mode 100644
index 000000000000..5a468e344829
--- /dev/null
+++ b/scala-package/examples/scripts/benchmark/run_java_inference_bm.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -e
+
+hw_type=cpu
+if [ "$USE_GPU" = "1" ]
+then
+ hw_type=gpu
+fi
+
+platform=linux-x86_64
+
+if [[ $OSTYPE = [darwin]* ]]
+then
+ platform=osx-x86_64
+fi
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../../.."; pwd)
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/$platform-$hw_type/target/*:$MXNET_ROOT/scala-package/examples/target/*
+
+java -Xmx8G -Dmxnet.traceLeakedObjects=true -cp $CLASS_PATH \
+ org.apache.mxnetexamples.javaapi.benchmark.JavaBenchmark $@
+
diff --git a/scala-package/examples/scripts/benchmark/run_text_charrnn_bm.sh b/scala-package/examples/scripts/benchmark/run_text_charrnn_bm.sh
new file mode 100755
index 000000000000..bf6b7b314e71
--- /dev/null
+++ b/scala-package/examples/scripts/benchmark/run_text_charrnn_bm.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -e
+
+echo $OSTYPE
+
+hw_type=cpu
+if [[ $1 = gpu ]]
+then
+ hw_type=gpu
+fi
+
+platform=linux-x86_64
+
+if [[ $OSTYPE = [darwin]* ]]
+then
+ platform=osx-x86_64
+fi
+
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../../.."; pwd)
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/$platform-$hw_type/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*
+
+MODEL_NAME=$2
+
+RUNS=$3
+
+# model dir
+MODEL_PATH_PREFIX=$4
+# input image
+DATA_PATH=$5
+
+# feel free to change the starter sentence
+STARTER_SENTENCE="The joke"
+
+java -Xmx8G -Dmxnet.traceLeakedObjects=false -cp $CLASS_PATH \
+ org.apache.mxnetexamples.benchmark.ScalaInferenceBenchmark \
+ --example $MODEL_NAME \
+ --count $RUNS \
+ --model-prefix $MODEL_PATH_PREFIX \
+ --data-path $DATA_PATH \
+ --starter-sentence "$STARTER_SENTENCE"
+
diff --git a/scala-package/examples/scripts/infer/imageclassifier/get_resnet_18_data.sh b/scala-package/examples/scripts/infer/imageclassifier/get_resnet_18_data.sh
new file mode 100755
index 000000000000..1ce996e5c851
--- /dev/null
+++ b/scala-package/examples/scripts/infer/imageclassifier/get_resnet_18_data.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -e
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../.."; pwd)
+
+data_path=$MXNET_ROOT/scripts/infer/models/resnet-18/
+
+image_path=$MXNET_ROOT/scripts/infer/images/
+
+if [ ! -d "$data_path" ]; then
+ mkdir -p "$data_path"
+fi
+
+if [ ! -d "$image_path" ]; then
+ mkdir -p "$image_path"
+fi
+
+if [ ! -f "$data_path" ]; then
+ wget https://s3.us-east-2.amazonaws.com/scala-infer-models/resnet-18/resnet-18-symbol.json -P $data_path
+ wget https://s3.us-east-2.amazonaws.com/scala-infer-models/resnet-18/resnet-18-0000.params -P $data_path
+ wget https://s3.us-east-2.amazonaws.com/scala-infer-models/resnet-18/synset.txt -P $data_path
+ wget https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/resnet152/kitten.jpg -P $image_path
+fi
diff --git a/scala-package/examples/scripts/infer/imageclassifier/get_resnet_data.sh b/scala-package/examples/scripts/infer/imageclassifier/get_resnet_data.sh
index b68e2f317378..6fd85e4f4400 100755
--- a/scala-package/examples/scripts/infer/imageclassifier/get_resnet_data.sh
+++ b/scala-package/examples/scripts/infer/imageclassifier/get_resnet_data.sh
@@ -34,8 +34,8 @@ if [ ! -d "$image_path" ]; then
fi
if [ ! -f "$data_path" ]; then
- wget http://data.mxnet.io/models/imagenet-11k/resnet-152/resnet-152-0000.params -P $data_path
- wget http://data.mxnet.io/models/imagenet-11k/resnet-152/resnet-152-symbol.json -P $data_path
- wget http://data.mxnet.io/models/imagenet-11k/synset.txt -P $data_path
- wget https://s3.amazonaws.com/model-server/inputs/kitten.jpg -P $image_path
+ wget https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/resnet152/resnet-152-0000.params -P $data_path
+ wget https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/resnet152/resnet-152-symbol.json -P $data_path
+ wget https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/resnet152/synset.txt -P $data_path
+ wget https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/resnet152/kitten.jpg -P $image_path
fi
diff --git a/scala-package/examples/scripts/infer/objectdetector/get_ssd_data.sh b/scala-package/examples/scripts/infer/objectdetector/get_ssd_data.sh
index ab231d49404b..8787d6382204 100755
--- a/scala-package/examples/scripts/infer/objectdetector/get_ssd_data.sh
+++ b/scala-package/examples/scripts/infer/objectdetector/get_ssd_data.sh
@@ -37,7 +37,7 @@ fi
if [ ! -f "$data_path" ]; then
wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-symbol.json -P $data_path
wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-0000.params -P $data_path
- wget https://raw.githubusercontent.com/awslabs/mxnet-model-server/master/examples/ssd/synset.txt -P $data_path
+ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/synset.txt -P $data_path
cd $image_path
wget https://cloud.githubusercontent.com/assets/3307514/20012566/cbb53c76-a27d-11e6-9aaa-91939c9a1cd5.jpg -O 000001.jpg
wget https://cloud.githubusercontent.com/assets/3307514/20012567/cbb60336-a27d-11e6-93ff-cbc3f09f5c9e.jpg -O dog.jpg
diff --git a/scala-package/examples/scripts/infer/objectdetector/run_ssd_example.sh b/scala-package/examples/scripts/infer/objectdetector/run_ssd_example.sh
index 8cea892b5809..6b4edb7c4c94 100755
--- a/scala-package/examples/scripts/infer/objectdetector/run_ssd_example.sh
+++ b/scala-package/examples/scripts/infer/objectdetector/run_ssd_example.sh
@@ -17,9 +17,21 @@
# specific language governing permissions and limitations
# under the License.
+hw_type=cpu
+if [[ $4 = gpu ]]
+then
+ hw_type=gpu
+fi
+
+platform=linux-x86_64
+
+if [[ $OSTYPE = [darwin]* ]]
+then
+ platform=osx-x86_64
+fi
MXNET_ROOT=$(cd "$(dirname $0)/../../../../../"; pwd)
-CLASS_PATH=$MXNET_ROOT/scala-package/assembly/osx-x86_64-cpu/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/$platform-$hw_type/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*
# model dir and prefix
MODEL_DIR=$1
diff --git a/scala-package/examples/scripts/infer/objectdetector/run_ssd_java_example.sh b/scala-package/examples/scripts/infer/objectdetector/run_ssd_java_example.sh
new file mode 100755
index 000000000000..00ed793a7bb5
--- /dev/null
+++ b/scala-package/examples/scripts/infer/objectdetector/run_ssd_java_example.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+hw_type=cpu
+if [[ $4 = gpu ]]
+then
+ hw_type=gpu
+fi
+
+platform=linux-x86_64
+
+if [[ $OSTYPE = [darwin]* ]]
+then
+ platform=osx-x86_64
+fi
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../../../../"; pwd)
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/$platform-$hw_type/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*:$MXNET_ROOT/scala-package/examples/src/main/scala/org/apache/mxnetexamples/api/java/infer/imageclassifier/*
+
+# model dir and prefix
+MODEL_DIR=$1
+# input image
+INPUT_IMG=$2
+# which input image dir
+INPUT_DIR=$3
+
+java -Xmx8G -cp $CLASS_PATH \
+ org.apache.mxnetexamples.javaapi.infer.objectdetector.SSDClassifierExample \
+ --model-path-prefix $MODEL_DIR \
+ --input-image $INPUT_IMG \
+ --input-dir $INPUT_DIR
diff --git a/scala-package/examples/scripts/infer/predictor/run_predictor_java_example.sh b/scala-package/examples/scripts/infer/predictor/run_predictor_java_example.sh
new file mode 100755
index 000000000000..4ebcc3076a78
--- /dev/null
+++ b/scala-package/examples/scripts/infer/predictor/run_predictor_java_example.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+hw_type=cpu
+if [[ $3 = gpu ]]
+then
+ hw_type=gpu
+fi
+
+platform=linux-x86_64
+
+if [[ $OSTYPE = [darwin]* ]]
+then
+ platform=osx-x86_64
+fi
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../../../../"; pwd)
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/$platform-$hw_type/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*
+
+# model dir and prefix
+MODEL_DIR=$1
+# input image
+INPUT_IMG=$2
+
+java -Xmx8G -cp $CLASS_PATH \
+ org.apache.mxnetexamples.javaapi.infer.predictor.PredictorExample \
+ --model-path-prefix $MODEL_DIR \
+ --input-image $INPUT_IMG
diff --git a/scala-package/examples/scripts/run_train_mnist.sh b/scala-package/examples/scripts/run_train_mnist.sh
new file mode 100755
index 000000000000..ea53c1ade66f
--- /dev/null
+++ b/scala-package/examples/scripts/run_train_mnist.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -e
+
+MXNET_ROOT=$(cd "$(dirname $0)/../../.."; pwd)
+echo $MXNET_ROOT
+CLASS_PATH=$MXNET_ROOT/scala-package/assembly/linux-x86_64-cpu/target/*:$MXNET_ROOT/scala-package/examples/target/*:$MXNET_ROOT/scala-package/examples/target/classes/lib/*:$MXNET_ROOT/scala-package/infer/target/*
+
+# model dir
+DATA_PATH=$2
+
+java -XX:+PrintGC -Xms256M -Xmx512M -Dmxnet.traceLeakedObjects=false -cp $CLASS_PATH \
+ org.apache.mxnetexamples.imclassification.TrainMnist \
+ --data-dir /home/ubuntu/mxnet_scala/scala-package/examples/mnist/ \
+ --num-epochs 10000000 \
+ --batch-size 1024
\ No newline at end of file
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/InferBase.java b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/InferBase.java
new file mode 100644
index 000000000000..fdcde6b4152c
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/InferBase.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.mxnetexamples.javaapi.benchmark;
+
+import org.apache.mxnet.javaapi.Context;
+import org.kohsuke.args4j.Option;
+
+import java.util.List;
+
+abstract class InferBase {
+ @Option(name = "--num-runs", usage = "Number of runs")
+ public int numRun = 1;
+ @Option(name = "--model-name", usage = "Name of the model")
+ public String modelName = "";
+ @Option(name = "--batchsize", usage = "Size of the batch")
+ public int batchSize = 1;
+
+ public abstract void preProcessModel(List context);
+ public abstract void runSingleInference();
+ public abstract void runBatchInference();
+}
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/JavaBenchmark.java b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/JavaBenchmark.java
new file mode 100644
index 000000000000..4a6bb2dd38bf
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/JavaBenchmark.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.javaapi.benchmark;
+
+import org.apache.mxnet.javaapi.Context;
+import org.kohsuke.args4j.CmdLineParser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class JavaBenchmark {
+
+ private static boolean runBatch = false;
+
+ private static void parse(Object inst, String[] args) {
+ CmdLineParser parser = new CmdLineParser(inst);
+ try {
+ parser.parseArgument(args);
+ } catch (Exception e) {
+ System.err.println(e.getMessage() + e);
+ parser.printUsage(System.err);
+ System.exit(1);
+ }
+ }
+
+ private static long percentile(int p, long[] seq) {
+ Arrays.sort(seq);
+ int k = (int) Math.ceil((seq.length - 1) * (p / 100.0));
+ return seq[k];
+ }
+
+ private static void printStatistics(long[] inferenceTimesRaw, String metricsPrefix) {
+ long[] inferenceTimes = inferenceTimesRaw;
+ // remove head and tail
+ if (inferenceTimes.length > 2) {
+ inferenceTimes = Arrays.copyOfRange(inferenceTimesRaw,
+ 1, inferenceTimesRaw.length - 1);
+ }
+ double p50 = percentile(50, inferenceTimes) / 1.0e6;
+ double p99 = percentile(99, inferenceTimes) / 1.0e6;
+ double p90 = percentile(90, inferenceTimes) / 1.0e6;
+ long sum = 0;
+ for (long time: inferenceTimes) sum += time;
+ double average = sum / (inferenceTimes.length * 1.0e6);
+
+ System.out.println(
+ String.format("\n%s_p99 %fms\n%s_p90 %fms\n%s_p50 %fms\n%s_average %1.2fms",
+ metricsPrefix, p99, metricsPrefix, p90,
+ metricsPrefix, p50, metricsPrefix, average)
+ );
+
+ }
+
+ private static List bindToDevice() {
+ List context = new ArrayList();
+ if (System.getenv().containsKey("SCALA_TEST_ON_GPU") &&
+ Integer.valueOf(System.getenv("SCALA_TEST_ON_GPU")) == 1) {
+ context.add(Context.gpu());
+ } else {
+ context.add(Context.cpu());
+ }
+ return context;
+ }
+
+ public static void main(String[] args) {
+ if (args.length < 2) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Please follow the format:");
+ sb.append("\n --model-name ");
+ sb.append("\n --num-runs ");
+ sb.append("\n --batchsize ");
+ System.out.println(sb.toString());
+ return;
+ }
+ String modelName = args[1];
+ InferBase model = null;
+ switch(modelName) {
+ case "ObjectDetection":
+ runBatch = true;
+ ObjectDetectionBenchmark inst = new ObjectDetectionBenchmark();
+ parse(inst, args);
+ model = inst;
+ break;
+ default:
+ System.err.println("Model name not found! " + modelName);
+ System.exit(1);
+ }
+ List context = bindToDevice();
+ long[] result = new long[model.numRun];
+ model.preProcessModel(context);
+ if (runBatch) {
+ for (int i =0;i < model.numRun; i++) {
+ long currTime = System.nanoTime();
+ model.runBatchInference();
+ result[i] = System.nanoTime() - currTime;
+ }
+ System.out.println("Batchsize: " + model.batchSize);
+ System.out.println("Num of runs: " + model.numRun);
+ printStatistics(result, modelName +"batch_inference");
+ }
+
+ model.batchSize = 1;
+ model.preProcessModel(context);
+ result = new long[model.numRun];
+ for (int i = 0; i < model.numRun; i++) {
+ long currTime = System.nanoTime();
+ model.runSingleInference();
+ result[i] = System.nanoTime() - currTime;
+ }
+ System.out.println("Num of runs: " + model.numRun);
+ printStatistics(result, modelName + "single_inference");
+ }
+}
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/ObjectDetectionBenchmark.java b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/ObjectDetectionBenchmark.java
new file mode 100644
index 000000000000..257ea3241626
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/benchmark/ObjectDetectionBenchmark.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.javaapi.benchmark;
+
+import org.apache.mxnet.infer.javaapi.ObjectDetector;
+import org.apache.mxnet.javaapi.*;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class ObjectDetectionBenchmark extends InferBase {
+ @Option(name = "--model-path-prefix", usage = "input model directory and prefix of the model")
+ public String modelPathPrefix = "/model/ssd_resnet50_512";
+ @Option(name = "--input-image", usage = "the input image")
+ public String inputImagePath = "/images/dog.jpg";
+
+ private ObjectDetector objDet;
+ private NDArray img;
+ private NDArray$ NDArray = NDArray$.MODULE$;
+
+ public void preProcessModel(List context) {
+ Shape inputShape = new Shape(new int[] {this.batchSize, 3, 512, 512});
+ List inputDescriptors = new ArrayList<>();
+ inputDescriptors.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+ objDet = new ObjectDetector(modelPathPrefix, inputDescriptors, context, 0);
+ img = ObjectDetector.bufferedImageToPixels(
+ ObjectDetector.reshapeImage(
+ ObjectDetector.loadImageFromFile(inputImagePath), 512, 512
+ ),
+ new Shape(new int[] {1, 3, 512, 512})
+ );
+ }
+
+ public void runSingleInference() {
+ List nd = new ArrayList<>();
+ nd.add(img);
+ objDet.objectDetectWithNDArray(nd, 3);
+ }
+
+ public void runBatchInference() {
+ List nd = new ArrayList<>();
+ NDArray[] temp = new NDArray[batchSize];
+ for (int i = 0; i < batchSize; i++) temp[i] = img.copy();
+ NDArray batched = NDArray.concat(temp, batchSize, 0, null)[0];
+ nd.add(batched);
+ objDet.objectDetectWithNDArray(nd, 3);
+ }
+}
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/objectdetector/README.md b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/objectdetector/README.md
new file mode 100644
index 000000000000..681253f39a88
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/objectdetector/README.md
@@ -0,0 +1,97 @@
+# Single Shot Multi Object Detection using Java Inference API
+
+In this example, you will learn how to use Java Inference API to run Inference on pre-trained Single Shot Multi Object Detection (SSD) MXNet model.
+
+The model is trained on the [Pascal VOC 2012 dataset](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/index.html). The network is a SSD model built on Resnet50 as base network to extract image features. The model is trained to detect the following entities (classes): ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor']. For more details about the model, you can refer to the [MXNet SSD example](https://github.com/apache/incubator-mxnet/tree/master/example/ssd).
+
+
+## Contents
+
+1. [Prerequisites](#prerequisites)
+2. [Download artifacts](#download-artifacts)
+3. [Setup datapath and parameters](#setup-datapath-and-parameters)
+4. [Run the image inference example](#run-the-image-inference-example)
+5. [Infer APIs](#infer-api-details)
+6. [Next steps](#next-steps)
+
+
+## Prerequisites
+
+1. MXNet
+2. MXNet Scala Package
+3. [IntelliJ IDE (or alternative IDE) project setup](http://mxnet.incubator.apache.org/tutorials/java/mxnet_java_on_intellij.html) with the MXNet Scala/Java Package
+4. wget
+
+
+## Setup Guide
+
+### Download Artifacts
+#### Step 1
+You can download the files using the script `get_ssd_data.sh`. It will download and place the model files in a `model` folder and the test image files in a `image` folder in the current directory.
+From the `scala-package/examples/scripts/infer/objectdetector/` folder run:
+
+```bash
+./get_ssd_data.sh
+```
+
+**Note**: You may need to run `chmod +x get_ssd_data.sh` before running this script.
+
+In the pre-trained model, the `input_name` is `data` and shape is `(1, 3, 512, 512)`.
+This shape translates to: a batch of `1` image, the image has color and uses `3` channels (RGB), and the image has the dimensions of `512` pixels in height by `512` pixels in width.
+
+`image/jpeg` is the expected input type, since this example's image pre-processor only supports the handling of binary JPEG images.
+
+The output shape is `(1, 6132, 6)`. As with the input, the `1` is the number of images. `6132` is the number of prediction results, and `6` is for the size of each prediction. Each prediction contains the following components:
+- `Class`
+- `Accuracy`
+- `Xmin`
+- `Ymin`
+- `Xmax`
+- `Ymax`
+
+
+### Setup Datapath and Parameters
+#### Step 2
+The followings is the parameters defined for this example, you can find more information in the `class SSDClassifierExample`.
+
+| Argument | Comments |
+| ----------------------------- | ---------------------------------------- |
+| `model-path-prefix` | Folder path with prefix to the model (including json, params, and any synset file). |
+| `input-image` | The image to run inference on. |
+| `input-dir` | The directory of images to run inference on. |
+
+
+## How to Run Inference
+After the previous steps, you should be able to run the code using the following script that will pass all of the required parameters to the Infer API.
+
+From the `scala-package/examples/scripts/inferexample/objectdetector/` folder run:
+
+```bash
+./run_ssd_example.sh ../models/resnet50_ssd/resnet50_ssd/resnet50_ssd_model ../images/dog.jpg ../images
+```
+
+**Notes**:
+* These are relative paths to this script.
+* You may need to run `chmod +x run_ssd_example.sh` before running this script.
+
+The example should give expected output as shown below:
+```
+Class: car
+Probabilties: 0.99847263
+(Coord:,312.21335,72.0291,456.01443,150.66176)
+Class: bicycle
+Probabilties: 0.90473825
+(Coord:,155.95807,149.96362,383.8369,418.94513)
+Class: dog
+Probabilties: 0.8226818
+(Coord:,83.82353,179.13998,206.63783,476.7875)
+```
+the outputs come from the the input image, with top3 predictions picked.
+
+
+## Infer API Details
+This example uses ObjectDetector class provided by MXNet's Java Infer APIs. It provides methods to load the images, create NDArray out of Java BufferedImage and run prediction using Classifier and Predictor APIs.
+
+
+## References
+This documentation used the model and inference setup guide from the [MXNet Model Server SSD example](https://github.com/awslabs/mxnet-model-server/blob/master/examples/ssd/README.md).
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/objectdetector/SSDClassifierExample.java b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/objectdetector/SSDClassifierExample.java
new file mode 100644
index 000000000000..a9c00f7f1d81
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/objectdetector/SSDClassifierExample.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.javaapi.infer.objectdetector;
+
+import org.apache.mxnet.infer.javaapi.ObjectDetectorOutput;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.mxnet.javaapi.*;
+import org.apache.mxnet.infer.javaapi.ObjectDetector;
+
+// scalastyle:off
+import java.awt.image.BufferedImage;
+// scalastyle:on
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import java.io.File;
+
+public class SSDClassifierExample {
+ @Option(name = "--model-path-prefix", usage = "input model directory and prefix of the model")
+ private String modelPathPrefix = "/model/ssd_resnet50_512";
+ @Option(name = "--input-image", usage = "the input image")
+ private String inputImagePath = "/images/dog.jpg";
+ @Option(name = "--input-dir", usage = "the input batch of images directory")
+ private String inputImageDir = "/images/";
+
+ final static Logger logger = LoggerFactory.getLogger(SSDClassifierExample.class);
+
+ static List>
+ runObjectDetectionSingle(String modelPathPrefix, String inputImagePath, List context) {
+ Shape inputShape = new Shape(new int[]{1, 3, 512, 512});
+ List inputDescriptors = new ArrayList();
+ inputDescriptors.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+ BufferedImage img = ObjectDetector.loadImageFromFile(inputImagePath);
+ ObjectDetector objDet = new ObjectDetector(modelPathPrefix, inputDescriptors, context, 0);
+ return objDet.imageObjectDetect(img, 3);
+ }
+
+ static List>>
+ runObjectDetectionBatch(String modelPathPrefix, String inputImageDir, List context) {
+ Shape inputShape = new Shape(new int[]{1, 3, 512, 512});
+ List inputDescriptors = new ArrayList();
+ inputDescriptors.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+ ObjectDetector objDet = new ObjectDetector(modelPathPrefix, inputDescriptors, context, 0);
+
+ // Loading batch of images from the directory path
+ List> batchFiles = generateBatches(inputImageDir, 20);
+ List>> outputList
+ = new ArrayList>>();
+
+ for (List batchFile : batchFiles) {
+ List imgList = ObjectDetector.loadInputBatch(batchFile);
+ // Running inference on batch of images loaded in previous step
+ List> tmp
+ = objDet.imageBatchObjectDetect(imgList, 5);
+ outputList.add(tmp);
+ }
+ return outputList;
+ }
+
+ static List> generateBatches(String inputImageDirPath, int batchSize) {
+ File dir = new File(inputImageDirPath);
+
+ List> output = new ArrayList>();
+ List batch = new ArrayList();
+ for (File imgFile : dir.listFiles()) {
+ batch.add(imgFile.getPath());
+ if (batch.size() == batchSize) {
+ output.add(batch);
+ batch = new ArrayList();
+ }
+ }
+ if (batch.size() > 0) {
+ output.add(batch);
+ }
+ return output;
+ }
+
+ public static void main(String[] args) {
+ SSDClassifierExample inst = new SSDClassifierExample();
+ CmdLineParser parser = new CmdLineParser(inst);
+ try {
+ parser.parseArgument(args);
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ parser.printUsage(System.err);
+ System.exit(1);
+ }
+
+ String mdprefixDir = inst.modelPathPrefix;
+ String imgPath = inst.inputImagePath;
+ String imgDir = inst.inputImageDir;
+
+ if (!checkExist(Arrays.asList(mdprefixDir + "-symbol.json", imgDir, imgPath))) {
+ logger.error("Model or input image path does not exist");
+ System.exit(1);
+ }
+
+ List context = new ArrayList();
+ if (System.getenv().containsKey("SCALA_TEST_ON_GPU") &&
+ Integer.valueOf(System.getenv("SCALA_TEST_ON_GPU")) == 1) {
+ context.add(Context.gpu());
+ } else {
+ context.add(Context.cpu());
+ }
+
+ try {
+ Shape inputShape = new Shape(new int[]{1, 3, 512, 512});
+ Shape outputShape = new Shape(new int[]{1, 6132, 6});
+
+
+ int width = inputShape.get(2);
+ int height = inputShape.get(3);
+ StringBuilder outputStr = new StringBuilder().append("\n");
+
+ List> output
+ = runObjectDetectionSingle(mdprefixDir, imgPath, context);
+
+ for (List ele : output) {
+ for (ObjectDetectorOutput i : ele) {
+ outputStr.append("Class: " + i.getClassName() + "\n");
+ outputStr.append("Probabilties: " + i.getProbability() + "\n");
+
+ List coord = Arrays.asList(i.getXMin() * width,
+ i.getXMax() * height, i.getYMin() * width, i.getYMax() * height);
+ StringBuilder sb = new StringBuilder();
+ for (float c : coord) {
+ sb.append(", ").append(c);
+ }
+ outputStr.append("Coord:" + sb.substring(2) + "\n");
+ }
+ }
+ logger.info(outputStr.toString());
+
+ List>> outputList =
+ runObjectDetectionBatch(mdprefixDir, imgDir, context);
+
+ outputStr = new StringBuilder().append("\n");
+ int index = 0;
+ for (List> i : outputList) {
+ for (List j : i) {
+ outputStr.append("*** Image " + (index + 1) + "***" + "\n");
+ for (ObjectDetectorOutput k : j) {
+ outputStr.append("Class: " + k.getClassName() + "\n");
+ outputStr.append("Probabilties: " + k.getProbability() + "\n");
+ List coord = Arrays.asList(k.getXMin() * width,
+ k.getXMax() * height, k.getYMin() * width, k.getYMax() * height);
+
+ StringBuilder sb = new StringBuilder();
+ for (float c : coord) {
+ sb.append(", ").append(c);
+ }
+ outputStr.append("Coord:" + sb.substring(2) + "\n");
+ }
+ index++;
+ }
+ }
+ logger.info(outputStr.toString());
+
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ parser.printUsage(System.err);
+ System.exit(1);
+ }
+ System.exit(0);
+ }
+
+ static Boolean checkExist(List arr) {
+ Boolean exist = true;
+ for (String item : arr) {
+ if (!(new File(item).exists())) {
+ logger.error("Cannot find: " + item);
+ exist = false;
+ }
+ }
+ return exist;
+ }
+}
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/predictor/PredictorExample.java b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/predictor/PredictorExample.java
new file mode 100644
index 000000000000..c9b4426f52b3
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/predictor/PredictorExample.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.javaapi.infer.predictor;
+
+import org.apache.mxnet.infer.javaapi.Predictor;
+import org.apache.mxnet.javaapi.*;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.imageio.ImageIO;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This Class is a demo to show how users can use Predictor APIs to do
+ * Image Classification with all hand-crafted Pre-processing.
+ * All helper functions for image pre-processing are
+ * currently available in ObjectDetector class.
+ */
+public class PredictorExample {
+ @Option(name = "--model-path-prefix", usage = "input model directory and prefix of the model")
+ private String modelPathPrefix = "/model/ssd_resnet50_512";
+ @Option(name = "--input-image", usage = "the input image")
+ private String inputImagePath = "/images/dog.jpg";
+
+ final static Logger logger = LoggerFactory.getLogger(PredictorExample.class);
+
+ /**
+ * Load the image from file to buffered image
+ * It can be replaced by loadImageFromFile from ObjectDetector
+ * @param inputImagePath input image Path in String
+ * @return Buffered image
+ */
+ private static BufferedImage loadIamgeFromFile(String inputImagePath) {
+ BufferedImage buf = null;
+ try {
+ buf = ImageIO.read(new File(inputImagePath));
+ } catch (IOException e) {
+ System.err.println(e);
+ }
+ return buf;
+ }
+
+ /**
+ * Reshape the current image using ImageIO and Graph2D
+ * It can be replaced by reshapeImage from ObjectDetector
+ * @param buf Buffered image
+ * @param newWidth desired width
+ * @param newHeight desired height
+ * @return a reshaped bufferedImage
+ */
+ private static BufferedImage reshapeImage(BufferedImage buf, int newWidth, int newHeight) {
+ BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g = resizedImage.createGraphics();
+ g.drawImage(buf, 0, 0, newWidth, newHeight, null);
+ g.dispose();
+ return resizedImage;
+ }
+
+ /**
+ * Convert an image from a buffered image into pixels float array
+ * It can be replaced by bufferedImageToPixels from ObjectDetector
+ * @param buf buffered image
+ * @return Float array
+ */
+ private static float[] imagePreprocess(BufferedImage buf) {
+ // Get height and width of the image
+ int w = buf.getWidth();
+ int h = buf.getHeight();
+
+ // get an array of integer pixels in the default RGB color mode
+ int[] pixels = buf.getRGB(0, 0, w, h, null, 0, w);
+
+ // 3 times height and width for R,G,B channels
+ float[] result = new float[3 * h * w];
+
+ int row = 0;
+ // copy pixels to array vertically
+ while (row < h) {
+ int col = 0;
+ // copy pixels to array horizontally
+ while (col < w) {
+ int rgb = pixels[row * w + col];
+ // getting red color
+ result[0 * h * w + row * w + col] = (rgb >> 16) & 0xFF;
+ // getting green color
+ result[1 * h * w + row * w + col] = (rgb >> 8) & 0xFF;
+ // getting blue color
+ result[2 * h * w + row * w + col] = rgb & 0xFF;
+ col += 1;
+ }
+ row += 1;
+ }
+ buf.flush();
+ return result;
+ }
+
+ /**
+ * Helper class to print the maximum prediction result
+ * @param probabilities The float array of probability
+ * @param modelPathPrefix model Path needs to load the synset.txt
+ */
+ private static String printMaximumClass(float[] probabilities,
+ String modelPathPrefix) throws IOException {
+ String synsetFilePath = modelPathPrefix.substring(0,
+ 1 + modelPathPrefix.lastIndexOf(File.separator)) + "/synset.txt";
+ BufferedReader reader = new BufferedReader(new FileReader(synsetFilePath));
+ ArrayList list = new ArrayList<>();
+ String line = reader.readLine();
+
+ while (line != null){
+ list.add(line);
+ line = reader.readLine();
+ }
+ reader.close();
+
+ int maxIdx = 0;
+ for (int i = 1;i probabilities[maxIdx]) {
+ maxIdx = i;
+ }
+ }
+
+ return "Probability : " + probabilities[maxIdx] + " Class : " + list.get(maxIdx) ;
+ }
+
+ public static void main(String[] args) {
+ PredictorExample inst = new PredictorExample();
+ CmdLineParser parser = new CmdLineParser(inst);
+ try {
+ parser.parseArgument(args);
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ parser.printUsage(System.err);
+ System.exit(1);
+ }
+ // Prepare the model
+ List context = new ArrayList();
+ if (System.getenv().containsKey("SCALA_TEST_ON_GPU") &&
+ Integer.valueOf(System.getenv("SCALA_TEST_ON_GPU")) == 1) {
+ context.add(Context.gpu());
+ } else {
+ context.add(Context.cpu());
+ }
+ List inputDesc = new ArrayList<>();
+ Shape inputShape = new Shape(new int[]{1, 3, 224, 224});
+ inputDesc.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+ Predictor predictor = new Predictor(inst.modelPathPrefix, inputDesc, context,0);
+ // Prepare data
+ BufferedImage img = loadIamgeFromFile(inst.inputImagePath);
+
+ img = reshapeImage(img, 224, 224);
+ // predict
+ float[][] result = predictor.predict(new float[][]{imagePreprocess(img)});
+ try {
+ System.out.println("Predict with Float input");
+ System.out.println(printMaximumClass(result[0], inst.modelPathPrefix));
+ } catch (IOException e) {
+ System.err.println(e);
+ }
+ // predict with NDArray
+ NDArray nd = new NDArray(
+ imagePreprocess(img),
+ new Shape(new int[]{1, 3, 224, 224}),
+ Context.cpu());
+ List ndList = new ArrayList<>();
+ ndList.add(nd);
+ List ndResult = predictor.predictWithNDArray(ndList);
+ try {
+ System.out.println("Predict with NDArray");
+ System.out.println(printMaximumClass(ndResult.get(0).toArray(), inst.modelPathPrefix));
+ } catch (IOException e) {
+ System.err.println(e);
+ }
+ }
+
+}
diff --git a/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/predictor/README.md b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/predictor/README.md
new file mode 100644
index 000000000000..1f2c9e0e813c
--- /dev/null
+++ b/scala-package/examples/src/main/java/org/apache/mxnetexamples/javaapi/infer/predictor/README.md
@@ -0,0 +1,61 @@
+# Image Classification using Java Predictor
+
+In this example, you will learn how to use Java Inference API to
+build and run pre-trained Resnet 18 model.
+
+## Contents
+
+1. [Prerequisites](#prerequisites)
+2. [Download artifacts](#download-artifacts)
+3. [Setup datapath and parameters](#setup-datapath-and-parameters)
+4. [Run the image classifier example](#run-the-image-inference-example)
+
+## Prerequisites
+
+1. Build from source with [MXNet](https://mxnet.incubator.apache.org/install/index.html)
+2. [IntelliJ IDE (or alternative IDE) project setup](https://github.com/apache/incubator-mxnet/blob/master/docs/tutorials/java/mxnet_java_on_intellij.md) with the MXNet Java Package
+3. wget
+
+## Download Artifacts
+
+For this tutorial, you can get the model and sample input image by running following bash file. This script will use `wget` to download these artifacts from AWS S3.
+
+From the `scala-package/examples/scripts/infer/imageclassifier/` folder run:
+
+```bash
+./get_resnet_18_data.sh
+```
+
+**Note**: You may need to run `chmod +x get_resnet_18_data.sh` before running this script.
+
+### Setup Datapath and Parameters
+
+The available arguments are as follows:
+
+| Argument | Comments |
+| ----------------------------- | ---------------------------------------- |
+| `model-dir` | Folder path with prefix to the model (including json, params, and any synset file). |
+| `input-image` | The image to run inference on. |
+
+## Run the image classifier example
+
+After the previous steps, you should be able to run the code using the following script that will pass all of the required parameters to the Predictor API.
+
+From the `scala-package/examples/scripts/infer/predictor/` folder run:
+
+```bash
+bash run_predictor_java_example.sh ../models/resnet-18/resnet-18 ../images/kitten.jpg
+```
+
+**Notes**:
+* These are relative paths to this script.
+* You may need to run `chmod +x run_predictor_java_example.sh` before running this script.
+
+The example should give an output similar to the one shown below:
+```
+Predict with Float input
+Probability : 0.30337515 Class : n02123159 tiger cat
+Predict with NDArray
+Probability : 0.30337515 Class : n02123159 tiger cat
+```
+the outputs come from the the input image, with top1 predictions picked.
\ No newline at end of file
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/InferBase.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/InferBase.scala
new file mode 100644
index 000000000000..36a85c227f6e
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/InferBase.scala
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples
+
+import org.apache.mxnet._
+
+trait InferBase {
+
+ def loadModel(context: Array[Context], batchInference : Boolean): Any
+ def loadSingleData(): Any
+ def loadBatchFileList(batchSize: Int): List[Any]
+ def loadInputBatch(source: Any): Any
+ def runSingleInference(loadedModel: Any, input: Any): Any
+ def runBatchInference(loadedModel: Any, input: Any): Any
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/README.md b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/README.md
new file mode 100644
index 000000000000..753cb3125410
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/README.md
@@ -0,0 +1,83 @@
+# Benchmarking Scala Inference APIs
+
+This folder contains a base class [ScalaInferenceBenchmark](https://github.com/apache/incubator-mxnet/tree/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/) and provides a mechanism for benchmarking [MXNet Inference APIs]((https://github.com/apache/incubator-mxnet/tree/master/scala-package/infer)) in Scala.
+The benchmarking scripts provided runs an experiment for single inference calls and batch inference calls. It collects the time taken to perform an inference operation and emits the P99, P50 and Average values for these metrics. One can easily add/modify any new/existing examples to the ScalaInferenceBenchmark framework in order to get the benchmark numbers for inference calls.
+Currently the ScalaInferenceBenchmark script supports three Scala examples :
+1. [ImageClassification using ResNet-152](https://github.com/apache/incubator-mxnet/blob/master/scala-package/mxnet-demo/src/main/scala/sample/ImageClassificationExample.scala)
+2. [Object Detection Example](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/SSDClassifierExample.scala)
+3. [Text Generation through RNNs](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples/rnn/TestCharRnn.scala)
+
+This script can be easily placed in an automated environment to run benchmark regressions on the Scala APIs. The script automatically picks up whether you are running it on a CPU machine or on a GPU machine and appropriately uses that.
+
+## Contents
+
+1. [Prerequisites](#prerequisites)
+2. [Scripts](#scripts)
+
+## Prerequisites
+
+1. MXNet
+2. MXNet Scala Package
+3. [IntelliJ IDE (or alternative IDE) project setup](http://mxnet.incubator.apache.org/tutorials/scala/mxnet_scala_on_intellij.html) with the MXNet Scala Package
+4. Model files and datasets for the model one will try to benchmark
+
+## Scripts
+To help you easily run the benchmarks, a starter shell script has been provided for each of three examples mentioned above. The scripts can be found [here](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/scripts/benchmark).
+Each of the script takes some parameters as inputs, details of which can be found either in the bash scripts or in the example classes itself.
+
+* *ImageClassification Example*
+ The following shows an example of running ImageClassifier under the benchmark script. The script takes as parameters, the platform type (cpu/gpu), number of iterations for inference calls, the batch size for batch inference calls, the model path, input file, and input directory.
+For more details to run ImageClassificationExample as a standalone file, refer to the [README](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/imageclassifier/README.md) for ImageClassifierExample.
+You may need to run ```chmod u+x run_image_inference_bm.sh``` before running this script.
+ ```bash
+ cd /scala-package/examples/scripts/infer/imageclassifier
+ ./get_resnet_data.sh
+ cd /scala-package/examples/scripts/benchmark
+ ./run_image_inference_bm.sh gpu ImageClassifierExample 100 10 ../infer/models/resnet-152/resnet-152 ../infer/images/kitten.jpg ../infer/images/
+ ./run_image_inference_bm.sh cpu ImageClassifierExample 100 10 ../infer/models/resnet-152/resnet-152 ../infer/images/kitten.jpg ../infer/images/
+ ```
+ Upon running this script, you might see an output like this :
+ ```
+ [main] INFO org.apache.mxnetexamples.benchmark.CLIParserBase -
+ single_inference_latency p99 1663, single_inference_p50 729, single_inference_average 755.17
+ ...
+
+ INFO org.apache.mxnetexamples.benchmark.CLIParserBase -
+ batch_inference_latency p99 4241, batch_inference_p50 4241, batch_inference_average 4241.00
+ ```
+
+* *Object Detection Example*
+ The following shows an example of running SSDClassifier under the benchmark script. The script takes in the number of iterations for inference calls, the batch size for batch inference calls, the model path, input file, and input directory.
+For more details to run SSDClassifierExample as a standalone file, refer to the [README](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/README.md) for SSDClassifierExample.
+You may need to run ```chmod u+x run_image_inference_bm.sh``` before running this script.
+ ```bash
+ cd /scala-package/examples/scripts/infer/objectdetector
+ ./get_ssd_data.sh
+ cd /scala-package/examples/scripts/benchmark
+ ./run_image_inference_bm.sh cpu ObjectDetectionExample 100 10 ../infer/models/resnet50_ssd/resnet50_ssd_model ../infer/images/dog.jpg ../infer/images/
+ ```
+ Upon running this script, you might see an output like this :
+ ```
+ [main] INFO org.apache.mxnetexamples.benchmark.CLIParserBase -
+ single_inference_latency p99 1663, single_inference_p50 729, single_inference_average 755.17
+ ...
+
+ INFO org.apache.mxnetexamples.benchmark.CLIParserBase -
+ batch_inference_latency p99 4241, batch_inference_p50 4241, batch_inference_average 4241.00
+ ```
+
+* *Text Generation through RNNs*
+ The following shows an example of running TestCharRnn under the benchmark script. The script takes in the number of iterations for inference calls, the model path and the input text file.
+For more details to run TestCharRnn as a standalone file, refer to the [README](https://github.com/apache/incubator-mxnet/blob/master/scala-package/examples/src/main/scala/org/apache/mxnetexamples/rnn/README.md) for TextCharRnn.
+You may need to run ```chmod u+x run_text_charrnn_bm.sh``` before running this script.
+ ```bash
+ wget https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/RNN/obama.zip
+ unzip obama.zip
+ cd /scala-package/examples/scripts/benchmark
+ ./run_text_charrnn_bm.sh cpu CharRnn 100 /obama /obama.txt
+ ```
+ Upon running this script, you might see an output like this :
+ ```
+ [main] INFO org.apache.mxnetexamples.benchmark.CLIParserBase -
+ single_inference_latency p99 4097, single_inference_p50 2560, single_inference_average 2673.720000
+ ```
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/ScalaInferenceBenchmark.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/ScalaInferenceBenchmark.scala
new file mode 100644
index 000000000000..dc2faec9dd91
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/benchmark/ScalaInferenceBenchmark.scala
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.benchmark
+
+import org.apache.mxnetexamples.InferBase
+import org.apache.mxnetexamples.infer.imageclassifier.ImageClassifierExample
+import org.apache.mxnet._
+import org.apache.mxnetexamples.infer.objectdetector.SSDClassifierExample
+import org.apache.mxnetexamples.rnn.TestCharRnn
+import org.kohsuke.args4j.{CmdLineParser, Option}
+import org.slf4j.LoggerFactory
+
+import scala.collection.JavaConverters._
+
+object ScalaInferenceBenchmark {
+
+ private val logger = LoggerFactory.getLogger(classOf[CLIParserBase])
+
+ def loadModel(objectToRun: InferBase, context: Array[Context], batchInference : Boolean):
+ Any = {
+ objectToRun.loadModel(context, batchInference)
+ }
+
+ def loadDataSet(objectToRun: InferBase):
+ Any = {
+ objectToRun.loadSingleData()
+ }
+
+ def loadBatchDataSet(objectToRun: InferBase, batchSize: Int):
+ List[Any] = {
+ objectToRun.loadBatchFileList(batchSize)
+ }
+
+ def runInference(objectToRun: InferBase, loadedModel: Any, dataSet: Any, totalRuns: Int):
+ List[Long] = {
+ var inferenceTimes: List[Long] = List()
+ for (i <- 1 to totalRuns) {
+ NDArrayCollector.auto().withScope {
+ val startTimeSingle = System.currentTimeMillis()
+ objectToRun.runSingleInference(loadedModel, dataSet)
+ val estimatedTimeSingle = System.currentTimeMillis() - startTimeSingle
+ inferenceTimes = estimatedTimeSingle :: inferenceTimes
+ logger.info("Inference time at iteration: %d is : %d \n".format(i, estimatedTimeSingle))
+ }
+ }
+
+ inferenceTimes
+ }
+
+ def runBatchInference(objecToRun: InferBase, loadedModel: Any, dataSetBatches: List[Any]):
+ List[Long] = {
+
+ var inferenceTimes: List[Long] = List()
+ for (batch <- dataSetBatches) {
+ NDArrayCollector.auto().withScope {
+ val loadedBatch = objecToRun.loadInputBatch(batch)
+ val startTimeSingle = System.currentTimeMillis()
+ objecToRun.runBatchInference(loadedModel, loadedBatch)
+ val estimatedTimeSingle = System.currentTimeMillis() - startTimeSingle
+ inferenceTimes = estimatedTimeSingle :: inferenceTimes
+ logger.info("Batch Inference time is : %d \n".format(estimatedTimeSingle))
+ }
+ }
+
+ inferenceTimes
+ }
+
+ def percentile(p: Int, seq: Seq[Long]): Long = {
+ val sorted = seq.sorted
+ val k = math.ceil((seq.length - 1) * (p / 100.0)).toInt
+ sorted(k)
+ }
+
+ def printStatistics(inferenceTimes: List[Long], metricsPrefix: String) {
+
+ val times: Seq[Long] = inferenceTimes
+ val p50 = percentile(50, times)
+ val p99 = percentile(99, times)
+ val p90 = percentile(90, times)
+ val average = times.sum / (times.length * 1.0)
+
+ logger.info("\n%s_p99 %d, %s_p90 %d, %s_p50 %d, %s_average %1.2f".format(metricsPrefix,
+ p99, metricsPrefix, p90, metricsPrefix, p50, metricsPrefix, average))
+
+ }
+
+ def main(args: Array[String]): Unit = {
+
+ var context = Context.cpu()
+ if (System.getenv().containsKey("SCALA_TEST_ON_GPU") &&
+ System.getenv("SCALA_TEST_ON_GPU").toInt == 1) {
+ context = Context.gpu()
+ }
+ var baseCLI : CLIParserBase = null
+ try {
+ val exampleName = args(1)
+ val exampleToBenchmark : InferBase = exampleName match {
+ case "ImageClassifierExample" => {
+ val imParser = new org.apache.mxnetexamples.infer.imageclassifier.CLIParser
+ baseCLI = imParser
+ val parsedVals = new CmdLineParser(imParser).parseArgument(args.toList.asJava)
+ new ImageClassifierExample(imParser)
+ }
+ case "ObjectDetectionExample" => {
+ val imParser = new org.apache.mxnetexamples.infer.objectdetector.CLIParser
+ baseCLI = imParser
+ val parsedVals = new CmdLineParser(imParser).parseArgument(args.toList.asJava)
+ new SSDClassifierExample(imParser)
+ }
+ case "CharRnn" => {
+ val imParser = new org.apache.mxnetexamples.rnn.CLIParser
+ baseCLI = imParser
+ val parsedVals = new CmdLineParser(imParser).parseArgument(args.toList.asJava)
+ new TestCharRnn(imParser)
+ }
+ case _ => throw new Exception("Invalid example name to run")
+ }
+
+ logger.info("Running single inference call")
+ // Benchmarking single inference call
+ NDArrayCollector.auto().withScope {
+ val loadedModel = loadModel(exampleToBenchmark, context, false)
+ val dataSet = loadDataSet(exampleToBenchmark)
+ val inferenceTimes = runInference(exampleToBenchmark, loadedModel, dataSet, baseCLI.count)
+ printStatistics(inferenceTimes, "single_inference")
+ }
+
+ if (baseCLI.batchSize != 0) {
+ logger.info("Running for batch inference call")
+ // Benchmarking batch inference call
+ NDArrayCollector.auto().withScope {
+ val loadedModel = loadModel(exampleToBenchmark, context, true)
+ val batchDataSet = loadBatchDataSet(exampleToBenchmark, baseCLI.batchSize)
+ val inferenceTimes = runBatchInference(exampleToBenchmark, loadedModel, batchDataSet)
+ printStatistics(inferenceTimes, "batch_inference")
+ }
+ }
+
+ } catch {
+ case ex: Exception => {
+ logger.error(ex.getMessage, ex)
+ new CmdLineParser(baseCLI).printUsage(System.err)
+ sys.exit(1)
+ }
+ }
+ }
+
+}
+
+class CLIParserBase {
+ @Option(name = "--example", usage = "The scala example to benchmark")
+ val exampleName: String = "ImageClassifierExample"
+ @Option(name = "--count", usage = "number of times to run inference")
+ val count: Int = 1000
+ @Option(name = "--batchSize", usage = "BatchSize to run batchinference calls", required = false)
+ val batchSize: Int = 0
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/ModelTrain.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/ModelTrain.scala
deleted file mode 100644
index 1a77775b9857..000000000000
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/ModelTrain.scala
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.mxnetexamples.imclassification
-
-import org.apache.mxnet.Callback.Speedometer
-import org.apache.mxnet._
-import org.apache.mxnet.optimizer.SGD
-import org.slf4j.LoggerFactory
-
-object ModelTrain {
- private val logger = LoggerFactory.getLogger(classOf[ModelTrain])
-
- // scalastyle:off parameterNum
- def fit(dataDir: String, batchSize: Int, numExamples: Int, devs: Array[Context],
- network: Symbol, dataLoader: (String, Int, KVStore) => (DataIter, DataIter),
- kvStore: String, numEpochs: Int, modelPrefix: String = null, loadEpoch: Int = -1,
- lr: Float = 0.1f, lrFactor: Float = 1f, lrFactorEpoch: Float = 1f,
- clipGradient: Float = 0f, monitorSize: Int = -1): Accuracy = {
- // kvstore
- var kv = KVStore.create(kvStore)
-
- // load model
- val modelPrefixWithRank =
- if (modelPrefix == null) null
- else modelPrefix + s"-${kv.rank}"
-
- val (argParams, auxParams, beginEpoch) =
- if (loadEpoch >= 0) {
- require(modelPrefixWithRank != null)
- val tmp = FeedForward.load(modelPrefix, loadEpoch)
- (tmp.getArgParams, tmp.getAuxParams, loadEpoch)
- } else {
- (null, null, 0)
- }
-
- // save model
- val checkpoint: EpochEndCallback =
- if (modelPrefix == null) null
- else new EpochEndCallback {
- override def invoke(epoch: Int, symbol: Symbol,
- argParams: Map[String, NDArray],
- auxStates: Map[String, NDArray]): Unit = {
- Model.saveCheckpoint(modelPrefix, epoch + 1, symbol, argParams, auxParams)
- }
- }
-
- // data
- val (train, validation) = dataLoader(dataDir, batchSize, kv)
-
- // train
- val epochSize =
- if (kvStore == "dist_sync") numExamples / batchSize / kv.numWorkers
- else numExamples / batchSize
-
- val lrScheduler =
- if (lrFactor < 1f) {
- new FactorScheduler(step = Math.max((epochSize * lrFactorEpoch).toInt, 1),
- factor = lrFactor)
- } else {
- null
- }
- val optimizer: Optimizer = new SGD(learningRate = lr,
- lrScheduler = lrScheduler, clipGradient = clipGradient,
- momentum = 0.9f, wd = 0.00001f)
-
- // disable kvstore for single device
- if (kv.`type`.contains("local") && (devs.length == 1 || devs(0).deviceType != "gpu")) {
- kv.dispose()
- kv = null
- }
-
- val model = new FeedForward(ctx = devs,
- symbol = network,
- numEpoch = numEpochs,
- optimizer = optimizer,
- initializer = new Xavier(factorType = "in", magnitude = 2.34f),
- argParams = argParams,
- auxParams = auxParams,
- beginEpoch = beginEpoch,
- epochSize = epochSize)
- if (monitorSize > 0) {
- model.setMonitor(new Monitor(monitorSize))
- }
- val acc = new Accuracy()
- model.fit(trainData = train,
- evalData = validation,
- evalMetric = acc,
- kvStore = kv,
- batchEndCallback = new Speedometer(batchSize, 50),
- epochEndCallback = checkpoint)
- if (kv != null) {
- kv.dispose()
- }
- acc
- }
- // scalastyle:on parameterNum
-}
-
-class ModelTrain
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/README.md b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/README.md
index 5141f441b1ed..cec750acdc92 100644
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/README.md
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/README.md
@@ -1,17 +1,36 @@
-# MNIST Example for Scala
-This is the MNIST Training Example implemented for Scala type-safe api
+# Image Classification Models
+
+This examples contains a number of image classification models that can be run on various datasets.
+
+## Models
+
+Currently, the following models are supported:
+- MultiLayerPerceptron
+- Lenet
+- Resnet
+
+## Datasets
+
+Currently, the following datasets are supported:
+- MNIST
+
+#### Synthetic Benchmark Data
+
+Additionally, the datasets can be replaced by randomly generated data for benchmarking.
+Data is produced to match the shapes of the supported datasets above.
+
+The following additional dataset image shapes are also defined for use with the benchmark synthetic data:
+- imagenet
+
+
+
## Setup
-### Download the source File
+
+### MNIST
+
+For this dataset, the data must be downloaded and extracted from the source or
```$xslt
https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/mnist/mnist.zip
```
-### Unzip the file
-```$xslt
-unzip mnist.zip
-```
-### Arguement Configuration
-Then you need to define the arguments that you would like to pass in the model:
-```$xslt
---data-dir
-```
-You can find more information [here](https://github.com/apache/incubator-mxnet/blob/scala-package/examples/src/main/scala/org/apache/mxnet/examples/imclassification/TrainMnist.scala#L169-L207)
\ No newline at end of file
+
+Afterwards, the location of the data folder must be passed in through the `--data-dir` argument.
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/TrainMnist.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/TrainMnist.scala
deleted file mode 100644
index 2f024fd039bc..000000000000
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/TrainMnist.scala
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.mxnetexamples.imclassification
-
-import org.apache.mxnet._
-import org.kohsuke.args4j.{CmdLineParser, Option}
-import org.slf4j.LoggerFactory
-
-import scala.collection.JavaConverters._
-import scala.collection.mutable
-
-object TrainMnist {
- private val logger = LoggerFactory.getLogger(classOf[TrainMnist])
-
- // multi-layer perceptron
- def getMlp: Symbol = {
- val data = Symbol.Variable("data")
-
- // val fc1 = Symbol.FullyConnected(name = "relu")()(Map("data" -> data, "act_type" -> "relu"))
- val fc1 = Symbol.api.FullyConnected(data = Some(data), num_hidden = 128, name = "fc1")
- val act1 = Symbol.api.Activation (data = Some(fc1), "relu", name = "relu")
- val fc2 = Symbol.api.FullyConnected(Some(act1), None, None, 64, name = "fc2")
- val act2 = Symbol.api.Activation(data = Some(fc2), "relu", name = "relu2")
- val fc3 = Symbol.api.FullyConnected(Some(act2), None, None, 10, name = "fc3")
- val mlp = Symbol.api.SoftmaxOutput(name = "softmax", data = Some(fc3))
- mlp
- }
-
- def getLenet: Symbol = {
- val data = Symbol.Variable("data")
- // first conv
- val conv1 = Symbol.api.Convolution(data = Some(data), kernel = Shape(5, 5), num_filter = 20)
- val tanh1 = Symbol.api.tanh(data = Some(conv1))
- val pool1 = Symbol.api.Pooling(data = Some(tanh1), pool_type = Some("max"),
- kernel = Some(Shape(2, 2)), stride = Some(Shape(2, 2)))
- // second conv
- val conv2 = Symbol.api.Convolution(data = Some(pool1), kernel = Shape(5, 5), num_filter = 50)
- val tanh2 = Symbol.api.tanh(data = Some(conv2))
- val pool2 = Symbol.api.Pooling(data = Some(tanh2), pool_type = Some("max"),
- kernel = Some(Shape(2, 2)), stride = Some(Shape(2, 2)))
- // first fullc
- val flatten = Symbol.api.Flatten(data = Some(pool2))
- val fc1 = Symbol.api.FullyConnected(data = Some(flatten), num_hidden = 500)
- val tanh3 = Symbol.api.tanh(data = Some(fc1))
- // second fullc
- val fc2 = Symbol.api.FullyConnected(data = Some(tanh3), num_hidden = 10)
- // loss
- val lenet = Symbol.api.SoftmaxOutput(name = "softmax", data = Some(fc2))
- lenet
- }
-
- def getIterator(dataShape: Shape)
- (dataDir: String, batchSize: Int, kv: KVStore): (DataIter, DataIter) = {
- val flat = if (dataShape.size == 3) "False" else "True"
-
- val train = IO.MNISTIter(Map(
- "image" -> (dataDir + "train-images-idx3-ubyte"),
- "label" -> (dataDir + "train-labels-idx1-ubyte"),
- "label_name" -> "softmax_label",
- "input_shape" -> dataShape.toString,
- "batch_size" -> batchSize.toString,
- "shuffle" -> "True",
- "flat" -> flat,
- "num_parts" -> kv.numWorkers.toString,
- "part_index" -> kv.`rank`.toString))
-
- val eval = IO.MNISTIter(Map(
- "image" -> (dataDir + "t10k-images-idx3-ubyte"),
- "label" -> (dataDir + "t10k-labels-idx1-ubyte"),
- "label_name" -> "softmax_label",
- "input_shape" -> dataShape.toString,
- "batch_size" -> batchSize.toString,
- "flat" -> flat,
- "num_parts" -> kv.numWorkers.toString,
- "part_index" -> kv.`rank`.toString))
-
- (train, eval)
- }
-
- def test(dataPath : String) : Float = {
- NDArrayCollector.auto().withScope {
- val (dataShape, net) = (Shape(784), getMlp)
- val devs = Array(Context.cpu(0))
- val envs: mutable.Map[String, String] = mutable.HashMap.empty[String, String]
- val Acc = ModelTrain.fit(dataDir = dataPath,
- batchSize = 128, numExamples = 60000, devs = devs,
- network = net, dataLoader = getIterator(dataShape),
- kvStore = "local", numEpochs = 10)
- logger.info("Finish test fit ...")
- val (_, num) = Acc.get
- num(0)
- }
- }
-
-
- def main(args: Array[String]): Unit = {
- val inst = new TrainMnist
- val parser: CmdLineParser = new CmdLineParser(inst)
- try {
- parser.parseArgument(args.toList.asJava)
-
- val dataPath = if (inst.dataDir == null) System.getenv("MXNET_HOME")
- else inst.dataDir
-
- val (dataShape, net) =
- if (inst.network == "mlp") (Shape(784), getMlp)
- else (Shape(1, 28, 28), getLenet)
-
- val devs =
- if (inst.gpus != null) inst.gpus.split(',').map(id => Context.gpu(id.trim.toInt))
- else if (inst.cpus != null) inst.cpus.split(',').map(id => Context.cpu(id.trim.toInt))
- else Array(Context.cpu(0))
-
- val envs: mutable.Map[String, String] = mutable.HashMap.empty[String, String]
- envs.put("DMLC_ROLE", inst.role)
- if (inst.schedulerHost != null) {
- require(inst.schedulerPort > 0, "scheduler port not specified")
- envs.put("DMLC_PS_ROOT_URI", inst.schedulerHost)
- envs.put("DMLC_PS_ROOT_PORT", inst.schedulerPort.toString)
- require(inst.numWorker > 0, "Num of workers must > 0")
- envs.put("DMLC_NUM_WORKER", inst.numWorker.toString)
- require(inst.numServer > 0, "Num of servers must > 0")
- envs.put("DMLC_NUM_SERVER", inst.numServer.toString)
- logger.info("Init PS environments")
- KVStoreServer.init(envs.toMap)
- }
-
- if (inst.role != "worker") {
- logger.info("Start KVStoreServer for scheduler & servers")
- KVStoreServer.start()
- } else {
- ModelTrain.fit(dataDir = inst.dataDir,
- batchSize = inst.batchSize, numExamples = inst.numExamples, devs = devs,
- network = net, dataLoader = getIterator(dataShape),
- kvStore = inst.kvStore, numEpochs = inst.numEpochs,
- modelPrefix = inst.modelPrefix, loadEpoch = inst.loadEpoch,
- lr = inst.lr, lrFactor = inst.lrFactor, lrFactorEpoch = inst.lrFactorEpoch,
- monitorSize = inst.monitor)
- logger.info("Finish fit ...")
- }
- } catch {
- case ex: Exception => {
- logger.error(ex.getMessage, ex)
- parser.printUsage(System.err)
- sys.exit(1)
- }
- }
- }
-}
-
-class TrainMnist {
- @Option(name = "--network", usage = "the cnn to use: ['mlp', 'lenet']")
- private val network: String = "mlp"
- @Option(name = "--data-dir", usage = "the input data directory")
- private val dataDir: String = "mnist/"
- @Option(name = "--gpus", usage = "the gpus will be used, e.g. '0,1,2,3'")
- private val gpus: String = null
- @Option(name = "--cpus", usage = "the cpus will be used, e.g. '0,1,2,3'")
- private val cpus: String = null
- @Option(name = "--num-examples", usage = "the number of training examples")
- private val numExamples: Int = 60000
- @Option(name = "--batch-size", usage = "the batch size")
- private val batchSize: Int = 128
- @Option(name = "--lr", usage = "the initial learning rate")
- private val lr: Float = 0.1f
- @Option(name = "--model-prefix", usage = "the prefix of the model to load/save")
- private val modelPrefix: String = null
- @Option(name = "--num-epochs", usage = "the number of training epochs")
- private val numEpochs = 10
- @Option(name = "--load-epoch", usage = "load the model on an epoch using the model-prefix")
- private val loadEpoch: Int = -1
- @Option(name = "--kv-store", usage = "the kvstore type")
- private val kvStore = "local"
- @Option(name = "--lr-factor",
- usage = "times the lr with a factor for every lr-factor-epoch epoch")
- private val lrFactor: Float = 1f
- @Option(name = "--lr-factor-epoch", usage = "the number of epoch to factor the lr, could be .5")
- private val lrFactorEpoch: Float = 1f
- @Option(name = "--monitor", usage = "monitor the training process every N batch")
- private val monitor: Int = -1
-
- @Option(name = "--role", usage = "scheduler/server/worker")
- private val role: String = "worker"
- @Option(name = "--scheduler-host", usage = "Scheduler hostname / ip address")
- private val schedulerHost: String = null
- @Option(name = "--scheduler-port", usage = "Scheduler port")
- private val schedulerPort: Int = 0
- @Option(name = "--num-worker", usage = "# of workers")
- private val numWorker: Int = 1
- @Option(name = "--num-server", usage = "# of servers")
- private val numServer: Int = 1
-}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/TrainModel.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/TrainModel.scala
new file mode 100644
index 000000000000..f6c283c3dfb2
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/TrainModel.scala
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification
+
+import java.util.concurrent._
+
+import org.apache.mxnetexamples.imclassification.models._
+import org.apache.mxnetexamples.imclassification.util.Trainer
+import org.apache.mxnet._
+import org.apache.mxnetexamples.imclassification.datasets.{MnistIter, SyntheticDataIter}
+import org.kohsuke.args4j.{CmdLineParser, Option}
+import org.slf4j.LoggerFactory
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+object TrainModel {
+ private val logger = LoggerFactory.getLogger(classOf[TrainModel])
+
+ /**
+ * Simple model training and execution
+ * @param model The model identifying string
+ * @param dataPath Path to location of image data
+ * @param numExamples Number of image data examples
+ * @param numEpochs Number of epochs to train for
+ * @param benchmark Whether to use benchmark synthetic data instead of real image data
+ * @return The final validation accuracy
+ */
+ def test(model: String, dataPath: String, numExamples: Int = 60000,
+ numEpochs: Int = 10, benchmark: Boolean = false): Float = {
+ ResourceScope.using() {
+ val devs = Array(Context.cpu(0))
+ val envs: mutable.Map[String, String] = mutable.HashMap.empty[String, String]
+ val (dataLoader, net) = dataLoaderAndModel("mnist", model, dataPath,
+ numExamples = numExamples, benchmark = benchmark)
+ val Acc = Trainer.fit(batchSize = 128, numExamples, devs = devs,
+ network = net, dataLoader = dataLoader,
+ kvStore = "local", numEpochs = numEpochs)
+ logger.info("Finish test fit ...")
+ val (_, num) = Acc.get
+ num(0)
+ }
+ }
+
+ /**
+ * Gets dataset iterator and model symbol
+ * @param dataset The dataset identifying string
+ * @param model The model identifying string
+ * @param dataDir Path to location of image data
+ * @param numLayers The number of model layers (resnet only)
+ * @param numExamples The number of examples in the dataset
+ * @param benchmark Whether to use benchmark synthetic data instead of real image data
+ * @return Data iterator (partially applied function) and model symbol
+ */
+ def dataLoaderAndModel(dataset: String, model: String, dataDir: String = "",
+ numLayers: Int = 50, numExamples: Int = 60000,
+ benchmark: Boolean = false
+ ): ((Int, KVStore) => (DataIter, DataIter), Symbol) = {
+ val (imageShape, numClasses) = dataset match {
+ case "mnist" => (List(1, 28, 28), 10)
+ case "imagenet" => (List(3, 224, 224), 1000)
+ case _ => throw new Exception("Invalid image data collection")
+ }
+
+ val List(channels, height, width) = imageShape
+ val dataSize: Int = channels * height * width
+ val (datumShape, net) = model match {
+ case "mlp" => (List(dataSize), MultiLayerPerceptron.getSymbol(numClasses))
+ case "lenet" => (List(channels, height, width), Lenet.getSymbol(numClasses))
+ case "resnet" => (List(channels, height, width), Resnet.getSymbol(numClasses,
+ numLayers, imageShape))
+ case _ => throw new Exception("Invalid model name")
+ }
+
+ val dataLoader: (Int, KVStore) => (DataIter, DataIter) = if (benchmark) {
+ (batchSize: Int, kv: KVStore) => {
+ val iter = new SyntheticDataIter(numClasses, batchSize, datumShape, List(), numExamples)
+ (iter, iter)
+ }
+ } else {
+ dataset match {
+ case "mnist" => MnistIter.getIterator(Shape(datumShape), dataDir)
+ case _ => throw new Exception("This image data collection only supports the"
+ + "synthetic benchmark iterator. Use --benchmark to enable")
+ }
+ }
+ (dataLoader, net)
+ }
+
+ /**
+ * Runs image classification training from CLI with various options
+ * @param args CLI args
+ */
+ def main(args: Array[String]): Unit = {
+ val inst = new TrainModel
+ val parser: CmdLineParser = new CmdLineParser(inst)
+ try {
+ ResourceScope.using() {
+ parser.parseArgument(args.toList.asJava)
+
+ val dataPath = if (inst.dataDir == null) System.getenv("MXNET_HOME")
+ else inst.dataDir
+
+ val (dataLoader, net) = dataLoaderAndModel(inst.dataset, inst.network, dataPath,
+ inst.numLayers, inst.numExamples, inst.benchmark)
+
+ val devs =
+ if (inst.gpus != null) inst.gpus.split(',').map(id => Context.gpu(id.trim.toInt))
+ else if (inst.cpus != null) inst.cpus.split(',').map(id => Context.cpu(id.trim.toInt))
+ else Array(Context.cpu(0))
+
+ val envs: mutable.Map[String, String] = mutable.HashMap.empty[String, String]
+ envs.put("DMLC_ROLE", inst.role)
+ if (inst.schedulerHost != null) {
+ require(inst.schedulerPort > 0, "scheduler port not specified")
+ envs.put("DMLC_PS_ROOT_URI", inst.schedulerHost)
+ envs.put("DMLC_PS_ROOT_PORT", inst.schedulerPort.toString)
+ require(inst.numWorker > 0, "Num of workers must > 0")
+ envs.put("DMLC_NUM_WORKER", inst.numWorker.toString)
+ require(inst.numServer > 0, "Num of servers must > 0")
+ envs.put("DMLC_NUM_SERVER", inst.numServer.toString)
+ logger.info("Init PS environments")
+ KVStoreServer.init(envs.toMap)
+ }
+
+ if (inst.role != "worker") {
+ logger.info("Start KVStoreServer for scheduler & servers")
+ KVStoreServer.start()
+ } else {
+ Trainer.fit(batchSize = inst.batchSize, numExamples = inst.numExamples, devs = devs,
+ network = net, dataLoader = dataLoader,
+ kvStore = inst.kvStore, numEpochs = inst.numEpochs,
+ modelPrefix = inst.modelPrefix, loadEpoch = inst.loadEpoch,
+ lr = inst.lr, lrFactor = inst.lrFactor, lrFactorEpoch = inst.lrFactorEpoch,
+ monitorSize = inst.monitor)
+ logger.info("Finish fit ...")
+ }
+ }
+ } catch {
+ case ex: Exception => {
+ logger.error(ex.getMessage, ex)
+ parser.printUsage(System.err)
+ sys.exit(1)
+ }
+ }
+ }
+}
+
+class TrainModel {
+ @Option(name = "--network", usage = "the cnn to use: ['mlp', 'lenet', 'resnet']")
+ private val network: String = "mlp"
+ @Option(name = "--num-layers", usage = "the number of resnet layers to use")
+ private val numLayers: Int = 50
+ @Option(name = "--data-dir", usage = "the input data directory")
+ private val dataDir: String = "mnist/"
+
+ @Option(name = "--dataset", usage = "the images to classify: ['mnist', 'imagenet']")
+ private val dataset: String = "mnist"
+ @Option(name = "--benchmark", usage = "Benchmark to use synthetic data to measure performance")
+ private val benchmark: Boolean = false
+
+ @Option(name = "--gpus", usage = "the gpus will be used, e.g. '0,1,2,3'")
+ private val gpus: String = null
+ @Option(name = "--cpus", usage = "the cpus will be used, e.g. '0,1,2,3'")
+ private val cpus: String = null
+ @Option(name = "--num-examples", usage = "the number of training examples")
+ private val numExamples: Int = 60000
+ @Option(name = "--batch-size", usage = "the batch size")
+ private val batchSize: Int = 128
+ @Option(name = "--lr", usage = "the initial learning rate")
+ private val lr: Float = 0.1f
+ @Option(name = "--model-prefix", usage = "the prefix of the model to load/save")
+ private val modelPrefix: String = null
+ @Option(name = "--num-epochs", usage = "the number of training epochs")
+ private val numEpochs = 10
+ @Option(name = "--load-epoch", usage = "load the model on an epoch using the model-prefix")
+ private val loadEpoch: Int = -1
+ @Option(name = "--kv-store", usage = "the kvstore type")
+ private val kvStore = "local"
+ @Option(name = "--lr-factor",
+ usage = "times the lr with a factor for every lr-factor-epoch epoch")
+ private val lrFactor: Float = 1f
+ @Option(name = "--lr-factor-epoch", usage = "the number of epoch to factor the lr, could be .5")
+ private val lrFactorEpoch: Float = 1f
+ @Option(name = "--monitor", usage = "monitor the training process every N batch")
+ private val monitor: Int = -1
+
+ @Option(name = "--role", usage = "scheduler/server/worker")
+ private val role: String = "worker"
+ @Option(name = "--scheduler-host", usage = "Scheduler hostname / ip address")
+ private val schedulerHost: String = null
+ @Option(name = "--scheduler-port", usage = "Scheduler port")
+ private val schedulerPort: Int = 0
+ @Option(name = "--num-worker", usage = "# of workers")
+ private val numWorker: Int = 1
+ @Option(name = "--num-server", usage = "# of servers")
+ private val numServer: Int = 1
+}
+
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/datasets/MnistIter.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/datasets/MnistIter.scala
new file mode 100644
index 000000000000..9e6e1c2a3269
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/datasets/MnistIter.scala
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification.datasets
+
+import org.apache.mxnet._
+
+object MnistIter {
+ /**
+ * Returns an iterator over the MNIST dataset
+ * @param dataShape Image size (channels, height, width)
+ * @param dataDir The path to the image data
+ * @param batchSize Number of images per batch
+ * @param kv KVStore to use
+ * @return
+ */
+ def getIterator(dataShape: Shape, dataDir: String)
+ (batchSize: Int, kv: KVStore): (DataIter, DataIter) = {
+ val flat = if (dataShape.size == 3) "False" else "True"
+
+ val train = IO.MNISTIter(Map(
+ "image" -> (dataDir + "train-images-idx3-ubyte"),
+ "label" -> (dataDir + "train-labels-idx1-ubyte"),
+ "label_name" -> "softmax_label",
+ "input_shape" -> dataShape.toString,
+ "batch_size" -> batchSize.toString,
+ "shuffle" -> "True",
+ "flat" -> flat,
+ "num_parts" -> kv.numWorkers.toString,
+ "part_index" -> kv.`rank`.toString))
+
+ val eval = IO.MNISTIter(Map(
+ "image" -> (dataDir + "t10k-images-idx3-ubyte"),
+ "label" -> (dataDir + "t10k-labels-idx1-ubyte"),
+ "label_name" -> "softmax_label",
+ "input_shape" -> dataShape.toString,
+ "batch_size" -> batchSize.toString,
+ "flat" -> flat,
+ "num_parts" -> kv.numWorkers.toString,
+ "part_index" -> kv.`rank`.toString))
+
+ (train, eval)
+ }
+
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/datasets/SyntheticDataIter.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/datasets/SyntheticDataIter.scala
new file mode 100644
index 000000000000..9421f1021619
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/datasets/SyntheticDataIter.scala
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification.datasets
+
+import org.apache.mxnet.DType.DType
+import org.apache.mxnet._
+
+import scala.collection.immutable.ListMap
+import scala.util.Random
+
+class SyntheticDataIter(numClasses: Int, val batchSize: Int, datumShape: List[Int],
+ labelShape: List[Int], maxIter: Int, dtype: DType = DType.Float32
+ ) extends DataIter {
+ var curIter = 0
+ val random = new Random()
+ val shape = Shape(batchSize :: datumShape)
+ val batchLabelShape = Shape(batchSize :: labelShape)
+
+ val maxLabel = if (labelShape.isEmpty) numClasses.toFloat else 1f
+ var label: IndexedSeq[NDArray] = IndexedSeq(
+ NDArray.api.random_uniform(Some(0f), Some(maxLabel), shape = Some(batchLabelShape)))
+ var data: IndexedSeq[NDArray] = IndexedSeq(
+ NDArray.api.random_uniform(shape = Some(shape)))
+
+ val provideDataDesc: IndexedSeq[DataDesc] = IndexedSeq(
+ new DataDesc("data", shape, dtype, Layout.UNDEFINED))
+ val provideLabelDesc: IndexedSeq[DataDesc] = IndexedSeq(
+ new DataDesc("softmax_label", batchLabelShape, dtype, Layout.UNDEFINED))
+ val getPad: Int = 0
+
+ override def getData(): IndexedSeq[NDArray] = data
+
+ override def getIndex: IndexedSeq[Long] = IndexedSeq(curIter)
+
+ override def getLabel: IndexedSeq[NDArray] = label
+
+ override def hasNext: Boolean = curIter < maxIter - 1
+
+ override def next(): DataBatch = {
+ if (hasNext) {
+ curIter += batchSize
+ new DataBatch(data, label, getIndex, getPad, null, null, null)
+ } else {
+ throw new NoSuchElementException
+ }
+ }
+
+ override def reset(): Unit = {
+ curIter = 0
+ }
+
+ override def provideData: ListMap[String, Shape] = ListMap("data" -> shape)
+
+ override def provideLabel: ListMap[String, Shape] = ListMap("softmax_label" -> batchLabelShape)
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/Lenet.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/Lenet.scala
new file mode 100644
index 000000000000..76fb7bb66022
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/Lenet.scala
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification.models
+
+import org.apache.mxnet._
+
+object Lenet {
+
+ /**
+ * Gets Lenet Model Symbol
+ * @param numClasses Number of classes to classify into
+ * @return model symbol
+ */
+ def getSymbol(numClasses: Int): Symbol = {
+ val data = Symbol.Variable("data")
+ // first conv
+ val conv1 = Symbol.api.Convolution(data = Some(data), kernel = Shape(5, 5), num_filter = 20)
+ val tanh1 = Symbol.api.tanh(data = Some(conv1))
+ val pool1 = Symbol.api.Pooling(data = Some(tanh1), pool_type = Some("max"),
+ kernel = Some(Shape(2, 2)), stride = Some(Shape(2, 2)))
+ // second conv
+ val conv2 = Symbol.api.Convolution(data = Some(pool1), kernel = Shape(5, 5), num_filter = 50)
+ val tanh2 = Symbol.api.tanh(data = Some(conv2))
+ val pool2 = Symbol.api.Pooling(data = Some(tanh2), pool_type = Some("max"),
+ kernel = Some(Shape(2, 2)), stride = Some(Shape(2, 2)))
+ // first fullc
+ val flatten = Symbol.api.Flatten(data = Some(pool2))
+ val fc1 = Symbol.api.FullyConnected(data = Some(flatten), num_hidden = 500)
+ val tanh3 = Symbol.api.tanh(data = Some(fc1))
+ // second fullc
+ val fc2 = Symbol.api.FullyConnected(data = Some(tanh3), num_hidden = numClasses)
+ // loss
+ val lenet = Symbol.api.SoftmaxOutput(name = "softmax", data = Some(fc2))
+ lenet
+ }
+
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/MultiLayerPerceptron.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/MultiLayerPerceptron.scala
new file mode 100644
index 000000000000..5d880bbe0619
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/MultiLayerPerceptron.scala
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification.models
+
+import org.apache.mxnet._
+
+object MultiLayerPerceptron {
+
+ /**
+ * Gets MultiLayer Perceptron Model Symbol
+ * @param numClasses Number of classes to classify into
+ * @return model symbol
+ */
+ def getSymbol(numClasses: Int): Symbol = {
+ val data = Symbol.Variable("data")
+
+ val fc1 = Symbol.api.FullyConnected(data = Some(data), num_hidden = 128, name = "fc1")
+ val act1 = Symbol.api.Activation(data = Some(fc1), "relu", name = "relu")
+ val fc2 = Symbol.api.FullyConnected(Some(act1), None, None, 64, name = "fc2")
+ val act2 = Symbol.api.Activation(data = Some(fc2), "relu", name = "relu2")
+ val fc3 = Symbol.api.FullyConnected(Some(act2), None, None, numClasses, name = "fc3")
+ val mlp = Symbol.api.SoftmaxOutput(name = "softmax", data = Some(fc3))
+ mlp
+ }
+
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/Resnet.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/Resnet.scala
new file mode 100644
index 000000000000..c3f43d97e898
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/models/Resnet.scala
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification.models
+
+import org.apache.mxnet._
+
+object Resnet {
+ /**
+ * Helper to produce individual residual unit
+ */
+ def residualUnit(data: Symbol, numFilter: Int, stride: Shape, dimMatch: Boolean,
+ name: String = "", bottleNeck: Boolean = true, bnMom: Float = 0.9f,
+ workspace: Int = 256, memonger: Boolean = false): Symbol = {
+ val (act1, operated) = if (bottleNeck) {
+ val bn1 = Symbol.api.BatchNorm(data = Some(data), fix_gamma = Some(false), eps = Some(2e-5),
+ momentum = Some(bnMom), name = name + "_bn1")
+ val act1: Symbol = Symbol.api.Activation(data = Some(bn1), act_type = "relu",
+ name = name + "_relu1")
+ val conv1 = Symbol.api.Convolution(data = Some(act1), num_filter = (numFilter * 0.25).toInt,
+ kernel = Shape(1, 1), stride = Some(Shape(1, 1)), pad = Some(Shape(0, 0)),
+ no_bias = Some(true), workspace = Some(workspace), name = name + "_conv1")
+ val bn2 = Symbol.api.BatchNorm(data = Some(conv1), fix_gamma = Some(false),
+ eps = Some(2e-5), momentum = Some(bnMom), name = name + "_bn2")
+ val act2 = Symbol.api.Activation(data = Some(bn2), act_type = "relu", name = name + "_relu2")
+ val conv2 = Symbol.api.Convolution(data = Some(act2), num_filter = (numFilter * 0.25).toInt,
+ kernel = Shape(3, 3), stride = Some(stride), pad = Some(Shape(1, 1)),
+ no_bias = Some(true), workspace = Some(workspace), name = name + "_conv2")
+ val bn3 = Symbol.api.BatchNorm(data = Some(conv2), fix_gamma = Some(false),
+ eps = Some(2e-5), momentum = Some(bnMom), name = name + "_bn3")
+ val act3 = Symbol.api.Activation(data = Some(bn3), act_type = "relu", name = name + "_relu3")
+ val conv3 = Symbol.api.Convolution(data = Some(act3), num_filter = numFilter,
+ kernel = Shape(1, 1), stride = Some(Shape(1, 1)), pad = Some(Shape(0, 0)),
+ no_bias = Some(true), workspace = Some(workspace), name = name + "_conv3")
+ (act1, conv3)
+ } else {
+ val bn1 = Symbol.api.BatchNorm(data = Some(data), fix_gamma = Some(false),
+ eps = Some(2e-5), momentum = Some(bnMom), name = name + "_bn1")
+ val act1 = Symbol.api.Activation(data = Some(bn1), act_type = "relu", name = name + "_relu1")
+ val conv1 = Symbol.api.Convolution(data = Some(act1), num_filter = numFilter,
+ kernel = Shape(3, 3), stride = Some(stride), pad = Some(Shape(1, 1)),
+ no_bias = Some(true), workspace = Some(workspace), name = name + "_conv1")
+ val bn2 = Symbol.api.BatchNorm(data = Some(conv1), fix_gamma = Some(false),
+ eps = Some(2e-5), momentum = Some(bnMom), name = name + "_bn2")
+ val act2 = Symbol.api.Activation(data = Some(bn2), act_type = "relu", name = name + "_relu2")
+ val conv2 = Symbol.api.Convolution(data = Some(act2), num_filter = numFilter,
+ kernel = Shape(3, 3), stride = Some(Shape(1, 1)), pad = Some(Shape(1, 1)),
+ no_bias = Some(true), workspace = Some(workspace), name = name + "_conv2")
+ (act1, conv2)
+ }
+ val shortcut = if (dimMatch) {
+ data
+ } else {
+ Symbol.api.Convolution(Some(act1), num_filter = numFilter, kernel = Shape(1, 1),
+ stride = Some(stride), no_bias = Some(true), workspace = Some(workspace),
+ name = name + "_sc")
+ }
+ operated + shortcut
+ }
+
+ /**
+ * Helper for building the resnet Symbol
+ */
+ def resnet(units: List[Int], numStages: Int, filterList: List[Int], numClasses: Int,
+ imageShape: List[Int], bottleNeck: Boolean = true, bnMom: Float = 0.9f,
+ workspace: Int = 256, dtype: String = "float32", memonger: Boolean = false): Symbol = {
+ assert(units.size == numStages)
+ var data = Symbol.Variable("data", shape = Shape(List(4) ::: imageShape), dType = DType.Float32)
+ if (dtype == "float32") {
+ data = Symbol.api.identity(Some(data), "id")
+ } else if (dtype == "float16") {
+ data = Symbol.api.cast(Some(data), "float16")
+ }
+ data = Symbol.api.BatchNorm(Some(data), fix_gamma = Some(true), eps = Some(2e-5),
+ momentum = Some(bnMom), name = "bn_data")
+ val List(channels, height, width) = imageShape
+ var body = if (height <= 32) {
+ Symbol.api.Convolution(Some(data), num_filter = filterList.head, kernel = Shape(7, 7),
+ stride = Some(Shape(1, 1)), pad = Some(Shape(1, 1)), no_bias = Some(true), name = "conv0",
+ workspace = Some(workspace))
+ } else {
+ var body0 = Symbol.api.Convolution(Some(data), num_filter = filterList.head,
+ kernel = Shape(3, 3), stride = Some(Shape(2, 2)), pad = Some(Shape(3, 3)),
+ no_bias = Some(true), name = "conv0", workspace = Some(workspace))
+ body0 = Symbol.api.BatchNorm(Some(body0), fix_gamma = Some(false), eps = Some(2e-5),
+ momentum = Some(bnMom), name = "bn0")
+ body0 = Symbol.api.Activation(Some(body0), act_type = "relu", name = "relu0")
+ Symbol.api.Pooling(Some(body0), kernel = Some(Shape(3, 3)), stride = Some(Shape(2, 2)),
+ pad = Some(Shape(1, 1)), pool_type = Some("max"))
+ }
+ for (((filter, i), unit) <- filterList.tail.zipWithIndex.zip(units)) {
+ val stride = Shape(if (i == 0) 1 else 2, if (i == 0) 1 else 2)
+ body = residualUnit(body, filter, stride, false, name = s"stage${i + 1}_unit${1}",
+ bottleNeck = bottleNeck, workspace = workspace, memonger = memonger)
+ for (j <- 0 until unit - 1) {
+ body = residualUnit(body, filter, Shape(1, 1), true, s"stage${i + 1}_unit${j + 2}",
+ bottleNeck, workspace = workspace, memonger = memonger)
+ }
+ }
+ val bn1 = Symbol.api.BatchNorm(Some(body), fix_gamma = Some(false), eps = Some(2e-5),
+ momentum = Some(bnMom), name = "bn1")
+ val relu1 = Symbol.api.Activation(Some(bn1), act_type = "relu", name = "relu1")
+ val pool1 = Symbol.api.Pooling(Some(relu1), global_pool = Some(true),
+ kernel = Some(Shape(7, 7)), pool_type = Some("avg"), name = "pool1")
+ val flat = Symbol.api.Flatten(Some(pool1))
+ var fc1 = Symbol.api.FullyConnected(Some(flat), num_hidden = numClasses, name = "fc1")
+ if (dtype == "float16") {
+ fc1 = Symbol.api.cast(Some(fc1), "float32")
+ }
+ Symbol.api.SoftmaxOutput(Some(fc1), name = "softmax")
+ }
+
+ /**
+ * Gets the resnet model symbol
+ * @param numClasses Number of classes to classify into
+ * @param numLayers Number of residual layers
+ * @param imageShape The image shape as List(channels, height, width)
+ * @param convWorkspace Maximum temporary workspace allowed (MB) in convolutions
+ * @param dtype Type of data (float16, float32, etc) to use during computation
+ * @return Model symbol
+ */
+ def getSymbol(numClasses: Int, numLayers: Int, imageShape: List[Int], convWorkspace: Int = 256,
+ dtype: String = "float32"): Symbol = {
+ val List(channels, height, width) = imageShape
+ val (numStages, units, filterList, bottleNeck): (Int, List[Int], List[Int], Boolean) =
+ if (height <= 28) {
+ val (perUnit, filterList, bottleNeck) = if ((numLayers - 2) % 9 == 0 && numLayers > 165) {
+ (List(Math.floor((numLayers - 2) / 9).toInt),
+ List(16, 64, 128, 256),
+ true)
+ } else if ((numLayers - 2) % 6 == 0 && numLayers < 164) {
+ (List(Math.floor((numLayers - 2) / 6).toInt),
+ List(16, 16, 32, 64),
+ false)
+ } else {
+ throw new Exception(s"Invalid number of layers: ${numLayers}")
+ }
+ val numStages = 3
+ val units = (1 to numStages).map(_ => perUnit.head).toList
+ (numStages, units, filterList, bottleNeck)
+ } else {
+ val (filterList, bottleNeck) = if (numLayers >= 50) {
+ (List(64, 256, 512, 1024, 2048), true)
+ } else {
+ (List(64, 64, 128, 256, 512), false)
+ }
+ val units: List[Int] = Map(
+ 18 -> List(2, 2, 2, 2),
+ 34 -> List(3, 4, 6, 3),
+ 50 -> List(3, 4, 6, 3),
+ 101 -> List(3, 4, 23, 3),
+ 152 -> List(3, 8, 36, 3),
+ 200 -> List(3, 24, 36, 3),
+ 269 -> List(3, 30, 48, 8)
+ ).get(numLayers) match {
+ case Some(x) => x
+ case None => throw new Exception(s"Invalid number of layers: ${numLayers}")
+ }
+ (4, units, filterList, bottleNeck)
+ }
+ resnet(units, numStages, filterList, numClasses, imageShape, bottleNeck,
+ workspace = convWorkspace, dtype = dtype)
+ }
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/util/Trainer.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/util/Trainer.scala
new file mode 100644
index 000000000000..276816cf8c8c
--- /dev/null
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/imclassification/util/Trainer.scala
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnetexamples.imclassification.util
+
+import org.apache.mxnet.Callback.Speedometer
+import org.apache.mxnet._
+import org.apache.mxnet.optimizer.SGD
+import org.slf4j.LoggerFactory
+
+object Trainer {
+ private val logger = LoggerFactory.getLogger(classOf[Trainer])
+
+ /**
+ * Fits a model
+ * @param batchSize Number of images per training batch
+ * @param numExamples Total number of image examples
+ * @param devs List of device contexts to use
+ * @param network The model to train
+ * @param dataLoader Function to get data loaders for training and validation data
+ * @param kvStore KVStore to use
+ * @param numEpochs Number of times to train on each image
+ * @param modelPrefix Prefix to model identification
+ * @param loadEpoch Loads a saved checkpoint at this epoch when set
+ * @param lr The learning rate
+ * @param lrFactor Learning rate factor (see FactorScheduler)
+ * @param lrFactorEpoch Learning rate factor epoch (see FactorScheduler)
+ * @param clipGradient Maximum gradient during optimization
+ * @param monitorSize (See Monitor)
+ * @return Final accuracy
+ */
+ // scalastyle:off parameterNum
+ def fit(batchSize: Int, numExamples: Int, devs: Array[Context],
+ network: Symbol, dataLoader: (Int, KVStore) => (DataIter, DataIter),
+ kvStore: String, numEpochs: Int, modelPrefix: String = null, loadEpoch: Int = -1,
+ lr: Float = 0.1f, lrFactor: Float = 1f, lrFactorEpoch: Float = 1f,
+ clipGradient: Float = 0f, monitorSize: Int = -1): Accuracy = {
+ // kvstore
+ ResourceScope.using() {
+ var kv = KVStore.create(kvStore)
+
+ // load model
+ val modelPrefixWithRank =
+ if (modelPrefix == null) null
+ else modelPrefix + s"-${kv.rank}"
+
+ val (argParams, auxParams, beginEpoch) =
+ if (loadEpoch >= 0) {
+ require(modelPrefixWithRank != null)
+ val tmp = FeedForward.load(modelPrefix, loadEpoch)
+ (tmp.getArgParams, tmp.getAuxParams, loadEpoch)
+ } else {
+ (null, null, 0)
+ }
+
+ // save model
+ val checkpoint: EpochEndCallback =
+ if (modelPrefix == null) null
+ else new EpochEndCallback {
+ override def invoke(epoch: Int, symbol: Symbol,
+ argParams: Map[String, NDArray],
+ auxStates: Map[String, NDArray]): Unit = {
+ Model.saveCheckpoint(modelPrefix, epoch + 1, symbol, argParams, auxParams)
+ }
+ }
+
+ // data
+ val (train, validation) = dataLoader(batchSize, kv)
+
+ // train
+ val epochSize =
+ if (kvStore == "dist_sync") numExamples / batchSize / kv.numWorkers
+ else numExamples / batchSize
+
+ val lrScheduler =
+ if (lrFactor < 1f) {
+ new FactorScheduler(step = Math.max((epochSize * lrFactorEpoch).toInt, 1),
+ factor = lrFactor)
+ } else {
+ null
+ }
+ val optimizer: Optimizer = new SGD(learningRate = lr,
+ lrScheduler = lrScheduler, clipGradient = clipGradient,
+ momentum = 0.9f, wd = 0.00001f)
+
+ // disable kvstore for single device
+ if (kv.`type`.contains("local") && (devs.length == 1 || devs(0).deviceType != "gpu")) {
+ kv.dispose()
+ kv = null
+ }
+
+ val model = new FeedForward(ctx = devs,
+ symbol = network,
+ numEpoch = numEpochs,
+ optimizer = optimizer,
+ initializer = new Xavier(factorType = "in", magnitude = 2.34f),
+ argParams = argParams,
+ auxParams = auxParams,
+ beginEpoch = beginEpoch,
+ epochSize = epochSize)
+ if (monitorSize > 0) {
+ model.setMonitor(new Monitor(monitorSize))
+ }
+ val acc = new Accuracy()
+ model.fit(trainData = train,
+ evalData = validation,
+ evalMetric = acc,
+ kvStore = kv,
+ batchEndCallback = new Speedometer(batchSize, 50),
+ epochEndCallback = checkpoint)
+ if (kv != null) {
+ kv.dispose()
+ }
+ acc
+ }
+ }
+ // scalastyle:on parameterNum
+}
+
+class Trainer
+
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExample.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExample.scala
index f6e4fe0941da..c7f2fdac30c3 100644
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExample.scala
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExample.scala
@@ -20,11 +20,18 @@ package org.apache.mxnetexamples.infer.imageclassifier
import org.apache.mxnet._
import org.kohsuke.args4j.{CmdLineParser, Option}
import org.slf4j.LoggerFactory
-import org.apache.mxnet.infer.ImageClassifier
+import org.apache.mxnet.infer.{Classifier, ImageClassifier}
import scala.collection.JavaConverters._
import java.io.File
+import org.apache.mxnetexamples.benchmark.CLIParserBase
+// scalastyle:off
+import java.awt.image.BufferedImage
+// scalastyle:on
+
+import org.apache.mxnetexamples.InferBase
+
import scala.collection.mutable.ListBuffer
// scalastyle:off
@@ -108,7 +115,7 @@ object ImageClassifierExample {
}
def main(args: Array[String]): Unit = {
- val inst = new ImageClassifierExample
+ val inst = new CLIParser
val parser: CmdLineParser = new CmdLineParser(inst)
var context = Context.cpu()
@@ -157,11 +164,73 @@ object ImageClassifierExample {
}
}
-class ImageClassifierExample {
+class CLIParser extends CLIParserBase{
@Option(name = "--model-path-prefix", usage = "the input model directory")
- private val modelPathPrefix: String = "/resnet-152/resnet-152"
+ val modelPathPrefix: String = "/resnet-152/resnet-152"
@Option(name = "--input-image", usage = "the input image")
- private val inputImagePath: String = "/images/kitten.jpg"
+ val inputImagePath: String = "/images/kitten.jpg"
@Option(name = "--input-dir", usage = "the input batch of images directory")
- private val inputImageDir: String = "/images/"
+ val inputImageDir: String = "/images/"
+}
+
+class ImageClassifierExample(CLIParser: CLIParser) extends InferBase{
+
+ override def loadModel(context: Array[Context],
+ batchInference : Boolean = false): Classifier = {
+ val dType = DType.Float32
+ val batchSize = if (batchInference) CLIParser.batchSize else 1
+ val inputShape = Shape(batchSize, 3, 224, 224)
+
+ val inputDescriptor = IndexedSeq(DataDesc("data", inputShape, dType, "NCHW"))
+
+ // Create object of ImageClassifier class
+ val imgClassifier: ImageClassifier = new ImageClassifier(CLIParser.modelPathPrefix,
+ inputDescriptor, context)
+ imgClassifier
+ }
+
+ override def loadSingleData(): Any = {
+ val img = ImageClassifier.loadImageFromFile(CLIParser.inputImagePath)
+ img
+ }
+
+ override def loadBatchFileList(batchSize: Int): List[Any] = {
+ val dir = new File(CLIParser.inputImageDir)
+ require(dir.exists && dir.isDirectory,
+ "input image directory: %s not found".format(CLIParser.inputImageDir))
+ val output = ListBuffer[List[String]]()
+ var batch = ListBuffer[String]()
+ for (imgFile: File <- dir.listFiles()){
+ batch += imgFile.getPath
+ if (batch.length == batchSize) {
+ output += batch.toList
+ batch = ListBuffer[String]()
+ }
+ }
+ if (batch.length > 0) {
+ output += batch.toList
+ }
+ output.toList
+ }
+
+ override def loadInputBatch(inputPaths: Any): Any = {
+ val batchFile = inputPaths.asInstanceOf[List[String]]
+ ImageClassifier.loadInputBatch(batchFile)
+ }
+
+ override def runSingleInference(loadedModel: Any, input: Any): Any = {
+ // Running inference on single image
+ val imageModel = loadedModel.asInstanceOf[ImageClassifier]
+ val imgInput = input.asInstanceOf[BufferedImage]
+ val output = imageModel.classifyImage(imgInput, Some(5))
+ output
+ }
+
+ override def runBatchInference(loadedModel: Any, input: Any): Any = {
+ val imageModel = loadedModel.asInstanceOf[ImageClassifier]
+ val imgInput = input.asInstanceOf[Traversable[BufferedImage]]
+ val output = imageModel.classifyImageBatch(imgInput, Some(5))
+ output
+ }
+
}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/README.md b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/README.md
index 69328a44bab6..77aec7bb5dee 100644
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/README.md
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/README.md
@@ -28,18 +28,13 @@ The model is trained on the [Pascal VOC 2012 dataset](http://host.robots.ox.ac.u
### Download Artifacts
#### Step 1
You can download the files using the script `get_ssd_data.sh`. It will download and place the model files in a `model` folder and the test image files in a `image` folder in the current directory.
-From the `scala-package/examples/scripts/infer/imageclassifier/` folder run:
+From the `scala-package/examples/scripts/infer/objectdetector/` folder run:
```bash
-./get_resnet_data.sh
+./get_ssd_data.sh
```
-**Note**: You may need to run `chmod +x get_resnet_data.sh` before running this script.
-
-Alternatively use the following links to download the Symbol and Params files via your browser:
-- [resnet50_ssd_model-symbol.json](https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-symbol.json)
-- [resnet50_ssd_model-0000.params](https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-0000.params)
-- [synset.txt](https://github.com/awslabs/mxnet-model-server/blob/master/examples/ssd/synset.txt)
+**Note**: You may need to run `chmod +x get_ssd_data.sh` before running this script.
In the pre-trained model, the `input_name` is `data` and shape is `(1, 3, 512, 512)`.
This shape translates to: a batch of `1` image, the image has color and uses `3` channels (RGB), and the image has the dimensions of `512` pixels in height by `512` pixels in width.
@@ -57,13 +52,6 @@ The output shape is `(1, 6132, 6)`. As with the input, the `1` is the number of
### Setup Datapath and Parameters
#### Step 2
-The code `Line 31: val baseDir = System.getProperty("user.dir")` in the example will automatically searches the work directory you have defined. Please put the files in your [work directory](https://stackoverflow.com/questions/16239130/java-user-dir-property-what-exactly-does-it-mean).
-
-Alternatively, if you would like to use your own path, please change line 31 into your own path
-```scala
-val baseDir =
-```
-
The followings is the parameters defined for this example, you can find more information in the `class SSDClassifierExample`.
| Argument | Comments |
@@ -79,7 +67,7 @@ After the previous steps, you should be able to run the code using the following
From the `scala-package/examples/scripts/inferexample/objectdetector/` folder run:
```bash
-./run_ssd_example.sh ../models/resnet50_ssd_model ../images/dog.jpg ../images
+./run_ssd_example.sh ../models/resnet50_ssd/resnet50_ssd_model ../images/dog.jpg ../images
```
**Notes**:
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/SSDClassifierExample.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/SSDClassifierExample.scala
index 0edde9e6516b..07d1cc82e927 100644
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/SSDClassifierExample.scala
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/infer/objectdetector/SSDClassifierExample.scala
@@ -16,7 +16,11 @@
*/
package org.apache.mxnetexamples.infer.objectdetector
+// scalastyle:off
+import java.awt.image.BufferedImage
+import org.apache.mxnetexamples.benchmark.CLIParserBase
+// scalastyle:on
import java.io.File
import org.apache.mxnet._
@@ -27,6 +31,8 @@ import org.slf4j.LoggerFactory
import scala.collection.JavaConverters._
import java.nio.file.{Files, Paths}
+import org.apache.mxnetexamples.InferBase
+
import scala.collection.mutable.ListBuffer
// scalastyle:off
@@ -37,15 +43,6 @@ import scala.collection.mutable.ListBuffer
* @see Instructions to run this example
*/
// scalastyle:on
-class SSDClassifierExample {
- @Option(name = "--model-path-prefix", usage = "the input model directory and prefix of the model")
- private val modelPathPrefix: String = "/model/ssd_resnet50_512"
- @Option(name = "--input-image", usage = "the input image")
- private val inputImagePath: String = "/images/dog.jpg"
- @Option(name = "--input-dir", usage = "the input batch of images directory")
- private val inputImageDir: String = "/images/"
-}
-
object SSDClassifierExample {
private val logger = LoggerFactory.getLogger(classOf[SSDClassifierExample])
@@ -111,7 +108,7 @@ object SSDClassifierExample {
}
def main(args: Array[String]): Unit = {
- val inst = new SSDClassifierExample
+ val inst = new CLIParser
val parser : CmdLineParser = new CmdLineParser(inst)
parser.parseArgument(args.toList.asJava)
val mdprefixDir = inst.modelPathPrefix
@@ -185,12 +182,73 @@ object SSDClassifierExample {
def checkExist(arr : Array[String]) : Boolean = {
var exist : Boolean = true
for (item <- arr) {
- exist = Files.exists(Paths.get(item)) && exist
- if (!exist) {
+ if (!(Files.exists(Paths.get(item)))) {
logger.error("Cannot find: " + item)
+ exist = false
}
}
exist
}
}
+
+class CLIParser extends CLIParserBase {
+ @Option(name = "--model-path-prefix", usage = "the input model directory and prefix of the model")
+ val modelPathPrefix: String = "/model/ssd_resnet50_512"
+ @Option(name = "--input-image", usage = "the input image")
+ val inputImagePath: String = "/images/dog.jpg"
+ @Option(name = "--input-dir", usage = "the input batch of images directory")
+ val inputImageDir: String = "/images/"
+}
+
+class SSDClassifierExample(CLIParser: CLIParser)
+ extends InferBase {
+ override def loadModel(context: Array[Context], batchInference: Boolean = false): Any = {
+ val dType = DType.Float32
+ val batchSize = if (batchInference) CLIParser.batchSize else 1
+ val inputShape = Shape(batchSize, 3, 512, 512)
+ val inputDescriptors = IndexedSeq(DataDesc("data", inputShape, dType, "NCHW"))
+ new ObjectDetector(CLIParser.modelPathPrefix, inputDescriptors, context)
+ }
+ override def loadSingleData(): Any = {
+ val img = ImageClassifier.loadImageFromFile(CLIParser.inputImagePath)
+ img
+ }
+
+ override def runSingleInference(loadedModel: Any, input: Any): Any = {
+ val detector = loadedModel.asInstanceOf[ObjectDetector]
+ val imgInput = input.asInstanceOf[BufferedImage]
+ detector.imageObjectDetect(imgInput)
+ }
+
+ override def loadInputBatch(inputPaths: Any): Any = {
+ val batchFile = inputPaths.asInstanceOf[List[String]]
+ ImageClassifier.loadInputBatch(batchFile)
+ }
+
+ override def loadBatchFileList(batchSize: Int): List[Any] = {
+ val dir = new File(CLIParser.inputImageDir)
+ require(dir.exists && dir.isDirectory,
+ "input image directory: %s not found".format(CLIParser.inputImageDir))
+ val output = ListBuffer[List[String]]()
+ var batch = ListBuffer[String]()
+ for (imgFile: File <- dir.listFiles()){
+ batch += imgFile.getPath
+ if (batch.length == batchSize) {
+ output += batch.toList
+ batch = ListBuffer[String]()
+ }
+ }
+ if (batch.length > 0) {
+ output += batch.toList
+ }
+ output.toList
+ }
+
+ override def runBatchInference(loadedModel: Any, input: Any): Any = {
+ val model = loadedModel.asInstanceOf[ObjectDetector]
+ val imgInput = input.asInstanceOf[Traversable[BufferedImage]]
+ val output = model.imageBatchObjectDetect(imgInput, Some(5))
+ output
+ }
+}
diff --git a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/rnn/TestCharRnn.scala b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/rnn/TestCharRnn.scala
index bd064dbd3518..bf2eba660388 100644
--- a/scala-package/examples/src/main/scala/org/apache/mxnetexamples/rnn/TestCharRnn.scala
+++ b/scala-package/examples/src/main/scala/org/apache/mxnetexamples/rnn/TestCharRnn.scala
@@ -18,8 +18,11 @@
package org.apache.mxnetexamples.rnn
import org.apache.mxnet._
+import org.apache.mxnetexamples.InferBase
+import org.apache.mxnetexamples.benchmark.CLIParserBase
import org.kohsuke.args4j.{CmdLineParser, Option}
import org.slf4j.LoggerFactory
+
import scala.collection.JavaConverters._
/**
@@ -83,7 +86,7 @@ object TestCharRnn {
}
def main(args: Array[String]): Unit = {
- val stcr = new TestCharRnn
+ val stcr = new CLIParser
val parser: CmdLineParser = new CmdLineParser(stcr)
try {
parser.parseArgument(args.toList.asJava)
@@ -99,11 +102,68 @@ object TestCharRnn {
}
}
-class TestCharRnn {
+class CLIParser extends CLIParserBase {
@Option(name = "--data-path", usage = "the input train data file")
- private val dataPath: String = "./data/obama.txt"
+ val dataPath: String = "./data/obama.txt"
@Option(name = "--model-prefix", usage = "the model prefix")
- private val modelPrefix: String = "./model/obama"
+ val modelPrefix: String = "./model/obama"
@Option(name = "--starter-sentence", usage = "the starter sentence")
- private val starterSentence: String = "The joke"
+ val starterSentence: String = "The joke"
+}
+
+class TestCharRnn(CLIParser: CLIParser) extends InferBase {
+
+ private var vocab : Map[String, Int] = null
+
+ override def loadModel(context: Array[Context], batchInference : Boolean = false): Any = {
+ val batchSize = 32
+ val buckets = List(129)
+ val numHidden = 512
+ val numEmbed = 256
+ val numLstmLayer = 3
+ val (_, argParams, _) = Model.loadCheckpoint(CLIParser.modelPrefix, 75)
+ this.vocab = Utils.buildVocab(CLIParser.dataPath)
+ var ctx = Context.cpu()
+ if (System.getenv().containsKey("SCALA_TEST_ON_GPU") &&
+ System.getenv("SCALA_TEST_ON_GPU").toInt == 1) {
+ ctx = Context.gpu()
+ }
+ val model = new RnnModel.LSTMInferenceModel(numLstmLayer, vocab.size + 1,
+ numHidden = numHidden, numEmbed = numEmbed,
+ numLabel = vocab.size + 1, argParams = argParams, dropout = 0.2f, ctx = ctx)
+ model
+ }
+
+ override def loadSingleData(): Any = {
+ val revertVocab = Utils.makeRevertVocab(vocab)
+ revertVocab
+ }
+
+ override def runSingleInference(loadedModel: Any, input: Any): Any = {
+ val model = loadedModel.asInstanceOf[RnnModel.LSTMInferenceModel]
+ val revertVocab = input.asInstanceOf[Map[Int, String]]
+ // generate a sequence of 1200 chars
+ val seqLength = 1200
+ val inputNdarray = NDArray.zeros(1)
+ // Feel free to change the starter sentence
+ var output = CLIParser.starterSentence
+ val randomSample = true
+ var newSentence = true
+ val ignoreLength = output.length()
+
+ for (i <- 0 until seqLength) {
+ if (i <= ignoreLength - 1) Utils.makeInput(output(i), vocab, inputNdarray)
+ else Utils.makeInput(output.takeRight(1)(0), vocab, inputNdarray)
+ val prob = model.forward(inputNdarray, newSentence)
+ newSentence = false
+ val nextChar = Utils.makeOutput(prob, revertVocab, randomSample)
+ if (nextChar == "") newSentence = true
+ if (i >= ignoreLength) output = output ++ nextChar
+ }
+ output
+ }
+
+ override def loadBatchFileList(batchSize: Int): List[Any] = null
+ override def loadInputBatch(source: Any): Any = null
+ override def runBatchInference(loadedModel: Any, input: Any): Any = null
}
diff --git a/scala-package/examples/src/test/scala/org/apache/mxnetexamples/benchmark/ScalaInferenceBenchmarkSuite.scala b/scala-package/examples/src/test/scala/org/apache/mxnetexamples/benchmark/ScalaInferenceBenchmarkSuite.scala
new file mode 100644
index 000000000000..0b7f4693c5fa
--- /dev/null
+++ b/scala-package/examples/src/test/scala/org/apache/mxnetexamples/benchmark/ScalaInferenceBenchmarkSuite.scala
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.mxnetexamples.benchmark
+
+import java.io.File
+
+import org.apache.mxnetexamples.Util
+import org.scalatest.{BeforeAndAfterAll, FunSuite}
+import org.slf4j.LoggerFactory
+
+import scala.sys.process.Process
+
+class ScalaInferenceBenchmarkSuite extends FunSuite with BeforeAndAfterAll {
+ private val logger = LoggerFactory.getLogger(classOf[ScalaInferenceBenchmarkSuite])
+ override def beforeAll(): Unit = {
+ }
+
+ test("Testing Benchmark -- Image Classification") {
+ logger.info("Downloading resnet-18 model")
+ val tempDirPath = System.getProperty("java.io.tmpdir")
+ logger.info("tempDirPath: %s".format(tempDirPath))
+ val baseUrl = "https://s3.us-east-2.amazonaws.com/scala-infer-models"
+ Util.downloadUrl(baseUrl + "/resnet-18/resnet-18-symbol.json",
+ tempDirPath + "/resnet18/resnet-18-symbol.json")
+ Util.downloadUrl(baseUrl + "/resnet-18/resnet-18-0000.params",
+ tempDirPath + "/resnet18/resnet-18-0000.params")
+ Util.downloadUrl(baseUrl + "/resnet-18/synset.txt",
+ tempDirPath + "/resnet18/synset.txt")
+ Util.downloadUrl("https://s3.amazonaws.com/model-server/inputs/Pug-Cookie.jpg",
+ tempDirPath + "/inputImages/resnet18/Pug-Cookie.jpg")
+ val modelDirPath = tempDirPath + File.separator + "resnet18/"
+ val inputImagePath = tempDirPath + File.separator +
+ "inputImages/resnet18/Pug-Cookie.jpg"
+ val inputImageDir = tempDirPath + File.separator + "inputImages/resnet18/"
+ val args = Array(
+ "--example", "ImageClassifierExample",
+ "--count", "1",
+ "--batchSize", "10",
+ "--model-path-prefix", s"$modelDirPath/resnet-18",
+ "--input-image", inputImagePath,
+ "--input-dir", inputImageDir
+ )
+ ScalaInferenceBenchmark.main(args)
+ }
+
+ test("Testing Benchmark -- Object Detection") {
+ logger.info("Downloading resnetssd model")
+ val tempDirPath = System.getProperty("java.io.tmpdir")
+ logger.info("tempDirPath: %s".format(tempDirPath))
+ val modelBase = "https://s3.amazonaws.com/model-server/models/resnet50_ssd/"
+ val imageBase = "https://s3.amazonaws.com/model-server/inputs/"
+ Util.downloadUrl(modelBase + "resnet50_ssd_model-symbol.json",
+ tempDirPath + "/resnetssd/resnet50_ssd_model-symbol.json")
+ Util.downloadUrl(modelBase + "resnet50_ssd_model-0000.params",
+ tempDirPath + "/resnetssd/resnet50_ssd_model-0000.params")
+ Util.downloadUrl(modelBase + "synset.txt",
+ tempDirPath + "/resnetssd/synset.txt")
+ Util.downloadUrl(imageBase + "dog-ssd.jpg",
+ tempDirPath + "/inputImages/resnetssd/dog-ssd.jpg")
+ val modelDirPath = tempDirPath + File.separator + "resnetssd/"
+ val inputImagePath = tempDirPath + File.separator +
+ "inputImages/resnetssd/dog-ssd.jpg"
+ val inputImageDir = tempDirPath + File.separator + "inputImages/resnetssd/"
+ val args = Array(
+ "--example", "ObjectDetectionExample",
+ "--count", "1",
+ "--batchSize", "10",
+ "--model-path-prefix", s"$modelDirPath/resnet50_ssd_model",
+ "--input-image", inputImagePath,
+ "--input-dir", inputImageDir
+ )
+ ScalaInferenceBenchmark.main(args)
+ }
+
+ test("Testing Benchmark -- charRNN Model") {
+ logger.info("Downloading LSTM model")
+ val tempDirPath = System.getProperty("java.io.tmpdir")
+ logger.info("tempDirPath: %s".format(tempDirPath))
+ val baseUrl = "https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci/RNN/"
+ Util.downloadUrl(baseUrl + "obama.zip", tempDirPath + "/RNN/obama.zip")
+ Util.downloadUrl(baseUrl + "sherlockholmes.train.txt",
+ tempDirPath + "/RNN/sherlockholmes.train.txt")
+ Util.downloadUrl(baseUrl + "sherlockholmes.valid.txt",
+ tempDirPath + "/RNN/sherlockholmes.valid.txt")
+ // TODO: Need to confirm with Windows
+ Process(s"unzip $tempDirPath/RNN/obama.zip -d $tempDirPath/RNN/") !
+
+ val args = Array(
+ "--example", "CharRnn",
+ "--count", "1",
+ "--data-path", s"$tempDirPath/RNN/obama.txt",
+ "--model-prefix", s"$tempDirPath/RNN/obama",
+ "--starter-sentence", "The joke"
+ )
+ ScalaInferenceBenchmark.main(args)
+ }
+
+}
diff --git a/scala-package/examples/src/test/scala/org/apache/mxnetexamples/imclassification/MNISTExampleSuite.scala b/scala-package/examples/src/test/scala/org/apache/mxnetexamples/imclassification/IMClassificationExampleSuite.scala
similarity index 53%
rename from scala-package/examples/src/test/scala/org/apache/mxnetexamples/imclassification/MNISTExampleSuite.scala
rename to scala-package/examples/src/test/scala/org/apache/mxnetexamples/imclassification/IMClassificationExampleSuite.scala
index 0fd3af02d9cf..6e9667abe9c0 100644
--- a/scala-package/examples/src/test/scala/org/apache/mxnetexamples/imclassification/MNISTExampleSuite.scala
+++ b/scala-package/examples/src/test/scala/org/apache/mxnetexamples/imclassification/IMClassificationExampleSuite.scala
@@ -18,9 +18,7 @@
package org.apache.mxnetexamples.imclassification
import java.io.File
-import java.net.URL
-import org.apache.commons.io.FileUtils
import org.apache.mxnet.Context
import org.apache.mxnetexamples.Util
import org.scalatest.{BeforeAndAfterAll, FunSuite}
@@ -31,27 +29,35 @@ import scala.sys.process.Process
/**
* Integration test for MNIST example.
*/
-class MNISTExampleSuite extends FunSuite with BeforeAndAfterAll {
- private val logger = LoggerFactory.getLogger(classOf[MNISTExampleSuite])
+class IMClassificationExampleSuite extends FunSuite with BeforeAndAfterAll {
+ private val logger = LoggerFactory.getLogger(classOf[IMClassificationExampleSuite])
test("Example CI: Test MNIST Training") {
- logger.info("Downloading mnist model")
- val baseUrl = "https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci"
- val tempDirPath = System.getProperty("java.io.tmpdir")
- val modelDirPath = tempDirPath + File.separator + "mnist/"
- logger.info("tempDirPath: %s".format(tempDirPath))
- Util.downloadUrl(baseUrl + "/mnist/mnist.zip",
- tempDirPath + "/mnist/mnist.zip")
- // TODO: Need to confirm with Windows
- Process("unzip " + tempDirPath + "/mnist/mnist.zip -d "
- + tempDirPath + "/mnist/") !
+ logger.info("Downloading mnist model")
+ val baseUrl = "https://s3.us-east-2.amazonaws.com/mxnet-scala/scala-example-ci"
+ val tempDirPath = System.getProperty("java.io.tmpdir")
+ val modelDirPath = tempDirPath + File.separator + "mnist/"
+ logger.info("tempDirPath: %s".format(tempDirPath))
+ Util.downloadUrl(baseUrl + "/mnist/mnist.zip",
+ tempDirPath + "/mnist/mnist.zip")
+ // TODO: Need to confirm with Windows
+ Process("unzip " + tempDirPath + "/mnist/mnist.zip -d "
+ + tempDirPath + "/mnist/") !
- var context = Context.cpu()
+ var context = Context.cpu()
- val output = TrainMnist.test(modelDirPath)
- Process("rm -rf " + modelDirPath) !
+ val valAccuracy = TrainModel.test("mlp", modelDirPath)
+ Process("rm -rf " + modelDirPath) !
- assert(output >= 0.95f)
+ assert(valAccuracy >= 0.95f)
}
+
+ for(model <- List("mlp", "lenet", "resnet")) {
+ test(s"Example CI: Test Image Classification Model ${model}") {
+ var context = Context.cpu()
+ val valAccuracy = TrainModel.test(model, "", 10, 1, benchmark = true)
+ }
+ }
+
}
diff --git a/scala-package/examples/src/test/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExampleSuite.scala b/scala-package/examples/src/test/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExampleSuite.scala
index 34d3bc97a005..d8631df54052 100644
--- a/scala-package/examples/src/test/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExampleSuite.scala
+++ b/scala-package/examples/src/test/scala/org/apache/mxnetexamples/infer/imageclassifier/ImageClassifierExampleSuite.scala
@@ -64,7 +64,7 @@ class ImageClassifierExampleSuite extends FunSuite with BeforeAndAfterAll {
}
val output = ImageClassifierExample.runInferenceOnSingleImage(modelDirPath + "resnet-18",
- inputImagePath, context)
+ inputImagePath, context)
val outputList = ImageClassifierExample.runInferenceOnBatchOfImage(modelDirPath + "resnet-18",
inputImageDir, context)
diff --git a/scala-package/infer/pom.xml b/scala-package/infer/pom.xml
index e50100169328..91a1e1b30d2f 100644
--- a/scala-package/infer/pom.xml
+++ b/scala-package/infer/pom.xml
@@ -13,6 +13,10 @@
mxnet-infer_2.11
MXNet Scala Package - Inference
+
+ true
+
+
unittest
@@ -20,12 +24,6 @@
false
-
- integrationtest
-
- true
-
-
osx-x86_64-cpu
diff --git a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ImageClassifier.scala b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ImageClassifier.scala
index 8d31d1f6b3d6..96be12179d42 100644
--- a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ImageClassifier.scala
+++ b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ImageClassifier.scala
@@ -76,13 +76,16 @@ class ImageClassifier(modelPathPrefix: String,
topK: Option[Int] = None): IndexedSeq[IndexedSeq[(String, Float)]] = {
val scaledImage = ImageClassifier.reshapeImage(inputImage, width, height)
- val pixelsNDArray = ImageClassifier.bufferedImageToPixels(scaledImage, inputShape)
+ val imageShape = inputShape.drop(1)
+ val pixelsNDArray = ImageClassifier.bufferedImageToPixels(scaledImage, imageShape)
+ val imgWithBatchNum = NDArray.api.expand_dims(pixelsNDArray, 0)
inputImage.flush()
scaledImage.flush()
+ handler.execute(pixelsNDArray.dispose())
- val output = super.classifyWithNDArray(IndexedSeq(pixelsNDArray), topK)
+ val output = super.classifyWithNDArray(IndexedSeq(imgWithBatchNum), topK)
- handler.execute(pixelsNDArray.dispose())
+ handler.execute(imgWithBatchNum.dispose())
IndexedSeq(output(0))
}
@@ -97,14 +100,16 @@ class ImageClassifier(modelPathPrefix: String,
def classifyImageBatch(inputBatch: Traversable[BufferedImage], topK: Option[Int] = None):
IndexedSeq[IndexedSeq[(String, Float)]] = {
- val imageBatch = ListBuffer[NDArray]()
- for (image <- inputBatch) {
- val scaledImage = ImageClassifier.reshapeImage(image, width, height)
- val pixelsNDArray = ImageClassifier.bufferedImageToPixels(scaledImage, inputShape)
- imageBatch += pixelsNDArray
- }
+ val inputBatchSeq = inputBatch.toIndexedSeq
+ val imageBatch = inputBatchSeq.indices.par.map(idx => {
+ val scaledImage = ImageClassifier.reshapeImage(inputBatchSeq(idx), width, height)
+ val imageShape = inputShape.drop(1)
+ val imgND = ImageClassifier.bufferedImageToPixels(scaledImage, imageShape)
+ val imgWithBatch = NDArray.api.expand_dims(imgND, 0).get
+ handler.execute(imgND.dispose())
+ imgWithBatch
+ }).toList
val op = NDArray.concatenate(imageBatch)
-
val result = super.classifyWithNDArray(IndexedSeq(op), topK)
handler.execute(op.dispose())
handler.execute(imageBatch.foreach(_.dispose()))
@@ -147,9 +152,9 @@ object ImageClassifier {
* returned by this method after the use.
*
* @param resizedImage BufferedImage to get pixels from
- * @param inputImageShape Input shape; for example for resnet it is (1,3,224,224).
+ * @param inputImageShape Input shape; for example for resnet it is (3,224,224).
Should be same as inputDescriptor shape.
- * @return NDArray pixels array
+ * @return NDArray pixels array with shape (3, 224, 224) in CHW format
*/
def bufferedImageToPixels(resizedImage: BufferedImage, inputImageShape: Shape): NDArray = {
// Get height and width of the image
diff --git a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ObjectDetector.scala b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ObjectDetector.scala
index ce84f53f2322..a9b21f8c1dcd 100644
--- a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ObjectDetector.scala
+++ b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/ObjectDetector.scala
@@ -72,9 +72,12 @@ class ObjectDetector(modelPathPrefix: String,
: IndexedSeq[IndexedSeq[(String, Array[Float])]] = {
val scaledImage = ImageClassifier.reshapeImage(inputImage, width, height)
- val pixelsNDArray = ImageClassifier.bufferedImageToPixels(scaledImage, inputShape)
- val output = objectDetectWithNDArray(IndexedSeq(pixelsNDArray), topK)
+ val imageShape = inputShape.drop(1)
+ val pixelsNDArray = ImageClassifier.bufferedImageToPixels(scaledImage, imageShape)
+ val pixelsNDWithBatch = NDArray.api.expand_dims(pixelsNDArray, 0)
handler.execute(pixelsNDArray.dispose())
+ val output = objectDetectWithNDArray(IndexedSeq(pixelsNDWithBatch), topK)
+ handler.execute(pixelsNDWithBatch.dispose())
output
}
@@ -147,13 +150,16 @@ class ObjectDetector(modelPathPrefix: String,
def imageBatchObjectDetect(inputBatch: Traversable[BufferedImage], topK: Option[Int] = None):
IndexedSeq[IndexedSeq[(String, Array[Float])]] = {
- val imageBatch = ListBuffer[NDArray]()
- for (image <- inputBatch) {
- val scaledImage = ImageClassifier.reshapeImage(image, width, height)
- val pixelsNdarray = ImageClassifier.bufferedImageToPixels(scaledImage, inputShape)
- imageBatch += pixelsNdarray
- }
- val op = NDArray.concatenate(imageBatch)
+ val inputBatchSeq = inputBatch.toIndexedSeq
+ val imageBatch = inputBatchSeq.indices.par.map(idx => {
+ val scaledImage = ImageClassifier.reshapeImage(inputBatchSeq(idx), width, height)
+ val imageShape = inputShape.drop(1)
+ val pixelsND = ImageClassifier.bufferedImageToPixels(scaledImage, imageShape)
+ val pixelsNDWithBatch = NDArray.api.expand_dims(pixelsND, 0).get
+ handler.execute(pixelsND.dispose())
+ pixelsNDWithBatch
+ })
+ val op = NDArray.concatenate(imageBatch.toList)
val result = objectDetectWithNDArray(IndexedSeq(op), topK)
handler.execute(op.dispose())
diff --git a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/Predictor.scala b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/Predictor.scala
index 3987c648b2ab..e2a0e7ca2893 100644
--- a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/Predictor.scala
+++ b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/Predictor.scala
@@ -181,6 +181,7 @@ class Predictor(modelPathPrefix: String,
// rebind with the new batchSize
if (batchSize != inputBatchSize) {
+ logger.info(s"Latency increased due to batchSize mismatch $batchSize vs $inputBatchSize")
val desc = iDescriptors.map((f : DataDesc) => new DataDesc(f.name,
Shape(f.shape.toVector.patch(batchIndex, Vector(inputBatchSize), 1)), f.dtype, f.layout) )
mxNetHandler.execute(mod.bind(desc, forceRebind = true,
diff --git a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/ObjectDetector.scala b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/ObjectDetector.scala
new file mode 100644
index 000000000000..08fffb410adf
--- /dev/null
+++ b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/ObjectDetector.scala
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.infer.javaapi
+
+// scalastyle:off
+import java.awt.image.BufferedImage
+// scalastyle:on
+
+import org.apache.mxnet.javaapi.{Context, DataDesc, NDArray, Shape}
+
+import scala.collection.JavaConverters
+import scala.collection.JavaConverters._
+
+/**
+ * The ObjectDetector class helps to run ObjectDetection tasks where the goal
+ * is to find bounding boxes and corresponding labels for objects in a image.
+ *
+ * @param modelPathPrefix Path prefix from where to load the model artifacts.
+ * These include the symbol, parameters, and synset.txt.
+ * Example: file://model-dir/ssd_resnet50_512 (containing
+ * ssd_resnet50_512-symbol.json, ssd_resnet50_512-0000.params,
+ * and synset.txt)
+ * @param inputDescriptors Descriptors defining the input node names, shape,
+ * layout and type parameters
+ * @param contexts Device contexts on which you want to run inference.
+ * Defaults to CPU.
+ * @param epoch Model epoch to load; defaults to 0
+ */
+class ObjectDetector private[mxnet] (val objDetector: org.apache.mxnet.infer.ObjectDetector){
+
+ def this(modelPathPrefix: String, inputDescriptors: java.util.List[DataDesc], contexts:
+ java.util.List[Context], epoch: Int)
+ = this {
+ val informationDesc = JavaConverters.asScalaIteratorConverter(inputDescriptors.iterator)
+ .asScala.toIndexedSeq map {a => a: org.apache.mxnet.DataDesc}
+ val inContexts = (contexts.asScala.toList map {a => a: org.apache.mxnet.Context}).toArray
+ // scalastyle:off
+ new org.apache.mxnet.infer.ObjectDetector(modelPathPrefix, informationDesc, inContexts, Some(epoch))
+ // scalastyle:on
+ }
+
+ /**
+ * Detects objects and returns bounding boxes with corresponding class/label
+ *
+ * @param inputImage Path prefix of the input image
+ * @param topK Number of result elements to return, sorted by probability
+ * @return List of list of tuples of
+ * (class, [probability, xmin, ymin, xmax, ymax])
+ */
+ def imageObjectDetect(inputImage: BufferedImage, topK: Int):
+ java.util.List[java.util.List[ObjectDetectorOutput]] = {
+ val ret = objDetector.imageObjectDetect(inputImage, Some(topK))
+ (ret map {a => (a map {e => new ObjectDetectorOutput(e._1, e._2)}).asJava}).asJava
+ }
+
+ /**
+ * Takes input images as NDArrays. Useful when you want to perform multiple operations on
+ * the input array, or when you want to pass a batch of input images.
+ *
+ * @param input Indexed Sequence of NDArrays
+ * @param topK (Optional) How many top_k (sorting will be based on the last axis)
+ * elements to return. If not passed, returns all unsorted output.
+ * @return List of list of tuples of
+ * (class, [probability, xmin, ymin, xmax, ymax])
+ */
+ def objectDetectWithNDArray(input: java.util.List[NDArray], topK: Int):
+ java.util.List[java.util.List[ObjectDetectorOutput]] = {
+ val ret = objDetector.objectDetectWithNDArray(convert(input.asScala.toIndexedSeq), Some(topK))
+ (ret map {a => (a map {e => new ObjectDetectorOutput(e._1, e._2)}).asJava}).asJava
+ }
+
+ /**
+ * To classify batch of input images according to the provided model
+ *
+ * @param inputBatch Input array of buffered images
+ * @param topK Number of result elements to return, sorted by probability
+ * @return List of list of tuples of (class, probability)
+ */
+ def imageBatchObjectDetect(inputBatch: java.util.List[BufferedImage], topK: Int):
+ java.util.List[java.util.List[ObjectDetectorOutput]] = {
+ val ret = objDetector.imageBatchObjectDetect(inputBatch.asScala, Some(topK))
+ (ret map {a => (a map {e => new ObjectDetectorOutput(e._1, e._2)}).asJava}).asJava
+ }
+
+ def convert[B, A <% B](l: IndexedSeq[A]): IndexedSeq[B] = l map { a => a: B }
+
+}
+
+
+object ObjectDetector {
+ implicit def fromObjectDetector(OD: org.apache.mxnet.infer.ObjectDetector):
+ ObjectDetector = new ObjectDetector(OD)
+
+ implicit def toObjectDetector(jOD: ObjectDetector):
+ org.apache.mxnet.infer.ObjectDetector = jOD.objDetector
+
+ def loadImageFromFile(inputImagePath: String): BufferedImage = {
+ org.apache.mxnet.infer.ImageClassifier.loadImageFromFile(inputImagePath)
+ }
+
+ def reshapeImage(img : BufferedImage, newWidth: Int, newHeight: Int): BufferedImage = {
+ org.apache.mxnet.infer.ImageClassifier.reshapeImage(img, newWidth, newHeight)
+ }
+
+ def bufferedImageToPixels(resizedImage: BufferedImage, inputImageShape: Shape): NDArray = {
+ org.apache.mxnet.infer.ImageClassifier.bufferedImageToPixels(resizedImage, inputImageShape)
+ }
+
+ def loadInputBatch(inputImagePaths: java.util.List[String]): java.util.List[BufferedImage] = {
+ org.apache.mxnet.infer.ImageClassifier
+ .loadInputBatch(inputImagePaths.asScala.toList).toList.asJava
+ }
+}
diff --git a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/ObjectDetectorOutput.scala b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/ObjectDetectorOutput.scala
new file mode 100644
index 000000000000..13369c8fcef5
--- /dev/null
+++ b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/ObjectDetectorOutput.scala
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.infer.javaapi
+
+class ObjectDetectorOutput (className: String, args: Array[Float]){
+
+ def getClassName: String = className
+
+ def getProbability: Float = args(0)
+
+ def getXMin: Float = args(1)
+
+ def getXMax: Float = args(2)
+
+ def getYMin: Float = args(3)
+
+ def getYMax: Float = args(4)
+
+}
diff --git a/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/Predictor.scala b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/Predictor.scala
new file mode 100644
index 000000000000..a5428e1c8219
--- /dev/null
+++ b/scala-package/infer/src/main/scala/org/apache/mxnet/infer/javaapi/Predictor.scala
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.infer.javaapi
+
+import org.apache.mxnet.javaapi.{Context, DataDesc, NDArray}
+
+import scala.collection.JavaConverters
+import scala.collection.JavaConverters._
+
+/**
+ * Implementation of prediction routines.
+ *
+ * @param modelPathPrefix Path prefix from where to load the model artifacts.
+ * These include the symbol, parameters, and synset.txt
+ * Example: file://model-dir/resnet-152 (containing
+ * resnet-152-symbol.json, resnet-152-0000.params, and synset.txt).
+ * @param inputDescriptors Descriptors defining the input node names, shape,
+ * layout and type parameters
+ * Note: If the input Descriptors is missing batchSize
+ * ('N' in layout), a batchSize of 1 is assumed for the model.
+ * @param contexts Device contexts on which you want to run inference; defaults to CPU
+ * @param epoch Model epoch to load; defaults to 0
+
+ */
+
+// JavaDoc description of class to be updated in https://issues.apache.org/jira/browse/MXNET-1178
+class Predictor private[mxnet] (val predictor: org.apache.mxnet.infer.Predictor){
+ def this(modelPathPrefix: String, inputDescriptors: java.util.List[DataDesc],
+ contexts: java.util.List[Context], epoch: Int)
+ = this {
+ val informationDesc = JavaConverters.asScalaIteratorConverter(inputDescriptors.iterator)
+ .asScala.toIndexedSeq map {a => a: org.apache.mxnet.DataDesc}
+ val inContexts = (contexts.asScala.toList map {a => a: org.apache.mxnet.Context}).toArray
+ new org.apache.mxnet.infer.Predictor(modelPathPrefix, informationDesc, inContexts, Some(epoch))
+ }
+
+ /**
+ * Takes input as Array of one dimensional arrays and creates the NDArray needed for inference
+ * The array will be reshaped based on the input descriptors.
+ *
+ * @param input: An Array of a one-dimensional array.
+ An extra Array is needed for when the model has more than one input.
+ * @return Indexed sequence array of outputs
+ */
+ def predict(input: Array[Array[Float]]):
+ Array[Array[Float]] = {
+ predictor.predict(input).toArray
+ }
+
+ /**
+ * Takes input as List of one dimensional arrays and creates the NDArray needed for inference
+ * The array will be reshaped based on the input descriptors.
+ *
+ * @param input: A List of a one-dimensional array.
+ An extra List is needed for when the model has more than one input.
+ * @return Indexed sequence array of outputs
+ */
+ def predict(input: java.util.List[java.util.List[Float]]):
+ java.util.List[java.util.List[Float]] = {
+ val in = JavaConverters.asScalaIteratorConverter(input.iterator).asScala.toIndexedSeq
+ (predictor.predict(in map {a => a.asScala.toArray}) map {b => b.toList.asJava}).asJava
+ }
+
+
+
+ /**
+ * Predict using NDArray as input
+ * This method is useful when the input is a batch of data
+ * Note: User is responsible for managing allocation/deallocation of input/output NDArrays.
+ *
+ * @param input List of NDArrays
+ * @return Output of predictions as NDArrays
+ */
+ def predictWithNDArray(input: java.util.List[NDArray]):
+ java.util.List[NDArray] = {
+ val ret = predictor.predictWithNDArray(convert(JavaConverters
+ .asScalaIteratorConverter(input.iterator).asScala.toIndexedSeq))
+ // TODO: For some reason the implicit wasn't working here when trying to use convert.
+ // So did it this way. Needs to be figured out
+ (ret map {a => new NDArray(a)}).asJava
+ }
+
+ private def convert[B, A <% B](l: IndexedSeq[A]): IndexedSeq[B] = l map { a => a: B }
+}
diff --git a/scala-package/infer/src/test/scala/org/apache/mxnet/infer/ImageClassifierSuite.scala b/scala-package/infer/src/test/scala/org/apache/mxnet/infer/ImageClassifierSuite.scala
index 948764ee8044..1c291e1e7b3c 100644
--- a/scala-package/infer/src/test/scala/org/apache/mxnet/infer/ImageClassifierSuite.scala
+++ b/scala-package/infer/src/test/scala/org/apache/mxnet/infer/ImageClassifierSuite.scala
@@ -65,9 +65,9 @@ class ImageClassifierSuite extends ClassifierSuite with BeforeAndAfterAll {
val image1 = new BufferedImage(100, 200, BufferedImage.TYPE_BYTE_GRAY)
val image2 = ImageClassifier.reshapeImage(image1, 2, 2)
- val result = ImageClassifier.bufferedImageToPixels(image2, Shape(1, 3, 2, 2))
+ val result = ImageClassifier.bufferedImageToPixels(image2, Shape(3, 2, 2))
- assert(result.shape == inputDescriptor(0).shape)
+ assert(result.shape == inputDescriptor(0).shape.drop(1))
}
test("ImageClassifierSuite-testWithInputImage") {
diff --git a/scala-package/macros/src/main/scala/org/apache/mxnet/APIDocGenerator.scala b/scala-package/macros/src/main/scala/org/apache/mxnet/APIDocGenerator.scala
index b4efa659443c..ce12dc7cd5a0 100644
--- a/scala-package/macros/src/main/scala/org/apache/mxnet/APIDocGenerator.scala
+++ b/scala-package/macros/src/main/scala/org/apache/mxnet/APIDocGenerator.scala
@@ -17,178 +17,247 @@
package org.apache.mxnet
-import org.apache.mxnet.init.Base._
-import org.apache.mxnet.utils.CToScalaUtils
import java.io._
import java.security.MessageDigest
-import scala.collection.mutable.{ArrayBuffer, ListBuffer}
+import scala.collection.mutable.ListBuffer
/**
* This object will generate the Scala documentation of the new Scala API
* Two file namely: SymbolAPIBase.scala and NDArrayAPIBase.scala
* The code will be executed during Macros stage and file live in Core stage
*/
-private[mxnet] object APIDocGenerator{
- case class absClassArg(argName : String, argType : String, argDesc : String, isOptional : Boolean)
- case class absClassFunction(name : String, desc : String,
- listOfArgs: List[absClassArg], returnType : String)
+private[mxnet] object APIDocGenerator extends GeneratorBase {
-
- def main(args: Array[String]) : Unit = {
+ def main(args: Array[String]): Unit = {
val FILE_PATH = args(0)
val hashCollector = ListBuffer[String]()
- hashCollector += absClassGen(FILE_PATH, true)
- hashCollector += absClassGen(FILE_PATH, false)
+ hashCollector += typeSafeClassGen(FILE_PATH, true)
+ hashCollector += typeSafeClassGen(FILE_PATH, false)
hashCollector += nonTypeSafeClassGen(FILE_PATH, true)
hashCollector += nonTypeSafeClassGen(FILE_PATH, false)
+ hashCollector += javaClassGen(FILE_PATH)
val finalHash = hashCollector.mkString("\n")
}
- def MD5Generator(input : String) : String = {
+ def MD5Generator(input: String): String = {
val md = MessageDigest.getInstance("MD5")
md.update(input.getBytes("UTF-8"))
val digest = md.digest()
org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(digest)
}
- def absClassGen(FILE_PATH : String, isSymbol : Boolean) : String = {
- // scalastyle:off
- val absClassFunctions = getSymbolNDArrayMethods(isSymbol)
- // Defines Operators that should not generated
- val notGenerated = Set("Custom")
- // TODO: Add Filter to the same location in case of refactor
- val absFuncs = absClassFunctions.filterNot(_.name.startsWith("_"))
- .filterNot(ele => notGenerated.contains(ele.name))
- .map(absClassFunction => {
- val scalaDoc = generateAPIDocFromBackend(absClassFunction)
- val defBody = generateAPISignature(absClassFunction, isSymbol)
- s"$scalaDoc\n$defBody"
- })
- val packageName = if (isSymbol) "SymbolAPIBase" else "NDArrayAPIBase"
- val apacheLicence = "/*\n* Licensed to the Apache Software Foundation (ASF) under one or more\n* contributor license agreements. See the NOTICE file distributed with\n* this work for additional information regarding copyright ownership.\n* The ASF licenses this file to You under the Apache License, Version 2.0\n* (the \"License\"); you may not use this file except in compliance with\n* the License. You may obtain a copy of the License at\n*\n* http://www.apache.org/licenses/LICENSE-2.0\n*\n* Unless required by applicable law or agreed to in writing, software\n* distributed under the License is distributed on an \"AS IS\" BASIS,\n* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n* See the License for the specific language governing permissions and\n* limitations under the License.\n*/\n"
- val scalaStyle = "// scalastyle:off"
- val packageDef = "package org.apache.mxnet"
- val imports = "import org.apache.mxnet.annotation.Experimental"
- val absClassDef = s"abstract class $packageName"
- val finalStr = s"$apacheLicence\n$scalaStyle\n$packageDef\n$imports\n$absClassDef {\n${absFuncs.mkString("\n")}\n}"
- val pw = new PrintWriter(new File(FILE_PATH + s"$packageName.scala"))
- pw.write(finalStr)
- pw.close()
- MD5Generator(finalStr)
+ def typeSafeClassGen(FILE_PATH: String, isSymbol: Boolean): String = {
+ val generated = typeSafeFunctionsToGenerate(isSymbol, isContrib = false)
+ .map { func =>
+ val scalaDoc = generateAPIDocFromBackend(func)
+ val decl = generateAPISignature(func, isSymbol)
+ s"$scalaDoc\n$decl"
+ }
+
+ writeFile(
+ FILE_PATH,
+ if (isSymbol) "SymbolAPIBase" else "NDArrayAPIBase",
+ "package org.apache.mxnet",
+ generated)
}
- def nonTypeSafeClassGen(FILE_PATH : String, isSymbol : Boolean) : String = {
- // scalastyle:off
- val absClassFunctions = getSymbolNDArrayMethods(isSymbol)
- val absFuncs = absClassFunctions.map(absClassFunction => {
- val scalaDoc = generateAPIDocFromBackend(absClassFunction, false)
- if (isSymbol) {
- val defBody = s"def ${absClassFunction.name}(name : String = null, attr : Map[String, String] = null)(args : org.apache.mxnet.Symbol*)(kwargs : Map[String, Any] = null): org.apache.mxnet.Symbol"
- s"$scalaDoc\n$defBody"
- } else {
- val defBodyWithKwargs = s"def ${absClassFunction.name}(kwargs: Map[String, Any] = null)(args: Any*) : org.apache.mxnet.NDArrayFuncReturn"
- val defBody = s"def ${absClassFunction.name}(args: Any*) : org.apache.mxnet.NDArrayFuncReturn"
- s"$scalaDoc\n$defBodyWithKwargs\n$scalaDoc\n$defBody"
+ def nonTypeSafeClassGen(FILE_PATH: String, isSymbol: Boolean): String = {
+ val absFuncs = functionsToGenerate(isSymbol, isContrib = false)
+ .map { func =>
+ val scalaDoc = generateAPIDocFromBackend(func, false)
+ if (isSymbol) {
+ s"""$scalaDoc
+ |def ${func.name}(name : String = null, attr : Map[String, String] = null)
+ | (args : org.apache.mxnet.Symbol*)(kwargs : Map[String, Any] = null):
+ | org.apache.mxnet.Symbol
+ """.stripMargin
+ } else {
+ s"""$scalaDoc
+ |def ${func.name}(kwargs: Map[String, Any] = null)
+ | (args: Any*): org.apache.mxnet.NDArrayFuncReturn
+ |
+ |$scalaDoc
+ |def ${func.name}(args: Any*): org.apache.mxnet.NDArrayFuncReturn
+ """.stripMargin
+ }
}
- })
- val packageName = if (isSymbol) "SymbolBase" else "NDArrayBase"
- val apacheLicence = "/*\n* Licensed to the Apache Software Foundation (ASF) under one or more\n* contributor license agreements. See the NOTICE file distributed with\n* this work for additional information regarding copyright ownership.\n* The ASF licenses this file to You under the Apache License, Version 2.0\n* (the \"License\"); you may not use this file except in compliance with\n* the License. You may obtain a copy of the License at\n*\n* http://www.apache.org/licenses/LICENSE-2.0\n*\n* Unless required by applicable law or agreed to in writing, software\n* distributed under the License is distributed on an \"AS IS\" BASIS,\n* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n* See the License for the specific language governing permissions and\n* limitations under the License.\n*/\n"
- val scalaStyle = "// scalastyle:off"
- val packageDef = "package org.apache.mxnet"
- val imports = "import org.apache.mxnet.annotation.Experimental"
- val absClassDef = s"abstract class $packageName"
- val finalStr = s"$apacheLicence\n$scalaStyle\n$packageDef\n$imports\n$absClassDef {\n${absFuncs.mkString("\n")}\n}"
- import java.io._
- val pw = new PrintWriter(new File(FILE_PATH + s"$packageName.scala"))
- pw.write(finalStr)
- pw.close()
- MD5Generator(finalStr)
+
+ writeFile(
+ FILE_PATH,
+ if (isSymbol) "SymbolBase" else "NDArrayBase",
+ "package org.apache.mxnet",
+ absFuncs)
}
- // Generate ScalaDoc type
- def generateAPIDocFromBackend(func : absClassFunction, withParam : Boolean = true) : String = {
- val desc = ArrayBuffer[String]()
- desc += " *
"
- func.desc.split("\n").foreach({ currStr =>
- desc += s" * $currStr"
- })
- desc += " * "
- val params = func.listOfArgs.map({ absClassArg =>
- val currArgName = absClassArg.argName match {
- case "var" => "vari"
- case "type" => "typeOf"
- case _ => absClassArg.argName
+ def javaClassGen(filePath : String) : String = {
+ val notGenerated = Set("Custom")
+ val absClassFunctions = functionsToGenerate(false, false, true)
+ val absFuncs = absClassFunctions.filterNot(ele => notGenerated.contains(ele.name))
+ .groupBy(_.name.toLowerCase).map(ele => {
+ /* Pattern matching for not generating deprecated method
+ * Group all method name in lowercase
+ * Kill the capital lettered method such as Cast vs cast
+ * As it defined by default it deprecated
+ */
+ if (ele._2.length == 1) ele._2.head
+ else {
+ if (ele._2.head.name.head.isLower) ele._2.head
+ else ele._2.last
}
- s" * @param $currArgName\t\t${absClassArg.argDesc}"
- })
+ }).map(absClassFunction => {
+ generateJavaAPISignature(absClassFunction)
+ }).toSeq
+ val packageName = "NDArrayBase"
+ val packageDef = "package org.apache.mxnet.javaapi"
+ writeFile(filePath + "javaapi/", packageName, packageDef, absFuncs)
+ }
+
+ def generateAPIDocFromBackend(func: Func, withParam: Boolean = true): String = {
+ def fixDesc(desc: String): String = {
+ var curDesc = desc
+ var prevDesc = ""
+ while ( curDesc != prevDesc ) {
+ prevDesc = curDesc
+ curDesc = curDesc.replace("[[", "`[ [").replace("]]", "] ]")
+ }
+ curDesc
+ }
+ val desc = fixDesc(func.desc).split("\n")
+ .mkString(" *\n * {{{\n *\n * ", "\n * ", "\n * }}}\n * ")
+
+ val params = func.listOfArgs.map { absClassArg =>
+ s" * @param ${absClassArg.safeArgName}\t\t${fixDesc(absClassArg.argDesc)}"
+ }
+
val returnType = s" * @return ${func.returnType}"
+
if (withParam) {
- s" /**\n${desc.mkString("\n")}\n${params.mkString("\n")}\n$returnType\n */"
+ s""" /**
+ |$desc
+ |${params.mkString("\n")}
+ |$returnType
+ | */""".stripMargin
+ } else {
+ s""" /**
+ |$desc
+ |$returnType
+ | */""".stripMargin
+ }
+ }
+
+ def generateAPISignature(func: Func, isSymbol: Boolean): String = {
+ val argDef = ListBuffer[String]()
+
+ argDef ++= typedFunctionCommonArgDef(func)
+
+ if (isSymbol) {
+ argDef += "name : String = null"
+ argDef += "attr : Map[String, String] = null"
} else {
- s" /**\n${desc.mkString("\n")}\n$returnType\n */"
+ argDef += "out : Option[NDArray] = None"
+
}
+
+ val returnType = func.returnType
+
+ s"""@Experimental
+ |def ${func.name} (${argDef.mkString(", ")}): $returnType""".stripMargin
}
- def generateAPISignature(func : absClassFunction, isSymbol : Boolean) : String = {
+ def generateJavaAPISignature(func : Func) : String = {
+ val useParamObject = func.listOfArgs.count(arg => arg.isOptional) >= 2
var argDef = ListBuffer[String]()
+ var classDef = ListBuffer[String]()
+ var requiredParam = ListBuffer[String]()
func.listOfArgs.foreach(absClassArg => {
- val currArgName = absClassArg.argName match {
- case "var" => "vari"
- case "type" => "typeOf"
- case _ => absClassArg.argName
- }
- if (absClassArg.isOptional) {
- argDef += s"$currArgName : Option[${absClassArg.argType}] = None"
+ val currArgName = absClassArg.safeArgName
+ // scalastyle:off
+ if (absClassArg.isOptional && useParamObject) {
+ classDef +=
+ s"""private var $currArgName: ${absClassArg.argType} = null
+ |/**
+ | * @param $currArgName\t\t${absClassArg.argDesc}
+ | */
+ |def set${currArgName.capitalize}($currArgName : ${absClassArg.argType}): ${func.name}Param = {
+ | this.$currArgName = $currArgName
+ | this
+ | }""".stripMargin
}
else {
+ requiredParam += s" * @param $currArgName\t\t${absClassArg.argDesc}"
argDef += s"$currArgName : ${absClassArg.argType}"
}
+ classDef += s"def get${currArgName.capitalize}() = this.$currArgName"
+ // scalastyle:on
})
- var returnType = func.returnType
- if (isSymbol) {
- argDef += "name : String = null"
- argDef += "attr : Map[String, String] = null"
+ val experimentalTag = "@Experimental"
+ val returnType = "Array[NDArray]"
+ val scalaDoc = generateAPIDocFromBackend(func)
+ val scalaDocNoParam = generateAPIDocFromBackend(func, false)
+ if(useParamObject) {
+ classDef +=
+ s"""private var out : org.apache.mxnet.NDArray = null
+ |def setOut(out : NDArray) : ${func.name}Param = {
+ | this.out = out
+ | this
+ | }
+ | def getOut() = this.out
+ | """.stripMargin
+ s"""$scalaDocNoParam
+ | $experimentalTag
+ | def ${func.name}(po: ${func.name}Param) : $returnType
+ | /**
+ | * This Param Object is specifically used for ${func.name}
+ | ${requiredParam.mkString("\n")}
+ | */
+ | class ${func.name}Param(${argDef.mkString(",")}) {
+ | ${classDef.mkString("\n ")}
+ | }""".stripMargin
} else {
- argDef += "out : Option[NDArray] = None"
- returnType = "org.apache.mxnet.NDArrayFuncReturn"
+ argDef += "out : NDArray"
+ s"""$scalaDoc
+ |$experimentalTag
+ | def ${func.name}(${argDef.mkString(", ")}) : $returnType
+ | """.stripMargin
}
- val experimentalTag = "@Experimental"
- s"$experimentalTag\ndef ${func.name} (${argDef.mkString(", ")}) : $returnType"
}
+ def writeFile(FILE_PATH: String, className: String, packageDef: String,
+ absFuncs: Seq[String]): String = {
- // List and add all the atomic symbol functions to current module.
- private def getSymbolNDArrayMethods(isSymbol : Boolean): List[absClassFunction] = {
- val opNames = ListBuffer.empty[String]
- val returnType = if (isSymbol) "Symbol" else "NDArray"
- _LIB.mxListAllOpNames(opNames)
- // TODO: Add '_linalg_', '_sparse_', '_image_' support
- // TODO: Add Filter to the same location in case of refactor
- opNames.map(opName => {
- val opHandle = new RefLong
- _LIB.nnGetOpHandle(opName, opHandle)
- makeAtomicSymbolFunction(opHandle.value, opName, "org.apache.mxnet." + returnType)
- }).toList.filterNot(_.name.startsWith("_"))
- }
+ val finalStr =
+ s"""/*
+ |* Licensed to the Apache Software Foundation (ASF) under one or more
+ |* contributor license agreements. See the NOTICE file distributed with
+ |* this work for additional information regarding copyright ownership.
+ |* The ASF licenses this file to You under the Apache License, Version 2.0
+ |* (the "License"); you may not use this file except in compliance with
+ |* the License. You may obtain a copy of the License at
+ |*
+ |* http://www.apache.org/licenses/LICENSE-2.0
+ |*
+ |* Unless required by applicable law or agreed to in writing, software
+ |* distributed under the License is distributed on an "AS IS" BASIS,
+ |* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ |* See the License for the specific language governing permissions and
+ |* limitations under the License.
+ |*/
+ |
+ |$packageDef
+ |
+ |import org.apache.mxnet.annotation.Experimental
+ |
+ |// scalastyle:off
+ |abstract class $className {
+ |${absFuncs.mkString("\n")}
+ |}""".stripMargin
- // Create an atomic symbol function by handle and function name.
- private def makeAtomicSymbolFunction(handle: SymbolHandle, aliasName: String, returnType : String)
- : absClassFunction = {
- val name = new RefString
- val desc = new RefString
- val keyVarNumArgs = new RefString
- val numArgs = new RefInt
- val argNames = ListBuffer.empty[String]
- val argTypes = ListBuffer.empty[String]
- val argDescs = ListBuffer.empty[String]
-
- _LIB.mxSymbolGetAtomicSymbolInfo(
- handle, name, desc, numArgs, argNames, argTypes, argDescs, keyVarNumArgs)
- val argList = argNames zip argTypes zip argDescs map { case ((argName, argType), argDesc) =>
- val typeAndOption = CToScalaUtils.argumentCleaner(argName, argType, returnType)
- new absClassArg(argName, typeAndOption._1, argDesc, typeAndOption._2)
- }
- new absClassFunction(aliasName, desc.value, argList.toList, returnType)
+
+ val pw = new PrintWriter(new File(FILE_PATH + s"$className.scala"))
+ pw.write(finalStr)
+ pw.close()
+ MD5Generator(finalStr)
}
+
}
diff --git a/scala-package/macros/src/main/scala/org/apache/mxnet/GeneratorBase.scala b/scala-package/macros/src/main/scala/org/apache/mxnet/GeneratorBase.scala
new file mode 100644
index 000000000000..9245ef1b437f
--- /dev/null
+++ b/scala-package/macros/src/main/scala/org/apache/mxnet/GeneratorBase.scala
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet
+
+import org.apache.mxnet.init.Base.{RefInt, RefLong, RefString, _LIB}
+import org.apache.mxnet.utils.{CToScalaUtils, OperatorBuildUtils}
+
+import scala.collection.mutable.ListBuffer
+import scala.reflect.macros.blackbox
+
+abstract class GeneratorBase {
+ type Handle = Long
+
+ case class Arg(argName: String, argType: String, argDesc: String, isOptional: Boolean) {
+ def safeArgName: String = argName match {
+ case "var" => "vari"
+ case "type" => "typeOf"
+ case _ => argName
+ }
+ }
+
+ case class Func(name: String, desc: String, listOfArgs: List[Arg], returnType: String)
+
+ def functionsToGenerate(isSymbol: Boolean, isContrib: Boolean,
+ isJava: Boolean = false): List[Func] = {
+ val l = getBackEndFunctions(isSymbol, isJava)
+ if (isContrib) {
+ l.filter(func => func.name.startsWith("_contrib_") || !func.name.startsWith("_"))
+ } else {
+ l.filterNot(_.name.startsWith("_"))
+ }
+ }
+
+ def typeSafeFunctionsToGenerate(isSymbol: Boolean, isContrib: Boolean): List[Func] = {
+ // Operators that should not be generated
+ val notGenerated = Set("Custom")
+
+ val l = getBackEndFunctions(isSymbol)
+ val res = if (isContrib) {
+ l.filter(func => func.name.startsWith("_contrib_") || !func.name.startsWith("_"))
+ } else {
+ l.filterNot(_.name.startsWith("_"))
+ }
+ res.filterNot(ele => notGenerated.contains(ele.name))
+ }
+
+ protected def getBackEndFunctions(isSymbol: Boolean, isJava: Boolean = false): List[Func] = {
+ val opNames = ListBuffer.empty[String]
+ _LIB.mxListAllOpNames(opNames)
+ opNames.map(opName => {
+ val opHandle = new RefLong
+ _LIB.nnGetOpHandle(opName, opHandle)
+ makeAtomicFunction(opHandle.value, opName, isSymbol, isJava)
+ }).toList
+ }
+
+ private def makeAtomicFunction(handle: Handle, aliasName: String,
+ isSymbol: Boolean, isJava: Boolean): Func = {
+ val name = new RefString
+ val desc = new RefString
+ val keyVarNumArgs = new RefString
+ val numArgs = new RefInt
+ val argNames = ListBuffer.empty[String]
+ val argTypes = ListBuffer.empty[String]
+ val argDescs = ListBuffer.empty[String]
+
+ _LIB.mxSymbolGetAtomicSymbolInfo(
+ handle, name, desc, numArgs, argNames, argTypes, argDescs, keyVarNumArgs)
+ val paramStr = OperatorBuildUtils.ctypes2docstring(argNames, argTypes, argDescs)
+ val extraDoc: String = if (keyVarNumArgs.value != null && keyVarNumArgs.value.length > 0) {
+ s"This function support variable length of positional input (${keyVarNumArgs.value})."
+ } else {
+ ""
+ }
+ val realName = if (aliasName == name.value) "" else s"(a.k.a., ${name.value})"
+ val docStr = s"$aliasName $realName\n${desc.value}\n\n$paramStr\n$extraDoc\n"
+
+ val argList = argNames zip argTypes zip argDescs map { case ((argName, argType), argDesc) =>
+ val family = if (isJava) "org.apache.mxnet.javaapi.NDArray"
+ else if (isSymbol) "org.apache.mxnet.Symbol"
+ else "org.apache.mxnet.NDArray"
+ val typeAndOption =
+ CToScalaUtils.argumentCleaner(argName, argType, family)
+ Arg(argName, typeAndOption._1, argDesc, typeAndOption._2)
+ }
+ val returnType =
+ if (isJava) "Array[org.apache.mxnet.javaapi.NDArray]"
+ else if (isSymbol) "org.apache.mxnet.Symbol"
+ else "org.apache.mxnet.NDArrayFuncReturn"
+ Func(aliasName, desc.value, argList.toList, returnType)
+ }
+
+ /**
+ * Generate class structure for all function APIs
+ *
+ * @param c
+ * @param funcDef DefDef type of function definitions
+ * @param annottees
+ * @return
+ */
+ protected def structGeneration(c: blackbox.Context)
+ (funcDef: List[c.universe.DefDef], annottees: c.Expr[Any]*)
+ : c.Expr[Any] = {
+ import c.universe._
+ val inputs = annottees.map(_.tree).toList
+ // pattern match on the inputs
+ val modDefs = inputs map {
+ case ClassDef(mods, name, something, template) =>
+ val q = template match {
+ case Template(superMaybe, emptyValDef, defs) =>
+ Template(superMaybe, emptyValDef, defs ++ funcDef)
+ case ex =>
+ throw new IllegalArgumentException(s"Invalid template: $ex")
+ }
+ ClassDef(mods, name, something, q)
+ case ModuleDef(mods, name, template) =>
+ val q = template match {
+ case Template(superMaybe, emptyValDef, defs) =>
+ Template(superMaybe, emptyValDef, defs ++ funcDef)
+ case ex =>
+ throw new IllegalArgumentException(s"Invalid template: $ex")
+ }
+ ModuleDef(mods, name, q)
+ case ex =>
+ throw new IllegalArgumentException(s"Invalid macro input: $ex")
+ }
+ // wrap the result up in an Expr, and return it
+ val result = c.Expr(Block(modDefs, Literal(Constant())))
+ result
+ }
+
+ protected def typedFunctionCommonArgDef(func: Func): List[String] = {
+ // build function argument definition, with optionality, and safe names
+ func.listOfArgs.map(arg =>
+ if (arg.isOptional) {
+ // let's avoid a stupid Option[Array[...]]
+ if (arg.argType.startsWith("Array[")) {
+ s"${arg.safeArgName} : ${arg.argType} = Array.empty"
+ } else {
+ s"${arg.safeArgName} : Option[${arg.argType}] = None"
+ }
+ }
+ else {
+ s"${arg.safeArgName} : ${arg.argType}"
+ }
+ )
+ }
+}
diff --git a/scala-package/macros/src/main/scala/org/apache/mxnet/NDArrayMacro.scala b/scala-package/macros/src/main/scala/org/apache/mxnet/NDArrayMacro.scala
index 2d3a1c7ec5af..d85abe1ecc4f 100644
--- a/scala-package/macros/src/main/scala/org/apache/mxnet/NDArrayMacro.scala
+++ b/scala-package/macros/src/main/scala/org/apache/mxnet/NDArrayMacro.scala
@@ -17,11 +17,8 @@
package org.apache.mxnet
-import org.apache.mxnet.init.Base._
-import org.apache.mxnet.utils.{CToScalaUtils, OperatorBuildUtils}
-
import scala.annotation.StaticAnnotation
-import scala.collection.mutable.{ArrayBuffer, ListBuffer}
+import scala.collection.mutable.ListBuffer
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@@ -30,207 +27,111 @@ private[mxnet] class AddNDArrayFunctions(isContrib: Boolean) extends StaticAnnot
}
private[mxnet] class AddNDArrayAPIs(isContrib: Boolean) extends StaticAnnotation {
- private[mxnet] def macroTransform(annottees: Any*) = macro NDArrayMacro.typeSafeAPIDefs
+ private[mxnet] def macroTransform(annottees: Any*) = macro TypedNDArrayAPIMacro.typeSafeAPIDefs
}
-private[mxnet] object NDArrayMacro {
- case class NDArrayArg(argName: String, argType: String, isOptional : Boolean)
- case class NDArrayFunction(name: String, listOfArgs: List[NDArrayArg])
-
- // scalastyle:off havetype
- def addDefs(c: blackbox.Context)(annottees: c.Expr[Any]*) = {
- impl(c)(annottees: _*)
- }
- def typeSafeAPIDefs(c: blackbox.Context)(annottees: c.Expr[Any]*) = {
- typeSafeAPIImpl(c)(annottees: _*)
- }
- // scalastyle:off havetype
-
- private val ndarrayFunctions: List[NDArrayFunction] = initNDArrayModule()
+private[mxnet] object NDArrayMacro extends GeneratorBase {
- private def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
+ def addDefs(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
-
val isContrib: Boolean = c.prefix.tree match {
case q"new AddNDArrayFunctions($b)" => c.eval[Boolean](c.Expr(b))
}
- val newNDArrayFunctions = {
- if (isContrib) ndarrayFunctions.filter(_.name.startsWith("_contrib_"))
- else ndarrayFunctions.filterNot(_.name.startsWith("_"))
- }
-
- val functionDefs = newNDArrayFunctions flatMap { NDArrayfunction =>
- val funcName = NDArrayfunction.name
- val termName = TermName(funcName)
- Seq(
- // scalastyle:off
- // (yizhi) We are investigating a way to make these functions type-safe
- // and waiting to see the new approach is stable enough.
- // Thus these functions may be deprecated in the future.
- // e.g def transpose(kwargs: Map[String, Any] = null)(args: Any*)
- q"def $termName(kwargs: Map[String, Any] = null)(args: Any*) = {genericNDArrayFunctionInvoke($funcName, args, kwargs)}".asInstanceOf[DefDef],
- // e.g def transpose(args: Any*)
- q"def $termName(args: Any*) = {genericNDArrayFunctionInvoke($funcName, args, null)}".asInstanceOf[DefDef]
- // scalastyle:on
- )
- }
-
- structGeneration(c)(functionDefs, annottees : _*)
+ impl(c)(isContrib, annottees: _*)
}
- private def typeSafeAPIImpl(c: blackbox.Context)(annottees: c.Expr[Any]*) : c.Expr[Any] = {
+ private def impl(c: blackbox.Context)
+ (isContrib: Boolean, annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
- val isContrib: Boolean = c.prefix.tree match {
- case q"new AddNDArrayAPIs($b)" => c.eval[Boolean](c.Expr(b))
- }
- // Defines Operators that should not generated
- val notGenerated = Set("Custom")
-
- val newNDArrayFunctions = {
- if (isContrib) ndarrayFunctions.filter(
- func => func.name.startsWith("_contrib_") || !func.name.startsWith("_"))
- else ndarrayFunctions.filterNot(_.name.startsWith("_"))
- }.filterNot(ele => notGenerated.contains(ele.name))
-
- val functionDefs = newNDArrayFunctions.map { ndarrayfunction =>
-
- // Construct argument field
- var argDef = ListBuffer[String]()
- // Construct Implementation field
- var impl = ListBuffer[String]()
- impl += "val map = scala.collection.mutable.Map[String, Any]()"
- impl += "val args = scala.collection.mutable.ArrayBuffer.empty[NDArray]"
- ndarrayfunction.listOfArgs.foreach({ ndarrayarg =>
- // var is a special word used to define variable in Scala,
- // need to changed to something else in order to make it work
- val currArgName = ndarrayarg.argName match {
- case "var" => "vari"
- case "type" => "typeOf"
- case default => ndarrayarg.argName
- }
- if (ndarrayarg.isOptional) {
- argDef += s"${currArgName} : Option[${ndarrayarg.argType}] = None"
- }
- else {
- argDef += s"${currArgName} : ${ndarrayarg.argType}"
- }
- // NDArray arg implementation
- val returnType = "org.apache.mxnet.NDArray"
-
- // TODO: Currently we do not add place holder for NDArray
- // Example: an NDArray operator like the following format
- // nd.foo(arg1: NDArray(required), arg2: NDArray(Optional), arg3: NDArray(Optional)
- // If we place nd.foo(arg1, arg3 = arg3), do we need to add place holder for arg2?
- // What it should be?
- val base =
- if (ndarrayarg.argType.equals(returnType)) {
- s"args += $currArgName"
- } else if (ndarrayarg.argType.equals(s"Array[$returnType]")){
- s"args ++= $currArgName"
- } else {
- "map(\"" + ndarrayarg.argName + "\") = " + currArgName
- }
- impl.append(
- if (ndarrayarg.isOptional) s"if (!$currArgName.isEmpty) $base.get"
- else base
- )
- })
- // add default out parameter
- argDef += "out : Option[NDArray] = None"
- impl += "if (!out.isEmpty) map(\"out\") = out.get"
- // scalastyle:off
- impl += "org.apache.mxnet.NDArray.genericNDArrayFunctionInvoke(\"" + ndarrayfunction.name + "\", args.toSeq, map.toMap)"
- // scalastyle:on
- // Combine and build the function string
- val returnType = "org.apache.mxnet.NDArrayFuncReturn"
- var finalStr = s"def ${ndarrayfunction.name}"
- finalStr += s" (${argDef.mkString(",")}) : $returnType"
- finalStr += s" = {${impl.mkString("\n")}}"
- c.parse(finalStr).asInstanceOf[DefDef]
+ val functions = functionsToGenerate(isSymbol = false, isContrib)
+
+ val functionDefs = functions.flatMap { NDArrayfunction =>
+ val funcName = NDArrayfunction.name
+ val termName = TermName(funcName)
+ Seq(
+ // e.g def transpose(kwargs: Map[String, Any] = null)(args: Any*)
+ q"""
+ def $termName(kwargs: Map[String, Any] = null)(args: Any*) = {
+ genericNDArrayFunctionInvoke($funcName, args, kwargs)
+ }
+ """.asInstanceOf[DefDef],
+ // e.g def transpose(args: Any*)
+ q"""
+ def $termName(args: Any*) = {
+ genericNDArrayFunctionInvoke($funcName, args, null)
+ }
+ """.asInstanceOf[DefDef]
+ )
}
- structGeneration(c)(functionDefs, annottees : _*)
+ structGeneration(c)(functionDefs, annottees: _*)
}
+}
- private def structGeneration(c: blackbox.Context)
- (funcDef : List[c.universe.DefDef], annottees: c.Expr[Any]*)
- : c.Expr[Any] = {
+private[mxnet] object TypedNDArrayAPIMacro extends GeneratorBase {
+
+ def typeSafeAPIDefs(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
- val inputs = annottees.map(_.tree).toList
- // pattern match on the inputs
- val modDefs = inputs map {
- case ClassDef(mods, name, something, template) =>
- val q = template match {
- case Template(superMaybe, emptyValDef, defs) =>
- Template(superMaybe, emptyValDef, defs ++ funcDef)
- case ex =>
- throw new IllegalArgumentException(s"Invalid template: $ex")
- }
- ClassDef(mods, name, something, q)
- case ModuleDef(mods, name, template) =>
- val q = template match {
- case Template(superMaybe, emptyValDef, defs) =>
- Template(superMaybe, emptyValDef, defs ++ funcDef)
- case ex =>
- throw new IllegalArgumentException(s"Invalid template: $ex")
- }
- ModuleDef(mods, name, q)
- case ex =>
- throw new IllegalArgumentException(s"Invalid macro input: $ex")
+ val isContrib: Boolean = c.prefix.tree match {
+ case q"new AddNDArrayAPIs($b)" => c.eval[Boolean](c.Expr(b))
}
- // wrap the result up in an Expr, and return it
- val result = c.Expr(Block(modDefs, Literal(Constant())))
- result
+
+ val functions = typeSafeFunctionsToGenerate(isSymbol = false, isContrib)
+
+ val functionDefs = functions.map(f => buildTypedFunction(c)(f))
+ structGeneration(c)(functionDefs, annottees: _*)
}
+ protected def buildTypedFunction(c: blackbox.Context)
+ (function: Func): c.universe.DefDef = {
+ import c.universe._
+ val returnType = "org.apache.mxnet.NDArrayFuncReturn"
+ val ndarrayType = "org.apache.mxnet.NDArray"
+ // Construct argument field
+ val argDef = ListBuffer[String]()
+ argDef ++= typedFunctionCommonArgDef(function)
+ argDef += "out : Option[NDArray] = None"
- // List and add all the atomic symbol functions to current module.
- private def initNDArrayModule(): List[NDArrayFunction] = {
- val opNames = ListBuffer.empty[String]
- _LIB.mxListAllOpNames(opNames)
- opNames.map(opName => {
- val opHandle = new RefLong
- _LIB.nnGetOpHandle(opName, opHandle)
- makeNDArrayFunction(opHandle.value, opName)
- }).toList
- }
+ // Construct Implementation field
+ var impl = ListBuffer[String]()
+ impl += "val map = scala.collection.mutable.Map[String, Any]()"
+ impl += s"val args = scala.collection.mutable.ArrayBuffer.empty[$ndarrayType]"
- // Create an atomic symbol function by handle and function name.
- private def makeNDArrayFunction(handle: NDArrayHandle, aliasName: String)
- : NDArrayFunction = {
- val name = new RefString
- val desc = new RefString
- val keyVarNumArgs = new RefString
- val numArgs = new RefInt
- val argNames = ListBuffer.empty[String]
- val argTypes = ListBuffer.empty[String]
- val argDescs = ListBuffer.empty[String]
-
- _LIB.mxSymbolGetAtomicSymbolInfo(
- handle, name, desc, numArgs, argNames, argTypes, argDescs, keyVarNumArgs)
- val paramStr = OperatorBuildUtils.ctypes2docstring(argNames, argTypes, argDescs)
- val extraDoc: String = if (keyVarNumArgs.value != null && keyVarNumArgs.value.length > 0) {
- s"This function support variable length of positional input (${keyVarNumArgs.value})."
- } else {
- ""
- }
- val realName = if (aliasName == name.value) "" else s"(a.k.a., ${name.value})"
- val docStr = s"$aliasName $realName\n${desc.value}\n\n$paramStr\n$extraDoc\n"
- // scalastyle:off println
- if (System.getenv("MXNET4J_PRINT_OP_DEF") != null
- && System.getenv("MXNET4J_PRINT_OP_DEF").toLowerCase == "true") {
- println("NDArray function definition:\n" + docStr)
- }
- // scalastyle:on println
- val argList = argNames zip argTypes map { case (argName, argType) =>
- val typeAndOption =
- CToScalaUtils.argumentCleaner(argName, argType, "org.apache.mxnet.NDArray")
- new NDArrayArg(argName, typeAndOption._1, typeAndOption._2)
+ // NDArray arg implementation
+ impl ++= function.listOfArgs.map { arg =>
+ if (arg.argType.equals(s"Array[$ndarrayType]")) {
+ s"args ++= ${arg.safeArgName}"
+ } else {
+ val base =
+ if (arg.argType.equals(ndarrayType)) {
+ // ndarrays go to args
+ s"args += ${arg.safeArgName}"
+ } else {
+ // other types go to kwargs
+ s"""map("${arg.argName}") = ${arg.safeArgName}"""
+ }
+ if (arg.isOptional) s"if (!${arg.safeArgName}.isEmpty) $base.get"
+ else base
+ }
}
- new NDArrayFunction(aliasName, argList.toList)
+
+ impl +=
+ s"""if (!out.isEmpty) map("out") = out.get
+ |org.apache.mxnet.NDArray.genericNDArrayFunctionInvoke(
+ | "${function.name}", args.toSeq, map.toMap)
+ """.stripMargin
+
+ // Combine and build the function string
+ val finalStr =
+ s"""def ${function.name}
+ | (${argDef.mkString(",")}) : $returnType
+ | = {${impl.mkString("\n")}}
+ """.stripMargin
+
+ c.parse(finalStr).asInstanceOf[DefDef]
}
}
diff --git a/scala-package/macros/src/main/scala/org/apache/mxnet/SymbolMacro.scala b/scala-package/macros/src/main/scala/org/apache/mxnet/SymbolMacro.scala
index 42aa11781d8f..ab864e1ef195 100644
--- a/scala-package/macros/src/main/scala/org/apache/mxnet/SymbolMacro.scala
+++ b/scala-package/macros/src/main/scala/org/apache/mxnet/SymbolMacro.scala
@@ -21,222 +21,106 @@ import scala.annotation.StaticAnnotation
import scala.collection.mutable.ListBuffer
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
-import org.apache.mxnet.init.Base._
-import org.apache.mxnet.utils.{CToScalaUtils, OperatorBuildUtils}
private[mxnet] class AddSymbolFunctions(isContrib: Boolean) extends StaticAnnotation {
- private[mxnet] def macroTransform(annottees: Any*) = macro SymbolImplMacros.addDefs
+ private[mxnet] def macroTransform(annottees: Any*) = macro SymbolMacro.addDefs
}
private[mxnet] class AddSymbolAPIs(isContrib: Boolean) extends StaticAnnotation {
- private[mxnet] def macroTransform(annottees: Any*) = macro SymbolImplMacros.typeSafeAPIDefs
+ private[mxnet] def macroTransform(annottees: Any*) = macro TypedSymbolAPIMacro.typeSafeAPIDefs
}
-private[mxnet] object SymbolImplMacros {
- case class SymbolArg(argName: String, argType: String, isOptional : Boolean)
- case class SymbolFunction(name: String, listOfArgs: List[SymbolArg])
+private[mxnet] object SymbolMacro extends GeneratorBase {
- // scalastyle:off havetype
- def addDefs(c: blackbox.Context)(annottees: c.Expr[Any]*) = {
- impl(c)(annottees: _*)
- }
- def typeSafeAPIDefs(c: blackbox.Context)(annottees: c.Expr[Any]*) = {
- typedAPIImpl(c)(annottees: _*)
- }
- // scalastyle:on havetype
-
- private val symbolFunctions: List[SymbolFunction] = initSymbolModule()
-
- /**
- * Implementation for fixed input API structure
- */
- private def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
+ def addDefs(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
-
val isContrib: Boolean = c.prefix.tree match {
case q"new AddSymbolFunctions($b)" => c.eval[Boolean](c.Expr(b))
}
- val newSymbolFunctions = {
- if (isContrib) symbolFunctions.filter(
- func => func.name.startsWith("_contrib_") || !func.name.startsWith("_"))
- else symbolFunctions.filter(!_.name.startsWith("_"))
- }
+ impl(c)(isContrib, annottees: _*)
+ }
+
+ private def impl(c: blackbox.Context)
+ (isContrib: Boolean, annottees: c.Expr[Any]*): c.Expr[Any] = {
+ import c.universe._
+ val functions = functionsToGenerate(isSymbol = false, isContrib)
- val functionDefs = newSymbolFunctions map { symbolfunction =>
- val funcName = symbolfunction.name
- val tName = TermName(funcName)
- q"""
+ val functionDefs = functions.map { symbolfunction =>
+ val funcName = symbolfunction.name
+ val tName = TermName(funcName)
+ q"""
def $tName(name : String = null, attr : Map[String, String] = null)
- (args : org.apache.mxnet.Symbol*)(kwargs : Map[String, Any] = null)
- : org.apache.mxnet.Symbol = {
- createSymbolGeneral($funcName,name,attr,args,kwargs)
- }
+ (args : org.apache.mxnet.Symbol*)(kwargs : Map[String, Any] = null)
+ : org.apache.mxnet.Symbol = {
+ createSymbolGeneral($funcName,name,attr,args,kwargs)
+ }
""".asInstanceOf[DefDef]
- }
+ }
- structGeneration(c)(functionDefs, annottees : _*)
+ structGeneration(c)(functionDefs, annottees: _*)
}
+}
- /**
- * Implementation for Dynamic typed API Symbol.api.
- */
- private def typedAPIImpl(c: blackbox.Context)(annottees: c.Expr[Any]*) : c.Expr[Any] = {
- import c.universe._
+private[mxnet] object TypedSymbolAPIMacro extends GeneratorBase {
+ def typeSafeAPIDefs(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
+ import c.universe._
val isContrib: Boolean = c.prefix.tree match {
case q"new AddSymbolAPIs($b)" => c.eval[Boolean](c.Expr(b))
}
- // Defines Operators that should not generated
- val notGenerated = Set("Custom")
-
- // TODO: Put Symbol.api.foo --> Stable APIs
- // Symbol.contrib.bar--> Contrib APIs
- val newSymbolFunctions = {
- if (isContrib) symbolFunctions.filter(
- func => func.name.startsWith("_contrib_") || !func.name.startsWith("_"))
- else symbolFunctions.filter(!_.name.startsWith("_"))
- }.filterNot(ele => notGenerated.contains(ele.name))
-
- val functionDefs = newSymbolFunctions map { symbolfunction =>
-
- // Construct argument field
- var argDef = ListBuffer[String]()
- // Construct Implementation field
- var impl = ListBuffer[String]()
- impl += "val map = scala.collection.mutable.Map[String, Any]()"
- impl += "var args = Seq[org.apache.mxnet.Symbol]()"
- symbolfunction.listOfArgs.foreach({ symbolarg =>
- // var is a special word used to define variable in Scala,
- // need to changed to something else in order to make it work
- val currArgName = symbolarg.argName match {
- case "var" => "vari"
- case "type" => "typeOf"
- case default => symbolarg.argName
- }
- if (symbolarg.isOptional) {
- argDef += s"${currArgName} : Option[${symbolarg.argType}] = None"
- }
- else {
- argDef += s"${currArgName} : ${symbolarg.argType}"
- }
- // Symbol arg implementation
- val returnType = "org.apache.mxnet.Symbol"
- val base =
- if (symbolarg.argType.equals(s"Array[$returnType]")) {
- if (symbolarg.isOptional) s"if (!$currArgName.isEmpty) args = $currArgName.get.toSeq"
- else s"args = $currArgName.toSeq"
- } else {
- if (symbolarg.isOptional) {
- // scalastyle:off
- s"if (!$currArgName.isEmpty) map(" + "\"" + symbolarg.argName + "\"" + s") = $currArgName.get"
- // scalastyle:on
- }
- else "map(\"" + symbolarg.argName + "\"" + s") = $currArgName"
- }
+ val functions = typeSafeFunctionsToGenerate(isSymbol = true, isContrib)
- impl += base
- })
- argDef += "name : String = null"
- argDef += "attr : Map[String, String] = null"
- // scalastyle:off
- // TODO: Seq() here allows user to place Symbols rather than normal arguments to run, need to fix if old API deprecated
- impl += "org.apache.mxnet.Symbol.createSymbolGeneral(\"" + symbolfunction.name + "\", name, attr, args, map.toMap)"
- // scalastyle:on
- // Combine and build the function string
- val returnType = "org.apache.mxnet.Symbol"
- var finalStr = s"def ${symbolfunction.name}"
- finalStr += s" (${argDef.mkString(",")}) : $returnType"
- finalStr += s" = {${impl.mkString("\n")}}"
- c.parse(finalStr).asInstanceOf[DefDef]
- }
- structGeneration(c)(functionDefs, annottees : _*)
+ val functionDefs = functions.map(f => buildTypedFunction(c)(f))
+ structGeneration(c)(functionDefs, annottees: _*)
}
- /**
- * Generate class structure for all function APIs
- * @param c
- * @param funcDef DefDef type of function definitions
- * @param annottees
- * @return
- */
- private def structGeneration(c: blackbox.Context)
- (funcDef : List[c.universe.DefDef], annottees: c.Expr[Any]*)
- : c.Expr[Any] = {
+ protected def buildTypedFunction(c: blackbox.Context)
+ (function: Func): c.universe.DefDef = {
import c.universe._
- val inputs = annottees.map(_.tree).toList
- // pattern match on the inputs
- val modDefs = inputs map {
- case ClassDef(mods, name, something, template) =>
- val q = template match {
- case Template(superMaybe, emptyValDef, defs) =>
- Template(superMaybe, emptyValDef, defs ++ funcDef)
- case ex =>
- throw new IllegalArgumentException(s"Invalid template: $ex")
- }
- ClassDef(mods, name, something, q)
- case ModuleDef(mods, name, template) =>
- val q = template match {
- case Template(superMaybe, emptyValDef, defs) =>
- Template(superMaybe, emptyValDef, defs ++ funcDef)
- case ex =>
- throw new IllegalArgumentException(s"Invalid template: $ex")
- }
- ModuleDef(mods, name, q)
- case ex =>
- throw new IllegalArgumentException(s"Invalid macro input: $ex")
- }
- // wrap the result up in an Expr, and return it
- val result = c.Expr(Block(modDefs, Literal(Constant())))
- result
- }
- // List and add all the atomic symbol functions to current module.
- private def initSymbolModule(): List[SymbolFunction] = {
- val opNames = ListBuffer.empty[String]
- _LIB.mxListAllOpNames(opNames)
- // TODO: Add '_linalg_', '_sparse_', '_image_' support
- opNames.map(opName => {
- val opHandle = new RefLong
- _LIB.nnGetOpHandle(opName, opHandle)
- makeAtomicSymbolFunction(opHandle.value, opName)
- }).toList
- }
+ val returnType = "org.apache.mxnet.Symbol"
+ val symbolType = "org.apache.mxnet.Symbol"
- // Create an atomic symbol function by handle and function name.
- private def makeAtomicSymbolFunction(handle: SymbolHandle, aliasName: String)
- : SymbolFunction = {
- val name = new RefString
- val desc = new RefString
- val keyVarNumArgs = new RefString
- val numArgs = new RefInt
- val argNames = ListBuffer.empty[String]
- val argTypes = ListBuffer.empty[String]
- val argDescs = ListBuffer.empty[String]
-
- _LIB.mxSymbolGetAtomicSymbolInfo(
- handle, name, desc, numArgs, argNames, argTypes, argDescs, keyVarNumArgs)
- val paramStr = OperatorBuildUtils.ctypes2docstring(argNames, argTypes, argDescs)
- val extraDoc: String = if (keyVarNumArgs.value != null && keyVarNumArgs.value.length > 0) {
- s"This function support variable length of positional input (${keyVarNumArgs.value})."
+ // Construct argument field
+ val argDef = ListBuffer[String]()
+ argDef ++= typedFunctionCommonArgDef(function)
+ argDef += "name : String = null"
+ argDef += "attr : Map[String, String] = null"
+
+ // Construct Implementation field
+ val impl = ListBuffer[String]()
+ impl += "val map = scala.collection.mutable.Map[String, Any]()"
+ impl += s"var args = scala.collection.Seq[$symbolType]()"
+
+ // Symbol arg implementation
+ impl ++= function.listOfArgs.map { arg =>
+ if (arg.argType.equals(s"Array[$symbolType]")) {
+ s"if (!${arg.safeArgName}.isEmpty) args = ${arg.safeArgName}.toSeq"
} else {
- ""
+ // all go in kwargs
+ if (arg.isOptional) {
+ s"""if (!${arg.safeArgName}.isEmpty) map("${arg.argName}") = ${arg.safeArgName}.get"""
+ } else {
+ s"""map("${arg.argName}") = ${arg.safeArgName}"""
+ }
}
- val realName = if (aliasName == name.value) "" else s"(a.k.a., ${name.value})"
- val docStr = s"$aliasName $realName\n${desc.value}\n\n$paramStr\n$extraDoc\n"
- // scalastyle:off println
- if (System.getenv("MXNET4J_PRINT_OP_DEF") != null
- && System.getenv("MXNET4J_PRINT_OP_DEF").toLowerCase == "true") {
- println("Symbol function definition:\n" + docStr)
}
- // scalastyle:on println
- val argList = argNames zip argTypes map { case (argName, argType) =>
- val typeAndOption =
- CToScalaUtils.argumentCleaner(argName, argType, "org.apache.mxnet.Symbol")
- new SymbolArg(argName, typeAndOption._1, typeAndOption._2)
- }
- new SymbolFunction(aliasName, argList.toList)
+
+ impl +=
+ s"""org.apache.mxnet.Symbol.createSymbolGeneral(
+ | "${function.name}", name, attr, args, map.toMap)
+ """.stripMargin
+
+ // Combine and build the function string
+ val finalStr =
+ s"""def ${function.name}
+ | (${argDef.mkString(",")}) : $returnType
+ | = {${impl.mkString("\n")}}
+ """.stripMargin
+
+ c.parse(finalStr).asInstanceOf[DefDef]
}
}
diff --git a/scala-package/macros/src/main/scala/org/apache/mxnet/javaapi/JavaNDArrayMacro.scala b/scala-package/macros/src/main/scala/org/apache/mxnet/javaapi/JavaNDArrayMacro.scala
new file mode 100644
index 000000000000..4dfd6eb044a1
--- /dev/null
+++ b/scala-package/macros/src/main/scala/org/apache/mxnet/javaapi/JavaNDArrayMacro.scala
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mxnet.javaapi
+
+import org.apache.mxnet.GeneratorBase
+
+import scala.annotation.StaticAnnotation
+import scala.collection.mutable.ListBuffer
+import scala.language.experimental.macros
+import scala.reflect.macros.blackbox
+
+private[mxnet] class AddJNDArrayAPIs(isContrib: Boolean) extends StaticAnnotation {
+ private[mxnet] def macroTransform(annottees: Any*) = macro JavaNDArrayMacro.typeSafeAPIDefs
+}
+
+private[mxnet] object JavaNDArrayMacro extends GeneratorBase {
+
+ // scalastyle:off havetype
+ def typeSafeAPIDefs(c: blackbox.Context)(annottees: c.Expr[Any]*) = {
+ typeSafeAPIImpl(c)(annottees: _*)
+ }
+ // scalastyle:off havetype
+
+ private def typeSafeAPIImpl(c: blackbox.Context)(annottees: c.Expr[Any]*) : c.Expr[Any] = {
+ import c.universe._
+
+ val isContrib: Boolean = c.prefix.tree match {
+ case q"new AddJNDArrayAPIs($b)" => c.eval[Boolean](c.Expr(b))
+ }
+ // Defines Operators that should not generated
+ val notGenerated = Set("Custom")
+
+ val newNDArrayFunctions = functionsToGenerate(false, false, true)
+ .filterNot(ele => notGenerated.contains(ele.name)).groupBy(_.name.toLowerCase).map(ele => {
+ /* Pattern matching for not generating deprecated method
+ * Group all method name in lowercase
+ * Kill the capital lettered method such as Cast vs cast
+ * As it defined by default it deprecated
+ */
+ if (ele._2.length == 1) ele._2.head
+ else {
+ if (ele._2.head.name.head.isLower) ele._2.head
+ else ele._2.last
+ }
+ })
+
+ val functionDefs = ListBuffer[DefDef]()
+ val classDefs = ListBuffer[ClassDef]()
+
+ newNDArrayFunctions.foreach { ndarrayfunction =>
+
+ val useParamObject = ndarrayfunction.listOfArgs.count(arg => arg.isOptional) >= 2
+ // Construct argument field with all required args
+ var argDef = ListBuffer[String]()
+ // Construct function Implementation field (e.g norm)
+ var impl = ListBuffer[String]()
+ impl += "val map = scala.collection.mutable.Map[String, Any]()"
+ impl +=
+ "val args= scala.collection.mutable.ArrayBuffer.empty[org.apache.mxnet.NDArray]"
+ ndarrayfunction.listOfArgs.foreach({ ndarrayArg =>
+ // var is a special word used to define variable in Scala,
+ // need to changed to something else in order to make it work
+ var currArgName = ndarrayArg.safeArgName
+ if (useParamObject) currArgName = s"po.get${currArgName.capitalize}()"
+ argDef += s"$currArgName : ${ndarrayArg.argType}"
+ // NDArray arg implementation
+ val returnType = "org.apache.mxnet.javaapi.NDArray"
+ val base =
+ if (ndarrayArg.argType.equals(returnType)) {
+ s"args += $currArgName"
+ } else if (ndarrayArg.argType.equals(s"Array[$returnType]")){
+ s"$currArgName.foreach(args+=_)"
+ } else {
+ "map(\"" + ndarrayArg.argName + "\") = " + currArgName
+ }
+ impl.append(
+ if (ndarrayArg.isOptional) s"if ($currArgName != null) $base"
+ else base
+ )
+ })
+ // add default out parameter
+ argDef += s"out: org.apache.mxnet.javaapi.NDArray"
+ if (useParamObject) {
+ impl += "if (po.getOut() != null) map(\"out\") = po.getOut()"
+ } else {
+ impl += "if (out != null) map(\"out\") = out"
+ }
+ val returnType = "Array[org.apache.mxnet.javaapi.NDArray]"
+ // scalastyle:off
+ // Combine and build the function string
+ impl += "val finalArr = org.apache.mxnet.NDArray.genericNDArrayFunctionInvoke(\"" +
+ ndarrayfunction.name + "\", args.toSeq, map.toMap).arr"
+ impl += "finalArr.map(ele => new NDArray(ele))"
+ if (useParamObject) {
+ val funcDef =
+ s"""def ${ndarrayfunction.name}(po: ${ndarrayfunction.name}Param): $returnType = {
+ | ${impl.mkString("\n")}
+ | }""".stripMargin
+ functionDefs += c.parse(funcDef).asInstanceOf[DefDef]
+ } else {
+ val funcDef =
+ s"""def ${ndarrayfunction.name}(${argDef.mkString(",")}): $returnType = {
+ | ${impl.mkString("\n")}
+ | }""".stripMargin
+ functionDefs += c.parse(funcDef).asInstanceOf[DefDef]
+ }
+ }
+ structGeneration(c)(functionDefs.toList, annottees : _*)
+ }
+}
diff --git a/scala-package/macros/src/main/scala/org/apache/mxnet/utils/CToScalaUtils.scala b/scala-package/macros/src/main/scala/org/apache/mxnet/utils/CToScalaUtils.scala
index d0ebe5b1d2cb..2fd8b2e73c7a 100644
--- a/scala-package/macros/src/main/scala/org/apache/mxnet/utils/CToScalaUtils.scala
+++ b/scala-package/macros/src/main/scala/org/apache/mxnet/utils/CToScalaUtils.scala
@@ -21,19 +21,20 @@ private[mxnet] object CToScalaUtils {
// Convert C++ Types to Scala Types
- def typeConversion(in : String, argType : String = "",
- argName : String, returnType : String) : String = {
+ def typeConversion(in : String, argType : String = "", argName : String,
+ returnType : String) : String = {
+ val header = returnType.split("\\.").dropRight(1)
in match {
- case "Shape(tuple)" | "ShapeorNone" => "org.apache.mxnet.Shape"
+ case "Shape(tuple)" | "ShapeorNone" => s"${header.mkString(".")}.Shape"
case "Symbol" | "NDArray" | "NDArray-or-Symbol" => returnType
case "Symbol[]" | "NDArray[]" | "NDArray-or-Symbol[]" | "SymbolorSymbol[]"
=> s"Array[$returnType]"
- case "float" | "real_t" | "floatorNone" => "org.apache.mxnet.Base.MXFloat"
- case "int" | "intorNone" | "int(non-negative)" => "Int"
- case "long" | "long(non-negative)" => "Long"
- case "double" | "doubleorNone" => "Double"
+ case "float" | "real_t" | "floatorNone" => "java.lang.Float"
+ case "int" | "intorNone" | "int(non-negative)" => "java.lang.Integer"
+ case "long" | "long(non-negative)" => "java.lang.Long"
+ case "double" | "doubleorNone" => "java.lang.Double"
case "string" => "String"
- case "boolean" | "booleanorNone" => "Boolean"
+ case "boolean" | "booleanorNone" => "java.lang.Boolean"
case "tupleof" | "tupleof" | "tupleof<>" | "ptr" | "" => "Any"
case default => throw new IllegalArgumentException(
s"Invalid type for args: $default\nString argType: $argType\nargName: $argName")
@@ -52,8 +53,8 @@ private[mxnet] object CToScalaUtils {
* @param argType Raw arguement Type description
* @return (Scala_Type, isOptional)
*/
- def argumentCleaner(argName: String,
- argType : String, returnType : String) : (String, Boolean) = {
+ def argumentCleaner(argName: String, argType : String,
+ returnType : String) : (String, Boolean) = {
val spaceRemoved = argType.replaceAll("\\s+", "")
var commaRemoved : Array[String] = new Array[String](0)
// Deal with the case e.g: stype : {'csr', 'default', 'row_sparse'}
diff --git a/scala-package/macros/src/test/scala/org/apache/mxnet/MacrosSuite.scala b/scala-package/macros/src/test/scala/org/apache/mxnet/MacrosSuite.scala
index c3a7c58c1afc..4404b0885d57 100644
--- a/scala-package/macros/src/test/scala/org/apache/mxnet/MacrosSuite.scala
+++ b/scala-package/macros/src/test/scala/org/apache/mxnet/MacrosSuite.scala
@@ -36,7 +36,7 @@ class MacrosSuite extends FunSuite with BeforeAndAfterAll {
)
val output = List(
("org.apache.mxnet.Symbol", true),
- ("Int", false),
+ ("java.lang.Integer", false),
("org.apache.mxnet.Shape", true),
("String", true),
("Any", false)
diff --git a/scala-package/mxnet-demo/java-demo/Makefile b/scala-package/mxnet-demo/java-demo/Makefile
new file mode 100644
index 000000000000..340a50f75965
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/Makefile
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+SCALA_VERSION_PROFILE := 2.11
+SCALA_VERSION := 2.11.8
+MXNET_VERSION := 1.3.1-SNAPSHOT
+
+ifeq ($(OS),Windows_NT)
+ UNAME_S := Windows
+else
+ UNAME_S := $(shell uname -s)
+endif
+
+ifeq ($(UNAME_S), Windows)
+ # TODO: currently scala package does not support windows
+ SCALA_PKG_PROFILE := windows
+else
+ ifeq ($(UNAME_S), Darwin)
+ SCALA_PKG_PROFILE := osx-x86_64-cpu
+ else
+ SCALA_PKG_PROFILE := linux-x86_64
+ ifeq ($(USE_CUDA), 1)
+ SCALA_PKG_PROFILE := $(SCALA_PKG_PROFILE)-gpu
+ else
+ SCALA_PKG_PROFILE := $(SCALA_PKG_PROFILE)-cpu
+ endif
+ endif
+endif
+
+javademo:
+ (mvn package -Dmxnet.profile=$(SCALA_PKG_PROFILE) \
+ -Dmxnet.scalaprofile=$(SCALA_VERSION_PROFILE) \
+ -Dmxnet.version=$(MXNET_VERSION) \
+ -Dscala.version=$(SCALA_VERSION))
+
+javaclean:
+ (mvn clean -Dmxnet.profile=$(SCALA_PKG_PROFILE) \
+ -Dmxnet.scalaprofile=$(SCALA_VERSION_PROFILE) \
+ -Dmxnet.version=$(MXNET_VERSION) \
+ -Dscala.version=$(SCALA_VERSION))
\ No newline at end of file
diff --git a/scala-package/mxnet-demo/java-demo/README.md b/scala-package/mxnet-demo/java-demo/README.md
new file mode 100644
index 000000000000..ffe614a29287
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/README.md
@@ -0,0 +1,76 @@
+# MXNet Java Sample Project
+This is an project created to use Maven-published Scala/Java package with two Java examples.
+## Setup
+Please copy the downloaded MXNet Java package jar file under the `java-demo` folder.
+
+User are required to use `mvn package` to build the package,
+ which are shown below:
+```Bash
+export SCALA_VERSION_PROFILE=2.11 SCALA_VERSION=2.11.8 MXNET_VERSION=1.3.1-SNAPSHOT
+export SCALA_PKG_PROFILE=
+mvn package -Dmxnet.profile=$(SCALA_PKG_PROFILE) \
+ -Dmxnet.scalaprofile=$(SCALA_VERSION_PROFILE) \
+ -Dmxnet.version=$(MXNET_VERSION) \
+ -Dscala.version=$(SCALA_VERSION)
+```
+These environment variable (`SCALA_PKG_PROFILE`, `SCALA_VERSION_PROFILE`, `MXNET_VERSION`, `SCALA_VERSION`)
+should be set before executing the line above.
+
+You can also use the `Makefile` as an alternative to do the same thing. Simply do the following:
+```Bash
+make javademo
+```
+This will load the default parameter for all the environment variable.
+ If you want to run with GPU on Linux, just simply add `USE_CUDA=1` when you run the make file
+
+## Run
+### Hello World
+The Scala file is being executed using Java. You can execute the helloWorld example as follows:
+```Bash
+java -cp $CLASSPATH sample.HelloWorld
+```
+However, you have to define the Classpath before you run the demo code. More information can be found in the `demo.sh` And you can run the bash script as follows:
+```Bash
+bash bin/java_sample.sh
+```
+It will load the library automatically and run the example
+### Object Detection using Inference API
+We also provide an example to do object detection, which downloads a ImageNet trained resnet50 model and runs inference on an image to return the classification result as
+```Bash
+Class: car
+Probabilties: 0.99847263
+Coord:312.21335, 72.02908, 456.01443, 150.66176
+Class: bicycle
+Probabilties: 0.9047381
+Coord:155.9581, 149.96365, 383.83694, 418.94516
+Class: dog
+Probabilties: 0.82268167
+Coord:83.82356, 179.14001, 206.63783, 476.78754
+```
+
+you can run using the command shown below:
+```Bash
+java -cp $CLASSPATH sample.ObjectDetection
+```
+or script as follows:
+```Bash
+bash bin/run_od.sh
+```
+
+If you want to test run on GPU, you can set a environment variable as follows:
+```Bash
+export SCALA_TEST_ON_GPU=1
+```
+## Clean up
+Clean up for Maven package is simple, you can run the pre-configed `Makefile` as:
+```Bash
+make javaclean
+```
+
+## Q & A
+If you are facing opencv issue on Ubuntu, please try as follows to install opencv 3.4 (required by 1.2.0 package and above)
+```Bash
+sudo add-apt-repository ppa:timsc/opencv-3.4
+sudo apt-get update
+sudo apt install libopencv-imgcodecs3.4
+```
\ No newline at end of file
diff --git a/scala-package/mxnet-demo/java-demo/bin/java_sample.sh b/scala-package/mxnet-demo/java-demo/bin/java_sample.sh
new file mode 100644
index 000000000000..50e7fb9eb97d
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/bin/java_sample.sh
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#!/bin/bash
+CURR_DIR=$(cd $(dirname $0)/../; pwd)
+CLASSPATH=$CLASSPATH:$CURR_DIR/target/*:$CLASSPATH:$CURR_DIR/*
+java -Xmx8G -cp $CLASSPATH sample.HelloWorld
\ No newline at end of file
diff --git a/scala-package/mxnet-demo/java-demo/bin/run_od.sh b/scala-package/mxnet-demo/java-demo/bin/run_od.sh
new file mode 100644
index 000000000000..5cbc53fbcefe
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/bin/run_od.sh
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#!/bin/bash
+CURR_DIR=$(cd $(dirname $0)/../; pwd)
+
+CLASSPATH=$CLASSPATH:$CURR_DIR/target/*:$CLASSPATH:$CURR_DIR/*
+java -Xmx8G -cp $CLASSPATH sample.ObjectDetection
\ No newline at end of file
diff --git a/scala-package/mxnet-demo/java-demo/pom.xml b/scala-package/mxnet-demo/java-demo/pom.xml
new file mode 100644
index 000000000000..5014d2e09f55
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+ Demo
+ mxnet-java-demo
+ 1.0-SNAPSHOT
+ MXNet Java Demo
+
+
+
+ org.apache.mxnet
+ mxnet-full_${mxnet.scalaprofile}-${mxnet.profile}
+ ${mxnet.version}
+ system
+ ${project.basedir}/mxnet-full_${mxnet.scalaprofile}-${mxnet.profile}-${mxnet.version}.jar
+
+
+ commons-io
+ commons-io
+ 2.4
+
+
+
\ No newline at end of file
diff --git a/scala-package/mxnet-demo/java-demo/src/main/java/sample/HelloWorld.java b/scala-package/mxnet-demo/java-demo/src/main/java/sample/HelloWorld.java
new file mode 100644
index 000000000000..60619dc8a806
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/src/main/java/sample/HelloWorld.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import org.apache.mxnet.javaapi.*;
+import java.util.Arrays;
+
+public class HelloWorld {
+ public static void main(String[] args) {
+ System.out.println("Hello World!");
+ NDArray nd = new NDArray(new float[]{2.0f, 3.0f}, new Shape(new int[]{1, 2}), Context.cpu());
+ System.out.println(nd.shape());
+ }
+}
diff --git a/scala-package/mxnet-demo/java-demo/src/main/java/sample/ObjectDetection.java b/scala-package/mxnet-demo/java-demo/src/main/java/sample/ObjectDetection.java
new file mode 100644
index 000000000000..bf9a93ae8217
--- /dev/null
+++ b/scala-package/mxnet-demo/java-demo/src/main/java/sample/ObjectDetection.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+import org.apache.mxnet.infer.javaapi.ObjectDetectorOutput;
+import org.apache.mxnet.javaapi.*;
+import org.apache.mxnet.infer.javaapi.ObjectDetector;
+import org.apache.commons.io.FileUtils;
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class ObjectDetection {
+ private static String modelPath;
+ private static String imagePath;
+
+ private static void downloadUrl(String url, String filePath) {
+ File tmpFile = new File(filePath);
+ if (!tmpFile.exists()) {
+ try {
+ FileUtils.copyURLToFile(new URL(url), tmpFile);
+ } catch (Exception exception) {
+ System.err.println(exception);
+ }
+ }
+ }
+
+ public static void downloadModelImage() {
+ String tempDirPath = System.getProperty("java.io.tmpdir");
+ System.out.println("tempDirPath: %s".format(tempDirPath));
+ imagePath = tempDirPath + "/inputImages/resnetssd/dog-ssd.jpg";
+ String imgURL = "https://s3.amazonaws.com/model-server/inputs/dog-ssd.jpg";
+ downloadUrl(imgURL, imagePath);
+ modelPath = tempDirPath + "resnetssd/resnet50_ssd_model";
+ System.out.println("Download model files, this can take a while...");
+ String modelURL = "https://s3.amazonaws.com/model-server/models/resnet50_ssd/";
+ downloadUrl(modelURL + "resnet50_ssd_model-symbol.json",
+ tempDirPath + "/resnetssd/resnet50_ssd_model-symbol.json");
+ downloadUrl(modelURL + "resnet50_ssd_model-0000.params",
+ tempDirPath + "/resnetssd/resnet50_ssd_model-0000.params");
+ downloadUrl(modelURL + "synset.txt",
+ tempDirPath + "/resnetssd/synset.txt");
+ }
+
+ static List>
+ runObjectDetectionSingle(String modelPathPrefix, String inputImagePath, List context) {
+ Shape inputShape = new Shape(new int[] {1, 3, 512, 512});
+ List inputDescriptors = new ArrayList();
+ inputDescriptors.add(new DataDesc("data", inputShape, DType.Float32(), "NCHW"));
+ ObjectDetector objDet = new ObjectDetector(modelPathPrefix, inputDescriptors, context, 0);
+ return objDet.imageObjectDetect(ObjectDetector.loadImageFromFile(inputImagePath), 3);
+ }
+
+ public static void main(String[] args) {
+ List context = new ArrayList();
+ if (System.getenv().containsKey("SCALA_TEST_ON_GPU") &&
+ Integer.valueOf(System.getenv("SCALA_TEST_ON_GPU")) == 1) {
+ context.add(Context.gpu());
+ } else {
+ context.add(Context.cpu());
+ }
+ downloadModelImage();
+ Shape inputShape = new Shape(new int[] {1, 3, 512, 512});
+ Shape outputShape = new Shape(new int[] {1, 6132, 6});
+ int width = inputShape.get(2);
+ int height = inputShape.get(3);
+ List> output
+ = runObjectDetectionSingle(modelPath, imagePath, context);
+ String outputStr = "\n";
+ for (List ele : output) {
+ for (ObjectDetectorOutput i : ele) {
+ outputStr += "Class: " + i.getClassName() + "\n";
+ outputStr += "Probabilties: " + i.getProbability() + "\n";
+
+ List coord = Arrays.asList(i.getXMin() * width,
+ i.getXMax() * height, i.getYMin() * width, i.getYMax() * height);
+ StringBuilder sb = new StringBuilder();
+ for (float c: coord) {
+ sb.append(", ").append(c);
+ }
+ outputStr += "Coord:" + sb.substring(2)+ "\n";
+ }
+ }
+ System.out.println(outputStr);
+ }
+}
\ No newline at end of file
diff --git a/scala-package/mxnet-demo/Makefile b/scala-package/mxnet-demo/scala-demo/Makefile
similarity index 98%
rename from scala-package/mxnet-demo/Makefile
rename to scala-package/mxnet-demo/scala-demo/Makefile
index 227697ba2e8a..458077d13904 100644
--- a/scala-package/mxnet-demo/Makefile
+++ b/scala-package/mxnet-demo/scala-demo/Makefile
@@ -17,7 +17,7 @@
SCALA_VERSION_PROFILE := 2.11
SCALA_VERSION := 2.11.8
-MXNET_VERSION := 1.2.0
+MXNET_VERSION := 1.3.0
ifeq ($(OS),Windows_NT)
UNAME_S := Windows
diff --git a/scala-package/mxnet-demo/README.md b/scala-package/mxnet-demo/scala-demo/README.md
similarity index 88%
rename from scala-package/mxnet-demo/README.md
rename to scala-package/mxnet-demo/scala-demo/README.md
index e30a61a2fc13..300fc7b2e108 100644
--- a/scala-package/mxnet-demo/README.md
+++ b/scala-package/mxnet-demo/scala-demo/README.md
@@ -4,7 +4,7 @@ This is an project created to use Maven-published Scala package with two Scala e
User are required to use `mvn package` to build the package,
which are shown below:
```Bash
-export SCALA_VERSION_PROFILE=2.11 SCALA_VERSION=2.11.8 MXNET_VERSION=1.2.0
+export SCALA_VERSION_PROFILE=2.11 SCALA_VERSION=2.11.8 MXNET_VERSION=1.3.0
export SCALA_PKG_PROFILE=
mvn package -Dmxnet.profile=$(SCALA_PKG_PROFILE) \
-Dmxnet.scalaprofile=$(SCALA_VERSION_PROFILE) \
@@ -12,7 +12,9 @@ mvn package -Dmxnet.profile=$(SCALA_PKG_PROFILE) \
-Dscala.version=$(SCALA_VERSION)
```
These environment variable (`SCALA_PKG_PROFILE`, `SCALA_VERSION_PROFILE`, `MXNET_VERSION`, `SCALA_VERSION`)
-should be set before executing the line above.
+should be set before executing the line above.
+
+To obtain the most recent MXNet version, please click [here](https://mvnrepository.com/search?q=org.apache.mxnet)
You can also use the `Makefile` as an alternative to do the same thing. Simply do the following:
```Bash
@@ -25,7 +27,7 @@ This will load the default parameter for all the environment variable.
### Hello World
The Scala file is being executed using Java. You can execute the helloWorld example as follows:
```Bash
-java -Xmx8G -cp $CLASSPATH sample.HelloWorld
+java -cp $CLASSPATH sample.HelloWorld
```
However, you have to define the Classpath before you run the demo code. More information can be found in the `demo.sh` And you can run the bash script as follows:
```Bash
@@ -41,7 +43,7 @@ You can review the complete example [here](https://github.com/apache/incubator-m
you can run using the command shown below:
```Bash
-java -Xmx8G -cp $CLASSPATH sample.ImageClassificationExample
+java -cp $CLASSPATH sample.ImageClassificationExample
```
or script as follows:
```Bash
@@ -59,7 +61,7 @@ make scalaclean
```
## Q & A
-If you are facing opencv issue on Ubuntu, please try as follows to install opencv 3.4 (required by 1.2.0 package)
+If you are facing opencv issue on Ubuntu, please try as follows to install opencv 3.4 (required by 1.2.0 package and above)
```Bash
sudo add-apt-repository ppa:timsc/opencv-3.4
sudo apt-get update
diff --git a/scala-package/mxnet-demo/bin/demo.sh b/scala-package/mxnet-demo/scala-demo/bin/demo.sh
similarity index 100%
rename from scala-package/mxnet-demo/bin/demo.sh
rename to scala-package/mxnet-demo/scala-demo/bin/demo.sh
diff --git a/scala-package/mxnet-demo/bin/run_im.sh b/scala-package/mxnet-demo/scala-demo/bin/run_im.sh
similarity index 100%
rename from scala-package/mxnet-demo/bin/run_im.sh
rename to scala-package/mxnet-demo/scala-demo/bin/run_im.sh
diff --git a/scala-package/mxnet-demo/pom.xml b/scala-package/mxnet-demo/scala-demo/pom.xml
similarity index 100%
rename from scala-package/mxnet-demo/pom.xml
rename to scala-package/mxnet-demo/scala-demo/pom.xml
diff --git a/scala-package/mxnet-demo/src/main/scala/sample/HelloWorld.scala b/scala-package/mxnet-demo/scala-demo/src/main/scala/sample/HelloWorld.scala
similarity index 100%
rename from scala-package/mxnet-demo/src/main/scala/sample/HelloWorld.scala
rename to scala-package/mxnet-demo/scala-demo/src/main/scala/sample/HelloWorld.scala
diff --git a/scala-package/mxnet-demo/src/main/scala/sample/ImageClassificationExample.scala b/scala-package/mxnet-demo/scala-demo/src/main/scala/sample/ImageClassificationExample.scala
similarity index 100%
rename from scala-package/mxnet-demo/src/main/scala/sample/ImageClassificationExample.scala
rename to scala-package/mxnet-demo/scala-demo/src/main/scala/sample/ImageClassificationExample.scala
diff --git a/scala-package/native/src/main/native/org_apache_mxnet_native_c_api.cc b/scala-package/native/src/main/native/org_apache_mxnet_native_c_api.cc
index 95325f3a6a2e..17d166eac345 100644
--- a/scala-package/native/src/main/native/org_apache_mxnet_native_c_api.cc
+++ b/scala-package/native/src/main/native/org_apache_mxnet_native_c_api.cc
@@ -1581,26 +1581,29 @@ JNIEXPORT jint JNICALL Java_org_apache_mxnet_LibInfo_mxSymbolInferShape
env->ReleaseIntArrayElements(jargShapeData, argShapeData, 0);
env->ReleaseIntArrayElements(jargIndPtr, argIndPtr, 0);
- jclass listClass = env->FindClass("scala/collection/mutable/ListBuffer");
- jmethodID listAppend = env->GetMethodID(listClass,
- "$plus$eq", "(Ljava/lang/Object;)Lscala/collection/mutable/ListBuffer;");
+ if (ret == 0) {
+ jclass listClass = env->FindClass("scala/collection/mutable/ListBuffer");
+ jmethodID listAppend = env->GetMethodID(listClass,
+ "$plus$eq", "(Ljava/lang/Object;)Lscala/collection/mutable/ListBuffer;");
- if (FillSymbolInferShape(env, listAppend, jinShapeData, inShapeSize, inShapeNdim, inShapeData)) {
- // TODO(Yizhi): out of memory error thrown, return a specific error code ?
- return -1;
- }
- if (FillSymbolInferShape(
- env, listAppend, joutShapeData, outShapeSize, outShapeNdim, outShapeData)) {
- // TODO(Yizhi): out of memory error thrown, return a specific error code ?
- return -1;
- }
- if (FillSymbolInferShape(
- env, listAppend, jauxShapeData, auxShapeSize, auxShapeNdim, auxShapeData)) {
- // TODO(Yizhi): out of memory error thrown, return a specific error code ?
- return -1;
- }
+ if (FillSymbolInferShape(
+ env, listAppend, jinShapeData, inShapeSize, inShapeNdim, inShapeData)) {
+ // TODO(Yizhi): out of memory error thrown, return a specific error code ?
+ return -1;
+ }
+ if (FillSymbolInferShape(
+ env, listAppend, joutShapeData, outShapeSize, outShapeNdim, outShapeData)) {
+ // TODO(Yizhi): out of memory error thrown, return a specific error code ?
+ return -1;
+ }
+ if (FillSymbolInferShape(
+ env, listAppend, jauxShapeData, auxShapeSize, auxShapeNdim, auxShapeData)) {
+ // TODO(Yizhi): out of memory error thrown, return a specific error code ?
+ return -1;
+ }
- SetIntField(env, jcomplete, complete);
+ SetIntField(env, jcomplete, complete);
+ }
// release allocated memory
if (jkeys != NULL) {
diff --git a/scala-package/pom.xml b/scala-package/pom.xml
index fe78a629ed20..3240c144e822 100644
--- a/scala-package/pom.xml
+++ b/scala-package/pom.xml
@@ -190,8 +190,8 @@
maven-compiler-plugin
3.3
- 1.6
- 1.6
+ 1.7
+ 1.7
UTF-8
@@ -215,6 +215,7 @@
2.19
true
+ false
@@ -331,9 +332,9 @@
1.10
- log4j
- log4j
- 1.2.17
+ org.apache.logging.log4j
+ log4j-core
+ 2.11.1
provided
diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc
index 56e318097a3c..80bd60538ff5 100644
--- a/src/c_api/c_api.cc
+++ b/src/c_api/c_api.cc
@@ -122,7 +122,18 @@ int MXGetGPUCount(int* out) {
API_END();
}
+// Deprecated: use MXGetGPUMemoryInformation64() instead.
int MXGetGPUMemoryInformation(int dev, int *free_mem, int *total_mem) {
+ API_BEGIN();
+ uint64_t free_mem64 = 0UL;
+ uint64_t total_mem64 = 0UL;
+ Context::GetGPUMemoryInformation(dev, &free_mem64, &total_mem64);
+ *free_mem = static_cast(free_mem64);
+ *total_mem = static_cast(total_mem64);
+ API_END();
+}
+
+int MXGetGPUMemoryInformation64(int dev, uint64_t *free_mem, uint64_t *total_mem) {
API_BEGIN();
Context::GetGPUMemoryInformation(dev, free_mem, total_mem);
API_END();
diff --git a/src/c_api/c_api_executor.cc b/src/c_api/c_api_executor.cc
index c3a64736c010..1f936b164326 100644
--- a/src/c_api/c_api_executor.cc
+++ b/src/c_api/c_api_executor.cc
@@ -168,7 +168,7 @@ int MXExecutorBindEX(SymbolHandle symbol_handle,
for (mx_uint i = 0; i < len; ++i) {
in_args_vec.push_back(*(in_args_ptr[i]));
if (arg_grad_ptr[i] == nullptr) {
- arg_grad_vec.push_back(NDArray());
+ arg_grad_vec.emplace_back();
grad_req_vec.push_back(kNullOp);
} else {
arg_grad_vec.push_back(*(arg_grad_ptr[i]));
diff --git a/src/c_api/c_api_function.cc b/src/c_api/c_api_function.cc
index cea8c9553ccf..7091be2e72c5 100644
--- a/src/c_api/c_api_function.cc
+++ b/src/c_api/c_api_function.cc
@@ -55,7 +55,7 @@ std::vector Gradient(
g->inputs = out_grads;
std::vector ret;
- for (index_t i = 0; i < g->num_outputs(); ++i) {
+ for (uint32_t i = 0; i < g->num_outputs(); ++i) {
ret.emplace_back(nnvm::NodeEntry{g, i, 0});
}
diff --git a/src/c_api/c_api_ndarray.cc b/src/c_api/c_api_ndarray.cc
index 34bd4b20aa54..18f6c411e039 100644
--- a/src/c_api/c_api_ndarray.cc
+++ b/src/c_api/c_api_ndarray.cc
@@ -176,7 +176,7 @@ int MXCreateCachedOpEx(SymbolHandle handle,
API_BEGIN();
std::vector > flags;
for (int i = 0; i < num_flags; ++i) {
- flags.push_back({keys[i], vals[i]});
+ flags.emplace_back(keys[i], vals[i]);
}
*out = new CachedOpPtr(new CachedOp(*sym, flags));
API_END();
diff --git a/src/c_api/c_api_symbolic.cc b/src/c_api/c_api_symbolic.cc
index 35ecec7e11f6..d4625de80110 100644
--- a/src/c_api/c_api_symbolic.cc
+++ b/src/c_api/c_api_symbolic.cc
@@ -31,6 +31,7 @@
#include "./c_api_common.h"
#include "../operator/operator_common.h"
#include "../executor/exec_pass.h"
+#include "../operator/subgraph/subgraph_property.h"
namespace mxnet {
namespace op {
@@ -645,30 +646,29 @@ int MXSymbolGrad(SymbolHandle sym, mx_uint num_wrt, const char** wrt, SymbolHand
int MXQuantizeSymbol(SymbolHandle sym_handle,
SymbolHandle *ret_sym_handle,
- const mx_uint num_excluded_symbols,
- const SymbolHandle *excluded_symbols,
+ const mx_uint num_excluded_op_names,
+ const char **excluded_op_names,
const mx_uint num_offline,
const char **offline_params,
- const char *quantized_dtype) {
+ const char *quantized_dtype,
+ const bool calib_quantize) {
nnvm::Symbol *s = new nnvm::Symbol();
API_BEGIN();
nnvm::Symbol *sym = static_cast(sym_handle);
nnvm::Graph g = Symbol2Graph(*sym);
- std::unordered_set excluded_nodes;
- for (size_t i = 0; i < num_excluded_symbols; ++i) {
- nnvm::Symbol* sym = static_cast(excluded_symbols[i]);
- for (const auto& e : sym->outputs) {
- excluded_nodes.emplace(e.node);
- }
+ std::unordered_set excluded_node_names;
+ for (size_t i = 0; i < num_excluded_op_names; ++i) {
+ excluded_node_names.emplace(excluded_op_names[i]);
}
- g.attrs["excluded_nodes"] = std::make_shared(std::move(excluded_nodes));
std::unordered_set offline;
for (size_t i = 0; i < num_offline; ++i) {
offline.emplace(offline_params[i]);
}
std::string quantized_type(quantized_dtype);
+ g.attrs["excluded_nodes"] = std::make_shared(std::move(excluded_node_names));
g.attrs["offline_params"] = std::make_shared(std::move(offline));
g.attrs["quantized_dtype"] = std::make_shared(std::move(quantized_type));
+ g.attrs["calib_quantize"] = std::make_shared(calib_quantize);
g = ApplyPass(std::move(g), "QuantizeGraph");
s->outputs = g.outputs;
*ret_sym_handle = s;
@@ -696,3 +696,21 @@ int MXSetCalibTableToQuantizedSymbol(SymbolHandle qsym_handle,
*ret_qsym_handle = s;
API_END_HANDLE_ERROR(delete s);
}
+
+int MXGenBackendSubgraph(SymbolHandle sym_handle, const char *backend,
+ SymbolHandle *ret_sym_handle) {
+ nnvm::Symbol *s = new nnvm::Symbol();
+ API_BEGIN();
+ nnvm::Symbol *sym = static_cast(sym_handle);
+ *s = sym->Copy();
+ nnvm::Graph g = Symbol2Graph(*s);
+ mxnet::op::SubgraphPropertyPtr property =
+ mxnet::op::SubgraphPropertyRegistry::Get()->CreateSubgraphProperty(
+ backend);
+ g.attrs["subgraph_property"] =
+ std::make_shared(std::move(property));
+ g = ApplyPass(std::move(g), "PartitionGraph");
+ s->outputs = g.outputs;
+ *ret_sym_handle = s;
+ API_END_HANDLE_ERROR(delete s);
+}
diff --git a/src/c_api/c_predict_api.cc b/src/c_api/c_predict_api.cc
index 24358e449472..cae5c68aa512 100644
--- a/src/c_api/c_predict_api.cc
+++ b/src/c_api/c_predict_api.cc
@@ -191,7 +191,7 @@ int _CreatePartialOut(const char* symbol_json_str,
if (known_shape.count(key) != 0) {
in_shapes.push_back(known_shape[key]);
} else {
- in_shapes.push_back(TShape());
+ in_shapes.emplace_back();
}
}
nnvm::Graph g; g.outputs = sym.outputs;
@@ -369,7 +369,7 @@ int MXPredReshape(mx_uint num_input_nodes,
if (new_shape.count(key) != 0) {
in_shapes.push_back(new_shape[key]);
} else {
- in_shapes.push_back(TShape());
+ in_shapes.emplace_back();
}
}
nnvm::Graph g; g.outputs = ret->sym.outputs;
diff --git a/src/common/cuda_utils.h b/src/common/cuda_utils.h
index b4b10c2c75bc..047edde88a53 100644
--- a/src/common/cuda_utils.h
+++ b/src/common/cuda_utils.h
@@ -68,6 +68,110 @@ inline __device__ bool __is_supported_cuda_architecture() {
}
#endif // __CUDACC__
+/*!
+ * \brief Check CUDA error.
+ * \param msg Message to print if an error occured.
+ */
+#define CHECK_CUDA_ERROR(msg) \
+ { \
+ cudaError_t e = cudaGetLastError(); \
+ CHECK_EQ(e, cudaSuccess) << (msg) << " CUDA: " << cudaGetErrorString(e); \
+ }
+
+/*!
+ * \brief Protected CUDA call.
+ * \param func Expression to call.
+ *
+ * It checks for CUDA errors after invocation of the expression.
+ */
+#define CUDA_CALL(func) \
+ { \
+ cudaError_t e = (func); \
+ CHECK(e == cudaSuccess || e == cudaErrorCudartUnloading) \
+ << "CUDA: " << cudaGetErrorString(e); \
+ }
+
+/*!
+ * \brief Protected cuBLAS call.
+ * \param func Expression to call.
+ *
+ * It checks for cuBLAS errors after invocation of the expression.
+ */
+#define CUBLAS_CALL(func) \
+ { \
+ cublasStatus_t e = (func); \
+ CHECK_EQ(e, CUBLAS_STATUS_SUCCESS) \
+ << "cuBLAS: " << mxnet::common::cuda::CublasGetErrorString(e); \
+ }
+
+/*!
+ * \brief Protected cuSolver call.
+ * \param func Expression to call.
+ *
+ * It checks for cuSolver errors after invocation of the expression.
+ */
+#define CUSOLVER_CALL(func) \
+ { \
+ cusolverStatus_t e = (func); \
+ CHECK_EQ(e, CUSOLVER_STATUS_SUCCESS) \
+ << "cuSolver: " << mxnet::common::cuda::CusolverGetErrorString(e); \
+ }
+
+/*!
+ * \brief Protected cuRAND call.
+ * \param func Expression to call.
+ *
+ * It checks for cuRAND errors after invocation of the expression.
+ */
+#define CURAND_CALL(func) \
+ { \
+ curandStatus_t e = (func); \
+ CHECK_EQ(e, CURAND_STATUS_SUCCESS) \
+ << "cuRAND: " << mxnet::common::cuda::CurandGetErrorString(e); \
+ }
+
+/*!
+ * \brief Protected NVRTC call.
+ * \param func Expression to call.
+ *
+ * It checks for NVRTC errors after invocation of the expression.
+ */
+#define NVRTC_CALL(x) \
+ { \
+ nvrtcResult result = x; \
+ CHECK_EQ(result, NVRTC_SUCCESS) \
+ << #x " failed with error " \
+ << nvrtcGetErrorString(result); \
+ }
+
+/*!
+ * \brief Protected CUDA driver call.
+ * \param func Expression to call.
+ *
+ * It checks for CUDA driver errors after invocation of the expression.
+ */
+#define CUDA_DRIVER_CALL(func) \
+ { \
+ CUresult e = (func); \
+ if (e != CUDA_SUCCESS) { \
+ char const * err_msg = nullptr; \
+ if (cuGetErrorString(e, &err_msg) == CUDA_ERROR_INVALID_VALUE) { \
+ LOG(FATAL) << "CUDA Driver: Unknown error " << e; \
+ } else { \
+ LOG(FATAL) << "CUDA Driver: " << err_msg; \
+ } \
+ } \
+ }
+
+
+#if !defined(_MSC_VER)
+#define CUDA_UNROLL _Pragma("unroll")
+#define CUDA_NOUNROLL _Pragma("nounroll")
+#else
+#define CUDA_UNROLL
+#define CUDA_NOUNROLL
+#endif
+
namespace mxnet {
namespace common {
/*! \brief common utils for cuda */
@@ -179,113 +283,31 @@ inline DType __device__ CudaMin(DType a, DType b) {
return a < b ? a : b;
}
-} // namespace cuda
-} // namespace common
-} // namespace mxnet
-
-/*!
- * \brief Check CUDA error.
- * \param msg Message to print if an error occured.
- */
-#define CHECK_CUDA_ERROR(msg) \
- { \
- cudaError_t e = cudaGetLastError(); \
- CHECK_EQ(e, cudaSuccess) << (msg) << " CUDA: " << cudaGetErrorString(e); \
- }
-
-/*!
- * \brief Protected CUDA call.
- * \param func Expression to call.
- *
- * It checks for CUDA errors after invocation of the expression.
- */
-#define CUDA_CALL(func) \
- { \
- cudaError_t e = (func); \
- CHECK(e == cudaSuccess || e == cudaErrorCudartUnloading) \
- << "CUDA: " << cudaGetErrorString(e); \
- }
-
-/*!
- * \brief Protected cuBLAS call.
- * \param func Expression to call.
- *
- * It checks for cuBLAS errors after invocation of the expression.
- */
-#define CUBLAS_CALL(func) \
- { \
- cublasStatus_t e = (func); \
- CHECK_EQ(e, CUBLAS_STATUS_SUCCESS) \
- << "cuBLAS: " << mxnet::common::cuda::CublasGetErrorString(e); \
- }
-
-/*!
- * \brief Protected cuSolver call.
- * \param func Expression to call.
- *
- * It checks for cuSolver errors after invocation of the expression.
- */
-#define CUSOLVER_CALL(func) \
- { \
- cusolverStatus_t e = (func); \
- CHECK_EQ(e, CUSOLVER_STATUS_SUCCESS) \
- << "cuSolver: " << mxnet::common::cuda::CusolverGetErrorString(e); \
- }
-
-/*!
- * \brief Protected cuRAND call.
- * \param func Expression to call.
- *
- * It checks for cuRAND errors after invocation of the expression.
- */
-#define CURAND_CALL(func) \
- { \
- curandStatus_t e = (func); \
- CHECK_EQ(e, CURAND_STATUS_SUCCESS) \
- << "cuRAND: " << mxnet::common::cuda::CurandGetErrorString(e); \
+class DeviceStore {
+ public:
+ /*! \brief default constructor- only optionally restores previous device */
+ explicit DeviceStore(bool restore = true) : restore_(restore) {
+ if (restore_)
+ CUDA_CALL(cudaGetDevice(&restore_device_));
}
-/*!
- * \brief Protected NVRTC call.
- * \param func Expression to call.
- *
- * It checks for NVRTC errors after invocation of the expression.
- */
-#define NVRTC_CALL(x) \
- { \
- nvrtcResult result = x; \
- CHECK_EQ(result, NVRTC_SUCCESS) \
- << #x " failed with error " \
- << nvrtcGetErrorString(result); \
+ ~DeviceStore() {
+ if (restore_)
+ CUDA_CALL(cudaSetDevice(restore_device_));
}
-/*!
- * \brief Protected CUDA driver call.
- * \param func Expression to call.
- *
- * It checks for CUDA driver errors after invocation of the expression.
- */
-#define CUDA_DRIVER_CALL(func) \
- { \
- CUresult e = (func); \
- if (e != CUDA_SUCCESS) { \
- char const * err_msg = nullptr; \
- if (cuGetErrorString(e, &err_msg) == CUDA_ERROR_INVALID_VALUE) { \
- LOG(FATAL) << "CUDA Driver: Unknown error " << e; \
- } else { \
- LOG(FATAL) << "CUDA Driver: " << err_msg; \
- } \
- } \
+ void SetDevice(int device) {
+ CUDA_CALL(cudaSetDevice(device));
}
+ private:
+ int restore_device_;
+ bool restore_;
+};
-#if !defined(_MSC_VER)
-#define CUDA_UNROLL _Pragma("unroll")
-#define CUDA_NOUNROLL _Pragma("nounroll")
-#else
-#define CUDA_UNROLL
-#define CUDA_NOUNROLL
-#endif
+} // namespace cuda
+} // namespace common
+} // namespace mxnet
/*!
* \brief Determine major version number of the gpu's cuda compute architecture.
@@ -374,6 +396,22 @@ inline bool GetEnvAllowTensorCore() {
return allow_tensor_core;
}
+// The policy if the user hasn't set the environment variable
+// CUDNN_TENSOR_OP_MATH_ALLOW_CONVERSION
+#define MXNET_CUDA_TENSOR_OP_MATH_ALLOW_CONVERSION_DEFAULT false
+
+/*!
+ * \brief Returns global policy for TensorCore implicit type casting
+ */
+inline bool GetEnvAllowTensorCoreConversion() {
+ // Use of optional here permits: "0", "1", "true" and "false" to all be
+ // legal.
+ bool default_value = MXNET_CUDA_TENSOR_OP_MATH_ALLOW_CONVERSION_DEFAULT;
+ return dmlc::GetEnv("MXNET_CUDA_TENSOR_OP_MATH_ALLOW_CONVERSION",
+ dmlc::optional(default_value))
+ .value();
+}
+
#if CUDA_VERSION >= 9000
// Sets the cuBLAS math mode that determines the 'allow TensorCore' policy. Returns previous.
inline cublasMath_t SetCublasMathMode(cublasHandle_t blas_handle, cublasMath_t new_math_type) {
diff --git a/src/common/rtc.cc b/src/common/rtc.cc
index da083c9244ca..ea20a60948c5 100644
--- a/src/common/rtc.cc
+++ b/src/common/rtc.cc
@@ -77,11 +77,12 @@ CUfunction CudaModule::Chunk::GetFunction(
CHECK_EQ(ctx.dev_mask(), Context::kGPU)
<< "CUDA Runtime compilation only supports Nvidia GPU.";
auto iter = mod_.find(ctx.dev_id);
+ mxnet::common::cuda::DeviceStore device_store;
CUmodule module;
if (iter != mod_.end()) {
module = iter->second;
} else {
- CUDA_CALL(cudaSetDevice(ctx.dev_id));
+ device_store.SetDevice(ctx.dev_id);
CUDA_DRIVER_CALL(cuModuleLoadDataEx(&module, ptx_, 0, 0, 0));
mod_[ctx.dev_id] = module;
}
diff --git a/src/engine/engine.cc b/src/engine/engine.cc
index 1c72f33d24e1..a33f0b2c1442 100644
--- a/src/engine/engine.cc
+++ b/src/engine/engine.cc
@@ -48,7 +48,7 @@ inline Engine* CreateEngine() {
ret = CreateNaiveEngine();
#endif
- if (ret ==nullptr) {
+ if (ret == nullptr) {
LOG(FATAL) << "Cannot find Engine " << type;
}
if (!default_engine) {
diff --git a/src/engine/openmp.cc b/src/engine/openmp.cc
index 8fe3939892d2..64899b09660e 100644
--- a/src/engine/openmp.cc
+++ b/src/engine/openmp.cc
@@ -73,18 +73,14 @@ void OpenMP::set_reserve_cores(int cores) {
CHECK_GE(cores, 0);
reserve_cores_ = cores;
#ifdef _OPENMP
- if (reserve_cores_ >= omp_thread_max_) {
- omp_set_num_threads(1);
- } else {
- omp_set_num_threads(omp_thread_max_ - reserve_cores_);
- }
+ omp_thread_max_ = std::max(omp_thread_max_ - reserve_cores_, 1);
#endif
}
int OpenMP::GetRecommendedOMPThreadCount(bool exclude_reserved) const {
#ifdef _OPENMP
if (omp_num_threads_set_in_environment_) {
- return omp_get_max_threads();
+ return omp_thread_max_;
}
if (enabled_) {
int thread_count = omp_get_max_threads();
@@ -101,10 +97,8 @@ int OpenMP::GetRecommendedOMPThreadCount(bool exclude_reserved) const {
}
return omp_thread_max_;
}
- return 1;
-#else
- return 1;
#endif
+ return 1;
}
OpenMP *__init_omp__ = OpenMP::Get();
diff --git a/src/engine/stream_manager.h b/src/engine/stream_manager.h
index ddbfde81f055..d4ac042ff401 100644
--- a/src/engine/stream_manager.h
+++ b/src/engine/stream_manager.h
@@ -65,6 +65,9 @@ template
RunContext StreamManager::GetRunContext(
Context const& ctx) {
RunContext ret;
+#if MXNET_USE_CUDA
+ mxnet::common::cuda::DeviceStore device_store;
+#endif
switch (ctx.dev_mask()) {
case cpu::kDevMask:
ret = RunContext{ctx, nullptr};
@@ -72,7 +75,7 @@ RunContext StreamManager::GetRunContext(
case gpu::kDevMask: {
#if MXNET_USE_CUDA
std::size_t use_counter;
- CUDA_CALL(cudaSetDevice(ctx.dev_id));
+ device_store.SetDevice(ctx.dev_id);
{
std::lock_guard lock{mutex_};
auto&& counter = gpu_cnt_.at(ctx.dev_id);
@@ -101,13 +104,16 @@ template
RunContext StreamManager::GetIORunContext(
Context const& ctx) {
RunContext ret;
+#if MXNET_USE_CUDA
+ mxnet::common::cuda::DeviceStore device_store;
+#endif
switch (ctx.dev_mask()) {
case cpu::kDevMask:
ret = RunContext{ctx, nullptr};
break;
case gpu::kDevMask: {
#if MXNET_USE_CUDA
- CUDA_CALL(cudaSetDevice(ctx.dev_id));
+ device_store.SetDevice(ctx.dev_id);
{
std::lock_guard lock{mutex_};
if (gpu_io_streams_.at(ctx.dev_id) == nullptr) {
diff --git a/src/engine/threaded_engine.h b/src/engine/threaded_engine.h
index a2c1a2b943aa..ccfd09d64ea8 100644
--- a/src/engine/threaded_engine.h
+++ b/src/engine/threaded_engine.h
@@ -182,7 +182,7 @@ class ThreadedVar final
private:
// TODO(hotpxl) change this to spinlock for faster runtime
// TODO(hotpxl) consider rename head
- /*! \brief inetrnal mutex of the ThreadedVar */
+ /*! \brief internal mutex of the ThreadedVar */
std::mutex mutex_;
/*!
* \brief number of pending reads operation in the variable.
diff --git a/src/executor/attach_op_execs_pass.cc b/src/executor/attach_op_execs_pass.cc
index 0e415ef5112a..a0176fab0a04 100644
--- a/src/executor/attach_op_execs_pass.cc
+++ b/src/executor/attach_op_execs_pass.cc
@@ -159,9 +159,13 @@ class StatefulComputeExExecutor : public OpExecutor {
op_ctx.run_ctx = rctx;
#if MXNET_USE_MKLDNN == 1
InvalidateOutputs(out_array, req);
- CreateDefaultInputs(in_array, &in_array_fallback);
- fcompute_(state_, op_ctx, in_array_fallback, req, out_array);
- return;
+ // TODO(alex): (MXNET-847) Remove this fallback feature after subgraph implemented
+ const auto is_mkldnn = Op::GetAttr("TIsMKLDNN");
+ if (!is_mkldnn.get(attrs_.op, false)) {
+ CreateDefaultInputs(in_array, &in_array_fallback);
+ fcompute_(state_, op_ctx, in_array_fallback, req, out_array);
+ return;
+ }
#endif
fcompute_(state_, op_ctx, in_array, req, out_array);
}
@@ -180,12 +184,14 @@ class StatefulComputeExExecutor : public OpExecutor {
return state_;
}
- explicit StatefulComputeExExecutor(const OpStatePtr& state,
+ explicit StatefulComputeExExecutor(const NodeAttrs& attrs,
+ const OpStatePtr& state,
const FStatefulComputeEx& fcompute,
ExecType exec_type)
- : state_(state), fcompute_(fcompute), exec_type_(exec_type) {}
+ : attrs_(attrs), state_(state), fcompute_(fcompute), exec_type_(exec_type) {}
private:
+ NodeAttrs attrs_;
OpStatePtr state_;
FStatefulComputeEx fcompute_;
ExecType exec_type_;
@@ -302,7 +308,8 @@ void CreateOpExecs(const Graph& g, OpExecVector* p_ret, size_t i) {
op, "FStatefulComputeEx", vctx[i]);
// FStatefulComputeEx is dispatched only when dispatch_mode is DispatchMode::kFComputeEx
if (fcompute_ex != nullptr && dispatch_modes[i] == DispatchMode::kFComputeEx) {
- ret[i] = std::make_shared(state, fcompute_ex, exec_type);
+ ret[i] = std::make_shared(inode.source->attrs, state,
+ fcompute_ex, exec_type);
} else {
FStatefulCompute fcompute = common::GetFCompute(
op, "FStatefulCompute", vctx[i]);
@@ -322,7 +329,8 @@ void CreateOpExecs(const Graph& g, OpExecVector* p_ret, size_t i) {
// FStatefulComputeEx is dispatched only when dispatch_mode is DispatchMode::kFComputeEx
if (fcompute_ex != nullptr && dispatch_modes[i] == DispatchMode::kFComputeEx) {
ret[i] = std::make_shared(
- ret[fwd_id].get()->state(), fcompute_ex, exec_type);
+ inode.source->attrs, ret[fwd_id].get()->state(), fcompute_ex,
+ exec_type);
} else {
FStatefulCompute fcompute = common::GetFCompute(
op, "FStatefulCompute", vctx[i]);
diff --git a/src/executor/graph_executor.cc b/src/executor/graph_executor.cc
index 54a8d224ff42..136917a60d95 100644
--- a/src/executor/graph_executor.cc
+++ b/src/executor/graph_executor.cc
@@ -44,6 +44,7 @@ GraphExecutor::GraphExecutor() {
log_verbose_ = dmlc::GetEnv("MXNET_EXEC_VERBOSE_LOGGING", false);
need_grad_ = false;
subgraph_property_ = dmlc::GetEnv("MXNET_SUBGRAPH_BACKEND", std::string());
+ engine_ref_ = Engine::_GetSharedRef();
}
GraphExecutor::~GraphExecutor() {
@@ -1307,7 +1308,7 @@ void GraphExecutor::ExecuteMonCallback(size_t nid) {
}
}
CHECK_EQ(opnode.exec->out_array.size(), output_names.size());
- for (index_t i = 0; i < opnode.exec->out_array.size(); ++i) {
+ for (size_t i = 0; i < opnode.exec->out_array.size(); ++i) {
NDArray *cpy = new NDArray(opnode.exec->out_array[i]);
std::string name = inode.source->attrs.name + "_" + output_names[i];
this->monitor_callback_(name.c_str(), reinterpret_cast(cpy));
@@ -1531,14 +1532,14 @@ static nnvm::Symbol PartitionGraph(const nnvm::Symbol& src,
// This is for bind flow.
static nnvm::Symbol PartitionGraph(const nnvm::Symbol& src,
const std::string& prop_name,
- const std::vector &in_args,
+ std::vector *in_args,
const std::vector &aux_states,
const Context& default_ctx,
const std::map& ctx_map) {
const std::vector input_names = src.ListInputNames(Symbol::kAll);
const std::vector arg_names = src.ListInputNames(nnvm::Symbol::kReadOnlyArgs);
const std::vector aux_names = src.ListInputNames(nnvm::Symbol::kAuxiliaryStates);
- CHECK_EQ(arg_names.size(), in_args.size());
+ CHECK_EQ(arg_names.size(), in_args->size());
CHECK_EQ(aux_names.size(), aux_states.size());
nnvm::ShapeVector arg_shapes; // all input shapes
arg_shapes.reserve(input_names.size());
@@ -1546,7 +1547,7 @@ static nnvm::Symbol PartitionGraph(const nnvm::Symbol& src,
arg_dtypes.reserve(input_names.size());
StorageTypeVector arg_stypes; // all input stypes
arg_stypes.reserve(input_names.size());
- std::vector in_arg_ctxes(in_args.size());
+ std::vector in_arg_ctxes(in_args->size());
std::vector aux_state_ctxes(aux_states.size());
size_t i1 = 0, i2 = 0;
@@ -1560,15 +1561,32 @@ static nnvm::Symbol PartitionGraph(const nnvm::Symbol& src,
} else {
CHECK(i1 < arg_names.size());
CHECK_EQ(arg_names[i1], input_names[i]);
- arg_shapes.push_back(in_args[i1].shape());
- arg_dtypes.push_back(in_args[i1].dtype());
- arg_stypes.push_back(in_args[i1].storage_type());
- in_arg_ctxes[i1] = in_args[i1].ctx();
+ arg_shapes.push_back(in_args->at(i1).shape());
+ arg_dtypes.push_back(in_args->at(i1).dtype());
+ arg_stypes.push_back(in_args->at(i1).storage_type());
+ in_arg_ctxes[i1] = in_args->at(i1).ctx();
++i1;
}
}
- return PartitionGraph(src, prop_name, arg_shapes, arg_dtypes, arg_stypes,
- default_ctx, ctx_map, in_arg_ctxes, aux_state_ctxes);
+
+ // setup in_args_map
+ std::unordered_map in_args_map;
+ for (size_t i = 0; i < in_args->size(); ++i) {
+ in_args_map[arg_names[i]] = in_args->at(i);
+ }
+ auto result = PartitionGraph(src, prop_name, arg_shapes, arg_dtypes, arg_stypes, default_ctx,
+ ctx_map, in_arg_ctxes, aux_state_ctxes);
+ // Reorder in_args into new_in_args according to partitioned symbol input sequence
+ std::vector new_in_args(in_args->size());
+ // get new symbol in_arg names
+ std::vector new_arg_names = result.ListInputNames(nnvm::Symbol::kReadOnlyArgs);
+ CHECK_EQ(arg_names.size(), new_arg_names.size());
+ in_args->clear();
+ for (auto arg_name : new_arg_names) {
+ CHECK(in_args_map.count(arg_name));
+ in_args->push_back(in_args_map[arg_name]);
+ }
+ return result;
}
} // namespace exec
@@ -1612,12 +1630,13 @@ Executor *Executor::Bind(nnvm::Symbol symbol,
const std::vector &aux_states,
Executor* shared_exec) {
auto exec = new exec::GraphExecutor();
+ std::vector tmp_in_args = in_args;
if (!exec->subgraph_property().empty()) {
- symbol = exec::PartitionGraph(symbol, exec->subgraph_property(), in_args, aux_states,
+ symbol = exec::PartitionGraph(symbol, exec->subgraph_property(), &tmp_in_args, aux_states,
default_ctx, group2ctx);
}
exec->Init(symbol, default_ctx, group2ctx,
- in_args, arg_grad_store, grad_req_type, aux_states,
+ tmp_in_args, arg_grad_store, grad_req_type, aux_states,
reinterpret_cast(shared_exec));
return exec;
}
diff --git a/src/executor/graph_executor.h b/src/executor/graph_executor.h
index b94bb4377786..f5f032e3f2e6 100644
--- a/src/executor/graph_executor.h
+++ b/src/executor/graph_executor.h
@@ -260,6 +260,8 @@ class GraphExecutor : public Executor {
bool log_verbose_ = false;
// subgraph property name
std::string subgraph_property_;
+ // ref of engine
+ std::shared_ptr engine_ref_;
};
} // namespace exec
diff --git a/src/executor/infer_graph_attr_pass.cc b/src/executor/infer_graph_attr_pass.cc
index 0abee04b59d5..c14482affbde 100644
--- a/src/executor/infer_graph_attr_pass.cc
+++ b/src/executor/infer_graph_attr_pass.cc
@@ -330,7 +330,7 @@ nnvm::Graph InferShape(nnvm::Graph&& graph,
graph.attrs["shape_inputs"] = std::make_shared(std::move(shape_inputs));
}
if (shape_attr_key.length() != 0) {
- graph.attrs["shape_attr_key"] = std::make_shared(std::move(shape_attr_key));
+ graph.attrs["shape_attr_key"] = std::make_shared(shape_attr_key);
}
return InferAttr(
std::move(graph), nnvm::TShape(),
@@ -348,7 +348,7 @@ nnvm::Graph InferType(nnvm::Graph&& graph,
graph.attrs["dtype_inputs"] = std::make_shared(std::move(dtype_inputs));
}
if (dtype_attr_key.length() != 0) {
- graph.attrs["dtype_attr_key"] = std::make_shared(std::move(dtype_attr_key));
+ graph.attrs["dtype_attr_key"] = std::make_shared(dtype_attr_key);
}
return InferAttr(
std::move(graph), -1,
@@ -366,7 +366,7 @@ nnvm::Graph InferStorageType(nnvm::Graph&& graph,
graph.attrs["storage_type_inputs"] = std::make_shared(std::move(storage_type_inputs));
}
if (storage_type_attr_key.length() != 0) {
- graph.attrs["storage_type_attr_key"] = std::make_shared(std::move(storage_type_attr_key));
+ graph.attrs["storage_type_attr_key"] = std::make_shared(storage_type_attr_key);
}
// initialize unknown values for dispatch modes
if (graph.attrs.count("dispatch_mode") == 0) {
diff --git a/src/imperative/cached_op.cc b/src/imperative/cached_op.cc
index 1f115cd64ad5..a836765f51a7 100644
--- a/src/imperative/cached_op.cc
+++ b/src/imperative/cached_op.cc
@@ -855,10 +855,15 @@ OpStatePtr CachedOp::Forward(
int prev_bulk_size = Engine::Get()->set_bulk_size(config_.forward_bulk_size);
OpStatePtr op_state;
- if (config_.static_alloc) {
- op_state = StaticForward(default_ctx, inputs, outputs);
- } else {
- op_state = DynamicForward(default_ctx, inputs, outputs);
+ try {
+ if (config_.static_alloc) {
+ op_state = StaticForward(default_ctx, inputs, outputs);
+ } else {
+ op_state = DynamicForward(default_ctx, inputs, outputs);
+ }
+ } catch (const dmlc::Error& e) {
+ Engine::Get()->set_bulk_size(prev_bulk_size);
+ throw e;
}
Engine::Get()->set_bulk_size(prev_bulk_size);
@@ -1058,10 +1063,15 @@ void CachedOp::Backward(
int prev_bulk_size = Engine::Get()->set_bulk_size(config_.backward_bulk_size);
- if (config_.static_alloc) {
- StaticBackward(retain_graph, state, inputs, reqs, outputs);
- } else {
- DynamicBackward(retain_graph, state, inputs, reqs, outputs);
+ try {
+ if (config_.static_alloc) {
+ StaticBackward(retain_graph, state, inputs, reqs, outputs);
+ } else {
+ DynamicBackward(retain_graph, state, inputs, reqs, outputs);
+ }
+ } catch (const dmlc::Error& e) {
+ Engine::Get()->set_bulk_size(prev_bulk_size);
+ throw e;
}
Engine::Get()->set_bulk_size(prev_bulk_size);
diff --git a/src/imperative/imperative.cc b/src/imperative/imperative.cc
index 0c5ff8417754..32ff8d338131 100644
--- a/src/imperative/imperative.cc
+++ b/src/imperative/imperative.cc
@@ -494,9 +494,16 @@ std::vector Imperative::Backward(
bool prev_training = set_is_training(is_train);
int prev_bulk_size = Engine::Get()->set_bulk_size(backward_bulk_size_);
- RunGraph(retain_graph, idx, arrays, num_forward_nodes, idx.num_nodes(),
- std::move(array_reqs), std::move(ref_count), &states, dispatch_modes,
- is_recording());
+ try {
+ RunGraph(retain_graph, idx, arrays, num_forward_nodes, idx.num_nodes(),
+ std::move(array_reqs), std::move(ref_count), &states, dispatch_modes,
+ is_recording());
+ } catch (const dmlc::Error& e) {
+ Engine::Get()->set_bulk_size(prev_bulk_size);
+ set_is_recording(prev_recording);
+ set_is_training(prev_training);
+ throw e;
+ }
Engine::Get()->set_bulk_size(prev_bulk_size);
set_is_recording(prev_recording);
diff --git a/src/io/image_aug_default.cc b/src/io/image_aug_default.cc
index bea2e2c0749e..01ccbb809482 100644
--- a/src/io/image_aug_default.cc
+++ b/src/io/image_aug_default.cc
@@ -209,7 +209,6 @@ class DefaultImageAugmenter : public ImageAugmenter {
public:
// contructor
DefaultImageAugmenter() {
- rotateM_ = cv::Mat(2, 3, CV_32F);
seed_init_state = false;
}
void Init(const std::vector >& kwargs) override {
@@ -550,8 +549,6 @@ class DefaultImageAugmenter : public ImageAugmenter {
private:
// temporal space
cv::Mat temp_;
- // rotation param
- cv::Mat rotateM_;
// eigval and eigvec for adding pca noise
// store eigval * eigvec as eigvec
float eigvec[3][3] = { { 55.46f * -0.5675f, 4.794f * 0.7192f, 1.148f * 0.4009f },
diff --git a/src/io/image_io.cc b/src/io/image_io.cc
index a996a2208d79..b3f7c40b2b1a 100644
--- a/src/io/image_io.cc
+++ b/src/io/image_io.cc
@@ -143,11 +143,8 @@ void ImdecodeImpl(int flag, bool to_rgb, void* data, size_t size,
cv::Mat dst;
if (out->is_none()) {
cv::Mat res = cv::imdecode(buf, flag);
- if (res.empty()) {
- LOG(INFO) << "Decoding failed. Invalid image file.";
- *out = NDArray();
- return;
- }
+ CHECK(!res.empty()) << "Decoding failed. Invalid image file.";
+
*out = NDArray(mshadow::Shape3(res.rows, res.cols, flag == 0 ? 1 : 3),
Context::CPU(), false, mshadow::kUint8);
dst = cv::Mat(out->shape()[0], out->shape()[1], flag == 0 ? CV_8U : CV_8UC3,
@@ -189,6 +186,8 @@ void Imdecode(const nnvm::NodeAttrs& attrs,
uint8_t* str_img = inputs[0].data().dptr();
size_t len = inputs[0].shape().Size();
+ CHECK(len > 0) << "Input cannot be an empty buffer";
+
TShape oshape(3);
oshape[2] = param.flag == 0 ? 1 : 3;
if (get_jpeg_size(str_img, len, &oshape[1], &oshape[0])) {
diff --git a/src/io/image_iter_common.h b/src/io/image_iter_common.h
index 8580ff8f9f9c..a2324a4b5c5b 100644
--- a/src/io/image_iter_common.h
+++ b/src/io/image_iter_common.h
@@ -42,7 +42,7 @@ class ImageLabelMap {
* \param label_width predefined label_width
*/
explicit ImageLabelMap(const char *path_imglist,
- mshadow::index_t label_width,
+ index_t label_width,
bool silent) {
this->label_width = label_width;
image_index_.clear();
@@ -58,7 +58,7 @@ class ImageLabelMap {
// skip space
while (isspace(*p) && p != end) ++p;
image_index_.push_back(static_cast(atol(p)));
- for (size_t i = 0; i < label_width; ++i) {
+ for (index_t i = 0; i < label_width; ++i) {
// skip till space
while (!isspace(*p) && p != end) ++p;
// skip space
@@ -171,7 +171,7 @@ struct ImageRecParserParam : public dmlc::Parameter {
// Batch parameters
struct BatchParam : public dmlc::Parameter {
/*! \brief label width */
- index_t batch_size;
+ uint32_t batch_size;
/*! \brief use round roubin to handle overflow batch */
bool round_batch;
// declare parameters
diff --git a/src/io/iter_image_det_recordio.cc b/src/io/iter_image_det_recordio.cc
index 62e362c17277..8bfded75f098 100644
--- a/src/io/iter_image_det_recordio.cc
+++ b/src/io/iter_image_det_recordio.cc
@@ -237,9 +237,11 @@ class ImageDetRecordIOParser {
std::unique_ptr label_map_;
/*! \brief temp space */
mshadow::TensorContainer img_;
+ /*! \brief OMPException obj to store and rethrow exceptions from omp blocks*/
+ dmlc::OMPException omp_exc_;
};
-template
+template
inline void ImageDetRecordIOParser::Init(
const std::vector >& kwargs) {
#if MXNET_USE_OPENCV
@@ -282,8 +284,9 @@ inline void ImageDetRecordIOParser::Init(
<< ", use " << threadget << " threads for decoding..";
}
source_.reset(dmlc::InputSplit::Create(
- param_.path_imgrec.c_str(), param_.part_index,
- param_.num_parts, "recordio"));
+ param_.path_imgrec.c_str(),
+ param_.part_index, param_.num_parts,
+ "recordio"));
// estimate padding width for labels
int max_label_width = 0;
@@ -295,38 +298,41 @@ inline void ImageDetRecordIOParser::Init(
while (source_->NextChunk(&chunk)) {
#pragma omp parallel num_threads(param_.preprocess_threads)
{
- CHECK(omp_get_num_threads() == param_.preprocess_threads);
- int max_width = 0;
- int tid = omp_get_thread_num();
- dmlc::RecordIOChunkReader reader(chunk, tid, param_.preprocess_threads);
- ImageRecordIO rec;
- dmlc::InputSplit::Blob blob;
- while (reader.NextRecord(&blob)) {
- rec.Load(blob.dptr, blob.size);
- if (rec.label != nullptr) {
- if (param_.label_width > 0) {
- CHECK_EQ(param_.label_width, rec.num_label)
- << "rec file provide " << rec.num_label << "-dimensional label "
- "but label_width is set to " << param_.label_width;
+ omp_exc_.Run([&] {
+ CHECK(omp_get_num_threads() == param_.preprocess_threads);
+ int max_width = 0;
+ int tid = omp_get_thread_num();
+ dmlc::RecordIOChunkReader reader(chunk, tid,
+ param_.preprocess_threads);
+ ImageRecordIO rec;
+ dmlc::InputSplit::Blob blob;
+ while (reader.NextRecord(&blob)) {
+ rec.Load(blob.dptr, blob.size);
+ if (rec.label != nullptr) {
+ if (param_.label_width > 0) {
+ CHECK_EQ(param_.label_width, rec.num_label)
+ << "rec file provide " << rec.num_label << "-dimensional label "
+ "but label_width is set to " << param_.label_width;
+ }
+ // update max value
+ max_width = std::max(max_width, rec.num_label);
+ } else {
+ LOG(FATAL) << "Not enough label packed in img_list or rec file.";
}
- // update max value
- max_width = std::max(max_width, rec.num_label);
- } else {
- LOG(FATAL) << "Not enough label packed in img_list or rec file.";
}
- }
- #pragma omp critical
- {
- max_label_width = std::max(max_label_width, max_width);
- }
+ #pragma omp critical
+ {
+ max_label_width = std::max(max_label_width, max_width);
+ }
+ });
}
+ omp_exc_.Rethrow();
}
}
if (max_label_width > param_.label_pad_width) {
if (param_.label_pad_width > 0) {
LOG(FATAL) << "ImageDetRecordIOParser: label_pad_width: "
- << param_.label_pad_width << " smaller than estimated width: "
- << max_label_width;
+ << param_.label_pad_width << " smaller than estimated width: " << max_label_width;
}
param_.label_pad_width = max_label_width;
}
@@ -336,19 +342,20 @@ inline void ImageDetRecordIOParser::Init(
}
source_.reset(dmlc::InputSplit::Create(
- param_.path_imgrec.c_str(), param_.part_index,
- param_.num_parts, "recordio"));
+ param_.path_imgrec.c_str(),
+ param_.part_index, param_.num_parts,
+ "recordio"));
if (param_.shuffle_chunk_size > 0) {
if (param_.shuffle_chunk_size > 4096) {
LOG(INFO) << "Chunk size: " << param_.shuffle_chunk_size
- << " MB which is larger than 4096 MB, please set "
- "smaller chunk size";
+ << " MB which is larger than 4096 MB, please set "
+ "smaller chunk size";
}
if (param_.shuffle_chunk_size < 4) {
LOG(INFO) << "Chunk size: " << param_.shuffle_chunk_size
- << " MB which is less than 4 MB, please set "
- "larger chunk size";
+ << " MB which is less than 4 MB, please set "
+ "larger chunk size";
}
// 1.1 ratio is for a bit more shuffle parts to avoid boundary issue
unsigned num_shuffle_parts =
@@ -381,92 +388,100 @@ ParseNext(std::vector> *out_vec) {
out_vec->resize(param_.preprocess_threads);
#pragma omp parallel num_threads(param_.preprocess_threads)
{
- CHECK(omp_get_num_threads() == param_.preprocess_threads);
- int tid = omp_get_thread_num();
- dmlc::RecordIOChunkReader reader(chunk, tid, param_.preprocess_threads);
- ImageRecordIO rec;
- dmlc::InputSplit::Blob blob;
- // image data
- InstVector &out = (*out_vec)[tid];
- out.Clear();
- while (reader.NextRecord(&blob)) {
- // Opencv decode and augments
- cv::Mat res;
- rec.Load(blob.dptr, blob.size);
- cv::Mat buf(1, rec.content_size, CV_8U, rec.content);
- switch (param_.data_shape[0]) {
- case 1:
- res = cv::imdecode(buf, 0);
- break;
- case 3:
- res = cv::imdecode(buf, 1);
- break;
- case 4:
- // -1 to keep the number of channel of the encoded image, and not force gray or color.
- res = cv::imdecode(buf, -1);
- CHECK_EQ(res.channels(), 4)
- << "Invalid image with index " << rec.image_index()
- << ". Expected 4 channels, got " << res.channels();
- break;
- default:
- LOG(FATAL) << "Invalid output shape " << param_.data_shape;
- }
- const int n_channels = res.channels();
- // load label before augmentations
- std::vector label_buf;
- if (this->label_map_ != nullptr) {
- label_buf = label_map_->FindCopy(rec.image_index());
- } else if (rec.label != nullptr) {
- if (param_.label_width > 0) {
- CHECK_EQ(param_.label_width, rec.num_label)
- << "rec file provide " << rec.num_label << "-dimensional label "
- "but label_width is set to " << param_.label_width;
+ omp_exc_.Run([&] {
+ CHECK(omp_get_num_threads() == param_.preprocess_threads);
+ int tid = omp_get_thread_num();
+ dmlc::RecordIOChunkReader reader(chunk, tid, param_.preprocess_threads);
+ ImageRecordIO rec;
+ dmlc::InputSplit::Blob blob;
+ // image data
+ InstVector &out = (*out_vec)[tid];
+ out.Clear();
+ while (reader.NextRecord(&blob)) {
+ // Opencv decode and augments
+ cv::Mat res;
+ rec.Load(blob.dptr, blob.size);
+ cv::Mat buf(1, rec.content_size, CV_8U, rec.content);
+ switch (param_.data_shape[0]) {
+ case 1:
+ res = cv::imdecode(buf, 0);
+ break;
+ case 3:
+ res = cv::imdecode(buf, 1);
+ break;
+ case 4:
+ // -1 to keep the number of channel of the encoded image, and not
+ // force gray or color.
+ res = cv::imdecode(buf, -1);
+ CHECK_EQ(res.channels(), 4)
+ << "Invalid image with index " << rec.image_index()
+ << ". Expected 4 channels, got " << res.channels();
+ break;
+ default:
+ LOG(FATAL) << "Invalid output shape " << param_.data_shape;
}
- label_buf.assign(rec.label, rec.label + rec.num_label);
- } else {
- LOG(FATAL) << "Not enough label packed in img_list or rec file.";
- }
- for (auto& aug : this->augmenters_[tid]) {
- res = aug->Process(res, &label_buf, this->prnds_[tid].get());
- }
- out.Push(static_cast(rec.image_index()),
- mshadow::Shape3(n_channels, param_.data_shape[1], param_.data_shape[2]),
- mshadow::Shape1(param_.label_pad_width + 4));
+ const int n_channels = res.channels();
+ // load label before augmentations
+ std::vector label_buf;
+ if (this->label_map_ != nullptr) {
+ label_buf = label_map_->FindCopy(rec.image_index());
+ } else if (rec.label != nullptr) {
+ if (param_.label_width > 0) {
+ CHECK_EQ(param_.label_width, rec.num_label)
+ << "rec file provide " << rec.num_label
+ << "-dimensional label "
+ "but label_width is set to "
+ << param_.label_width;
+ }
+ label_buf.assign(rec.label, rec.label + rec.num_label);
+ } else {
+ LOG(FATAL) << "Not enough label packed in img_list or rec file.";
+ }
+ for (auto &aug : this->augmenters_[tid]) {
+ res = aug->Process(res, &label_buf, this->prnds_[tid].get());
+ }
+ out.Push(static_cast(rec.image_index()),
+ mshadow::Shape3(n_channels, param_.data_shape[1],
+ param_.data_shape[2]),
+ mshadow::Shape1(param_.label_pad_width + 4));
- mshadow::Tensor data = out.data().Back();
+ mshadow::Tensor data = out.data().Back();
- // For RGB or RGBA data, swap the B and R channel:
- // OpenCV store as BGR (or BGRA) and we want RGB (or RGBA)
- std::vector swap_indices;
- if (n_channels == 1) swap_indices = {0};
- if (n_channels == 3) swap_indices = {2, 1, 0};
- if (n_channels == 4) swap_indices = {2, 1, 0, 3};
+ // For RGB or RGBA data, swap the B and R channel:
+ // OpenCV store as BGR (or BGRA) and we want RGB (or RGBA)
+ std::vector swap_indices;
+ if (n_channels == 1) swap_indices = {0};
+ if (n_channels == 3) swap_indices = {2, 1, 0};
+ if (n_channels == 4) swap_indices = {2, 1, 0, 3};
- for (int i = 0; i < res.rows; ++i) {
- uchar* im_data = res.ptr