Machine Learning para la web con TensorFlow.js


Hace apenas un par de semanas Google lanzó TensorFlow.js, un port casi completo de la librería TensorFlow para navegadores.

Con esta excusa, y animado por otros compañeros de la carrera, me decidí a introducirme en el mundo del Machine Learning creando una pequeña aplicación web movida por inteligencia artificial. Este es el resultado.

Hotdog or Not Hotdog para desarrolladores web

El proyecto que me decidí a hacer fue una sencilla aplicación que va por el camino de convertirse en el “Hola Mundo” por excelencia del Machine Learning: “Hotdog or Not Hotdog”.

Para los que no hayan visto la serie Silicon Valley, esta es una escena del capítulo 4×04 que muestra la app en acción:

Como puedes ver, el cometido de la aplicación esta bien claro: distinguir imágenes de perritos calientes de otras imágenes. Y, aunque en un principio parezca complicado de programar, lo cierto es que gracias a TensorFlow.js apenas tenemos que escribir código para obtener el resultado deseado.

Clasificando imágenes con MobileNet

Antes de nada necesitamos un modelo que sea capaz de obtener una serie de píxeles como entrada y devolver los objetos reconocidos. La arquitectura de Machine Learning por la que he optado es MobileNet al ser la más utilizada para este tipo de procesamiento (clasificación de imágenes).

Ahora solo faltaría descargar una implementación de MobileNet de entre las múltiples existentes en GitHub y entrenar el modelo con fotografías de perritos calientes. Muchas fotografías.

Pero obviamente no voy a hacer eso cuando ya existen modelos open source de la familia MobileNet pre-entrenados que son capaces de reconocer 1.000 tipos distintos de objetos con una precisión realmente competente.

Una vez escogido el modelo (en mi caso MobileNet 2.5 de 224 píxeles) hay que convertirlo a un formato que TensorFlow.js sea capaz de entender utilizando el paquete tfjs-converter que se encuentra disponible para Python.

¡Y ya está! Ya tenemos el modelo listo para empaquetar con nuestra aplicación de detección de perritos calientes sin necesidad de haber estado días entrenándolo o buscando fotografías.

El modelo resultante tras convertirlo con tfjs-converter. ¡Solo pesa 1,89 MB!

Utilizando TensorFlow.js

Como ya tenemos el modelo, el resto es coser y cantar con TensorFlow.js. Lo primero que tenemos que hacer es descargarnos la última versión de la librería desde https://github.com/tensorflow/tfjs/releases e importarla a nuestro proyecto como haríamos con cualquier otro archivo de JavaScript.

En el caso de “Not Hotdog”, el código principal de la aplicación es el siguiente:

connectToCamera().then(function() {
overlayDialog.innerHTML = 'Cargando modelo...';
loadModel().then(function() {
overlayDialog.style.display = 'none';
startPredicting();
});
}).catch(function() {
overlayDialog.innerHTML = 'Esta app necesita acceder a la '
+ 'cámara de tu dispositivo para funcionar';
});

Esta pieza de código realiza los siguientes pasos:

  1. Obtiene permisos para acceder a la cámara del dispositivo al llamar a la funciónconnectToCamera .
  2. Cargar en memoria el modelo con loadModel .
  3. Empieza a realizar predicciones en segundo plano con startPredicting .

Para cargar el modelo se utiliza el método tf.loadModel de la API de TensorFlow.js, que recibe la URL del archivo de definición en JSON del modelo y se encarga de leer el resto de shards. También leemos model.classes.json , que contiene la lista de clases del modelo:

function loadModel() {
return new Promise(function(resolve, reject) {
tf.loadModel('model/mobilenet.json').then(function(m) {
model = m;
fetch('model/mobilenet.classes.json').then(function(res) {
res.json().then(function(json) {
modelClasses = json;
resolve();
});
});
}).catch(reject);
});
}

Pasemos ahora a las funciones relativas a realizar las predicciones. La primera que nos encontramos es startPredicting , que simplemente llama a predict para obtener un booleano (es Hotdog o no), actualiza la interfaz gráfica de la app y prepara la siguiente predicción para dentro de un segundo.

function startPredicting() {
predict().then(function(isHotdog) {
// Update UI
if (isHotdog) console.log('Hotdog found!');
updateUI(isHotdog);
// Schedule next prediction
setTimeout(function() {
startPredicting();
}, 1000);
});
}

La función predict se comunica con el modelo para obtener las neuronas que se activan en la última capa (TensorFlow llama a sus valores logits) y obtiene los nombres de las clases correspondientes para cada una de ellas. Por último, comprueba si alguna de esas clases contiene la palabra “hotdog”, devolviendo true al darse el caso.

Profundizando un poco más en la función, la secuencia de comandos que ocurre es la siguiente:

  1. Llamamos a tf.tidy para que todas las variables de TensorFlow que creemos se limpien después de hacer la predicción.
  2. Obtenemos la imagen de la cámara como un elementos canvas con getCameraImage y lo convertimos a un tensor de decimales (un array de números).
  3. Pasamos el sistema de coordenadas de la imagen de [0,255] a números entre [-1,1], ya que MobileNet ha sido entrenado para aceptar ese tipo de datos.
  4. Reordenamos el tensor (de array a matriz) para coincidir con el formato de la capa de entrada del modelo mediante tf.reshape .
  5. Le pasamos los datos de entrada a model.predict y nos devuelve los logits deseados.
function predict() {
return new Promise(function(resolve, reject) {
tf.tidy(function() {
// Get camera pixels and covert them to a tensor
var image = tf.fromPixels(getCameraImage()).toFloat();

// Change coordinates from [0,255] to [-1,1]
var offset = tf.scalar(127.5);
image = image.sub(offset).div(offset);

// Convert linear array to matrix
image = image.reshape([1, 224, 224, 3]);

// Make a prediction using loaded model
var logits = model.predict(image);
getTopClasses(logits, 8).then(function(classes) {
console.log('Found classes', classes);

// Validate whether is Hotdog or Not Hotdog
var isHotdog = false;
for (var i=0; i<classes.length; i++) {
if (classes[i].name.indexOf('hotdog') > -1) {
isHotdog = true;
break;
}
}
resolve(isHotdog);
});
});
});
}

NOTA: las imágenes deben ser de 224×224 píxeles, sin excepción, para coincidir con el formato de entrada del modelo. Hay otras versiones de la familia MobileNet que admiten otros tamaños de imagen.

La única función de relevancia para TensorFlow.js que todavía no hemos visto es getTopClasses , que recoge los logits de una predicción y obtiene los nombres de las clases (elementos) que aparecen con mayor probabilidad:

function getTopClasses(logits, maxClasses) {
return new Promise(function(resolve, reject) {
// Get raw data from logits
logits.data().then(function(values) {
// Sort data by value, while keeping index
var sortedData = [];
for (var i=0; i<values.length; i++) {
sortedData.push({value: values[i], index: i});
}
sortedData.sort(function(a, b) {
return b.value - a.value;
});

// Get top entries
var topValues = new Float32Array(maxClasses);
var topIndices = new Int32Array(maxClasses);
for (var i=0; i<maxClasses; i++) {
topValues[i] = sortedData[i].value;
topIndices[i] = sortedData[i].index;
}

// Get top classes name
var topClasses = [];
for (var i=0; i<topIndices.length; i++) {
topClasses.push({
name: modelClasses[topIndices[i]],
probability: topValues[i]
});
}
resolve(topClasses);
});
});
}

Quizá este sea el código más caótico de toda la app (mea culpa), así que vamos a verlo paso por paso:

  1. Llamamos a la función logits.data para extraer los valores en tanto por uno de la activación de cada neurona de la capa de salida. Esto es un array de 1.000 elementos (las clases que tiene este modelo) de las probabilidades correspondientes.
  2. Recorremos logits.data para guardar en otro array objetos que contengan el valor y el índice de la neurona. Así podemos ordenar el nuevo array (sortedData ) por el campo value de mayor a menor y quedarnos con las neuronas que más se han activado conservando su índice.
  3. Separamos sortedData en dos variables: topValues y topIndices .
  4. Recorremos topIndices (o topValues , daría igual) para generar la salida definitiva de esta función (topClasses ), que contiene la probabilidad con la que se activa la neurona y el nombre de su clase, este último obtenido a través de modelClasses que hemos cargado previamente con la función loadModel .

El resultado final

Pues eso es todo, ya tenemos una aplicación web que reconoce perritos calientes con ayuda de TensorFlow.js y MobileNet.

Si quieres probarla puedes hacerlo desde este enlace o descargar el código fuente de la app desde el repositorio del proyecto en GitHub.

Source: Deep Learning on Medium