TensorFlow.js - Riconoscimento cifre delle cifre scritte a mano libera con le CNN

1. Introduzione

In questo tutorial, creeremo un modello TensorFlow.js per riconoscere le cifre scritte a mano libera con una rete neurale convoluzionale. Innanzitutto, addestriamo il classificatore in modo che migliaia di immagini scritte a mano libera con le relative etichette. Successivamente, valuteremo l'accuratezza del classificatore utilizzando dati di test che il modello non ha mai rilevato.

Questa attività è considerata una classificazione poiché stiamo addestrando il modello ad assegnare una categoria (la cifra che appare nell'immagine) all'immagine di input. Addestreremo il modello mostrandogli molti esempi di input insieme all'output corretto. In questo caso si parla di apprendimento supervisionato.

Cosa creerai

Creerai una pagina web che utilizza TensorFlow.js per addestrare un modello nel browser. Data un'immagine in bianco e nero di una dimensione particolare, classificherà il numero che compare nell'immagine. I passaggi da seguire sono:

  • Carica i dati.
  • Definire l'architettura del modello.
  • Addestra il modello e monitorane le prestazioni durante l'addestramento.
  • Valuta il modello addestrato facendo alcune previsioni.

Cosa imparerai a fare

  • Sintassi TensorFlow.js per creare modelli convoluzionali utilizzando l'API Livelli di TensorFlow.js.
  • Formulazione di attività di classificazione in TensorFlow.js
  • Come monitorare l'addestramento nel browser utilizzando la libreria tfjs-vis.

Che cosa ti serve

Dovresti anche acquisire familiarità con il materiale del nostro primo tutorial di formazione.

2. Configurazione

Crea una pagina HTML e includi il codice JavaScript

96914ff65fc3b74c.pngCopia il seguente codice in un file HTML denominato

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.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 data file -->
  <script src="data.js" type="module"></script>

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

</head>

<body>
</body>
</html>

Crea i file JavaScript per i dati e il codice

  1. Nella stessa cartella del file HTML riportato sopra, crea un file denominato data.js e copia i contenuti da questo link in quel file.
  2. Nella stessa cartella del primo passaggio, crea un file denominato script.js e inserisci al suo interno il codice seguente.
console.log('Hello TensorFlow');

Mettiti alla prova

Ora che hai creato i file HTML e JavaScript, provali. Apri il file index.html nel browser e apri la console devtools.

Se tutto funziona correttamente, dovrebbero essere create due variabili globali. tf è un riferimento alla libreria TensorFlow.js, tfvis è un riferimento alla libreria tfjs-vis.

Dovresti vedere il messaggio Hello TensorFlow, in questo caso puoi andare al passaggio successivo.

3. Carica i dati

In questo tutorial imparerai a riconoscere un modello per imparare a riconoscere le cifre nelle immagini come quelle che seguono. Si tratta di immagini in scala di grigi di 28 x 28 px provenienti da un set di dati chiamato MNIST.

mnistro 4 mnist 3 mnistro 8

Abbiamo fornito il codice per caricare queste immagini da uno speciale file sprite (circa 10 MB) che abbiamo creato per te, in modo da poterci concentrare sulla parte di addestramento.

Puoi esaminare il file data.js per capire come vengono caricati i dati. Oppure, dopo aver completato questo tutorial, crea il tuo approccio al caricamento dei dati.

Il codice fornito contiene una classe MnistData con due metodi pubblici:

  • nextTrainBatch(batchSize): restituisce un batch casuale di immagini e le relative etichette dal set di addestramento.
  • nextTestBatch(batchSize): restituisce un gruppo di immagini e le relative etichette dal set di test

La classe MnistData esegue anche i passaggi importanti di shuffling e normalizzazione dei dati.

Abbiamo un totale di 65.000 immagini. Useremo fino a 55.000 immagini per addestrare il modello, risparmiando 10.000 immagini da usare per testare le prestazioni del modello una volta terminato. E faremo tutto questo direttamente nel browser.

Carica i dati e verifica che siano caricati correttamente.

96914ff65fc3b74c.png Aggiungi il seguente codice al file script.js.

import {MnistData} from './data.js';

async function showExamples(data) {
  // Create a container in the visor
  const surface =
    tfvis.visor().surface({ name: 'Input Data Examples', tab: 'Input Data'});  

  // Get the examples
  const examples = data.nextTestBatch(20);
  const numExamples = examples.xs.shape[0];
  
  // Create a canvas element to render each example
  for (let i = 0; i < numExamples; i++) {
    const imageTensor = tf.tidy(() => {
      // Reshape the image to 28x28 px
      return examples.xs
        .slice([i, 0], [1, examples.xs.shape[1]])
        .reshape([28, 28, 1]);
    });
    
    const canvas = document.createElement('canvas');
    canvas.width = 28;
    canvas.height = 28;
    canvas.style = 'margin: 4px;';
    await tf.browser.toPixels(imageTensor, canvas);
    surface.drawArea.appendChild(canvas);

    imageTensor.dispose();
  }
}

async function run() {  
  const data = new MnistData();
  await data.load();
  await showExamples(data);
}

document.addEventListener('DOMContentLoaded', run);

Aggiorna la pagina. Dopo qualche secondo dovresti vedere un riquadro sulla sinistra con una serie di immagini.

6dff857738b54eed.png

4. Concettualizzare il nostro compito

I nostri dati di input hanno il seguente aspetto.

6dff857738b54eed.png

Il nostro obiettivo è addestrare un modello che prenda un'immagine e impari a prevedere un punteggio per ognuna delle 10 classi a cui l'immagine può appartenere (le cifre da 0 a 9).

Ogni immagine è larga 28 px e alta 28 px e ha un canale 1 colore poiché è un'immagine in scala di grigi. La forma di ogni immagine è quindi [28, 28, 1].

Ricorda che eseguiamo una mappatura da uno a dieci, nonché la forma di ogni esempio di input, poiché è importante per la sezione successiva.

5. Definire l'architettura del modello

In questa sezione scriveremo il codice per descrivere l'architettura del modello. L'architettura del modello è un modo sofisticato per dire "quali funzioni verranno eseguite dal modello durante l'esecuzione" o, in alternativa, "quale algoritmo utilizzerà il modello per calcolare le risposte".

Nel machine learning definiamo un'architettura (o algoritmo) e lasciamo che il processo di addestramento apprenda i parametri di quell'algoritmo.

96914ff65fc3b74c.png Aggiungi la seguente funzione al tuo

script.js per definire l'architettura del modello

function getModel() {
  const model = tf.sequential();
  
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const IMAGE_CHANNELS = 1;  
  
  // In the first layer of our convolutional neural network we have 
  // to specify the input shape. Then we specify some parameters for 
  // the convolution operation that takes place in this layer.
  model.add(tf.layers.conv2d({
    inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
    kernelSize: 5,
    filters: 8,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));

  // The MaxPooling layer acts as a sort of downsampling using max values
  // in a region instead of averaging.  
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  
  // Repeat another conv2d + maxPooling stack. 
  // Note that we have more filters in the convolution.
  model.add(tf.layers.conv2d({
    kernelSize: 5,
    filters: 16,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  
  // Now we flatten the output from the 2D filters into a 1D vector to prepare
  // it for input into our last layer. This is common practice when feeding
  // higher dimensional data to a final classification output layer.
  model.add(tf.layers.flatten());

  // Our last layer is a dense layer which has 10 output units, one for each
  // output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
  const NUM_OUTPUT_CLASSES = 10;
  model.add(tf.layers.dense({
    units: NUM_OUTPUT_CLASSES,
    kernelInitializer: 'varianceScaling',
    activation: 'softmax'
  }));

  
  // Choose an optimizer, loss function and accuracy metric,
  // then compile and return the model
  const optimizer = tf.train.adam();
  model.compile({
    optimizer: optimizer,
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy'],
  });

  return model;
}

Analizziamola più nel dettaglio.

Convoluzioni

model.add(tf.layers.conv2d({
  inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'varianceScaling'
}));

In questo caso utilizziamo un modello sequenziale.

Utilizziamo uno strato conv2d anziché uno denso. Non possiamo scendere nel dettaglio del funzionamento delle convoluzioni, ma ecco alcune risorse che spiegano l'operazione alla base:

Analizziamo ogni argomento nell'oggetto di configurazione per conv2d:

  • inputShape. La forma dei dati che confluiranno nel primo livello del modello. In questo caso, i nostri esempi MNIST sono immagini in bianco e nero di 28 x 28 pixel. Il formato canonico per i dati delle immagini è [row, column, depth], quindi qui vogliamo configurare una forma di [28, 28, 1]. 28 righe e colonne per il numero di pixel in ogni dimensione e una profondità pari a 1 perché le nostre immagini hanno un solo canale di colore. Tieni presente che non specifichiamo una dimensione del batch nella forma di input. I livelli sono progettati per essere indipendenti dalle dimensioni del batch, in modo che durante l'inferenza tu possa passare un tensore di qualsiasi dimensione del batch.
  • kernelSize. Le dimensioni delle finestre di filtro convoluzionali scorrevoli da applicare ai dati di input. Qui, impostiamo un kernelSize di 5, che specifica una finestra convoluzionale quadrata 5x5.
  • filters Il numero di finestre filtro di dimensioni kernelSize da applicare ai dati di input. In questo caso, verranno applicati 8 filtri ai dati.
  • strides La "dimensione del passo" della finestra scorrevole, ovvero di quanti pixel si sposta il filtro ogni volta che si sposta sull'immagine. Qui specifichiamo un passo di 1, il che significa che il filtro scorrerà sull'immagine in passaggi di 1 pixel.
  • activation. La funzione di attivazione da applicare ai dati al termine della convoluzione. In questo caso, stiamo applicando una funzione di unità lineare rettificata (ReLU), che è una funzione di attivazione molto comune nei modelli ML.
  • kernelInitializer. Il metodo da utilizzare per inizializzare in modo casuale i pesi del modello, molto importante per l'addestramento delle dinamiche. Non entreremo nei dettagli dell'inizializzazione qui, ma VarianceScaling (usato qui) è generalmente una buona scelta di inizializzazione.

Appiattimento della rappresentazione dei dati

model.add(tf.layers.flatten());

Le immagini sono dati ad alta dimensionalità e le operazioni di convoluzione tendono ad aumentare le dimensioni dei dati inseriti. Prima di passarli al nostro strato di classificazione finale, dobbiamo appiattire i dati in un unico array lungo. Gli strati densi (che usiamo come strato finale) richiedono solo tensor1d secondi, quindi questo passaggio è comune in molte attività di classificazione.

Calcola la nostra distribuzione di probabilità finale

const NUM_OUTPUT_CLASSES = 10;
model.add(tf.layers.dense({
  units: NUM_OUTPUT_CLASSES,
  kernelInitializer: 'varianceScaling',
  activation: 'softmax'
}));

Utilizzeremo uno strato denso con attivazione softmax per calcolare le distribuzioni di probabilità nelle 10 possibili classi. La classe con il punteggio più alto sarà la cifra prevista.

Scegli una funzione di ottimizzazione e perdita

const optimizer = tf.train.adam();
model.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

Compila il modello specificando un ottimizzatore, una funzione di perdita e le metriche di cui vogliamo tenere traccia.

A differenza del nostro primo tutorial, qui utilizziamo categoricalCrossentropy come funzione di perdita. Come suggerisce il nome, viene utilizzato quando l'output del modello è una distribuzione di probabilità. categoricalCrossentropy misura l'errore tra la distribuzione di probabilità generata dall'ultimo livello del nostro modello e la distribuzione di probabilità data dall'etichetta vera.

Ad esempio, se la nostra cifra rappresenta davvero un 7, potremmo ottenere i seguenti risultati:

Indice

0

1

2

3

4

5

6

7

8

9

Etichetta True

0

0

0

0

0

0

0

1

0

0

Previsione

0,1

0,01

0,01

0,01

0,20

0,01

0,01

0,60

0,03

0,02

L'entropia incrociata categorica produce un singolo numero che indica quanto sia simile il vettore di previsione al vettore di etichetta vero.

La rappresentazione dei dati utilizzata qui per le etichette è chiamata codifica one-hot ed è comune nei problemi di classificazione. A ogni classe è associata una probabilità per ogni esempio. Quando sappiamo esattamente cosa dovrebbe essere, possiamo impostare la probabilità su 1 e le altre su 0. Per ulteriori informazioni sulla codifica one-hot, consulta questa pagina.

L'altra metrica che monitoreremo è accuracy, che per un problema di classificazione è la percentuale di previsioni corrette rispetto a tutte le previsioni.

6. Addestra il modello

96914ff65fc3b74c.pngCopia la seguente funzione nel file script.js.

async function train(model, data) {
  const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
  const container = {
    name: 'Model Training', tab: 'Model', styles: { height: '1000px' }
  };
  const fitCallbacks = tfvis.show.fitCallbacks(container, metrics);
  
  const BATCH_SIZE = 512;
  const TRAIN_DATA_SIZE = 5500;
  const TEST_DATA_SIZE = 1000;

  const [trainXs, trainYs] = tf.tidy(() => {
    const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
    return [
      d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  const [testXs, testYs] = tf.tidy(() => {
    const d = data.nextTestBatch(TEST_DATA_SIZE);
    return [
      d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  return model.fit(trainXs, trainYs, {
    batchSize: BATCH_SIZE,
    validationData: [testXs, testYs],
    epochs: 10,
    shuffle: true,
    callbacks: fitCallbacks
  });
}

96914ff65fc3b74c.png Quindi aggiungi il seguente codice al tuo

run funzione.

const model = getModel();
tfvis.show.modelSummary({name: 'Model Architecture', tab: 'Model'}, model);
  
await train(model, data);

Aggiorna la pagina e, dopo qualche secondo, dovresti vedere alcuni grafici che segnalano l'avanzamento dell'addestramento.

a2c7628dc47d465.png

Diamo un'occhiata più da vicino.

Monitorare le metriche

const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];

Qui decidiamo quali metriche monitorare. Monitoreremo la perdita e l'accuratezza sul set di addestramento, nonché la perdita e l'accuratezza nel set di convalida (rispettivamente val_loss e val_acc). Parleremo più diffusamente della serie di convalide di seguito.

Preparare i dati come tensori

const BATCH_SIZE = 512;
const TRAIN_DATA_SIZE = 5500;
const TEST_DATA_SIZE = 1000;

const [trainXs, trainYs] = tf.tidy(() => {
  const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
  return [
    d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

const [testXs, testYs] = tf.tidy(() => {
  const d = data.nextTestBatch(TEST_DATA_SIZE);
  return [
    d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

Qui creiamo due set di dati, un set di addestramento su cui addestreremo il modello e un set di convalida su cui testeremo il modello alla fine di ogni epoca. Tuttavia, i dati nel set di convalida non vengono mai mostrati al modello durante l'addestramento.

La classe di dati che abbiamo fornito semplifica il recupero dei tensori dai dati dell'immagine. Tuttavia, prima di poterli inviare al modello, rimodelleremo comunque i tensori nella forma prevista dal modello, [num_examples, image_width, image_height, channels]. Per ogni set di dati abbiamo sia gli input (le X) sia le etichette (le Y).

return model.fit(trainXs, trainYs, {
  batchSize: BATCH_SIZE,
  validationData: [testXs, testYs],
  epochs: 10,
  shuffle: true,
  callbacks: fitCallbacks
});

Chiamiamo model.fit per avviare il loop di addestramento. Passiamo anche una proprietà validationData per indicare i dati che il modello deve utilizzare per testare se stesso dopo ogni epoca (ma non per l'addestramento).

Se l'efficacia dei dati di addestramento è ottimale, ma non di quelli di convalida, significa che probabilmente il modello sta superando i dati di addestramento e non generalizza bene per l'input che non ha mai visto in precedenza.

7. Valuta il nostro modello

L'accuratezza della convalida fornisce una buona stima del rendimento del modello su dati che non ha mai rilevato prima (purché i dati somigliano in qualche modo al set di convalida). Tuttavia, potremmo aver bisogno di un'analisi più dettagliata del rendimento nelle varie classi.

Esistono un paio di metodi in tfjs-vis che possono aiutarti.

96914ff65fc3b74c.png Aggiungi il seguente codice in fondo al file script.js

const classNames = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}


async function showAccuracy(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = {name: 'Accuracy', tab: 'Evaluation'};
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}

async function showConfusion(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = {name: 'Confusion Matrix', tab: 'Evaluation'};
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}

Che cosa sta facendo questo codice?

  • Consente di fare una previsione.
  • Calcola le metriche di accuratezza.
  • Mostra le metriche

Diamo un'occhiata più da vicino a ogni passaggio.

Fai previsioni

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}      

Per prima cosa dobbiamo fare alcune previsioni. In questo caso, prenderemo 500 immagini e prevediamo quale numero contiene (puoi aumentare questo numero in un secondo momento per eseguire test su un insieme più ampio di immagini).

In particolare, la funzione argmax è ciò che ci dà l'indice della classe di probabilità più alta. Ricorda che il modello restituisce una probabilità per ogni classe. Qui troviamo la probabilità più alta e la assegniamo come previsione.

Potresti anche notare che possiamo fare previsioni su tutti i 500 esempi contemporaneamente. Questa è la potenza della vettorizzazione offerta da TensorFlow.js.

Mostra accuratezza per corso

async function showAccuracy() {
  const [preds, labels] = doPrediction();
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = { name: 'Accuracy', tab: 'Evaluation' };
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}      

Con un set di previsioni ed etichette possiamo calcolare l'accuratezza per ogni classe.

Mostra una matrice di confusione

async function showConfusion() {
  const [preds, labels] = doPrediction();
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = { name: 'Confusion Matrix', tab: 'Evaluation' };
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}  

Una matrice di confusione è simile all'accuratezza per classe, ma la suddivide ulteriormente per mostrare schemi di classificazione errata. Ti permette di capire se il modello viene confuso riguardo a una particolare coppia di classi.

Mostrare la valutazione

96914ff65fc3b74c.png Aggiungi il seguente codice in fondo alla funzione di esecuzione per mostrare la valutazione.

await showAccuracy(model, data);
await showConfusion(model, data);

Dovresti vedere un display simile al seguente.

82458197bd5e7f52.png

Complimenti! Hai appena addestrato una rete neurale convoluzionale.

8. Concetti principali

La previsione delle categorie per i dati di input è un'attività di classificazione.

Le attività di classificazione richiedono una rappresentazione appropriata dei dati per le etichette

  • Le rappresentazioni comuni delle etichette includono la codifica one-hot delle categorie

Prepara i dati:

  • È utile tenere da parte alcuni dati che il modello non vede mai durante l'addestramento e utilizzare per valutarlo. Questo è chiamato set di convalida.

Crea ed esegui il modello:

  • È stato dimostrato che i modelli convoluzionali si dimostrano efficaci per le attività legate alle immagini.
  • I problemi di classificazione di solito usano l'entropia incrociata categorica per le loro funzioni di perdita.
  • Monitora l'addestramento per capire se la perdita sta diminuendo e la precisione sta aumentando.

valuta il modello

  • Decidere in che modo valutare il modello una volta addestrato per vedere come sta funzionando sul problema iniziale che si voleva risolvere.
  • Le matrici di accuratezza e confusione per classe possono offrire una suddivisione più precisa delle prestazioni del modello rispetto alla sola accuratezza complessiva.