Regressione base: Prevedere il consumo di carburante

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza il sorgente su GitHub Scarica il notebook

In un problema di regressione, abbiamo l'obiettivo di prevedere l'andamento di un valore continuo, come un prezzo o una probabilità. Al contrario di un problema di classificazione, ove abbiamo l'obiettivo di scegliere una classe tra una lista di classi (per esempio, quando un'immagine contenga una mela o un'arancia, riconoscere quale frutto è nell'immagine).

Questo notebook usa il classico Dataset Auto MPG e costruisce un modello per prevedere il consumo di carburante delle auto della fine degli anni '70 e dell'inizio degli anni '80. Per farlo, forniremo al modello una descrizione di mote automobili di quel periodo. Questa descrizione include attributi come: cilindri, cilindrata, cavalli di potenza, e peso.

Questo esempio usa le API tf.keras, per i dettagli vedere questa guida.

# Use seaborn for pairplot
!pip install -q seaborn

# Use some functions from tensorflow_docs
!pip install -q git+https://github.com/tensorflow/docs
import pathlib

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

print(tf.__version__)
2.3.0

import tensorflow_docs as tfdocs
import tensorflow_docs.plots
import tensorflow_docs.modeling

Il dataset Auto MPG

Il dataset è disponibile su UCI Machine Learning Repository.

Otteniamo i dati

Per prima cosa, scarichiamo il dataset.

dataset_path = keras.utils.get_file("auto-mpg.data", "http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
dataset_path
Downloading data from http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data
32768/30286 [================================] - 0s 4us/step

'/home/kbuilder/.keras/datasets/auto-mpg.data'

Importiamolo usando pandas

column_names = ['MPG','Cylinders','Displacement','Horsepower','Weight',
                'Acceleration', 'Model Year', 'Origin']
raw_dataset = pd.read_csv(dataset_path, names=column_names,
                      na_values = "?", comment='\t',
                      sep=" ", skipinitialspace=True)

dataset = raw_dataset.copy()
dataset.tail()

Ripuliamo i dati

Il dataset contiene alcuni valori sconosciuti.

dataset.isna().sum()
MPG             0
Cylinders       0
Displacement    0
Horsepower      6
Weight          0
Acceleration    0
Model Year      0
Origin          0
dtype: int64

Per mantenere semplice questo tutorial iniziale, eliminiamo queste righe.

dataset = dataset.dropna()

La colonna "Origine", in verità, è una categoria, non un numero. Così la convertiamo in un indicatore:

dataset['Origin'] = dataset['Origin'].map(lambda x: {1: 'USA', 2: 'Europe', 3: 'Japan'}.get(x))
dataset = pd.get_dummies(dataset, prefix='', prefix_sep='')
dataset.tail()

Partizioniamo i dati in addestramento e verifica

Ora dividiamo in due il dataset in un insieme di addestramento ed uno di verifica.

Useremo l'insieme di verifica nella valutazione finale del nostro modello.

train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

Osserviamo i dati

Diamo un'occhiata alla distribuzione congiunta di alcune coppie di colonne dall'insieme di addestramento.

sns.pairplot(train_dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")
<seaborn.axisgrid.PairGrid at 0x7f68d75406a0>

png

Ed anche alle statistiche generali:

train_stats = train_dataset.describe()
train_stats.pop("MPG")
train_stats = train_stats.transpose()
train_stats

Separiamo le caratteristiche dalle etichette

Separiamo i valori obiettivo, o "etichette", dalle caratteristiche. Questa etichetta è il valore che il modello sarà addestrato a predire.

train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

normalizziamo i dati

Osserviamo di nuovo il blocco train_stats sopra, e notiamo come siano diversi gli intervalli di ciascuna caratteristica.

E' buona pratica normalizzare le caratteristiche che usano diversi intervalli e scale. Sebbene il modello possa convergere senza normalizzare le caratteristiche, ciò rende l'addestramento più difficile, e rende il modello risultante dipendente dalla scelta delle unità usate nell'input.

Nota: Sebbene, intenzionalmente, generiamo queste statistiche dal solo dataset di addestramento, esse potranno essere usate anche per normalizzare il dataset di validazione. Ciò è necessario per proiettare il dataset di validazione con la stessa distribuzione con cui è stato addestrato il modello.

def norm(x):
  return (x - train_stats['mean']) / train_stats['std']
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

Questi dati normalizzati saranno quelli che useremo per addestrare il modello.

Attenzione: Le statistiche utilizzate per normalizzare gli input (la media e la deviazione standard) devono essere applicate ad ogni altro valore con cui sia alimentato il modello, assieme alla codifica degli indicatori che abbiamo fatto prima. Inclusi: l'insieme di validazione ed i dati vivi, quando il modello verrà usato in produzione.

Il modello

Costruiamo il modello

Andiamo a realizzare il nostro modello. Useremo un modello Sequenziale con due livelli nascosti, densamente connessi, ed un livello di uscita che restituisce un valore singolo, continuo. I passi di costruzione del modello sono raccolti in una funzione, build_model, perché, in seguito, creeremo un secondo modello.

def build_model():
  model = keras.Sequential([
    layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
  ])

  optimizer = tf.keras.optimizers.RMSprop(0.001)

  model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae', 'mse'])
  return model
model = build_model()

Osserviamo il modello

Usiamo il metodo .summary per visualizzare una semplice descrizione del modello

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________

Ora proviamo il modello. Prendiamo un blocco di 10 esempi dai dati di addestramento e, su di essi, chiamiamo model.predict.

example_batch = normed_train_data[:10]
example_result = model.predict(example_batch)
example_result
array([[0.41037187],
       [0.3609022 ],
       [0.8292659 ],
       [0.58228904],
       [0.70353585],
       [0.26415387],
       [0.78764254],
       [0.8256647 ],
       [0.18449703],
       [1.1366624 ]], dtype=float32)

Sembra essere funzionante, e produce un risultato nel formato e del tipo attesi.

Addestriamo il modello

Addestriamo il modello per 1000 epoche, e registriamo l'accuratezza dell'addestramento e della validazione nell'oggetto history.

EPOCHS = 1000

history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split = 0.2, verbose=0,
  callbacks=[tfdocs.modeling.EpochDots()])

Epoch: 0, loss:537.9646,  mae:21.9256,  mse:537.9646,  val_loss:516.0488,  val_mae:21.4139,  val_mse:516.0488,  
....................................................................................................
Epoch: 100, loss:5.8615,  mae:1.7155,  mse:5.8615,  val_loss:8.4880,  val_mae:2.2005,  val_mse:8.4880,  
....................................................................................................
Epoch: 200, loss:5.1384,  mae:1.5519,  mse:5.1384,  val_loss:8.2369,  val_mae:2.1810,  val_mse:8.2369,  
....................................................................................................
Epoch: 300, loss:4.5985,  mae:1.4688,  mse:4.5985,  val_loss:8.2864,  val_mae:2.1962,  val_mse:8.2864,  
....................................................................................................
Epoch: 400, loss:4.1357,  mae:1.3550,  mse:4.1357,  val_loss:8.3909,  val_mae:2.1935,  val_mse:8.3909,  
....................................................................................................
Epoch: 500, loss:3.6589,  mae:1.2935,  mse:3.6589,  val_loss:8.5335,  val_mae:2.2304,  val_mse:8.5335,  
....................................................................................................
Epoch: 600, loss:3.3305,  mae:1.1970,  mse:3.3305,  val_loss:8.6683,  val_mae:2.2375,  val_mse:8.6683,  
....................................................................................................
Epoch: 700, loss:3.1900,  mae:1.1872,  mse:3.1900,  val_loss:8.3798,  val_mae:2.2475,  val_mse:8.3798,  
....................................................................................................
Epoch: 800, loss:2.7907,  mae:1.1028,  mse:2.7907,  val_loss:8.6257,  val_mae:2.2902,  val_mse:8.6257,  
....................................................................................................
Epoch: 900, loss:2.4181,  mae:0.9920,  mse:2.4181,  val_loss:9.7861,  val_mae:2.4477,  val_mse:9.7861,  
....................................................................................................

Visualizziamo il progresso del modello durante l'addestramento usando le statistiche immagazzinate nell'oggetto history.

hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()
plotter = tfdocs.plots.HistoryPlotter(smoothing_std=2)
plotter.plot({'Basic': history}, metric = "mae")
plt.ylim([0, 10])
plt.ylabel('MAE [MPG]')
Text(0, 0.5, 'MAE [MPG]')

png

plotter.plot({'Basic': history}, metric = "mse")
plt.ylim([0, 20])
plt.ylabel('MSE [MPG^2]')
Text(0, 0.5, 'MSE [MPG^2]')

png

Questo grafico mostra miglioramenti modesti, o anche peggioramenti nell'errore di validazione dopo circa 100 epoche. Andiamo a modificare la chiamata alla model.fit per fermare automaticamente l'addestramento quando il risultato della validazione non migliora. Useremo la EarlyStopping callback che controlla la condizione di addestramento ad ogni epoca. Se un dato numero di epoche passano senza mostrare miglioramenti, allora l'addestramento viene interrotto automaticamente.

Qui potete imparare di più a proposito di questa callback.

model = build_model()

# The patience parameter is the amount of epochs to check for improvement
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)

early_history = model.fit(normed_train_data, train_labels, 
                    epochs=EPOCHS, validation_split = 0.2, verbose=0, 
                    callbacks=[early_stop, tfdocs.modeling.EpochDots()])

Epoch: 0, loss:569.4721,  mae:22.5956,  mse:569.4720,  val_loss:556.5345,  val_mae:22.2034,  val_mse:556.5345,  
....................................................
plotter.plot({'Early Stopping': early_history}, metric = "mae")
plt.ylim([0, 10])
plt.ylabel('MAE [MPG]')
Text(0, 0.5, 'MAE [MPG]')

png

Il grafico mostra che l'errore medio sull'insieme di validazione è solitamente attorno a +/- 2 MPG. Va bene? Lasciamo a voi la decisione.

Andiamo a vedere quanto il modello generalizzi bene con l'insieme di test, che non abbiamo usato nell'addestramento del modello. Questo ci dice come ci possiamo aspettare che il modello possa comportarsi, quando lo usiamo nel mondo reale.

loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=2)

print("Testing set Mean Abs Error: {:5.2f} MPG".format(mae))
3/3 - 0s - loss: 5.9892 - mae: 1.8366 - mse: 5.9892
Testing set Mean Abs Error:  1.84 MPG

Facciamo previsioni

Finalmente, facciamo previsioni sui valori di MPG usando i dati nel l'insieme di test:

test_predictions = model.predict(normed_test_data).flatten()

a = plt.axes(aspect='equal')
plt.scatter(test_labels, test_predictions)
plt.xlabel('True Values [MPG]')
plt.ylabel('Predictions [MPG]')
lims = [0, 50]
plt.xlim(lims)
plt.ylim(lims)
_ = plt.plot(lims, lims)

png

Sembra che il nostro modello preveda ragionevolmente bene. Diamo un'occhiata alla distribuzione dell'errore.

error = test_predictions - test_labels
plt.hist(error, bins = 25)
plt.xlabel("Prediction Error [MPG]")
_ = plt.ylabel("Count")

png

Non è una gaussiana, ma potevamo aspettarcelo, perché il numero di campioni è molto piccolo.

Conclusioni

Questo notebook ha introdotto alcune tecniche per gestire un problema di regressione.

  • L'Errore Quadratico Medio (MSE) è una funzione perdita comunemente usata nei problemi di regressione (per i problemi di classificazione si usano altre funzioni perdita).
  • Analogamente, le metriche di valutazione usate per la regressione sono diverse da quelle della classificazione. Una metrica di comune per la regressione è l'Errore Assoluto Medio (MAE).
  • Quando caratteristiche numeriche di input hanno valori su intervalli diversi, ogni caratteristica deve essere scalata indipendentemente sullo stesso intervallo.
  • Se non ci sono abbastanza dati di addestramento, una soluzione è preferire una rete piccola con pochi livelli per evitare il sovra-allenamento.
  • L'interruzione precoce è una tecnica utile per prevenire il sovra-allenamento.

#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.