Dec. 18, 2018
Cómo hacer deploy de un modelo de keras
En este tutorial aprenderemos a hacer deploy de un modelo de keras en un servidor para que este disponible mediante una página web. Es recomendable tener conocimientos básicos sobre la terminal de linux o macOS y de como funcionan las páginas web. Usaremos flask, un framework de python para crear páginas dinamicas, javascript para crear las peticiones al servidor sin necesidad de recargar la página web y los servidores de Google Cloud para ejecutar el modelo con una GPU.
El siguiente link es el ejemplo de la app corriendo en heroku (La primera vez tarda en cargar unos segundos). Heroku es un servicio para subir páginas web y tienen una versión gratuita con 512mb de ram, el modelo que usaremos clasifica coches de la marca tesla, es un modelo muy ligero que puede correr sin problemas en hardware poco potente, pueden descargar el modelo y los pesos en el siguiente link, es un modelo que creamos en este tutorial, también pueden usar su propio modelo, solo necesitan guardar el modelo en formato json y los pesos en formato h5.
La app web
Empezaremos construyendo la app y ejecutandola en un entorno local, yo usare macOS para ejecutar la app de flask y el modelo en keras. Si estas usando otro sistema operativo y tienes problemas puedes mandarme un mensaje para pedir ayuda. El código de la app esta en este repositorio de github. Recomiendo descargarlo como Zip.
Es necesario tener instalado keras, python 3.6, tensorflow y flask.
Primero explicare el código por partes y despues mostrare como se ve el archivo app.py finalizado.
Flask y Python
Importamos las librerías necesarias:
#librerías para la app web
from flask import Flask, render_template, request, jsonify
#librerías para cargar el modelo con keras
from keras.models import model_from_json
#librerías para cargar la imagen
from PIL import Image
from io import BytesIO
from keras.preprocessing.image import img_to_array
import numpy as np
Tenemos las librerías para que la app web funcione, las librerías para cargar el modelo con keras y las librerías para cargar imagenes.
Ahora crearemos las funciones que se encargaran de recibir las peticiones del usuario en la página web:
app = Flask(__name__)
model = None
@app.route("/")
def index():
return render_template("index.html")
@app.route("/predict", methods=["POST"])
def predict():
image = request.files["image"].read()
image = load_request_image(image)
class_predicted = predict_class(image)
image_class = { "image_class": class_predicted }
return jsonify(image_class)
En la variable app tendremos acceso a los metodos de flask, la variable modelo es donde guardaremos la referencia al modelo tesla para poder usarlo en cualquier parte del código.
Las partes importantes son las funciones index y predict. index recibe la petición del usuario en la ruta principal "/" si nuestra pagina se llama modelotesla.com, cuando el usuario se dirija a esa dirección se ejecutara la función index, esta función muestra una pagina en html con render_template. La pagina html tiene que estar adentro de una carpeta llamada templates:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Tesla App</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="{{ url_for('static', filename='script.js') }}"></script>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<section class="header">
<h2>Tesla app</h2>
<p>Sube la imagen de un coche tesla:</p>
<input type="file" id="image" accept="image/*">
</section>
<section class="content">
<div class="image_content">
<img src="" alt="" id="imageContainer">
</div>
<p id="imageClass">
</p>
</section>
</body>
</html>
index.html
Este código está escrito en html y sirve para mostrar al usuario la página web, si no sabes de html la parte importante es la siguiente línea:
<input type="file" id="image" accept="image/*">
esta código crea un boton para pedirle al usuario una imagen de un coche tesla para despues cargarla con javascript y mandarla al servidor.
La segunda función en python predict recibe la imagen de un coche tesla para predecir el modelo del coche y despues regresar esta predicción. Aquí nos encontramos con dos funciones más load_request_image(image) y predict_class(image), la primera función se encarga de cargar la imagen con python:
def load_request_image(image):
image = Image.open(BytesIO(image))
if image.mode != "RGB":
image = image.convert("RGB")
image = image.resize((256, 256))
image = img_to_array(image)
image = np.expand_dims(image, axis=0)
image = image.astype('float32') / 255
return image
La función carga la imagen, la cambia de tamaño a 256x256 (recordemos que el modelo acepta imagenes con esas dimensiones) y la convierte a un array de numpy para usarla en el modelo.
Ya que tenemos la imagen procesada en la variable image, ejecutamos la función predict_class:
def predict_class(image_array):
clases = ["Model 3", "Model S", "Model X"]
y_pred = model.predict(image_array, batch_size=None, verbose=0, steps=None)
max_score = np.argmax(y_pred, axis=1)[0]
image_class = clases[max_score]
return image_class
Está función ejecuta model.predict para asignarle una clase a la imagen, con np.argmax tomamos la clase con mayor puntaje, que puede ser [0, 1, 2] y por ejemplo si la clase con mayor puntaje resulta ser 1, en image_class = clases[max_score] tomaremos el segundo texto del array clases = ["Model 3", "Model S", "Model X"].
Por ultimo regresamos el texto con la clase asignada para que javascript la muestre en la pagina html.
Nos faltan ver dos fragmentos de código, uno es la función para cargar el modelo:
def load_model():
json_file = open('./model/model.json', 'r')
model_json = json_file.read()
json_file.close()
global model
model = model_from_json(model_json)
model.load_weights("./model/weights.h5")
La función le asigna el modelo cargado a la variable model que vimos anteriormente, global es necesario para indicar que nos referimos a la variable que se creo anteriormente y no a una nueva variable.
El ultimo fragmento sirve para iniciar la app web de flask y para cargar el modelo:
if __name__ == "__main__":
load_model()
app.run(debug = False, threaded = False)
Con app.run flask empieza a cargar lo necesario para la app web.
El código completo de app.py es el siguiente:
#librerías para la app web
from flask import Flask, render_template, request, jsonify
#librerías para cargar el modelo con keras
from keras.models import model_from_json
#librerías para cargar la imagen
from PIL import Image
from io import BytesIO
from keras.preprocessing.image import img_to_array
import numpy as np
app = Flask(__name__)
model = None
def load_request_image(image):
image = Image.open(BytesIO(image))
if image.mode != "RGB":
image = image.convert("RGB")
image = image.resize((256, 256))
image = img_to_array(image)
image = np.expand_dims(image, axis=0)
image = image.astype('float32') / 255
return image
def load_model():
json_file = open('./model/model.json', 'r')
model_json = json_file.read()
json_file.close()
global model
model = model_from_json(model_json)
model.load_weights("./model/weights.h5")
def predict_class(image_array):
clases = ["Model 3", "Model S", "Model X"]
y_pred = model.predict(image_array, batch_size=None, verbose=0, steps=None)
max_score = np.argmax(y_pred, axis=1)[0]
image_class = clases[max_score]
return image_class
@app.route("/")
def index():
return render_template("index.html")
@app.route("/predict", methods=["POST"])
def predict():
# print(request.headers)
image = request.files["image"].read()
image = load_request_image(image)
class_predicted = predict_class(image)
image_class = { "image_class": class_predicted }
return jsonify(image_class)
if __name__ == "__main__":
load_model()
app.run(debug = False, threaded = False)
Javascript
Este script es el encargado de mandar la imagen que subamos a Flask y de recibir la clase del modelo para mostrarla en la página.
Primero tenemos que entender que javascript funciona con eventos, nosotros queremos escuchar dos eventos, el primero de ellos es cuando la página web se cargo por completo y el segundo es cuando alguien selecciona una imagen.
document.addEventListener("DOMContentLoaded", ready);
function ready() {
const inputFile = document.querySelector("#image");
inputFile.addEventListener('change', imageUploaded);
}
Pedimos al evento DOMContentLoaded que cuando ocurra ejecute la función ready, en esta función pedimos "escuchar" al evento change del elemento que contiene la imagen inputFile, cuando ocurra este evento queremos que se ejecute la función imageUploaded
function imageUploaded(event) {
const target = event.target;
const image = target.files[0];
if (!image) return;
const imageContainer = document.querySelector("#imageContainer");
imageContainer.src = window.URL.createObjectURL(image);
get_prediction(image);
}
Aquí nos aseguramos de que la imagen se haya seleccionado correctamente y si la imagen existe la imprimimos en la página web con imageContainer.src = window.URL.createObjectURL(image) y ejecutamos la función get_prediction:
function get_prediction(image) {
const formData = new FormData()
formData.append('image', image);
fetch("/predict", {
method: "POST",
body: formData
})
.then(response => {
response.json().then(data => {
const imageClassContainer = document.querySelector("#imageClass");
const imageClass = data["image_class"];
imageClassContainer.innerHTML = `Tesla: ${imageClass}`
});
})
.catch(error => {
console.log("Hubo un error :c");
});
}
Esta función se encarga de mandar la imagen a Flask y despues espera una respuesta, la respuesta es la clase del coche que guardamos en python como { "image_class": class_predicted }, entonces para acceder a esta información en forma de diccionario usamos data["image_class"], despues procedemos a mostrar la clase en la página web con imageClassContainer.innerHTML = Tesla: ${imageClass}
El archivo final tiene que llamarse script.js y estar dentro de una carpeta llamada static.
CSS
Para finalizar la app agregaremos estilos para que se vea un poco mejor la página web:
body {
font-family: helvetica;
margin: 0;
padding: 0;
}
.header {
width: 70%;
margin: 0 auto;
}
h2 {
font-size: 2em;
}
.content {
width: 70%;
margin: 0 auto;
}
.image_content {
margin: 20px auto;
width: 300px;
height: 200px;
}
#imageContainer {
width: 300px;
height: 200px;
}
style.css
No es necesario preocuparse por este archivo, solo tiene que estar dentro de la carpeta static junto con script.js.
Ejecutar app.py
Para iniciar la web app es necesario estar en el mismo directorio que el archivo app.py y escribir:
python3 app.py
la app web iniciará y estará disponible en http://127.0.0.1:5000/, ahora solo falta usar una imagen de algun coche tesla para probar que todo funciona bien.
Heroku
Ya que tenemos la app web lista la subiremos al primer servidor. Subir una app a heroku es muy sencillo ya que no tenemos que preocuparnos por administrar el servidor, la parte "dificil" es instalar todo lo necesario y es complicado si no tienes conocimientos en la terminal de linux. En este link esta la documentación para instalar heroku, también usaremos git, en la misma documentación se encuentran los pasos para instalarlo, despues tenemos que crear una app en el dashboard de heroku, el nombre de la app es importante ya que es el que aparecera en la url.
Lo primero que tenemos que hacer es crear una carpeta model y poner adentro la arquitectura model.json y los pesos weights.h5 del modelo y si descargaron el código de github tenemosque modificar el archivo .gitignore y eliminar la segunda línea, la que dice model. En heroku no podemos subir archivos extra de forma gratuita, entonces subiremos las partes del modelo junto con el código aunque esto sea una mala practica. Cuando usemos Google Cloud lo haremos de manera diferente.
Tenemos que agregar dos archivos más:
gunicorn==19.9.0
Keras==2.2.4
tensorflow==1.12.0
numpy==1.15.4
Flask==1.0.2
Pillow==3.0.0
requirements.txt
Este archivo sirve para indicar que librerías necesitaremos instalar.
web: gunicorn app:app
Procfile
Este archivo indica que usaremos gunicorn para ejecutar la app web.
para que keras pueda cargar el modelo cuando usamos gunicorn necesitamos agregar un bloque de código a app.py, ese bloque va al final del archivo:
if __name__ == "app":
load_model()
Git
Git es una herramienta para controlar versiones, la idea es ir guardando los diferentes cambios que hagamos al código y tener la posibilidad de regresar a un estado anterior, de esta manera podemos experimentar con el código o agregar funcionalidades que pueden romper la applicación sin preocuparnos de perder el trabajo anterior. No explicaré más detalles pero recomiendo mucho aprender git ya que es una herramienta muy útil e importante.
Solo necesitaremos ejecutar unos cuantos comandos (todos ellos se ejecutan en la misma carpeta donde esta la app):
git init
Este comando inicia un repositorio que empieza a observar los cambios que hagamos en el código.
git add .
Con este comando agregamos los archivos que tengan cambios.
git commit -m "app completa"
Con este comando guardamos los cambios para poder moverlos a un servidor, subirlos a github o poder regresar a ellos si hacemos más cambios posteriormente, también tenemos que escribir un mensaje indicando los cambios que hemos hecho.
heroku git:remote -a app
Ahora agregamos una referencia a heroku para poder subir el código a los servidores de heroku. app tiene que ser el nombre de su app en heroku.
git push heroku master
Con el ultimo comando subimos la app web a los servidores de heroku, si todo sale bien aparecera un mensaje indicando la url de la app.
Google Cloud
Para usar este servicio es necesario tener una cuenta de Google, si eres nuevo te regalan 300 dolares para usar durante un año, recomiendo que una vez la app este en el servidor y todo este funcionando borren el servidor para evitar que siga cobrando, como usaremos una GPU el costo sera muy alto y no vale la pena tener un servidor tan potente con un modelo que puede correr en cualquier hardware.
Lo primero que haremos es dirigirnos a Google console, usaremos el servicio llamado Compute Engine que sirve para crear servidores.
Usamos la siguiente configuración:
Tenemos que elegir en disco de arranque ubuntu 16.04 y en capacidad 15gb, permitir el trafico http y https.
Ahora en la pestaña que dice seguridad tenemos que añadir una clave ssh para poder conectarnos al servidor de manera fácil y segura.
En la terminal ejecutamos:
cd ./.ssh
Primero accedemos a la carpeta ssh que es donde se guardan las claves ssh.
ssh-keygen -t rsa -b 4096 -C "email@example.com"
remplazamos email@example.com con su email, la parte de email sera el nombre de usuario del servidor, de preferencia usen uno que no sea muy complejo.
cuando nos pida un nombre para el archivo pondremos google_cloud, las siguientes dos opciones las dejamos vacias.
Una vez creada la clave necesitaremos verla para agregarla a Google Cloud:
cat google_cloud.pub
copiamos la clave en la sección seguridad y finalizamos la creación del servidor.
Acceder al servidor
Para conectartos al servidor en una terminal escribimos:
ssh -i ./.ssh/google_cloud [username]@[ip]
donde username es el nombre de usuario del correo antes de @ e ip es la ip externa que aparece en la pestaña Instancias de VM de Google Cloud.
Una vez dentro del servidor tenemos que actualizarlo:
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install -y build-essential
instalaremos miniconda ya que viene con las librerías que necesitaremos
curl -O https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
source ~/.bashrc
Aceptaremos todo con "yes"
Para saber si tenemos la GPU añadida:
lspci | grep -i nvidia
00:04.0 3D controller: NVIDIA Corporation GK210GL Tesla K80 (rev a1)
Cuda
Primero instalaremos cuda que es una herramienta que permite a tensorflow acceder a la GPU del sistema. La instalamos de la siguiente manera:
curl -O https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/cuda-repo-ubuntu1604_9.2.88-1_amd64.deb
descargamos cuda.
para instalar los paquetes:
sudo dpkg -i cuda-repo-ubuntu1604_9.2.88-1_amd64.deb
Tal vez pida ejecutar un comando extra:
sudo apt-key adv --fetch-keys http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/7fa2af80.pub
si es el caso despues de instalar volvemos a ejecutar el comando:
sudo dpkg -i cuda-repo-ubuntu1604_9.2.88-1_amd64.deb
Ahora tendremos que instalar cuda:
sudo apt-get update
sudo apt-get install
Para ver si todo salio bien:
nvidia-smi
Este comando devuelve una tabla con información sobre la instalación.
cuDNN
La siguiente instalación es cuDNN que también es una herramienta que ayuda a tensorflow a comunicarse con la GPU.
Lo que haremos es descargar la librería desde la pagina de nvidia, necesitamos dos archivos cuDNN Runtime Library for Ubuntu16.04 (Deb) y cuDNN Developer Library for Ubuntu16.04 (Deb). (Por ahora la versión 10.0 es la ultima que ha salido y es la que usaremos) Es necesario estar registrado para descargar. Una vez tengamos los archivos tenemos que subirlos al servidor:
scp -i ~/.ssh/google_cloud libcudnn7_7.4.2.24-1+cuda10.0_amd64.deb [username]@[ip]:/home/[username]
scp -i ~/.ssh/google_cloud libcudnn7-dev_7.4.2.24-1+cuda10.0_amd64.deb [username]@[ip]:/home/[username]
Este comando se ejecuta en la computadora local en la carpeta donde esten los archivos descargados.
Ahora en el servidor instalamos ambos archivos:
sudo dpkg -i libcudnn7_7.4.2.24-1+cuda10.0_amd64.deb
sudo dpkg -i libcudnn7-dev_7.4.2.24-1+cuda10.0_amd64.deb
El primero en instalarse es el que no tiene la terminación dev.
Tensorflow
Creamos una carpeta llamada project dentro de /home/[username]:
mkdir project
Ahora para instalar la version de tensorflow compatible con la GPU primero ejecutamos dentro de la carpeta project:
conda create -n teslapp_conda python=3.5
Este comando crea un entorno virtual con python 3.5 que es compatible con tensorflow-gpu.
Para activar el entorno virtual:
source activate teslapp_conda
Ahora para instalar tensorflow:
pip install tensorflow-gpu
Para verificar que la instalación sea correcta escribimos los siguientes comandos en orden:
python
import tensorflow as tf
from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())
para salir usamos:
exit()
Tesla App
Tenemos que descargar los archivos de la app:
git clone https://github.com/vincent1bt/keras-teslapp.git
y el modelo con los pesos, si tienen su propio modelo pueden subirlo con el siguiente comando:
scp -i ~/.ssh/google_cloud model.zip [username]@35.192.221.152:/home/[username]/project/keras-teslapp
donde model.zip pueden remplazarlo con la ruta donde se encuentra el modelo.
Para descomprimir la carpeta:
unzip model.zip
Tal vez sea necesario instalar zip:
sudo apt-get install zip
Instalamos las librerías necesarias para que la app web funcione:
pip install Flask
pip install Pillow
pip install keras
pip install gunicorn
Instalamos Nginx para poder acceder a la app desde una computadora externa:
sudo apt-get install nginx
Ahora tenemos que configurar nginx:
sudo vim /etc/nginx/sites-enabled/default
en server hay que comentar todo excepto:
listen 80 default_server;
listen [::]:80 default_server;
y en location / hay que agregar y comentar:
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
# try_files $uri $uri/ =404;
proxy_pass http://127.0.0.1:8000;
}
reiniciamos Nginx para guardar los cambios:
sudo service nginx restart
por ultimo ejecutamos gunicorn para iniciar la app:
gunicorn app:app -b localhost:8000 &
No tenemos que modificar ningun archivo ya que keras automaticamente usara la GPU que este disponible.
Nos dirigimos a la misma ip que usamos para acceder al servidor y tendremos nuestra app disponible al igual que en heroku.
Despues de probar la app y jugar un poco con ella recomiendo eliminar el servidor ya que el costo de tener una GPU es muy elevado.
Esta fue una manera sencilla de subir un proyecto de machine learning a la web, se pueden hacer muchos cambios y mejorar el proceso de instalación, podemos usar docker para automatizar una parte del proceso y con git podemos tener siempre actualizada la ultima versión del código.