TensorFlow.js – Effectuer des prédictions à partir de données 2D

Dans cet atelier de programmation, vous allez entraîner un modèle à réaliser des prédictions à partir de données numériques décrivant un ensemble de voitures.

Il présente les étapes à suivre pour entraîner de nombreux types de modèles. Cependant, pour cet exercice, nous utiliserons un petit ensemble de données ainsi qu'un modèle simple (superficiel). L'objectif principal est de vous aider à vous familiariser avec la terminologie, les concepts et la syntaxe nécessaires pour entraîner des modèles avec TensorFlow.js, mais aussi de vous fournir des pistes d'exploration et d'apprentissage.

Notez que la tâche qui consiste à entraîner un modèle pour prédire des nombres continus est parfois appelée tâche de régression. Pour entraîner le modèle, nous lui fournirons de nombreux exemples d'entrées avec leur résultat correct. Cette méthode est appelée apprentissage supervisé.

Objectifs du tutoriel

Vous allez créer une page Web qui exploite TensorFlow.js pour entraîner un modèle dans le navigateur. Le modèle va apprendre à prédire la consommation de carburant d'une voiture en miles par gallon (mpg), en fonction de sa puissance en chevaux.

Pour ce faire, vous devrez :

  • charger les données et les préparer pour l'entraînement ;
  • définir l'architecture du modèle ;
  • entraîner le modèle et surveiller ses performances pendant l'entraînement ;
  • évaluer le modèle entraîné en effectuant des prédictions.

Points abordés

  • Bonnes pratiques de préparation des données (dont le brassage et la normalisation) pour le machine learning
  • Syntaxe TensorFlow.js servant à créer des modèles à l'aide de l'API layers.tf
  • Surveillance de l'entraînement dans le navigateur à l'aide de la bibliothèque tfjs-vis

Prérequis

Créer une page HTML et inclure le code JavaScript

96914ff65fc3b74c.png Copiez le code suivant dans un fichier HTML appelé

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>

Créer le fichier JavaScript pour le code

  1. Dans le même dossier que le fichier HTML ci-dessus, créez un fichier appelé script.js et insérez-y le code suivant.
console.log('Hello TensorFlow');

Tester les fichiers

Maintenant que vous disposez des fichiers HTML et JavaScript, testez-les. Ouvrez le fichier index.html dans votre navigateur et ouvrez la console d'outils de développement.

Si tout fonctionne correctement, vous devriez voir deux variables globales :

  • tf est une référence à la bibliothèque TensorFlow.js.
  • tfvis est une référence à la bibliothèque tfjs-vis.

Ouvrez les outils de développement de votre navigateur. Le message suivant devrait s'afficher dans la console : Hello TensorFlow. Si c'est le cas, vous pouvez passer à l'étape suivante.

Dans un premier temps, nous allons charger, mettre en forme et visualiser les données qui nous permettront d'entraîner le modèle.

Nous allons charger l'ensemble de données "cars" (voitures) à partir d'un fichier JSON hébergé par nos soins. Il contient de nombreuses caractéristiques différentes sur chacune des voitures données. Pour ce tutoriel, nous ne voulons extraire que les données concernant la puissance en chevaux et la consommation de carburant en miles par gallon de ces voitures.

96914ff65fc3b74c.png Ajoutez le code suivant au fichier

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;
}

Cette opération exclura également toutes les entrées pour lesquelles la puissance en chevaux ou la consommation en miles par gallon n'est pas définie. Représentons ces données dans un nuage de points pour voir à quoi elles ressemblent.

96914ff65fc3b74c.png Ajoutez le code suivant en bas du fichier

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);

Lorsque vous actualisez la page, un panneau avec un nuage de points représentant les données doit s'afficher à gauche. Voici un exemple :

cf44e823106c758e.png

Ce panneau, appelé "visualiseur", est fourni par tfjs-vis. C'est un outil idéal pour visualiser les données.

Lorsque vous travaillez sur des données, il est souvent utile de pouvoir les visualiser et les nettoyer si nécessaire. Dans le cas présent, nous avons dû supprimer certaines entrées de l'ensemble de données carsData, car elles ne contenaient pas tous les champs obligatoires. En visualisant les données, vous pouvez déterminer si celles-ci sont présentées selon une structure que le modèle peut apprendre.

Nous constatons ici qu'il existe une corrélation négative entre la puissance en chevaux et la consommation en mpg. Ainsi, plus la puissance en chevaux d'une voiture est importante, plus celle-ci consomme de carburant.

Conceptualiser la tâche

Nos données d'entrée devraient maintenant ressembler à ceci :

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

Notre objectif est d'entraîner un modèle qui exploitera un nombre correspondant à la puissance en chevaux, puis apprendra à prédire un nombre correspondant à la consommation en miles par gallon. Gardez à l'esprit qu'il s'agit d'un mappage "un pour un" ; c'est important pour la section suivante.

Nous allons transmettre ces exemples de puissance et de mpg à un réseau de neurones qui en tirera une formule (ou fonction) pour prédire les mpg d'une voiture en fonction de sa puissance en chevaux. Cette méthode, qui consiste à entraîner le modèle à partir d'exemples pour lesquels nous avons les bonnes réponses, est appelée apprentissage supervisé.

Dans cette section, nous allons rédiger du code pour décrire l'architecture du modèle. Celle-ci décrit simplement les fonctions que le modèle va exécuter ou l'algorithme que le modèle va utiliser pour calculer ses réponses.

Les modèles de ML sont des algorithmes qui génèrent des résultats à partir de données d'entrée. Dans le cas des réseaux de neurones, l'algorithme est constitué d'un ensemble de couches de neurones dont les "pondérations" (des nombres) régissent les résultats. Le processus d'entraînement consiste à apprendre les valeurs idéales pour ces pondérations.

96914ff65fc3b74c.png Pour définir l'architecture du modèle, ajoutez la fonction suivante au fichier

script.js

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;
}

Ce modèle est l'un des plus simples à définir dans TensorFlow.js. Examinons chaque ligne.

Instancier le modèle

const model = tf.sequential();

Cela instancie un objet tf.Model. Ce modèle est sequential (séquentiel), car les entrées génèrent directement des résultats. D'autres types de modèles peuvent comporter des branches, voire plusieurs entrées et sorties. Toutefois, dans de nombreux cas, vos modèles seront séquentiels. Les modèles de ce type sont également dotés d'une API plus facile à utiliser.

Ajouter des couches

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

Cela permet d'ajouter une couche d'entrée à notre réseau. Celle-ci est automatiquement connectée à une couche dense via une unité masquée. Une couche dense est un type de couche qui multiplie ses entrées par une matrice (appelée pondérations), puis ajoute un nombre (appelé biais) au résultat. Comme il s'agit de la première couche du réseau, nous devons définir notre inputShape (forme d'entrée). L'inputShape est de [1], car nous avons 1 nombre en entrée (la puissance en chevaux d'une voiture donnée).

units (unités) définit la taille de la matrice de pondération dans la couche. En le définissant sur 1, nous indiquons une pondération pour chacune des caractéristiques d'entrée des données.

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

Le code ci-dessus crée la couche de sortie. Nous avons défini units sur 1, car nous voulons obtenir 1 nombre.

Créer une instance

96914ff65fc3b74c.png Ajoutez le code suivant

à la fonction run définie précédemment.

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

Vous créez ainsi une instance du modèle, et un résumé des couches s'affiche sur la page Web.

Pour bénéficier des gains de performances offerts par TensorFlow.js, qui facilitent l'entraînement de modèles de machine learning, nous devons convertir les données en Tensors. Nous allons également effectuer des tâches de transformation des données, à savoir le brassage et la normalisation, conformément aux bonnes pratiques.

96914ff65fc3b74c.png Ajoutez le code suivant

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,
    }
  });
}

Regardons ce qui se passe ici.

Brasser les données

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

Ici, nous randomisons l'ordre des exemples qui seront utilisés par l'algorithme d'entraînement. Le brassage est important, car pendant l'entraînement, l'ensemble de données est généralement divisé en sous-ensembles plus petits, appelés lots, sur lesquels le modèle est entraîné. Le brassage permet à chaque lot de disposer de données diverses issues de la distribution des données. En procédant ainsi, nous faisons en sorte que le modèle :

  • n'apprenne pas des choses qui dépendent uniquement de l'ordre dans lequel les données sont fournies ;
  • ne soit pas sensible à la structure des sous-groupes (par exemple, s'il ne voit que des voitures à la puissance élevée pendant la première moitié de son entraînement, il peut apprendre une relation qui ne s'applique pas au reste de l'ensemble de données).

Convertir les données en Tensors

// 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]);

Ici, nous allons créer deux tableaux : un pour nos exemples d'entrée (puissance en chevaux) et un autre pour les valeurs de sortie réelles (appelées étiquettes dans le jargon du machine learning).

Nous convertirons ensuite chaque donnée de tableau en Tensor 2D. Le Tensor prendra cette forme : [num_examples, num_features_per_example] (nombre_exemples, nombre_caractéristiques_par_exemple). Ici, nous avons inputs.length exemples, et chaque exemple possède 1 caractéristique d'entrée (la puissance en chevaux).

Normaliser les données

//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));

Nous allons maintenant suivre une autre bonne pratique pour l'entraînement des modèles de machine learning, à savoir normaliser les données. Ici, nous normalisons les données dans la plage numérique 0-1 à l'aide de la mise à l'échelle min-max. L'étape de normalisation est importante, car les composants internes de nombreux modèles de machine learning créés avec TensorFlow.js sont conçus pour fonctionner avec des nombres qui ne sont pas trop gros. Les plages 0 to 1 (0 à 1) et -1 to 1 (-1 à 1) sont couramment utilisées pour normaliser les données. Vous entraînerez vos modèles plus efficacement si vous prenez l'habitude de normaliser vos données selon une plage raisonnable.

Renvoyer les données et les limites de normalisation

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

Nous voulons conserver les valeurs que nous avons utilisées pour normaliser les données pendant l'entraînement afin de pouvoir dénormaliser les résultats et les rapporter à l'échelle d'origine. Cela nous permettra de normaliser les futures données d'entrée de la même manière.

Une fois notre instance de modèle créée et nos données représentées sous forme de Tensors, nous pouvons lancer le processus d'entraînement.

96914ff65fc3b74c.png Copiez la fonction suivante

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'] }
    )
  });
}

Examinons cela dans le détail.

Préparer le modèle pour l'entraînement

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

Nous devons "compiler" le modèle avant de l'entraîner. Pour ce faire, vous devez spécifier un certain nombre d'éléments très importants.

  • optimizer : il s'agit de l'algorithme qui mettra le modèle à jour dès qu'il verra des exemples. De nombreux optimiseurs sont disponibles dans TensorFlow.js. Nous avons choisi l'optimiseur "adam", car il est efficace et ne nécessite aucune configuration.
  • loss : il s'agit de la fonction renseignant le modèle sur ses performances d'apprentissage pour chacun des lots (sous-ensembles de données) qu'on lui fournit. Ici, nous utilisons meanSquaredError (l'erreur quadratique moyenne) pour comparer les prédictions effectuées par le modèle avec les valeurs réelles.
const batchSize = 32;
const epochs = 50;

Nous choisissons ensuite une taille de lot et un nombre d'époques :

  • batchSize correspond à la taille des sous-ensembles de données que le modèle verra à chaque itération de l'entraînement. Les tailles de lot courantes sont généralement comprises entre 32 et 512. Il n'existe pas vraiment de taille de lot idéale qui résout tous les problèmes. De plus, ce tutoriel n'a pas été conçu pour aborder les motivations mathématiques derrière chaque taille de lot.
  • epochs correspond au nombre de fois où le modèle va examiner l'intégralité de l'ensemble de données que vous lui fournissez. Ici, nous allons appliquer 50 itérations de l'ensemble de données.

Démarrer la boucle d'entraînement

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

model.fit est la fonction que nous appelons pour démarrer la boucle d'entraînement. Comme il s'agit d'une fonction asynchrone, nous renvoyons la promesse donnée afin d'informer l'appelant que l'entraînement est terminé.

Pour surveiller la progression de l'entraînement, nous transmettons des rappels à model.fit. Nous utilisons tfvis.show.fitCallbacks pour générer des fonctions qui représentent dans un graphique les métriques "loss" (perte) et "mse" (erreur quadratique moyenne) que nous avons vues précédemment.

Faire la synthèse

Nous devons maintenant appeler les fonctions que nous avons définies à partir de la fonction run.

96914ff65fc3b74c.png Ajoutez le code suivant en bas de la fonction

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');

Actualisez la page. Le graphique suivant devrait être mis à jour au bout de quelques secondes.

c6d3214d6e8c3752.png

Il est créé par les rappels que nous avons implémentés précédemment. Il affiche la perte et l'erreur quadratique moyenne sur l'ensemble de données complet à la fin de chaque époque.

Lorsque nous entraînons un modèle, nous voulons voir la perte diminuer. Dans notre cas, étant donné que notre métrique est une mesure d'erreur, nous voulons également la voir diminuer.

Après avoir entraîné le modèle, nous voulons effectuer des prédictions. Évaluons le modèle en observant ce qu'il prédit pour une plage uniforme de nombres faibles et élevés de chevaux.

96914ff65fc3b74c.png Ajoutez la fonction suivante à votre fichier 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
    }
  );
}

Voici quelques points importants à noter concernant la fonction ci-dessus.

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

Nous générons 100 nouveaux "exemples" pour alimenter le modèle. Model.predict désigne la manière dont nous fournissons ces exemples au modèle. Notez qu'ils doivent se présenter sous la même forme ([num_examples, num_features_per_example]) que lors de l'entraînement.

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

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

Pour rétablir les données dans notre plage d'origine (au lieu de la plage 0 à 1), nous utilisons les valeurs que nous avons calculées lors de la normalisation, mais en inversant les opérations.

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

.dataSync() est une méthode que nous pouvons utiliser pour obtenir un typedarray (tableau typé) des valeurs stockées dans un Tensor. Cela nous permet de traiter ces valeurs en JavaScript standard. Il s'agit d'une version synchrone de la méthode .data() généralement recommandée.

Enfin, nous utilisons tfjs-vis pour représenter les données d'origine et les prédictions du modèle dans un graphique.

96914ff65fc3b74c.png Ajoutez le code suivant à la fonction

run

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

Actualisez la page. Une fois l'entraînement terminé, vous devriez voir quelque chose de semblable à ce qui suit.

fe610ff34708d4a.png

Félicitations ! Vous venez d'entraîner un modèle de machine learning simple. Il effectue actuellement ce que l'on appelle une régression linéaire, consistant à tenter d'ajuster une ligne à la tendance présente dans les données d'entrée.

L'entraînement d'un modèle de machine learning comporte les étapes suivantes.

Spécifiez votre tâche :

  • S'agit-il d'un problème de régression ou d'un problème de classification ?
  • L'apprentissage supervisé ou non supervisé peut-il être utilisé ?
  • Sous quelle forme les données d'entrée se présentent-elles ? À quoi devraient ressembler les données de sortie ?

Préparez vos données :

  • Nettoyez les données et inspectez-les manuellement pour y déceler un modèle lorsque cela est possible.
  • Brassez vos données avant de les utiliser pour l'entraînement.
  • Normalisez les données dans une plage raisonnable pour le réseau de neurones. Généralement, il s'agit des plages 0 à 1 et -1 à 1, car elles sont adaptées aux données numériques.
  • Convertissez vos données en Tensors.

Créez et exécutez votre modèle :

  • Définissez le modèle à l'aide de tf.sequential ou tf.model, puis ajoutez-y des couches avec tf.layers.*.
  • Choisissez un optimiseur (adam est généralement un bon choix) ainsi que des paramètres comme la taille du lot et le nombre d'époques.
  • Choisissez une fonction de perte appropriée pour votre problème, ainsi qu'une métrique de précision pour vous aider à évaluer votre progression. meanSquaredError est une fonction de perte couramment utilisée pour les problèmes de régression.
  • Surveillez l'entraînement pour voir si la perte diminue.

Évaluez le modèle :

  • Pour votre modèle, choisissez une métrique d'évaluation que vous pourrez surveiller pendant l'entraînement. Une fois l'entraînement terminé, effectuez des tests pour vous faire une idée de la qualité des prédictions.
  • Essayez de modifier le nombre d'époques. De combien d'époques avez-vous besoin avant que le graphique ne s'aplatisse ?
  • Essayez d'augmenter le nombre d'unités dans la couche cachée.
  • Essayez d'ajouter d'autres couches cachées entre la première couche cachée ajoutée et la couche de sortie finale. Le code de ces couches supplémentaires devrait ressembler à ceci.
model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));

Ce qu'il est important de savoir concernant ces couches cachées, c'est qu'elles introduisent une fonction d'activation non linéaire (ici, l'activation sigmoïde). Pour en savoir plus sur les fonctions d'activation, consultez cet article.

Essayez de voir si le modèle peut produire une sortie telle qu'illustrée dans l'image ci-dessous.

a21c5e6537cf81d.png