Vicente Rodríguez

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:

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.