In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

In [ ]:
#@title MIT License
#
# 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.

Regressione base: Prevedere il consumo di carburante

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.


In [ ]:
# Use seaborn for pairplot
!pip install seaborn

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

In [ ]:
import pathlib

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

In [ ]:
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

print(tf.__version__)

In [ ]:
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.


In [ ]:
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

Importiamolo usando pandas


In [ ]:
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.


In [ ]:
dataset.isna().sum()

Per mantenere semplice questo tutorial iniziale, eliminiamo queste righe.


In [ ]:
dataset = dataset.dropna()

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


In [ ]:
dataset['Origin'] = dataset['Origin'].map(lambda x: {1: 'USA', 2: 'Europe', 3: 'Japan'}.get(x))

In [ ]:
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.


In [ ]:
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.


In [ ]:
sns.pairplot(train_dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")

Ed anche alle statistiche generali:


In [ ]:
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.


In [ ]:
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.


In [ ]:
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.


In [ ]:
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

In [ ]:
model = build_model()

Osserviamo il modello

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


In [ ]:
model.summary()

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


In [ ]:
example_batch = normed_train_data[:10]
example_result = model.predict(example_batch)
example_result

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.


In [ ]:
EPOCHS = 1000

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

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


In [ ]:
hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()

In [ ]:
plotter = tfdocs.plots.HistoryPlotter(smoothing_std=2)

In [ ]:
plotter.plot({'Basic': history}, metric = "mae")
plt.ylim([0, 10])
plt.ylabel('MAE [MPG]')

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

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.


In [ ]:
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()])

In [ ]:
plotter.plot({'Early Stopping': early_history}, metric = "mae")
plt.ylim([0, 10])
plt.ylabel('MAE [MPG]')

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.


In [ ]:
loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=2)

print("Testing set Mean Abs Error: {:5.2f} MPG".format(mae))

Facciamo previsioni

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


In [ ]:
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)

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


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

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.