TensorFlow.js: Realiza predicciones a partir de datos en 2D

En este codelab, entrenarás un modelo para realizar predicciones a partir de datos numéricos que describen un conjunto de automóviles.

En este ejercicio, se mostrarán los pasos comunes para entrenar muchos tipos diferentes de modelos, pero se usará un conjunto de datos pequeño y un modelo simple (superficial). El objetivo principal es ayudarte a conocer la terminología básica, los conceptos y la sintaxis en torno al entrenamiento de modelos con TensorFlow.js, y proporcionarte una base para explorar y aprender más.

Debido a que entrenaremos un modelo para predecir números continuos, esta actividad se suele conocer como tarea de regresión. Para entrenar el modelo, le mostraremos varios ejemplos de entradas junto con el resultado correcto, lo cual se conoce como aprendizaje supervisado.

Qué compilarás

Crearás una página web que utiliza TensorFlow.js para entrenar un modelo en el navegador. Con la “potencia” de un automóvil, el modelo aprenderá a predecir la cantidad de “millas por galón” (MPG).

Para ello, deberás hacer lo siguiente:

  • Cargar los datos y prepararlos para el entrenamiento
  • Definir la arquitectura del modelo
  • Entrenar el modelo y supervisar su rendimiento mientras se entrena
  • Evaluar el modelo entrenado mediante algunas predicciones

Qué aprenderás

  • Prácticas recomendadas de preparación de datos para el aprendizaje automático, incluidas la distribución aleatoria y la normalización
  • Sintaxis de TensorFlow.js para crear modelos mediante la API de tf.layers
  • Cómo supervisar el entrenamiento en el navegador con la biblioteca tfjs‑vis

Requisitos

Crea una página HTML y, luego, incluye el código JavaScript

96914ff65fc3b74c.png Copia el siguiente código en un archivo HTML llamado

index.html

<!DOCTYPE html>
<html>
<head>
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
  <!-- Import tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>

  <!-- Import the main script file -->
  <script src="script.js"></script>

</head>

<body>
</body>
</html>

Crea el archivo JavaScript del código

  1. En la misma carpeta que el archivo HTML anterior, crea un archivo llamado script.js y escribe el siguiente código.
console.log('Hello TensorFlow');

Pruébalo

Ahora que creaste los archivos HTML y JavaScript, pruébalos. Abre el archivo index.html en el navegador y abre la consola de las Herramientas para desarrolladores.

Si todo funciona bien, debería haber dos variables globales creadas y disponibles en la consola de las Herramientas para desarrolladores:

  • tf es una referencia a la biblioteca de TensorFlow.js.
  • tfvis es una referencia a la biblioteca de tfjs‑vis.

Abre las herramientas para desarrolladores de tu navegador. Deberías ver un mensaje que dice Hello TensorFlow en la salida de la consola. Si es así, puedes continuar con el siguiente paso.

El primer paso es permitirnos cargar los datos sobre los que queremos entrenar el modelo, así como darles formato y visualizarlos.

Cargaremos el conjunto de datos “cars” desde un archivo JSON que alojamos para ti. Contiene muchas características diferentes acerca de cada automóvil específico. En este instructivo, solo queremos extraer datos sobre la potencia y las millas por galón.

96914ff65fc3b74c.png Agrega el siguiente código a tu

archivo script.js

/**
 * Get the car data reduced to just the variables we are interested
 * and cleaned of missing data.
 */
async function getData() {
  const carsDataResponse = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
  const carsData = await carsDataResponse.json();
  const cleaned = carsData.map(car => ({
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower,
  }))
  .filter(car => (car.mpg != null && car.horsepower != null));

  return cleaned;
}

Esto también quitará las entradas que no tengan definidas las millas por galón ni la potencia. También graficaremos estos datos en un diagrama de dispersión para ver cómo lucen.

96914ff65fc3b74c.png Agrega el siguiente código al final de tu

archivo script.js.

async function run() {
  // Load and plot the original input data that we are going to train on.
  const data = await getData();
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));

  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );

  // More code will be added below
}

document.addEventListener('DOMContentLoaded', run);

Cuando actualices la página, deberías ver un panel en el lado izquierdo con un diagrama de dispersión de los datos. Debería verse algo similar a esto.

cf44e823106c758e.png

Este panel se conoce como visor y se proporciona a través de tfjs‑vis. Es un lugar conveniente para mostrar visualizaciones.

Generalmente, cuando se trabaja con datos, se recomienda buscar formas de analizarlos y limpiarlos si es necesario. En este caso, tuvimos que quitar ciertas entradas de carsData que no tenían todos los campos obligatorios. Visualizar los datos nos puede indicar si existe una estructura que pueda aprender el modelo.

Gracias al gráfico anterior, podemos ver que hay una correlación negativa entre la potencia y las MPG. Es decir, a medida que aumenta la potencia, suelen disminuir las millas por galón de los automóviles.

Conceptualiza la tarea

Ahora, nuestros datos de entrada tendrán el siguiente aspecto.

...
{
  "mpg":15,
  "horsepower":165,
},
{
  "mpg":18,
  "horsepower":150,
},
{
  "mpg":16,
  "horsepower":150,
},
...

Nuestro objetivo es entrenar un modelo que acepte un número (la potencia) y aprenda a predecir otro (las millas por galón). Recuerda el mapeo uno a uno, ya que será importante en la próxima sección.

Transmitiremos estos ejemplos (la potencia y las MPG) a una red neuronal que, a partir de los ejemplos, aprenderá una fórmula (o función) para predecir las MPG de una potencia específica. El aprendizaje de los ejemplos para los que tenemos las respuestas correctas se llama aprendizaje supervisado.

En esta sección, escribiremos código para describir la arquitectura del modelo. “Arquitectura del modelo” es solo una forma sofisticada de decir “qué funciones ejecutará el modelo cuando esté en funcionamiento” o, de forma alternativa, “qué algoritmo usará el modelo para procesar sus respuestas”.

Los modelos de AA son algoritmos que toman una entrada y producen una salida. Cuando se usan redes neuronales, el algoritmo es un conjunto de capas de neuronas con “ponderaciones” (números) que rigen su salida. El proceso de entrenamiento aprende los valores ideales para esas ponderaciones.

96914ff65fc3b74c.png Agrega la siguiente función a tu archivo

script.js para definir la arquitectura del modelo.

function createModel() {
  // Create a sequential model
  const model = tf.sequential();

  // Add a single input layer
  model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));

  // Add an output layer
  model.add(tf.layers.dense({units: 1, useBias: true}));

  return model;
}

Este es uno de los modelos más sencillos que podemos definir en tensorflow.js. Desglosemos un poco cada línea.

Crea una instancia del modelo

const model = tf.sequential();

Este código crea una instancia de un objeto tf.Model. Este modelo es sequential porque sus entradas fluyen directamente a su salida. Otros tipos de modelos pueden tener ramas o incluso varias entradas y salidas, pero, en muchos casos, tus modelos serán secuenciales. Los modelos secuenciales también tienen una API más fácil de usar.

Agrega capas

model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));

De esta manera, se agrega una capa de entrada a nuestra red, que se conecta automáticamente a una capa dense con una unidad oculta. Una capa dense es un tipo de capa que multiplica sus entradas por una matriz (llamada ponderaciones) y, luego, agrega un número (llamado sesgo) al resultado. Como esta es la primera capa de la red, debemos definir nuestra inputShape. La inputShape es [1] porque tenemos la cantidad 1 como entrada (la potencia de un automóvil determinado).

units establece el tamaño de la matriz de ponderaciones en la capa. Cuando este valor se establece en 1, significa que habrá 1 ponderación para cada atributo de entrada de los datos.

model.add(tf.layers.dense({units: 1}));

El código anterior crea nuestra capa de salida. Configuramos units como 1 porque queremos generar 1 número.

Crea una instancia

96914ff65fc3b74c.png Agrega el siguiente código a la función

run que definimos antes.

// Create the model
const model = createModel();
tfvis.show.modelSummary({name: 'Model Summary'}, model);

Se creará una instancia del modelo y se mostrará un resumen de las capas en la página web.

Para obtener los beneficios de rendimiento de TensorFlow.js que hacen que los modelos de entrenamiento de aprendizaje automático sean prácticos, debemos convertir nuestros datos en tensores. Además, realizaremos una serie de transformaciones en nuestros datos que son prácticas recomendadas: distribución aleatoria y normalización.

96914ff65fc3b74c.png Agrega el siguiente código a tu

archivo script.js

/**
 * Convert the input data to tensors that we can use for machine
 * learning. We will also do the important best practices of _shuffling_
 * the data and _normalizing_ the data
 * MPG on the y-axis.
 */
function convertToTensor(data) {
  // Wrapping these calculations in a tidy will dispose any
  // intermediate tensors.

  return tf.tidy(() => {
    // Step 1. Shuffle the data
    tf.util.shuffle(data);

    // Step 2. Convert data to Tensor
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    //Step 3. Normalize the data to the range 0 - 1 using min-max scaling
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // Return the min/max bounds so we can use them later.
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    }
  });
}

Desglosemos lo que sucede aquí.

Distribuye los datos de forma aleatoria

// Step 1. Shuffle the data
tf.util.shuffle(data);

Aquí, aleatorizamos el orden de los ejemplos que enviaremos al algoritmo de entrenamiento. La distribución aleatoria es importante porque, por lo general, durante el entrenamiento, el conjunto de datos se divide en subconjuntos más pequeños, llamados lotes, en los que se entrena el modelo. La distribución aleatoria permite que cada lote tenga una variedad de datos de la distribución de datos. De esta manera, ayudamos a que el modelo no haga lo siguiente:

  • Aprenda acciones que dependan únicamente del orden en el que se enviaron los datos
  • Tenga en cuenta la estructura de los subgrupos (p. ej., si solo ve automóviles de potencia alta durante la primera mitad de su entrenamiento, podría aprender una relación que no se aplica al resto del conjunto de datos)

Convierte datos en tensores

// Step 2. Convert data to Tensor
const inputs = data.map(d => d.horsepower)
const labels = data.map(d => d.mpg);

const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

Aquí hacemos dos arrays, uno para nuestros ejemplos de entrada (las entradas de la potencia) y otro para los valores de salida verdaderos (que se conocen como etiquetas en el aprendizaje automático).

Luego, convertimos los datos de cada array en un tensor en 2D. El tensor tendrá una forma de [num_examples, num_features_per_example]. Aquí tenemos ejemplos de inputs.length y cada ejemplo tiene el atributo de entrada 1 (la potencia).

Normaliza los datos

//Step 3. Normalize the data to the range 0 - 1 using min-max scaling
const inputMax = inputTensor.max();
const inputMin = inputTensor.min();
const labelMax = labelTensor.max();
const labelMin = labelTensor.min();

const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

A continuación, aplicamos otra práctica recomendada para el entrenamiento de aprendizaje automático. Normalizamos los datos. Aquí normalizamos los datos en el rango numérico 0-1 con escalamiento mínimo y máximo. La normalización es importante porque los componentes internos de muchos modelos de aprendizaje automático que compilarás con tensorflow.js están diseñados para funcionar con números que no son muy grandes. Algunos rangos comunes para normalizar los datos son 0 to 1 o -1 to 1. Tendrás más éxito entrenando tus modelos si te acostumbras a normalizar tus datos en un rango razonable.

Muestra los datos y los límites de normalización

return {
  inputs: normalizedInputs,
  labels: normalizedLabels,
  // Return the min/max bounds so we can use them later.
  inputMax,
  inputMin,
  labelMax,
  labelMin,
}

Queremos mantener los valores que usamos en la normalización durante el entrenamiento a fin de que podamos normalizar las salidas para que vuelvan a nuestra escala original y normalizar los datos de entrada futuros de la misma manera.

Con la instancia de modelo creada y los datos representados como tensores, tenemos todo listo para comenzar el proceso de entrenamiento.

96914ff65fc3b74c.png Copia la siguiente función en tu

archivo script.js.

async function trainModel(model, inputs, labels) {
  // Prepare the model for training.
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ['mse'],
  });

  const batchSize = 32;
  const epochs = 50;

  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      { name: 'Training Performance' },
      ['loss', 'mse'],
      { height: 200, callbacks: ['onEpochEnd'] }
    )
  });
}

Desglosemos este código.

Prepárate para el entrenamiento

// Prepare the model for training.
model.compile({
  optimizer: tf.train.adam(),
  loss: tf.losses.meanSquaredError,
  metrics: ['mse'],
});

Tenemos que “compilar” el modelo antes de entrenarlo. Para ello, debemos especificar los siguientes elementos importantes:

  • optimizer: Este es el algoritmo que regirá las actualizaciones del modelo a medida que vea ejemplos. Existen muchos optimizadores disponibles en TensorFlow.js. Aquí, elegimos el optimizador adam, ya que es bastante eficaz en la práctica y no requiere configuración.
  • loss: Esta es una función que le indicará al modelo qué tan bien aprende cada uno de los lotes (subconjuntos de datos) que se le muestran. Aquí, usamos meanSquaredError para comparar las predicciones que hizo el modelo con los valores verdaderos.
const batchSize = 32;
const epochs = 50;

Luego, elegimos un batchSize y una cantidad de ciclos de entrenamiento:

  • batchSize hace referencia al tamaño de los subconjuntos de datos que verá el modelo en cada iteración de entrenamiento. Los tamaños de lote comunes suelen estar en el rango de 32 a 512. No existe un tamaño de lote ideal para todos los problemas, y describir las justificaciones matemáticas de los distintos tamaños de lote está fuera del alcance de este instructivo.
  • epochs se refiere a la cantidad de veces que el modelo analizará el conjunto de datos completo que proporciones. Aquí, realizaremos 50 iteraciones en el conjunto de datos.

Inicia el bucle de entrenamiento

return await model.fit(inputs, labels, {
  batchSize,
  epochs,
  callbacks: tfvis.show.fitCallbacks(
    { name: 'Training Performance' },
    ['loss', 'mse'],
    { height: 200, callbacks: ['onEpochEnd'] }
  )
});

model.fit es la función que llamamos para iniciar el bucle de entrenamiento. Es una función asíncrona, por lo que mostramos la promesa que nos proporciona para que el llamador pueda determinar el momento en que se completa el entrenamiento.

Para supervisar el progreso del entrenamiento, pasamos algunas devoluciones de llamada a model.fit. Usamos tfvis.show.fitCallbacks a fin de generar funciones que creen gráficos para las métricas “loss” y “mse” indicadas anteriormente.

Aplícalo todo

Ahora debemos llamar a las funciones que definimos desde nuestra función run.

96914ff65fc3b74c.png Agrega el siguiente código al final de tu

función run.

// Convert the data to a form we can use for training.
const tensorData = convertToTensor(data);
const {inputs, labels} = tensorData;

// Train the model
await trainModel(model, inputs, labels);
console.log('Done Training');

Cuando actualices la página, deberías ver que se actualizan los siguientes grafos después de unos segundos.

c6d3214d6e8c3752.png

Se crean a partir de las devoluciones de llamada que creamos anteriormente. Muestran la pérdida y el ECM promediados en todo el conjunto de datos, al final de cada ciclo de entrenamiento.

Cuando entrenamos un modelo, queremos ver cómo disminuye la pérdida. En este caso, como nuestra métrica es una medida de error, también queremos que disminuya.

Ahora que se entrenó nuestro modelo, queremos realizar algunas predicciones. Evaluemos el modelo viendo qué predice para un rango uniforme de números de potencia baja a alta.

96914ff65fc3b74c.png Agrega la siguiente función a tu archivo script.js

function testModel(model, inputData, normalizationData) {
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData;

  // Generate predictions for a uniform range of numbers between 0 and 1;
  // We un-normalize the data by doing the inverse of the min-max scaling
  // that we did earlier.
  const [xs, preds] = tf.tidy(() => {

    const xs = tf.linspace(0, 1, 100);
    const preds = model.predict(xs.reshape([100, 1]));

    const unNormXs = xs
      .mul(inputMax.sub(inputMin))
      .add(inputMin);

    const unNormPreds = preds
      .mul(labelMax.sub(labelMin))
      .add(labelMin);

    // Un-normalize the data
    return [unNormXs.dataSync(), unNormPreds.dataSync()];
  });

  const predictedPoints = Array.from(xs).map((val, i) => {
    return {x: val, y: preds[i]}
  });

  const originalPoints = inputData.map(d => ({
    x: d.horsepower, y: d.mpg,
  }));

  tfvis.render.scatterplot(
    {name: 'Model Predictions vs Original Data'},
    {values: [originalPoints, predictedPoints], series: ['original', 'predicted']},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
}

Algunos aspectos que debes tener en cuenta en la función anterior.

const xs = tf.linspace(0, 1, 100);
const preds = model.predict(xs.reshape([100, 1]));

Generamos 100 “ejemplos” nuevos para ingresarlos al modelo. Model.predict es la manera en que ingresamos esos ejemplos al modelo. Ten en cuenta que deben tener una forma similar ([num_examples, num_features_per_example]) a la que usamos en el entrenamiento.

// Un-normalize the data
const unNormXs = xs
  .mul(inputMax.sub(inputMin))
  .add(inputMin);

const unNormPreds = preds
  .mul(labelMax.sub(labelMin))
  .add(labelMin);

Para que los datos vuelvan a nuestro rango original (en vez de 0 a 1), usamos los valores que calculamos cuando normalizamos, pero solo invertimos las operaciones.

return [unNormXs.dataSync(), unNormPreds.dataSync()];

.dataSync() es un método que podemos usar para obtener un typedarray de los valores almacenados en un tensor, lo que nos permite procesar esos valores en JavaScript normal. Esta es una versión síncrona del método .data() que, por lo general, se prefiere.

Por último, usamos tfjs‑vis para graficar los datos originales y las predicciones del modelo.

96914ff65fc3b74c.png Agrega el siguiente código a tu

función run.

// Make some predictions using the model and compare them to the
// original data
testModel(model, data, tensorData);

Actualiza la página. Deberías ver algo como lo siguiente cuando termine el entrenamiento del modelo.

fe610ff34708d4a.png

¡Felicitaciones! Acabas de entrenar un modelo de aprendizaje automático sencillo. Actualmente realiza lo que se conoce como regresión lineal, la que intenta ajustar una línea a la tendencia presente en los datos de entrada.

Los pasos para entrenar un modelo de aprendizaje automático incluyen los siguientes:

Formular la tarea:

  • ¿Es un problema de regresión o uno de clasificación?
  • ¿Se puede hacer con aprendizaje supervisado o no supervisado?
  • ¿Cuál es la forma de los datos de entrada?, ¿qué aspecto deberían tener los datos de salida?

Preparar los datos:

  • Limpia tus datos y, luego, inspecciónalos manualmente para detectar patrones cuando sea posible.
  • Distribuye tus datos aleatoriamente antes de usarlos para el entrenamiento.
  • Normaliza tus datos en un rango razonable para la red neuronal. Por lo general, de 0 a 1 o de -1 a 1 son buenos rangos de datos numéricos.
  • Convierte tus datos en tensores.

Compilar y ejecutar tu modelo:

  • Define tu modelo con tf.sequential o tf.model y, luego, agrégale capas con tf.layers.*.
  • Elige un optimizador (adam suele dar buenos resultados) y parámetros, como el tamaño de lote y la cantidad de ciclos de entrenamiento.
  • Elige una función de pérdida apropiada para tu problema y una métrica de exactitud que ayude a evaluar el progreso. meanSquaredError es una función de pérdida común para los problemas de regresión.
  • Supervisa el entrenamiento para ver si disminuye la pérdida.

Evalúa el modelo

  • Elige una métrica de evaluación para tu modelo que puedas supervisar durante el entrenamiento. Cuando esté entrenado, intenta realizar algunas predicciones de prueba para tener una idea de la calidad de predicción.
  • Experimenta cambiando la cantidad de ciclos de entrenamiento. ¿Cuántos necesitas antes de que se aplane el grafo?
  • Experimenta aumentando la cantidad de unidades de la capa oculta.
  • Experimenta agregando más capas ocultas entre la primera que agregamos y la capa de salida final. El código de estas capas adicionales debería verse así.
model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));

La información nueva más importante sobre estas capas ocultas es que presentan una función de activación no lineal, en este caso, una activación sigmoidea. Para obtener más detalles sobre las funciones de activación, consulta este artículo.

Observa si puedes obtener el modelo para producir resultados como en la siguiente imagen.

a21c5e6537cf81d.png