Sovradattamento e sottoadattamento

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

Come sempre, il codice in questo esempio usa le API tf.keras, di cui potete imparare di più nella guida Keras di TensorFlow.

In entrambi gli esempi precedenti—classificazione di testo e previsione di consumo carburante — abbiamo visto come l'accuratezza del nostro modello sui dati di validazione raggiunga il picco dopo l'addestramento per un certo numero di epoche, e poi ristagni o cominci a decrescere.

In altre parole, come il nostro modello si sovradatti (overfit n.d.t.) con i dati addestramento. Imparare come gestire il sovradattamento è importante. Sebbene sia spesso possibile raggiungere un'accuratezza alta sul insieme di addestramento, ciò che realmente vogliamo è sviluppare un modello che generalizzi bene sul insieme di test (o su dati che non ha mai visto prima).

L'opposto del sovradattamento è il sottoadattamento. Il sottoadattamento si verifica quando c'è ancora spazio di miglioramento sui dati di test. Ciò può accadere per un certo numero di ragioni: Se il modello non è abbastanza potente, se è sovra-regolarizzato, o, semplicemente, non è stato addestrato abbastanza. Ciò significa che la rete non ha appreso i pattern rilevanti nei dati di addestramento.

Se lo allenate per un tempo troppo lungo, il modello inizierà a sovradattarsi ed ad imparare pattern dai dati di addestramento che non si generalizzeranno ai dati di test. Dobbiamo trovare un compromesso. Capire come addestrare per un numero appropriato di epoche, come vedremo nel seguito, è una competenza utile.

La soluzione migliore per prevenire il sovradattaemnto è usare dati di addestramento più completi. Il dataset dovrebbe coprire lo spettro completo degli input che il modello possa aspettarsi di trattare. Ulteriori dati possono essere utili solo se coprono nuovi ed interessanti casi.

Un modello addestrato su un insieme più completo di dati generalizzerà naturalmente meglio. Quando ciò non fosse possibile, la soluzione migliore è usare tecniche come la regolarizzazione. Esse impongono dei vincoli alla quantità ed al tipo di informazioni che il vostro modello può immagazzinare. Se una rete può memorizzare solo un piccolo numero di pattern, il processo di ottimizzazione lo costringerà a focalizzarsi sui patern più promettenti, che hanno la migliore probabilità di generalizzare bene.

In questo notebook, esploreremo diverse tecniche di regolarizzazione, e le useremo per migliorare un modello di classificazione.

Setup

Prima di iniziare,importiamo i pacchetti necessari:

from __future__ import absolute_import, division, print_function, unicode_literals

try:
  # %tensorflow_version only exists in Colab.
  %tensorflow_version 2.x
except Exception:
  pass
import tensorflow as tf

from tensorflow.keras import layers
from tensorflow.keras import regularizers

print(tf.__version__)
2.3.0

!pip install -q git+https://github.com/tensorflow/docs

import tensorflow_docs as tfdocs
import tensorflow_docs.modeling
import tensorflow_docs.plots
WARNING: You are using pip version 20.2.2; however, version 20.2.3 is available.
You should consider upgrading via the '/tmpfs/src/tf_docs_env/bin/python -m pip install --upgrade pip' command.

from  IPython import display
from matplotlib import pyplot as plt

import numpy as np

import pathlib
import shutil
import tempfile

logdir = pathlib.Path(tempfile.mkdtemp())/"tensorboard_logs"
shutil.rmtree(logdir, ignore_errors=True)

Il dataset Higgs

L'obiettivo di questo tutorial non è fare fisica delle particelle, perciò non approfondiamo i dettagli del dataset. Esso contiene 11 000 000 esempi, con 28 caratteristiche ciascuno, ed una etichetta di classificazione binaria.

gz = tf.keras.utils.get_file('HIGGS.csv.gz', 'https://archive.ics.uci.edu/ml/machine-learning-databases/00280/HIGGS.csv.gz')
Downloading data from https://archive.ics.uci.edu/ml/machine-learning-databases/00280/HIGGS.csv.gz
2816409600/2816407858 [==============================] - 123s 0us/step

FEATURES = 28

La classe tf.data.experimental.CsvDataset può essere usata per leggere record csv direttamente da un file gzip senza passi intermedi di decompressione.

ds = tf.data.experimental.CsvDataset(gz,[float(),]*(FEATURES+1), compression_type="GZIP")

La classe che legge il cvs restituisce una lista di scalari per ogni record. La funzione seguente re-impacchetta tale lista in una coppia (vettore_di_caratteristiche, etichetta).

def pack_row(*row):
  label = row[0]
  features = tf.stack(row[1:],1)
  return features, label

TensorFlow è più efficiente quando tratta grandi lotti di dati.

Così, invece di re-impacchettare ciascuna riga individualmente, costruiamo un nuovo Dataset che contiene lotti di 10000-esempi, applichiamo la funzione pack_row a ciascun lotto, e quindi separiamo i lotti in singoli record:

packed_ds = ds.batch(10000).map(pack_row).unbatch()

Diamo uno sguardo ad alcuni dei record di questo nuovo packed_ds.

Le caratteristiche non sono perfettamente normalizzate, ma questo non è un problema per questo tutorial.

for features,label in packed_ds.batch(1000).take(1):
  print(features[0])
  plt.hist(features.numpy().flatten(), bins = 101)
tf.Tensor(
[ 0.8692932  -0.6350818   0.22569026  0.32747006 -0.6899932   0.75420225
 -0.24857314 -1.0920639   0.          1.3749921  -0.6536742   0.9303491
  1.1074361   1.1389043  -1.5781983  -1.0469854   0.          0.65792954
 -0.01045457 -0.04576717  3.1019614   1.35376     0.9795631   0.97807616
  0.92000484  0.72165745  0.98875093  0.87667835], shape=(28,), dtype=float32)

png

Per mantenere questo tutorial relativamente breve, usiamo solo i primi 1000 campioni per la validazione, ed i successivi 10000 per l'addestramento:

N_VALIDATION = int(1e3)
N_TRAIN = int(1e4)
BUFFER_SIZE = int(1e4)
BATCH_SIZE = 500
STEPS_PER_EPOCH = N_TRAIN//BATCH_SIZE

I metodi Dataset.skip e Dataset.take rendono semplice la cosa.

Allo stesso tempo, usare il metodo Dataset.cache assicura che il caricatore non abbia bisogno di ri-leggerei dati dal file ad ogni nuova epoca:

validate_ds = packed_ds.take(N_VALIDATION).cache()
train_ds = packed_ds.skip(N_VALIDATION).take(N_TRAIN).cache()
train_ds
<CacheDataset shapes: ((28,), ()), types: (tf.float32, tf.float32)>

Questi dataset restituiscono esempi singoli. Usiamo il metodo .batch per creare lotti di dimensione appropriata per l'addestramento. Prima della confezione dei lotti ricordiamo anche di .shuffle (mescolare n.d.t.) e .repeat (ripetere n.d.t.) l'insieme di addestramento.

validate_ds = validate_ds.batch(BATCH_SIZE)
train_ds = train_ds.shuffle(BUFFER_SIZE).repeat().batch(BATCH_SIZE)

Dimostrazione di sovradattamento

La via più semplice per prevenire il sovradattamento è cominciare con un modello piccolo: un modello con un piccolo numero di parametri da apprendere (determinati dal numero di livelli e dal numero di unità per livello). Nel deep learning, spesso si fa riferimento al numero di parametri che possono essere appresi da un modello, come la "capacity" del modello.

Intuitivamente, un modello con più parametri avrà maggiore "capacità di memorizzazione" e quindi sarà capace di imparare facilmente una corrispondenza perfetta simil-vocabolario tra i campioni di addestramento ed i relativi obiettivi, una mappatura senza nessun potere di generalizzazione, ma ciò sarebbe inutile per fare previsioni su dati mai visti.

Teniamo sempre a mente: i modelli di deep learning tendono ad essere bravi nell'aderire ai dati di addestramneto, ma la sfida reale è la generalizzazione, non l'adattamento.

d'altra parte, se la rete ha limitate risorse di memorizzazione, non sarà capace di imparare la mappatura così facilmente. Per minimizzare la sua funzione obiettivo, dovrà apprendere rappresentazioni compresse che hanno maggiore potere predittivo. Allo stesso tempo, se fate il vostro modello troppo piccolo, esso ha difficoltà ad aderire ai dati di addestramento. Occorre trovare un punto di equilibrio tra "capacità eccessiva" e "capacità insufficiente".

Sfortunatamente, non c'è nessuna formula magica per stabilire la dimensione giusta o l'architettura del vostro modello (in altre parole, il numero dei livelli, o la dimensione giusta di ciascun livello). Dovrete fare esperimenti usando architetture diverse.

Per trovare la dimensione giusta del modello, è meglio partire con relativamente pochi livelli e parametri, quindi cominciare ad aumentare la dimensione dei livelli o ad aggiungere nuovi livelli fino a che non vedete diminuire gli effetti sulla funzione obiettivo di validazione.

Come punto di partenza, iniziate con un modello semplice che usa solo layers.Dense, poi create versioni più grandi e confrontatele.

Procedura di addestramento

Molti modelli si addestrano meglio se riducete gradualmente il tasso di apprendimento durante l'addestramento. Per ridurre il tasso di apprendimento nel tempo, usiamo optimizers.schedules:

lr_schedule = tf.keras.optimizers.schedules.InverseTimeDecay(
  0.001,
  decay_steps=STEPS_PER_EPOCH*1000,
  decay_rate=1,
  staircase=False)

def get_optimizer():
  return tf.keras.optimizers.Adam(lr_schedule)

Il codice di cui sopra usa schedules.InverseTimeDecay per diminuire iperbolicamente il tasso di apprendimento ad 1/2 del tasso base dopo 1000 epoche, ad 1/3 a 2000 epoche e così via.

step = np.linspace(0,100000)
lr = lr_schedule(step)
plt.figure(figsize = (8,6))
plt.plot(step/STEPS_PER_EPOCH, lr)
plt.ylim([0,max(plt.ylim())])
plt.xlabel('Epoch')
_ = plt.ylabel('Learning Rate')

png

Ogni modello in questo tutorial userà la medesima configurazione di addestramento. Perciò la impostiamo in modo riusabile, partendo dalla lista delle callback.

L'addestramento di questo tutorial gira per molte epoche brevi. Per ridurre lo sforzo di tracciamento usiamo tfdocs.EpochDots che utilizza un semplice . per ogni epoca e, un insieme completo di metriche ogni 100 epoche.

In seguito, includiamo callbacks.EarlyStopping per evitare lunghi tempi di addestramento non necessari. notate che questa callback è impostata per monitorare la val_binary_crossentropy, non la val_loss. Questa differenza sarà importante in seguito.

Usiamo callbacks.TensorBoard per generare i tracciati TensorBoard per l'addestramento.

def get_callbacks(name):
  return [
    tfdocs.modeling.EpochDots(),
    tf.keras.callbacks.EarlyStopping(monitor='val_binary_crossentropy', patience=200),
    tf.keras.callbacks.TensorBoard(logdir/name),
  ]

Analogamente, ogni modello userà le stesse impostazioni Model.compile e Model.fit settings:

def compile_and_fit(model, name, optimizer=None, max_epochs=10000):
  if optimizer is None:
    optimizer = get_optimizer()
  model.compile(optimizer=optimizer,
                loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
                metrics=[
                  tf.keras.losses.BinaryCrossentropy(
                      from_logits=True, name='binary_crossentropy'),
                  'accuracy'])

  model.summary()

  history = model.fit(
    train_ds,
    steps_per_epoch = STEPS_PER_EPOCH,
    epochs=max_epochs,
    validation_data=validate_ds,
    callbacks=get_callbacks(name),
    verbose=0)
  return history

Modello minuscolo

Iniziamo addestrando un modello lineare:

tiny_model = tf.keras.Sequential([
    layers.Dense(16, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(1)
])
size_histories = {}
size_histories['Tiny'] = compile_and_fit(tiny_model, 'sizes/Tiny')
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 16)                464       
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 17        
=================================================================
Total params: 481
Trainable params: 481
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/summary_ops_v2.py:1277: stop (from tensorflow.python.eager.profiler) is deprecated and will be removed after 2020-07-01.
Instructions for updating:
use `tf.profiler.experimental.stop` instead.
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0032s vs `on_train_batch_end` time: 0.0250s). Check your callbacks.

Epoch: 0, accuracy:0.5019,  binary_crossentropy:0.7729,  loss:0.7729,  val_accuracy:0.4980,  val_binary_crossentropy:0.7293,  val_loss:0.7293,  
....................................................................................................
Epoch: 100, accuracy:0.5882,  binary_crossentropy:0.6284,  loss:0.6284,  val_accuracy:0.5770,  val_binary_crossentropy:0.6293,  val_loss:0.6293,  
....................................................................................................
Epoch: 200, accuracy:0.6092,  binary_crossentropy:0.6161,  loss:0.6161,  val_accuracy:0.6250,  val_binary_crossentropy:0.6157,  val_loss:0.6157,  
....................................................................................................
Epoch: 300, accuracy:0.6357,  binary_crossentropy:0.6015,  loss:0.6015,  val_accuracy:0.6130,  val_binary_crossentropy:0.6043,  val_loss:0.6043,  
....................................................................................................
Epoch: 400, accuracy:0.6532,  binary_crossentropy:0.5939,  loss:0.5939,  val_accuracy:0.6270,  val_binary_crossentropy:0.5999,  val_loss:0.5999,  
....................................................................................................
Epoch: 500, accuracy:0.6604,  binary_crossentropy:0.5887,  loss:0.5887,  val_accuracy:0.6500,  val_binary_crossentropy:0.5965,  val_loss:0.5965,  
....................................................................................................
Epoch: 600, accuracy:0.6664,  binary_crossentropy:0.5853,  loss:0.5853,  val_accuracy:0.6410,  val_binary_crossentropy:0.5971,  val_loss:0.5971,  
....................................................................................................
Epoch: 700, accuracy:0.6705,  binary_crossentropy:0.5829,  loss:0.5829,  val_accuracy:0.6630,  val_binary_crossentropy:0.5959,  val_loss:0.5959,  
....................................................................................................
Epoch: 800, accuracy:0.6714,  binary_crossentropy:0.5807,  loss:0.5807,  val_accuracy:0.6520,  val_binary_crossentropy:0.5962,  val_loss:0.5962,  
....................................................................................................
Epoch: 900, accuracy:0.6712,  binary_crossentropy:0.5798,  loss:0.5798,  val_accuracy:0.6440,  val_binary_crossentropy:0.5967,  val_loss:0.5967,  
....................................................................................................
Epoch: 1000, accuracy:0.6740,  binary_crossentropy:0.5779,  loss:0.5779,  val_accuracy:0.6550,  val_binary_crossentropy:0.5938,  val_loss:0.5938,  
....................................................................................................
Epoch: 1100, accuracy:0.6752,  binary_crossentropy:0.5767,  loss:0.5767,  val_accuracy:0.6580,  val_binary_crossentropy:0.5943,  val_loss:0.5943,  
....................................................................................................
Epoch: 1200, accuracy:0.6833,  binary_crossentropy:0.5753,  loss:0.5753,  val_accuracy:0.6410,  val_binary_crossentropy:0.5953,  val_loss:0.5953,  
....................................................................................................
Epoch: 1300, accuracy:0.6776,  binary_crossentropy:0.5744,  loss:0.5744,  val_accuracy:0.6570,  val_binary_crossentropy:0.5933,  val_loss:0.5933,  
....................................................................................................
Epoch: 1400, accuracy:0.6802,  binary_crossentropy:0.5732,  loss:0.5732,  val_accuracy:0.6540,  val_binary_crossentropy:0.5929,  val_loss:0.5929,  
....................................................................................................
Epoch: 1500, accuracy:0.6782,  binary_crossentropy:0.5723,  loss:0.5723,  val_accuracy:0.6620,  val_binary_crossentropy:0.5923,  val_loss:0.5923,  
....................................................................................................
Epoch: 1600, accuracy:0.6751,  binary_crossentropy:0.5716,  loss:0.5716,  val_accuracy:0.6610,  val_binary_crossentropy:0.5916,  val_loss:0.5916,  
....................................................................................................
Epoch: 1700, accuracy:0.6771,  binary_crossentropy:0.5711,  loss:0.5711,  val_accuracy:0.6620,  val_binary_crossentropy:0.5913,  val_loss:0.5913,  
....................................................................................................
Epoch: 1800, accuracy:0.6829,  binary_crossentropy:0.5705,  loss:0.5705,  val_accuracy:0.6470,  val_binary_crossentropy:0.5937,  val_loss:0.5937,  
....................................................................................................
Epoch: 1900, accuracy:0.6799,  binary_crossentropy:0.5698,  loss:0.5698,  val_accuracy:0.6650,  val_binary_crossentropy:0.5918,  val_loss:0.5918,  
................................................................................................

Adesso controlliamo cos'ha fatto il modello:

plotter = tfdocs.plots.HistoryPlotter(metric = 'binary_crossentropy', smoothing_std=10)
plotter.plot(size_histories)
plt.ylim([0.5, 0.7])
(0.5, 0.7)

png

Modello piccolo

Per vedere se possiamo battere le prestazioni del modello piccolo, addestriamo progressivamente modelli più grandi.

Proviamo due livelli nascosti con 16 unità ciascuno:

small_model = tf.keras.Sequential([
    # `input_shape` is only required here so that `.summary` works.
    layers.Dense(16, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(16, activation='elu'),
    layers.Dense(1)
])
size_histories['Small'] = compile_and_fit(small_model, 'sizes/Small')
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_2 (Dense)              (None, 16)                464       
_________________________________________________________________
dense_3 (Dense)              (None, 16)                272       
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 17        
=================================================================
Total params: 753
Trainable params: 753
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0038s vs `on_train_batch_end` time: 0.0552s). Check your callbacks.

Epoch: 0, accuracy:0.5167,  binary_crossentropy:0.7671,  loss:0.7671,  val_accuracy:0.4850,  val_binary_crossentropy:0.7226,  val_loss:0.7226,  
....................................................................................................
Epoch: 100, accuracy:0.6179,  binary_crossentropy:0.6159,  loss:0.6159,  val_accuracy:0.5830,  val_binary_crossentropy:0.6282,  val_loss:0.6282,  
....................................................................................................
Epoch: 200, accuracy:0.6471,  binary_crossentropy:0.5970,  loss:0.5970,  val_accuracy:0.6190,  val_binary_crossentropy:0.6138,  val_loss:0.6138,  
....................................................................................................
Epoch: 300, accuracy:0.6693,  binary_crossentropy:0.5814,  loss:0.5814,  val_accuracy:0.6640,  val_binary_crossentropy:0.5975,  val_loss:0.5975,  
....................................................................................................
Epoch: 400, accuracy:0.6780,  binary_crossentropy:0.5733,  loss:0.5733,  val_accuracy:0.6590,  val_binary_crossentropy:0.5938,  val_loss:0.5938,  
....................................................................................................
Epoch: 500, accuracy:0.6829,  binary_crossentropy:0.5691,  loss:0.5691,  val_accuracy:0.6730,  val_binary_crossentropy:0.5945,  val_loss:0.5945,  
....................................................................................................
Epoch: 600, accuracy:0.6872,  binary_crossentropy:0.5645,  loss:0.5645,  val_accuracy:0.6800,  val_binary_crossentropy:0.5936,  val_loss:0.5936,  
.............

Modello medio

Ora proviamo 3 modelli nascosti con 64 unità ciascuno:

medium_model = tf.keras.Sequential([
    layers.Dense(64, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(64, activation='elu'),
    layers.Dense(64, activation='elu'),
    layers.Dense(1)
])

Ed addestriamo il modello usando i medesimi dati:

size_histories['Medium']  = compile_and_fit(medium_model, "sizes/Medium")
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_5 (Dense)              (None, 64)                1856      
_________________________________________________________________
dense_6 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_7 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_8 (Dense)              (None, 1)                 65        
=================================================================
Total params: 10,241
Trainable params: 10,241
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0038s vs `on_train_batch_end` time: 0.0544s). Check your callbacks.

Epoch: 0, accuracy:0.5028,  binary_crossentropy:0.6963,  loss:0.6963,  val_accuracy:0.5190,  val_binary_crossentropy:0.6808,  val_loss:0.6808,  
....................................................................................................
Epoch: 100, accuracy:0.7144,  binary_crossentropy:0.5330,  loss:0.5330,  val_accuracy:0.6580,  val_binary_crossentropy:0.6080,  val_loss:0.6080,  
....................................................................................................
Epoch: 200, accuracy:0.7841,  binary_crossentropy:0.4373,  loss:0.4373,  val_accuracy:0.6530,  val_binary_crossentropy:0.6862,  val_loss:0.6862,  
................................................................................................

Modello grande

Come esercizio, potete creare un modello ancora più grande, e vedere come comincia a sovradattarsi rapidamente. Poi, aggiungiamo a questa prova una rete con capacità ancora maggiore, molto più di quanto il problema richiederebbe:

large_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(512, activation='elu'),
    layers.Dense(512, activation='elu'),
    layers.Dense(512, activation='elu'),
    layers.Dense(1)
])

E ancora, addestrare il modello usando gli stessi dati:

size_histories['large'] = compile_and_fit(large_model, "sizes/large")
Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_9 (Dense)              (None, 512)               14848     
_________________________________________________________________
dense_10 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_11 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_12 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_13 (Dense)             (None, 1)                 513       
=================================================================
Total params: 803,329
Trainable params: 803,329
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0038s vs `on_train_batch_end` time: 0.0577s). Check your callbacks.

Epoch: 0, accuracy:0.5060,  binary_crossentropy:0.7680,  loss:0.7680,  val_accuracy:0.5500,  val_binary_crossentropy:0.6684,  val_loss:0.6684,  
....................................................................................................
Epoch: 100, accuracy:1.0000,  binary_crossentropy:0.0020,  loss:0.0020,  val_accuracy:0.6510,  val_binary_crossentropy:1.7937,  val_loss:1.7937,  
....................................................................................................
Epoch: 200, accuracy:1.0000,  binary_crossentropy:0.0001,  loss:0.0001,  val_accuracy:0.6610,  val_binary_crossentropy:2.4333,  val_loss:2.4333,  
....................

Traccia delle funzioni obiettivo in addestramento e validazione

Le linee continue mostrano la funzione obiettivo in addestramento, e le linee tratteggiate la funzione obiettivo in validazione (ricordate: una minore funzione obiettivo in validazione indica un modello migliore).

Mentre la costruzione di un modello più grande gli conferisce più potenza, se questa potenza non è vincolata in qualche modo, essa può facilmente sovradattarsi sull'insieme di addestramento.

In questo esempio, tipicamente, solo il modello "Minuscolo" riesce ad evitare completamente il sovradattamento, mentre ciascuno dei modelli più grandi si sovradatta ai dati più rapidamente. Ciò diventa così grave oer il modello "grande" che siamo costretti a modificare il grafico ad una scala logaritmica per vedere ciò che realmente accade.

La cosa è evidente se si tracciano e confrontano le metriche di validazione e le metriche di addestramento.

  • E' normale ci sia una piccola differenza.
  • Se entrambe le metriche si muovono nella stessa direzione, va tutto bene.
  • Se la metrica di validazione comincia a ristagnare mentre la metrica di addestramento continua a migliorare, siamo probabilmente vicini al sovradattamento.
  • Se la metrica di validazione va nella direzione sbagliata, il modello si sta chiaramente sovradattando.
plotter.plot(size_histories)
a = plt.xscale('log')
plt.xlim([5, max(plt.xlim())])
plt.ylim([0.5, 0.7])
plt.xlabel("Epochs [Log Scale]")
Text(0.5, 0, 'Epochs [Log Scale]')

png

Notare: tutti gli addestramenti precedenti girano usando callbacks.EarlyStopping per fermare l'addestramento una volta che sia chiaro che il modello non stia facendo progressi.

Visualizzazione in TensorBoard

Tutti questi modelli scrivono tracciati TensorBoard durante l'addestramento.

Per aprire un visualizzatore TensorBoard all'interno di un notebook, copiate il codice seguente in una code-cell:

%tensorboard --logdir {logdir}/sizes

Possiamo vedere il risultato di un'esecuzione precedente di questo notebook su TensorBoard.dev.

TensorBoard.dev è un ambiente gestito per ospitare, tracciare e condividere con chiunque esperimenti di ML.

Per semplicità, è anche incluso in un <iframe>:

display.IFrame(
    src="https://tensorboard.dev/experiment/vW7jmmF9TmKmy3rbheMQpw/#scalars&_smoothingWeight=0.97",
    width="100%", height="800px")

Se vogliamo condividere risultati TensorBoard, possiamo caricare i tracciati in TensorBoard.dev copiando il codice seguente in una code-cell.

Notare: Questo passo richiede un account Google.

tensorboard dev upload --logdir  {logdir}/sizes

Attenzione: Questo comando non termina. E' progettato per eseguire l'upload continuo dei risultati di un esperimento di lunga durata. Una volta che i nostri dati sono caricati lo dovete fermare usando l'opzione "interrupt execution (interrompi l'esecuzione n.d.t.)" nel vostro strumento di esecuzione dei notebook.

Strategie per prevenire il sovradattamento

Prima di andare all'interno del contenuto di questo paragrafo, copiamo i tracciati dell'addestramento del modello "Minuscolo" di cui sopra, per usarlo come termine di paragone.

shutil.rmtree(logdir/'regularizers/Tiny', ignore_errors=True)
shutil.copytree(logdir/'sizes/Tiny', logdir/'regularizers/Tiny')
PosixPath('/tmp/tmpryviy19y/tensorboard_logs/regularizers/Tiny')
regularizer_histories = {}
regularizer_histories['Tiny'] = size_histories['Tiny']

Aggiunta della regolarizzazione del peso

Potreste avere familiarità con il principio del Rasoio di Occam: date due spiegazioni per qualcosa, la spiegazione che ha più probabilità di essere corretta è la "più semplice", quella che fa meno ipotesi. Ciò si applica anche ai modelli appresi dalle reti neurali: dato un insieme di dati di addestramento ed un'architettura di rete, ci sono molteplici insiemi di valori dei pesi (molteplici modelli) che possono spiegare i dati, ed i modelli più semplici si sovradattano meno facilmente di quelli complessi.

In questo contesto, un "modello semplice" è un modello in cui la distribuzione dei valori dei parametri ha meno entropia (o un modello con meno parametri del tutto, come vediamo del paragrafo sopra). Così, un modo comune di mitigare il sovradattamento è porre dei vincoli alla complessità di una rete forzando i suoi pesi a prendere solo valori piccoli, il che rende la distribuzione dei valori dei pesi "regolare". Ciò è chiamato "regolarizzazione dei pesi", e lo si fa aggiungendo alla funzione dei pesi della rete un costo associato all'avere grandi pesi. Questo costo è di due tipi:

  • Regolarizzazione L1, in cui il costo aggiunto è proporzionale al valore assoluto dei coefficienti dei pesi (cioè a ciò che viene chiamata "norma L1" dei pesi).

  • regolarizzazione L2, in cui il costo aggiunto è proporzionale al quadrato dei coefficienti dei pesi (cioè a quella che è chiamata "norma L2" dei pesi). Nel contesto delle reti neurali, la regolarizzazione L2 è chiamata anche decadimento dei pesi. Non lasciate che il nome vi confonda: il decadimento dei pesi è matematicamente la regolarizzazione L2.

La regolarizzazione L1spinge i pesi esattamente verso lo zero, favorendo un modello sparso. La regolarizzazione L2 penalizzerà i parametri di peso senza renderli sparsi in quanto la penalità tende a zero per pesi piccoli. La ragione per cui L2 è più comune.

In tf.keras, la regolarizzazione del peso viene ottenuta passando ai livelli delle istanze di regolarizzazione del peso come argomenti delle parole chiave. Ora andiamo ad aggiungere la regolarizzazione L2.

l2_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001),
                 input_shape=(FEATURES,)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(1)
])

regularizer_histories['l2'] = compile_and_fit(l2_model, "regularizers/l2")
Model: "sequential_4"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_14 (Dense)             (None, 512)               14848     
_________________________________________________________________
dense_15 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_16 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_17 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_18 (Dense)             (None, 1)                 513       
=================================================================
Total params: 803,329
Trainable params: 803,329
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0040s vs `on_train_batch_end` time: 0.0567s). Check your callbacks.

Epoch: 0, accuracy:0.5087,  binary_crossentropy:0.7974,  loss:2.3082,  val_accuracy:0.4650,  val_binary_crossentropy:0.6883,  val_loss:2.1173,  
....................................................................................................
Epoch: 100, accuracy:0.6537,  binary_crossentropy:0.5965,  loss:0.6195,  val_accuracy:0.6650,  val_binary_crossentropy:0.5811,  val_loss:0.6040,  
....................................................................................................
Epoch: 200, accuracy:0.6729,  binary_crossentropy:0.5833,  loss:0.6066,  val_accuracy:0.6760,  val_binary_crossentropy:0.5819,  val_loss:0.6053,  
....................................................................................................
Epoch: 300, accuracy:0.6789,  binary_crossentropy:0.5717,  loss:0.5958,  val_accuracy:0.7130,  val_binary_crossentropy:0.5711,  val_loss:0.5953,  
....................................................................................................
Epoch: 400, accuracy:0.6914,  binary_crossentropy:0.5620,  loss:0.5882,  val_accuracy:0.6860,  val_binary_crossentropy:0.5720,  val_loss:0.5982,  
....................................................................................................
Epoch: 500, accuracy:0.7010,  binary_crossentropy:0.5533,  loss:0.5812,  val_accuracy:0.6960,  val_binary_crossentropy:0.5698,  val_loss:0.5977,  
....................................................................................................
Epoch: 600, accuracy:0.7057,  binary_crossentropy:0.5484,  loss:0.5771,  val_accuracy:0.6960,  val_binary_crossentropy:0.5701,  val_loss:0.5992,  
........................................................

l2(0.001) significa che ogni coefficiente nella matrice dei pesi del livello aggiunge 0.001 * weight_coefficient_value**2 al totale loss della rete.

Questo è il perché monitoriamo direttamente il binary_crossentropy. Perché non ha mescolata in se questa componente di regolarizzazione.

Così, lo stesso modello "Grande" con una regolarizzazione di penalità L2 ha migliori prestazioni:

plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])
(0.5, 0.7)

png

Come potete vedere, il modello "L2"-regolarizzato è ora molto più competitivo con il modello "Minuscolo". Questo modello "L2" è anche molto più resistente al sovradattamento del modello "Grande" su cui si basava pur avendo lo stesso numero di parametri.

Maggiori informazioni

Ci sono due cose importanti da notare a proposito di questo tipo di regolarizzazione.

Primo: se state scrivendo il vostro proprio ciclo di addestramento, allora dovete essere sicuri di chiedere al modello le sue perdite di regolarizzazione.

result = l2_model(features)
regularization_loss=tf.add_n(l2_model.losses)

Secondo: Questa implementazione funziona aggiungendo le penalizzazioni di peso alla funzione obiettivo del modello, e poi applicando una procedura di ottimizzazione standard.

C'è un secondo approccio che applica invece l'ottimizzatore sulla funzione obiettivo grezza, e poi mentre applica il passo calcolato, l'ottimizzatore applica anche un decadimento di peso. Questo "Decadimento di Peso Disaccoppiato" viene visto in ottimizzatori come optimizers.FTRL e optimizers.AdamW.

Aggiunta di un dropout

Il dropout ("punto di rinuncia" n.d.t.) è una delle tecniche di regolarizzazione più efficaci e più comunemente utilizzate per le reti neurali, sviluppata da Hinton ed i suoi studenti all'università di Toronto.

Una spiegazione intuitiva per il dropout è che: in virtù del fatto che i singoli nodi della rete non possono fare affidamento sull'output degli altri, ciascun nodo deve produrre caratteristiche di output utili in se stesse.

Il dropout, applicato ad un livello, consiste nel "dropping out (far decadere n.d.t.)" (cioè impostare a zero) un certo numero di caratteristiche di output del livello, durante l'addestramento. Supponiamo che un dato livello, normalmente, durante l'addestramento, restituisca un vettore [0.2, 0.5, 1.3, 0.8, 1.1] per un dato campione di input; dopo l'applicazione del dropout, questo vettore avrà alcuni valori a zero distribuiti casualmente, es: [0, 0.5, 1.3, 0, 1.1].

Il "tasso di dropout" è la frazione delle caratteristiche azzerate; di solito si fissa tra 0.2 e 0.5. Durante il test, non ci sono unità azzerate, ed invece i valori di uscita del livello sono scalati in basso di un fattore uguale al tasso di dropout, per bilanciare il fatto che ci sono più unità attive che durante l'addestramento.

In tf.keras potete inserire il dropout in una rete per mezzo di un livello Dropout, che viene applicato all'output el livello immediatamente precedente.

Aggiungiamo due livelli Dropout nella nostra rete per vedere quanto bene riducano il sovradattamento:

dropout_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu', input_shape=(FEATURES,)),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

regularizer_histories['dropout'] = compile_and_fit(dropout_model, "regularizers/dropout")
Model: "sequential_5"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_19 (Dense)             (None, 512)               14848     
_________________________________________________________________
dropout (Dropout)            (None, 512)               0         
_________________________________________________________________
dense_20 (Dense)             (None, 512)               262656    
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_21 (Dense)             (None, 512)               262656    
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_22 (Dense)             (None, 512)               262656    
_________________________________________________________________
dropout_3 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_23 (Dense)             (None, 1)                 513       
=================================================================
Total params: 803,329
Trainable params: 803,329
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0043s vs `on_train_batch_end` time: 0.0648s). Check your callbacks.

Epoch: 0, accuracy:0.5020,  binary_crossentropy:0.8002,  loss:0.8002,  val_accuracy:0.5340,  val_binary_crossentropy:0.6783,  val_loss:0.6783,  
....................................................................................................
Epoch: 100, accuracy:0.6512,  binary_crossentropy:0.5963,  loss:0.5963,  val_accuracy:0.6830,  val_binary_crossentropy:0.5850,  val_loss:0.5850,  
....................................................................................................
Epoch: 200, accuracy:0.6862,  binary_crossentropy:0.5613,  loss:0.5613,  val_accuracy:0.6730,  val_binary_crossentropy:0.5783,  val_loss:0.5783,  
....................................................................................................
Epoch: 300, accuracy:0.7233,  binary_crossentropy:0.5135,  loss:0.5135,  val_accuracy:0.6860,  val_binary_crossentropy:0.5878,  val_loss:0.5878,  
..
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])
(0.5, 0.7)

png

E' chiaro da questo grafico che entrambi questi approcci di regolarizzazione migliorano il comportamento del modello "Grande". Ma non reggono ancora il confronto con il "Minuscolo".

Nel seguito li proviamo, tutti e due insieme, e vediamo se fanno di meglio.

Dropout combinati L1 + L2

combined_model = tf.keras.Sequential([
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu', input_shape=(FEATURES,)),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

regularizer_histories['combined'] = compile_and_fit(combined_model, "regularizers/combined")
Model: "sequential_6"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_24 (Dense)             (None, 512)               14848     
_________________________________________________________________
dropout_4 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_25 (Dense)             (None, 512)               262656    
_________________________________________________________________
dropout_5 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_26 (Dense)             (None, 512)               262656    
_________________________________________________________________
dropout_6 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_27 (Dense)             (None, 512)               262656    
_________________________________________________________________
dropout_7 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_28 (Dense)             (None, 1)                 513       
=================================================================
Total params: 803,329
Trainable params: 803,329
Non-trainable params: 0
_________________________________________________________________
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0044s vs `on_train_batch_end` time: 0.0605s). Check your callbacks.

Epoch: 0, accuracy:0.5069,  binary_crossentropy:0.7872,  loss:0.9457,  val_accuracy:0.4890,  val_binary_crossentropy:0.6723,  val_loss:0.8300,  
....................................................................................................
Epoch: 100, accuracy:0.6515,  binary_crossentropy:0.6044,  loss:0.6334,  val_accuracy:0.6330,  val_binary_crossentropy:0.5903,  val_loss:0.6192,  
....................................................................................................
Epoch: 200, accuracy:0.6623,  binary_crossentropy:0.5895,  loss:0.6145,  val_accuracy:0.6660,  val_binary_crossentropy:0.5774,  val_loss:0.6025,  
....................................................................................................
Epoch: 300, accuracy:0.6737,  binary_crossentropy:0.5811,  loss:0.6092,  val_accuracy:0.6810,  val_binary_crossentropy:0.5657,  val_loss:0.5938,  
....................................................................................................
Epoch: 400, accuracy:0.6735,  binary_crossentropy:0.5741,  loss:0.6039,  val_accuracy:0.6960,  val_binary_crossentropy:0.5579,  val_loss:0.5877,  
....................................................................................................
Epoch: 500, accuracy:0.6755,  binary_crossentropy:0.5723,  loss:0.6041,  val_accuracy:0.7060,  val_binary_crossentropy:0.5492,  val_loss:0.5810,  
....................................................................................................
Epoch: 600, accuracy:0.6835,  binary_crossentropy:0.5653,  loss:0.5998,  val_accuracy:0.6930,  val_binary_crossentropy:0.5493,  val_loss:0.5839,  
....................................................................................................
Epoch: 700, accuracy:0.6904,  binary_crossentropy:0.5590,  loss:0.5955,  val_accuracy:0.6880,  val_binary_crossentropy:0.5540,  val_loss:0.5905,  
....................................................................................................
Epoch: 800, accuracy:0.6920,  binary_crossentropy:0.5527,  loss:0.5905,  val_accuracy:0.6850,  val_binary_crossentropy:0.5505,  val_loss:0.5883,  
.................
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])
(0.5, 0.7)

png

Questo modello con la regolarizzazione "Combinata" è ovviamente, fin'ora, il migliore.

Visualizzazione in TensorBoard

Anche questi modelli registrano tracciati TensorBoard.

Per aprire un visualizzatore tensorboard all'interno di un notebook, copiare il codice seguente in una code-cell:

%tensorboard --logdir {logdir}/regularizers

Potete vedere il risultato di un'esecuzione precedente di questo notebook su TensorDoard.dev.

Per praticità, esso è anche incluso in un <iframe>:

display.IFrame(
    src="https://tensorboard.dev/experiment/fGInKDo8TXes1z7HQku9mw/#scalars&_smoothingWeight=0.97",
    width = "100%",
    height="800px")

Che è stato caricato con:

tensorboard dev upload --logdir  {logdir}/regularizers

Conclusioni

Ricapitolando: questi sono i modi più comuni per prevenire il sovradattamento nelle reti neurali:

  • Usare più dati di addestramento.
  • Ridurre la capacità della rete.
  • Aggiungere la regolarizzazione del peso.
  • Aggiungere il dropout.

In questa guida non sono trattati due approcci importanti:

  • l'aumento dei dati
  • la normalizzazione a lotti

Ricordate che ogni metodo è utile in se stesso, ma spesso combinarli può essere più efficace.

# 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.