From 001a4024244a5f171bf22b782a0c31b7b8e3cbc6 Mon Sep 17 00:00:00 2001 From: Helin Wang Date: Tue, 12 Sep 2017 16:13:30 -0700 Subject: [PATCH] Add Paddle Serve example, and MNIST client example --- mnist-client/.eslintrc | 26 +++++ mnist-client/.gitignore | 5 + mnist-client/Procfile | 1 + mnist-client/README.md | 20 ++++ mnist-client/app.json | 7 ++ mnist-client/gulpfile.js | 19 +++ mnist-client/main.py | 13 +++ mnist-client/package.json | 29 +++++ mnist-client/requirements.txt | 1 + mnist-client/runtime.txt | 1 + mnist-client/src/js/main.js | 136 ++++++++++++++++++++++ mnist-client/static/css/bootstrap.min.css | 1 + mnist-client/static/js/jquery.min.js | 1 + mnist-client/templates/index.html | 76 ++++++++++++ serve/.gitignore | 3 + serve/README.md | 11 ++ serve/main.py | 60 ++++++++++ serve/requirements.txt | 2 + 18 files changed, 412 insertions(+) create mode 100644 mnist-client/.eslintrc create mode 100644 mnist-client/.gitignore create mode 100644 mnist-client/Procfile create mode 100644 mnist-client/README.md create mode 100644 mnist-client/app.json create mode 100644 mnist-client/gulpfile.js create mode 100644 mnist-client/main.py create mode 100644 mnist-client/package.json create mode 100644 mnist-client/requirements.txt create mode 100644 mnist-client/runtime.txt create mode 100644 mnist-client/src/js/main.js create mode 120000 mnist-client/static/css/bootstrap.min.css create mode 120000 mnist-client/static/js/jquery.min.js create mode 100644 mnist-client/templates/index.html create mode 100644 serve/.gitignore create mode 100644 serve/README.md create mode 100644 serve/main.py create mode 100644 serve/requirements.txt diff --git a/mnist-client/.eslintrc b/mnist-client/.eslintrc new file mode 100644 index 0000000..0a49151 --- /dev/null +++ b/mnist-client/.eslintrc @@ -0,0 +1,26 @@ +{ + "rules": { + "indent": [ + 2, + 4 + ], + "quotes": [ + 2, + "single" + ], + "linebreak-style": [ + 2, + "unix" + ], + "semi": [ + 2, + "always" + ] + }, + "env": { + "es6": true, + "node": true, + "browser": true + }, + "extends": "eslint:recommended" +} \ No newline at end of file diff --git a/mnist-client/.gitignore b/mnist-client/.gitignore new file mode 100644 index 0000000..e96aff1 --- /dev/null +++ b/mnist-client/.gitignore @@ -0,0 +1,5 @@ +venv +*.pyc +node_modules +static/js/main.js +index.html diff --git a/mnist-client/Procfile b/mnist-client/Procfile new file mode 100644 index 0000000..5e85b13 --- /dev/null +++ b/mnist-client/Procfile @@ -0,0 +1 @@ +web: gunicorn main:app --log-file=- diff --git a/mnist-client/README.md b/mnist-client/README.md new file mode 100644 index 0000000..ade569f --- /dev/null +++ b/mnist-client/README.md @@ -0,0 +1,20 @@ +# MNIST classification by PaddlePaddle + +Forked from https://github.com/sugyan/tensorflow-mnist + +![screencast](https://cloud.githubusercontent.com/assets/80381/11339453/f04f885e-923c-11e5-8845-33c16978c54d.gif) + +## Build + + $ docker build -t paddle-mnist . + +## Usage + + +1. Download `inference_topology.pkl` and `param.tar` to current directory +1. Run following commands: +```bash +docker run -v `pwd`:/data -d -p 8000:80 -e WITH_GPU=0 paddlepaddle/book:serve +docker run -it -p 5000:5000 paddlepaddle/book:mnist +``` +1. Visit http://localhost:5000 diff --git a/mnist-client/app.json b/mnist-client/app.json new file mode 100644 index 0000000..94e383a --- /dev/null +++ b/mnist-client/app.json @@ -0,0 +1,7 @@ +{ + "name": "paddlepaddle-mnist", + "buildpacks": [ + { "url": "https://github.com/heroku/heroku-buildpack-nodejs" }, + { "url": "https://github.com/heroku/heroku-buildpack-python" } + ] +} diff --git a/mnist-client/gulpfile.js b/mnist-client/gulpfile.js new file mode 100644 index 0000000..7758098 --- /dev/null +++ b/mnist-client/gulpfile.js @@ -0,0 +1,19 @@ +var gulp = require('gulp'); +var babel = require('gulp-babel'); +var sourcemaps = require('gulp-sourcemaps'); +var uglify = require('gulp-uglify'); + +gulp.task('build', function() { + return gulp.src('src/js/*.js') + .pipe(babel({ presets: ['es2015'] })) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(uglify()) + .pipe(sourcemaps.write()) + .pipe(gulp.dest('static/js')); +}); + +gulp.task('watch', function() { + gulp.watch('src/js/*.js', ['build']); +}); + +gulp.task('default', ['build']); diff --git a/mnist-client/main.py b/mnist-client/main.py new file mode 100644 index 0000000..88f162d --- /dev/null +++ b/mnist-client/main.py @@ -0,0 +1,13 @@ +from flask import Flask, jsonify, render_template, request + +# webapp +app = Flask(__name__) + + +@app.route('/') +def main(): + return render_template('index.html') + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, threaded=True) diff --git a/mnist-client/package.json b/mnist-client/package.json new file mode 100644 index 0000000..9cab177 --- /dev/null +++ b/mnist-client/package.json @@ -0,0 +1,29 @@ +{ + "name": "paddlepaddle-mnist", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "postinstall": "gulp" + }, + "keywords": [], + "author": "", + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/sugyan/tensorflow-mnist.git" + }, + "engines": { + "node": "6.x" + }, + "dependencies": { + "babel-preset-es2015": "^6.1.18", + "bootstrap": "^3.3.5", + "gulp": "^3.9.0", + "gulp-babel": "^6.1.0", + "gulp-sourcemaps": "^1.6.0", + "gulp-uglify": "^1.5.1", + "jquery": "^2.1.4" + } +} diff --git a/mnist-client/requirements.txt b/mnist-client/requirements.txt new file mode 100644 index 0000000..4a5cb4c --- /dev/null +++ b/mnist-client/requirements.txt @@ -0,0 +1 @@ +Flask==0.12 diff --git a/mnist-client/runtime.txt b/mnist-client/runtime.txt new file mode 100644 index 0000000..80aea67 --- /dev/null +++ b/mnist-client/runtime.txt @@ -0,0 +1 @@ +python-3.6.0 \ No newline at end of file diff --git a/mnist-client/src/js/main.js b/mnist-client/src/js/main.js new file mode 100644 index 0000000..53ac0f7 --- /dev/null +++ b/mnist-client/src/js/main.js @@ -0,0 +1,136 @@ +/* global $ */ +class Main { + constructor() { + this.canvas = document.getElementById('main'); + this.input = document.getElementById('input'); + this.canvas.width = 449; // 16 * 28 + 1 + this.canvas.height = 449; // 16 * 28 + 1 + this.ctx = this.canvas.getContext('2d'); + this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); + this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this)); + this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this)); + this.initialize(); + } + initialize() { + this.ctx.fillStyle = '#FFFFFF'; + this.ctx.fillRect(0, 0, 449, 449); + this.ctx.lineWidth = 1; + this.ctx.strokeRect(0, 0, 449, 449); + this.ctx.lineWidth = 0.05; + for (var i = 0; i < 27; i++) { + this.ctx.beginPath(); + this.ctx.moveTo((i + 1) * 16, 0); + this.ctx.lineTo((i + 1) * 16, 449); + this.ctx.closePath(); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo( 0, (i + 1) * 16); + this.ctx.lineTo(449, (i + 1) * 16); + this.ctx.closePath(); + this.ctx.stroke(); + } + this.drawInput(); + $('#output td').text('').removeClass('success'); + } + onMouseDown(e) { + this.canvas.style.cursor = 'default'; + this.drawing = true; + this.prev = this.getPosition(e.clientX, e.clientY); + } + onMouseUp() { + this.drawing = false; + this.drawInput(); + } + onMouseMove(e) { + if (this.drawing) { + var curr = this.getPosition(e.clientX, e.clientY); + this.ctx.lineWidth = 16; + this.ctx.lineCap = 'round'; + this.ctx.beginPath(); + this.ctx.moveTo(this.prev.x, this.prev.y); + this.ctx.lineTo(curr.x, curr.y); + this.ctx.stroke(); + this.ctx.closePath(); + this.prev = curr; + } + } + getPosition(clientX, clientY) { + var rect = this.canvas.getBoundingClientRect(); + return { + x: clientX - rect.left, + y: clientY - rect.top + }; + } + drawInput() { + var ctx = this.input.getContext('2d'); + var img = new Image(); + img.onload = () => { + var inputs = []; + var small = document.createElement('canvas').getContext('2d'); + small.drawImage(img, 0, 0, img.width, img.height, 0, 0, 28, 28); + var data = small.getImageData(0, 0, 28, 28).data; + for (var i = 0; i < 28; i++) { + for (var j = 0; j < 28; j++) { + var n = 4 * (i * 28 + j); + inputs[i * 28 + j] = (data[n + 0] + data[n + 1] + data[n + 2]) / 3; + ctx.fillStyle = 'rgb(' + [data[n + 0], data[n + 1], data[n + 2]].join(',') + ')'; + ctx.fillRect(j * 5, i * 5, 5, 5); + } + } + if (Math.min(...inputs) === 255) { + return; + } + for (var i = 0; i < 784; i++) { + if (inputs[i] == 255) { + // background + inputs[i] = -1.0 + } else { + inputs[i] = 1.0 + } + } + $.ajax({ + url: 'http://localhost:8000/', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({"img":inputs}), + success: (data) => { + data = data["data"][0] + var max = 0; + var max_index = 0; + for (let j = 0; j < 10; j++) { + var value = Math.round(data[j] * 1000); + if (value > max) { + max = value; + max_index = j; + } + var digits = String(value).length; + for (var k = 0; k < 3 - digits; k++) { + value = '0' + value; + } + var text = '0.' + value; + if (value > 999) { + text = '1.000'; + } + $('#output tr').eq(j + 1).find('td').text(text); + } + for (let j = 0; j < 10; j++) { + if (j === max_index) { + $('#output tr').eq(j + 1).find('td').addClass('success'); + } else { + $('#output tr').eq(j + 1).find('td').removeClass('success'); + } + } + } + }); + }; + img.src = this.canvas.toDataURL(); + } +} + +$(() => { + var main = new Main(); + $('#clear').click(() => { + main.initialize(); + }); +}); diff --git a/mnist-client/static/css/bootstrap.min.css b/mnist-client/static/css/bootstrap.min.css new file mode 120000 index 0000000..93c3bac --- /dev/null +++ b/mnist-client/static/css/bootstrap.min.css @@ -0,0 +1 @@ +../../node_modules/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file diff --git a/mnist-client/static/js/jquery.min.js b/mnist-client/static/js/jquery.min.js new file mode 120000 index 0000000..08ac9f2 --- /dev/null +++ b/mnist-client/static/js/jquery.min.js @@ -0,0 +1 @@ +../../node_modules/jquery/dist/jquery.min.js \ No newline at end of file diff --git a/mnist-client/templates/index.html b/mnist-client/templates/index.html new file mode 100644 index 0000000..115fc86 --- /dev/null +++ b/mnist-client/templates/index.html @@ -0,0 +1,76 @@ + + + + MNIST + + + + + + Fork me on GitHub +
+

MNIST

+
+
+

draw a digit here!

+ +

+ +

+
+
+

input:

+ +
+

output:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
classconfidence
0
1
2
3
4
5
6
7
8
9
+
+
+
+ + diff --git a/serve/.gitignore b/serve/.gitignore new file mode 100644 index 0000000..284f7c4 --- /dev/null +++ b/serve/.gitignore @@ -0,0 +1,3 @@ +*~ +.idea +index.html diff --git a/serve/README.md b/serve/README.md new file mode 100644 index 0000000..06bc0d5 --- /dev/null +++ b/serve/README.md @@ -0,0 +1,11 @@ +# PaddlePaddle Serving Example + + +## Build + + $ docker build -t serve . + +## Run + + $ docker run -v `pwd`:/data -it -p 8000:80 -e WITH_GPU=0 paddlepaddle/book:serve + $ curl -H "Content-Type: application/json" -X POST -d '{"img":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]}' http://localhost:8000/ diff --git a/serve/main.py b/serve/main.py new file mode 100644 index 0000000..3d9f4be --- /dev/null +++ b/serve/main.py @@ -0,0 +1,60 @@ +import os +import traceback + +import paddle.v2 as paddle +from flask import Flask, jsonify, request +from flask_cors import CORS + +tarfn = os.getenv('PARAMETER_TAR_PATH', None) + +if tarfn is None: + raise ValueError( + "please specify parameter tar file path with environment variable PARAMETER_TAR_PATH" + ) + +topology_filepath = os.getenv('TOPOLOGY_FILE_PATH', None) + +if topology_filepath is None: + raise ValueError( + "please specify topology file path with environment variable TOPOLOGY_FILE_PATH" + ) + +with_gpu = os.getenv('WITH_GPU', '0') != '0' + +port = int(os.getenv('PORT', '80')) + +app = Flask(__name__) +CORS(app) + + +def errorResp(msg): + return jsonify(code=-1, message=msg) + + +def successResp(data): + return jsonify(code=0, message="success", data=data) + + +@app.route('/', methods=['POST']) +def infer(): + global inferer + try: + feeding = {} + d = [] + for i, key in enumerate(request.json): + d.append(request.json[key]) + feeding[key] = i + r = inferer.infer([d], feeding=feeding) + except: + trace = traceback.format_exc() + return errorResp(trace) + return successResp(r.tolist()) + + +if __name__ == '__main__': + paddle.init(use_gpu=with_gpu) + with open(tarfn) as param_f, open(topology_filepath) as topo_f: + params = paddle.parameters.Parameters.from_tar(param_f) + inferer = paddle.inference.Inference(parameters=params, fileobj=topo_f) + print 'serving on port', port + app.run(host='0.0.0.0', port=port, threaded=True) diff --git a/serve/requirements.txt b/serve/requirements.txt new file mode 100644 index 0000000..8efb58a --- /dev/null +++ b/serve/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.12.2 +Flask-CORS==3.0.3 -- GitLab