Tensorflow para Javascript usando Tensorflow.js, ml5js y Processing (P5.js)

Empezando con Tensorflow.js, ml5js y Processing (P5.js)

En esta nueva entrada vamos a usar diferentes tecnologías para desarrollar algunos ejemplos muy curiosos y potentes.

Para unir todas estas tecnologías usaremos como punto de unión nuestro querido Javascript.

Antes de empezar con la magia es importante explicar que son y como se usan las diferentes herramientas que explicaremos durante este tutorial.

  • Tensorflow.jsEs una librería de Machine Learning (ML) para Javascript creada por Google.
  • Ml5jsEs una librería que nos proporciona acceso a los algoritmos y modelos de aprendizaje automático usando Javascript y como única dependencia tensorflow.js.
  • P5.jsEs otra librería de Javascript  extremadamente potente que nos permite realizar programación gráfica sin mucha complicación y esta basada en Processing.
  • ProcessingEs un software enfocado en la programación gráfica.

Tensorflow.js

Con esta potente librería para computación numérica podemos construir y entrenar redes neuronales que permiten detectar y descifrar patrones y correlaciones, análogos al aprendizaje y razonamiento usados por los humanos.

Para usarlo en Javascript podemos hacerlo de diferentes formas:

  • Usando el tag <script> en nuestra página.
    • <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
  • Instalándolo desde NPM / YARN.
    • YARN
      yarn add @tensorflow/tfjs
      
      NPM
      npm install @tensorflow/tfjs
  • Usando NPM/ YARN pero esta vez para Node.js
    • Instalando TensorFlow.js con enlaces nativos de C ++
    • YARN
      yarn add @tensorflow/tfjs-node
      
      NPM
      npm install @tensorflow/tfjs-node
    • Solo para linux y si tienes una tarjeta gráfica NVIDIA GPU con soporte CUDA, puedes usar estas dependencias para un mayor rendimiento.
    • YARN
      yarn add @tensorflow/tfjs-node-gpu
      
      NPM
      npm install @tensorflow/tfjs-node-gpu
    • Instalando la versión para Javascript, que es la opción más lenta en cuanto a rendimiento.
    • YARN
      yarn add @tensorflow/tfjs
      
      NPM
      npm install @tensorflow/tfjs

Si necesitas saber más de Tensorflow.js, en esta guía oficial de Tensorflow.js tienes toda la información necesaria para practicar y profundizar sobre esta impresionante librería.

Para nuestro tutorial usare los modelos pre-entrenados que nos ofrecen desde Tensorflow.

En el siguiente ejemplo usaremos Mobilenet, pero…., ¿que es Mobilenet?

Mobilenet son modelos pequeños, de baja latencia y baja potencia parametrizados para cumplir con las limitaciones de recursos de una variedad de casos de uso, pudiendo realizar la clasificación, detección, incrustaciones y segmentación de forma similar a cómo se utilizan otros modelos populares a gran escala.

La ventaja de usar este modelo de TensorFlow.js es que no requiere que sepas sobre el aprendizaje automático, pudiendo tomar como entrada cualquier tag de imagen basado en el navegador (img, video, canvas) y esta devuelve una serie de predicciones más probables y sus confidencias.

A continuación un ejemplo para ver el funcionamiento.

El código fuente esta disponible en Github, no obstante también lo añado en la entrada para un copy/paste rápido.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Clasificación de imágenes</title>
  <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">
  <link href="./style.css" rel="stylesheet">
</head>
<body>
  <h1>Clasificación de imágenes usando <b>Tensorflow.js</b> y <b>Mobilenet</b></h1>
  <div>
    <label for="image">Seleccionar una imagen</label>
    <input type="file" id="image" accept=".gif,.jpg,.jpeg,.png">
    <label for="classify" onclick="classify()" class="lock">Clasificar la imagen</label>
  </div>
  <img id="imgPreview" class="hide" />
  <div id="result"></div>
  <div class="loading hide">
    <div class="lds-ripple">
      <div></div>
      <div></div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.1"></script>
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@1.0.0"></script>
  <script src="./index.js"></script>
</body>
</html>
/* style.css */
body {
  font-family: 'Montserrat', sans-serif;
}
#imgPreview {
  box-shadow: 0px 5px 10px rgba(0,0,0,0.4);
  padding: 20px;
  margin-top: 20px;
}
input[type="file"]#image {
  width: 0.1px;
  height: 0.1px;
  opacity: 0;
  overflow: hidden;
  position: absolute;
  z-index: -1;
}
label {
  font-size: 14px;
  font-weight: 600;
  color: #ffffff;
  background-color: #106BA0;
  box-shadow: 0px 3px 6px rgba(0,0,0,0.3);
  display: inline-block;
  transition: all .5s;
  cursor: pointer;
  padding: 15px 40px;
  text-transform: uppercase;
  width: fit-content;
  text-align: center;
}
.lock {
  pointer-events: none;
  opacity: 0.3;
}
.hide {
  display: none;
}
.lds-ripple {
  display: inline-block;
  position: absolute;
  width: 80px;
  height: 80px;
  left: calc(50% - 40px);
  top: calc(50% - 40px);
}
.lds-ripple div {
  position: absolute;
  border: 4px solid #e90202;
  opacity: 1;
  border-radius: 50%;
  animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
  animation-delay: -0.5s;
}
@keyframes lds-ripple {
  0% {
    top: 36px;
    left: 36px;
    width: 0;
    height: 0;
    opacity: 1;
  }
  100% {
    top: 0px;
    left: 0px;
    width: 72px;
    height: 72px;
    opacity: 0;
  }
}
.loading {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  opacity: 0.7;
  background: #cccccc;
}
// index.js
const init = () => {
  document.querySelector("#image").onchange = evt => {
    let reader = new FileReader();
    reader.readAsDataURL(evt.target.files[0]);
    reader.onload = () => {
      const img = document.querySelector('#imgPreview');
      const container = document.querySelector('#result');
      img.setAttribute('src', reader.result);
      img.classList.remove('hide');
      document.querySelector('label[for="classify"]').classList.remove('lock');
      container.innerHTML = '';
    };
  }
};

const classify = () => {
  const loading = document.querySelector('.loading');
  loading.classList.remove('hide');
  const img = document.querySelector('#imgPreview');
  mobilenet.load().then(model => {
    model.classify(img).then(predictions => {
      const body = document.body;
      const container = document.querySelector('#result');
      container.innerHTML = '';
      predictions.forEach(data => {
        const porcent = parseFloat(data.probability * 100).toFixed(2);
        const progress = document.createElement('progress');
        const label = document.createElement('div');
        const info = document.createElement('span');
        label.innerHTML = `<b>Elemento reconocidos:</b> <i>${data.className}</i>`;
        progress.setAttribute('max', 100);
        progress.setAttribute('value', porcent);
        info.innerHTML = ` <b>${porcent}%</b>`;
        container.classList.add('result');
        container.appendChild(label);
        container.appendChild(progress);
        container.appendChild(info);
      });
      body.appendChild(container);
      loading.classList.add('hide');
    });
  });
};

window.onload = init();

Me gustaría seguir profundizando un poco más en el uso de Tensorflow.js, no solo para clasificar imágenes, ya que podríamos hacerlo también sobre videos, detectar objetos, detectar la segmentación de personas, estimación de posiciones, etc…, pero debemos continuar con el resto de las herramientas.

Ml5.js

Como habíamos comentado al principio, ml5.js es una interfaz de alto nivel, muy simple, de código abierto para TensorFlow.js, que nos permite manejar operaciones matemáticas aceleradas por GPU y gestión de memoria para algoritmos de aprendizaje automático.

Viendo el código anterior y lo que se puede conseguir, podríamos pensar que no se puede hacer mas fácil, pero si se puede hacer todavía mas sencillo y sobre todo más potente.

Para empezar usaremos el tag script para acceder al CDN de la librería y poder hacer uso de ella.

<script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script>

En el siguiente ejemplo haremos uso de la librería p5.js, la cual explicaremos en el siguiente punto.

Igual que en el ejemplo de Tensorflow.js, podéis acceder también a su repositorio en Github.

No obstante añadiré el código en el tutorial para un acceso mas rápido.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Ml5.js y P5.js</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.sound.min.js"></script>
  <script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script>
  <link rel="stylesheet" href="./style.css" />
</head>
<body>
  <div class="loading hide">
    Analizando imagen
  </div>
  <script src="./index.js"></script>
</body>
</html>
/* style.css */
.loading {
  position: absolute;
  left: calc(50% - 100px);
  top: calc(50% - 10px);
  width: 200px;
  text-align: center;
  background: #6677aa;
  padding: 20px 0px;
  border-radius: 100px;
  font-size: 20px;
  color: #ffffff;
}
.hide {
  display: none;
}
.result {
  padding: 10px;
  box-shadow: 0px 6px 12px rgba(0,0,0,0.3);
  margin-top: 20px;
  width: 500px;
}
canvas {
  box-shadow: 0px 6px 12px rgba(0,0,0,0.3);
  margin-bottom: 20px;
}
// index.js
let mobilenet;
let img;
window.onerror = (errorMsg, url, lineNumber) => {
  console.log('----- ERROR ------');
  console.log(errorMsg, url, lineNumber);
  console.log('------------------');
  return false;
}

const show = (el, status) => {
  document.querySelector(el).classList[status ? 'remove' : 'add']('hide');
};

function modelReady() {
  console.log('model is ready!!');
}

function gotResult(error, results) {
  show('.loading', false);
  if (error) {
    console.error(error);
  } else {
    console.log(results);
    const body = document.body;
    const container = document.createElement('div');
    results.forEach(data => {
      const porcent = nf(data.confidence, 0, 4) * 100;
      const progress = document.createElement('progress');
      const label = document.createElement('div');
      const info = document.createElement('span');
      label.innerHTML = `Elemento reconocidos: ${data.label}`;
      progress.setAttribute('max', 100);
      progress.setAttribute('value', porcent);
      info.innerHTML = ` ${porcent}%`;
      container.classList.add('result');
      container.appendChild(label);
      container.appendChild(progress);
      container.appendChild(info);
    });
    body.appendChild(container);
  }
}
window.onload = () => {
  show('.loading', true);
}
function imageReady() {
  image(img,0,0 , width, height);
  mobilenet.predict(img, gotResult);
}
function preload() {
  classifier = ml5.imageClassifier('MobileNet');
  img = loadImage('img1.jpg');
}

function setup() {
  createCanvas(640, 480);
  img = createImg('img1.jpg', imageReady);
  img.hide();
  background(0);
  mobilenet = ml5.imageClassifier('MobileNet',modelReady);
}

Si comparáis el código y el resultado, ambos realizan la misma acción, clasificar una imagen.

La diferencia esta en que usando la librería ml5.js podemos hacer uso de la GPU, y usando p5.js podemos crear canvas y realizar una programación gráfica mucho mas rápida ya que no tenemos que complicarnos con el canvas y como pintar en el, que aunque para este ejemplo es muy simple para otros puede ser algo más doloroso.

Como he dicho en el punto anterior, me gustaría continuar profundizando pero debemos continuar con la última herramienta.

Processing (p5.js)

Processing es un software que nos permite programar de una forma muy simple pero obteniendo unos resultado bastante buenos.

Esto suena maravilloso pero como siempre digo, «el movimiento se demuestra andando».

Primero el ejemplo funcionando.

Puedes encontrar el código en Github y para un acceso rápido añado también el código en la página.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Processing P5.JS</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
  <script src="./index.js"></script>
</body>
</html>
// index.js
function setup() {
  createCanvas(710, 400, WEBGL);
}

function draw() {
  background(0);

  translate(-240, -100, 0);
  normalMaterial();
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  plane(70);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  box(70, 70, 70);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  cylinder(70, 70);
  pop();

  translate(-240 * 2, 200, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  cone(70, 70);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  torus(70, 20);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  sphere(70);
  pop();
}

Como has podido comprobar el resultado es muy llamativo, pero lo mas llamativo son las pocas lineas de código que han sido necesarias para generar este ejemplo.

Con Processing (P5.js) damos por finalizado este pequeño tutorial de Tensorflow.js, no obstante en breve intentare publicar otra entrada con mas detalle sobre sobre Tensorflow.js y el ecosistema que ofrece.