Sur-ajustement et sous-ajustement

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Comme toujours, le code de cet exemple utilisera l'API tf.keras , sur laquelle vous pouvez en savoir plus dans le guide TensorFlow Keras .

Dans les deux exemples précédents ( classification de texte et prédiction de l'efficacité énergétique ), nous avons vu que la précision de notre modèle sur les données de validation culminerait après un entraînement pendant un certain nombre d'époques, puis stagnerait ou commencerait à diminuer.

En d'autres termes, notre modèle serait surajusté aux données d'entraînement. Apprendre à gérer le surajustement est important. Bien qu'il soit souvent possible d'obtenir une grande précision sur l' ensemble d'apprentissage , ce que nous voulons vraiment, c'est développer des modèles qui se généralisent bien à un ensemble de test (ou à des données qu'ils n'ont jamais vues auparavant).

Le contraire du surajustement est le sous-ajustement. Le sous-ajustement se produit lorsqu'il y a encore place à l'amélioration des données du train. Cela peut se produire pour un certain nombre de raisons : si le modèle n'est pas assez puissant, s'il est trop régularisé ou s'il n'a tout simplement pas été entraîné assez longtemps. Cela signifie que le réseau n'a pas appris les modèles pertinents dans les données de formation.

Si vous vous entraînez trop longtemps, le modèle commencera à sur-adapter et à apprendre des modèles à partir des données d'entraînement qui ne se généralisent pas aux données de test. Nous devons trouver un équilibre. Comprendre comment s'entraîner pour un nombre approprié d'époques, comme nous l'explorerons ci-dessous, est une compétence utile.

Pour éviter le surajustement, la meilleure solution consiste à utiliser des données d'entraînement plus complètes. L'ensemble de données doit couvrir la gamme complète des entrées que le modèle est censé gérer. Des données supplémentaires ne peuvent être utiles que si elles couvrent des cas nouveaux et intéressants.

Un modèle formé sur des données plus complètes généralisera naturellement mieux. Lorsque cela n'est plus possible, la meilleure solution consiste à utiliser des techniques telles que la régularisation. Celles-ci imposent des contraintes sur la quantité et le type d'informations que votre modèle peut stocker. Si un réseau ne peut se permettre de mémoriser qu'un petit nombre de modèles, le processus d'optimisation le forcera à se concentrer sur les modèles les plus importants, qui ont de meilleures chances de bien se généraliser.

Dans ce bloc-notes, nous allons explorer plusieurs techniques de régularisation courantes et les utiliser pour améliorer un modèle de classification.

Installer

Avant de commencer, importez les packages nécessaires :

import tensorflow as tf

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

print(tf.__version__)
2.8.0-rc1
!pip install git+https://github.com/tensorflow/docs

import tensorflow_docs as tfdocs
import tensorflow_docs.modeling
import tensorflow_docs.plots
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)

L'ensemble de données Higgs

Le but de ce tutoriel n'est pas de faire de la physique des particules, donc ne vous attardez pas sur les détails du jeu de données. Il contient 11 000 000 exemples, chacun avec 28 fonctionnalités, et une étiquette de classe binaire.

gz = tf.keras.utils.get_file('HIGGS.csv.gz', 'http://mlphysics.ics.uci.edu/data/higgs/HIGGS.csv.gz')
Downloading data from http://mlphysics.ics.uci.edu/data/higgs/HIGGS.csv.gz
2816409600/2816407858 [==============================] - 123s 0us/step
2816417792/2816407858 [==============================] - 123s 0us/step
FEATURES = 28

La classe tf.data.experimental.CsvDataset peut être utilisée pour lire des enregistrements csv directement à partir d'un fichier gzip sans étape de décompression intermédiaire.

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

Cette classe de lecteur csv renvoie une liste de scalaires pour chaque enregistrement. La fonction suivante reconditionne cette liste de scalaires dans une paire (feature_vector, label).

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

TensorFlow est plus efficace lorsqu'il fonctionne sur de gros lots de données.

Ainsi, au lieu de reconditionner chaque ligne individuellement, créez un nouvel ensemble de Dataset qui prend des lots de 10 000 exemples, applique la fonction pack_row à chaque lot, puis divise les lots en enregistrements individuels :

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

Jetez un œil à certains des enregistrements de ce nouveau packed_ds .

Les fonctionnalités ne sont pas parfaitement normalisées, mais cela suffit pour ce tutoriel.

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

Pour que ce didacticiel soit relativement court, utilisez uniquement les 1 000 premiers échantillons pour la validation et les 10 000 suivants pour la formation :

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

Les méthodes Dataset.skip et Dataset.take facilitent cette tâche.

Dans le même temps, utilisez la méthode Dataset.cache pour vous assurer que le chargeur n'a pas besoin de relire les données du fichier à chaque époque :

validate_ds = packed_ds.take(N_VALIDATION).cache()
train_ds = packed_ds.skip(N_VALIDATION).take(N_TRAIN).cache()
train_ds
<CacheDataset element_spec=(TensorSpec(shape=(28,), dtype=tf.float32, name=None), TensorSpec(shape=(), dtype=tf.float32, name=None))>

Ces ensembles de données renvoient des exemples individuels. Utilisez la méthode .batch pour créer des lots d'une taille appropriée pour la formation. Avant de regrouper, n'oubliez pas de .shuffle et de .repeat l'ensemble d'apprentissage.

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

Démontrer le surajustement

Le moyen le plus simple d'éviter le surajustement est de commencer avec un petit modèle : un modèle avec un petit nombre de paramètres apprenables (qui est déterminé par le nombre de couches et le nombre d'unités par couche). Dans l'apprentissage en profondeur, le nombre de paramètres apprenables dans un modèle est souvent appelé la "capacité" du modèle.

Intuitivement, un modèle avec plus de paramètres aura plus de "capacité de mémorisation" et sera donc capable d'apprendre facilement une cartographie parfaite de type dictionnaire entre les échantillons d'apprentissage et leurs cibles, une cartographie sans aucun pouvoir de généralisation, mais cela serait inutile pour faire des prédictions. sur des données inédites.

Gardez toujours cela à l'esprit : les modèles d'apprentissage en profondeur ont tendance à bien s'adapter aux données de formation, mais le véritable défi est la généralisation, pas l'ajustement.

En revanche, si le réseau dispose de ressources de mémorisation limitées, il ne pourra pas apprendre la cartographie aussi facilement. Pour minimiser sa perte, il devra apprendre des représentations compressées qui ont plus de pouvoir prédictif. Dans le même temps, si vous rendez votre modèle trop petit, il aura du mal à s'adapter aux données d'apprentissage. Il existe un équilibre entre "trop ​​de capacité" et "pas assez de capacité".

Malheureusement, il n'y a pas de formule magique pour déterminer la bonne taille ou l'architecture de votre modèle (en termes de nombre de couches, ou la bonne taille pour chaque couche). Vous devrez expérimenter en utilisant une série d'architectures différentes.

Pour trouver une taille de modèle appropriée, il est préférable de commencer avec relativement peu de couches et de paramètres, puis de commencer à augmenter la taille des couches ou à ajouter de nouvelles couches jusqu'à ce que vous voyiez des rendements décroissants sur la perte de validation.

Commencez avec un modèle simple utilisant uniquement layers.Dense comme ligne de base, puis créez des versions plus grandes et comparez-les.

Procédure de formation

De nombreux modèles s'entraînent mieux si vous réduisez progressivement le taux d'apprentissage pendant l'entraînement. Utilisez optimizers.schedules pour réduire le taux d'apprentissage au fil du temps :

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)

Le code ci-dessus définit un schedules.InverseTimeDecay pour réduire de manière hyperbolique le taux d'apprentissage à 1/2 du taux de base à 1000 époques, 1/3 à 2000 époques et ainsi de suite.

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

Chaque modèle de ce didacticiel utilisera la même configuration d'entraînement. Configurez-les donc de manière réutilisable, en commençant par la liste des rappels.

La formation pour ce didacticiel s'étend sur de nombreuses périodes courtes. Pour réduire le bruit de journalisation, utilisez le tfdocs.EpochDots qui imprime simplement un fichier . pour chaque époque, et un ensemble complet de métriques toutes les 100 époques.

Ensuite, incluez les callbacks.EarlyStopping pour éviter les temps de formation longs et inutiles. Notez que ce rappel est configuré pour surveiller le val_binary_crossentropy , pas le val_loss . Cette différence sera importante plus tard.

Utilisez callbacks.TensorBoard pour générer des journaux TensorBoard pour la formation.

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

De même, chaque modèle utilisera les mêmes paramètres Model.compile et Model.fit :

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

Petit modèle

Commencez par entraîner un modèle :

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
_________________________________________________________________

Epoch: 0, accuracy:0.4961,  binary_crossentropy:0.7294,  loss:0.7294,  val_accuracy:0.4840,  val_binary_crossentropy:0.7200,  val_loss:0.7200,  
....................................................................................................
Epoch: 100, accuracy:0.5931,  binary_crossentropy:0.6279,  loss:0.6279,  val_accuracy:0.5860,  val_binary_crossentropy:0.6288,  val_loss:0.6288,  
....................................................................................................
Epoch: 200, accuracy:0.6157,  binary_crossentropy:0.6178,  loss:0.6178,  val_accuracy:0.6200,  val_binary_crossentropy:0.6134,  val_loss:0.6134,  
....................................................................................................
Epoch: 300, accuracy:0.6370,  binary_crossentropy:0.6086,  loss:0.6086,  val_accuracy:0.6220,  val_binary_crossentropy:0.6055,  val_loss:0.6055,  
....................................................................................................
Epoch: 400, accuracy:0.6522,  binary_crossentropy:0.6008,  loss:0.6008,  val_accuracy:0.6260,  val_binary_crossentropy:0.5997,  val_loss:0.5997,  
....................................................................................................
Epoch: 500, accuracy:0.6513,  binary_crossentropy:0.5946,  loss:0.5946,  val_accuracy:0.6480,  val_binary_crossentropy:0.5911,  val_loss:0.5911,  
....................................................................................................
Epoch: 600, accuracy:0.6636,  binary_crossentropy:0.5894,  loss:0.5894,  val_accuracy:0.6390,  val_binary_crossentropy:0.5898,  val_loss:0.5898,  
....................................................................................................
Epoch: 700, accuracy:0.6696,  binary_crossentropy:0.5852,  loss:0.5852,  val_accuracy:0.6530,  val_binary_crossentropy:0.5870,  val_loss:0.5870,  
....................................................................................................
Epoch: 800, accuracy:0.6706,  binary_crossentropy:0.5824,  loss:0.5824,  val_accuracy:0.6590,  val_binary_crossentropy:0.5850,  val_loss:0.5850,  
....................................................................................................
Epoch: 900, accuracy:0.6709,  binary_crossentropy:0.5796,  loss:0.5796,  val_accuracy:0.6680,  val_binary_crossentropy:0.5831,  val_loss:0.5831,  
....................................................................................................
Epoch: 1000, accuracy:0.6780,  binary_crossentropy:0.5769,  loss:0.5769,  val_accuracy:0.6530,  val_binary_crossentropy:0.5851,  val_loss:0.5851,  
....................................................................................................
Epoch: 1100, accuracy:0.6735,  binary_crossentropy:0.5752,  loss:0.5752,  val_accuracy:0.6620,  val_binary_crossentropy:0.5807,  val_loss:0.5807,  
....................................................................................................
Epoch: 1200, accuracy:0.6759,  binary_crossentropy:0.5729,  loss:0.5729,  val_accuracy:0.6620,  val_binary_crossentropy:0.5792,  val_loss:0.5792,  
....................................................................................................
Epoch: 1300, accuracy:0.6849,  binary_crossentropy:0.5716,  loss:0.5716,  val_accuracy:0.6450,  val_binary_crossentropy:0.5859,  val_loss:0.5859,  
....................................................................................................
Epoch: 1400, accuracy:0.6790,  binary_crossentropy:0.5695,  loss:0.5695,  val_accuracy:0.6700,  val_binary_crossentropy:0.5776,  val_loss:0.5776,  
....................................................................................................
Epoch: 1500, accuracy:0.6824,  binary_crossentropy:0.5681,  loss:0.5681,  val_accuracy:0.6730,  val_binary_crossentropy:0.5761,  val_loss:0.5761,  
....................................................................................................
Epoch: 1600, accuracy:0.6828,  binary_crossentropy:0.5669,  loss:0.5669,  val_accuracy:0.6690,  val_binary_crossentropy:0.5766,  val_loss:0.5766,  
....................................................................................................
Epoch: 1700, accuracy:0.6874,  binary_crossentropy:0.5657,  loss:0.5657,  val_accuracy:0.6600,  val_binary_crossentropy:0.5774,  val_loss:0.5774,  
....................................................................................................
Epoch: 1800, accuracy:0.6845,  binary_crossentropy:0.5655,  loss:0.5655,  val_accuracy:0.6780,  val_binary_crossentropy:0.5752,  val_loss:0.5752,  
....................................................................................................
Epoch: 1900, accuracy:0.6837,  binary_crossentropy:0.5644,  loss:0.5644,  val_accuracy:0.6790,  val_binary_crossentropy:0.5753,  val_loss:0.5753,  
....................................................................................................
Epoch: 2000, accuracy:0.6853,  binary_crossentropy:0.5632,  loss:0.5632,  val_accuracy:0.6780,  val_binary_crossentropy:0.5753,  val_loss:0.5753,  
....................................................................................................
Epoch: 2100, accuracy:0.6871,  binary_crossentropy:0.5625,  loss:0.5625,  val_accuracy:0.6670,  val_binary_crossentropy:0.5769,  val_loss:0.5769,  
...................................

Vérifiez maintenant comment le modèle a fonctionné :

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

Petit modèle

Pour voir si vous pouvez battre les performances du petit modèle, entraînez progressivement des modèles plus grands.

Essayez deux couches cachées de 16 unités chacune :

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
_________________________________________________________________

Epoch: 0, accuracy:0.4864,  binary_crossentropy:0.7769,  loss:0.7769,  val_accuracy:0.4930,  val_binary_crossentropy:0.7211,  val_loss:0.7211,  
....................................................................................................
Epoch: 100, accuracy:0.6386,  binary_crossentropy:0.6052,  loss:0.6052,  val_accuracy:0.6020,  val_binary_crossentropy:0.6177,  val_loss:0.6177,  
....................................................................................................
Epoch: 200, accuracy:0.6697,  binary_crossentropy:0.5829,  loss:0.5829,  val_accuracy:0.6310,  val_binary_crossentropy:0.6018,  val_loss:0.6018,  
....................................................................................................
Epoch: 300, accuracy:0.6838,  binary_crossentropy:0.5721,  loss:0.5721,  val_accuracy:0.6490,  val_binary_crossentropy:0.5940,  val_loss:0.5940,  
....................................................................................................
Epoch: 400, accuracy:0.6911,  binary_crossentropy:0.5656,  loss:0.5656,  val_accuracy:0.6430,  val_binary_crossentropy:0.5985,  val_loss:0.5985,  
....................................................................................................
Epoch: 500, accuracy:0.6930,  binary_crossentropy:0.5607,  loss:0.5607,  val_accuracy:0.6430,  val_binary_crossentropy:0.6028,  val_loss:0.6028,  
.........................

Modèle moyen

Essayez maintenant 3 couches cachées avec 64 unités chacune :

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

Et entraînez le modèle en utilisant les mêmes données :

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
_________________________________________________________________

Epoch: 0, accuracy:0.5017,  binary_crossentropy:0.6840,  loss:0.6840,  val_accuracy:0.4790,  val_binary_crossentropy:0.6723,  val_loss:0.6723,  
....................................................................................................
Epoch: 100, accuracy:0.7173,  binary_crossentropy:0.5221,  loss:0.5221,  val_accuracy:0.6470,  val_binary_crossentropy:0.6111,  val_loss:0.6111,  
....................................................................................................
Epoch: 200, accuracy:0.7884,  binary_crossentropy:0.4270,  loss:0.4270,  val_accuracy:0.6390,  val_binary_crossentropy:0.7045,  val_loss:0.7045,  
..............................................................

Grand modèle

En tant qu'exercice, vous pouvez créer un modèle encore plus grand et voir à quelle vitesse il commence à se suradapter. Ajoutons ensuite à ce benchmark un réseau qui a beaucoup plus de capacité, bien plus que le problème ne le justifierait :

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

Et, encore une fois, entraînez le modèle en utilisant les mêmes données :

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
_________________________________________________________________

Epoch: 0, accuracy:0.5145,  binary_crossentropy:0.7740,  loss:0.7740,  val_accuracy:0.4980,  val_binary_crossentropy:0.6793,  val_loss:0.6793,  
....................................................................................................
Epoch: 100, accuracy:1.0000,  binary_crossentropy:0.0020,  loss:0.0020,  val_accuracy:0.6600,  val_binary_crossentropy:1.8540,  val_loss:1.8540,  
....................................................................................................
Epoch: 200, accuracy:1.0000,  binary_crossentropy:0.0001,  loss:0.0001,  val_accuracy:0.6560,  val_binary_crossentropy:2.5293,  val_loss:2.5293,  
..........................

Tracez les pertes de formation et de validation

Les lignes pleines montrent la perte d'entraînement et les lignes pointillées montrent la perte de validation (rappelez-vous : une perte de validation inférieure indique un meilleur modèle).

Bien que la construction d'un modèle plus grand lui donne plus de puissance, si cette puissance n'est pas limitée d'une manière ou d'une autre, elle peut facilement sur-adapter à l'ensemble d'entraînement.

Dans cet exemple, généralement, seul le modèle "Tiny" parvient à éviter complètement le surajustement, et chacun des modèles plus grands surajuste les données plus rapidement. Cela devient si grave pour le "large" modèle que vous devez passer l'intrigue à une échelle logarithmique pour vraiment voir ce qui se passe.

Cela est évident si vous tracez et comparez les métriques de validation aux métriques de formation.

  • C'est normal qu'il y ait une petite différence.
  • Si les deux métriques évoluent dans la même direction, tout va bien.
  • Si la métrique de validation commence à stagner alors que la métrique de formation continue de s'améliorer, vous êtes probablement proche du surajustement.
  • Si la métrique de validation va dans la mauvaise direction, le modèle est clairement surajusté.
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

Afficher dans TensorBoard

Ces modèles ont tous écrit des journaux TensorBoard pendant la formation.

Ouvrez une visionneuse TensorBoard intégrée dans un notebook :

#docs_infra: no_execute

# Load the TensorBoard notebook extension
%load_ext tensorboard

# Open an embedded TensorBoard viewer
%tensorboard --logdir {logdir}/sizes

Vous pouvez afficher les résultats d'une exécution précédente de ce notebook sur TensorBoard.dev .

TensorBoard.dev est une expérience gérée pour l'hébergement, le suivi et le partage d'expériences de ML avec tout le monde.

Il est également inclus dans un <iframe> pour plus de commodité :

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

Si vous souhaitez partager les résultats de TensorBoard, vous pouvez télécharger les journaux sur TensorBoard.dev en copiant ce qui suit dans une cellule de code.

tensorboard dev upload --logdir  {logdir}/sizes

Stratégies pour éviter le surajustement

Avant d'entrer dans le contenu de cette section, copiez les journaux d'entraînement du modèle "Tiny" ci-dessus, à utiliser comme référence pour la comparaison.

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

Ajouter une régularisation du poids

Vous connaissez peut-être le principe du rasoir d'Occam : étant donné deux explications pour quelque chose, l'explication la plus susceptible d'être correcte est la « plus simple », celle qui fait le moins d'hypothèses. Cela s'applique également aux modèles appris par les réseaux de neurones : compte tenu de certaines données d'entraînement et d'une architecture de réseau, il existe plusieurs ensembles de valeurs de poids (plusieurs modèles) qui pourraient expliquer les données, et les modèles plus simples sont moins susceptibles de sur-ajuster que les modèles complexes.

Un "modèle simple" dans ce contexte est un modèle où la distribution des valeurs de paramètres a moins d'entropie (ou un modèle avec moins de paramètres au total, comme nous l'avons vu dans la section ci-dessus). Ainsi, un moyen courant d'atténuer le surajustement consiste à imposer des contraintes sur la complexité d'un réseau en forçant ses poids à ne prendre que de petites valeurs, ce qui rend la distribution des valeurs de poids plus "régulière". C'est ce qu'on appelle la "régularisation des poids", et cela se fait en ajoutant à la fonction de perte du réseau un coût associé au fait d'avoir des poids importants. Ce coût se présente sous deux formes :

  • Régularisation L1 , où le surcoût est proportionnel à la valeur absolue des coefficients des poids (c'est-à-dire à ce qu'on appelle la « norme L1 » des poids).

  • Régularisation L2 , où le surcoût est proportionnel au carré de la valeur des coefficients des poids (c'est-à-dire à ce qu'on appelle le carré de la "norme L2" des poids). La régularisation L2 est également appelée décroissance de poids dans le contexte des réseaux de neurones. Ne laissez pas le nom différent vous confondre : la perte de poids est mathématiquement identique à la régularisation L2.

La régularisation L1 pousse les poids vers exactement zéro, encourageant un modèle clairsemé. La régularisation L2 pénalisera les paramètres de poids sans les rendre clairsemés puisque la pénalité passe à zéro pour les petits poids - une des raisons pour lesquelles L2 est plus courant.

Dans tf.keras , la régularisation du poids est ajoutée en passant des instances de régularisateur de poids aux couches en tant qu'arguments de mot-clé. Ajoutons maintenant la régularisation du poids 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
_________________________________________________________________

Epoch: 0, accuracy:0.5126,  binary_crossentropy:0.7481,  loss:2.2415,  val_accuracy:0.4950,  val_binary_crossentropy:0.6707,  val_loss:2.0653,  
....................................................................................................
Epoch: 100, accuracy:0.6625,  binary_crossentropy:0.5945,  loss:0.6173,  val_accuracy:0.6400,  val_binary_crossentropy:0.5871,  val_loss:0.6100,  
....................................................................................................
Epoch: 200, accuracy:0.6690,  binary_crossentropy:0.5864,  loss:0.6079,  val_accuracy:0.6650,  val_binary_crossentropy:0.5856,  val_loss:0.6076,  
....................................................................................................
Epoch: 300, accuracy:0.6790,  binary_crossentropy:0.5762,  loss:0.5976,  val_accuracy:0.6550,  val_binary_crossentropy:0.5881,  val_loss:0.6095,  
....................................................................................................
Epoch: 400, accuracy:0.6843,  binary_crossentropy:0.5697,  loss:0.5920,  val_accuracy:0.6650,  val_binary_crossentropy:0.5878,  val_loss:0.6101,  
....................................................................................................
Epoch: 500, accuracy:0.6897,  binary_crossentropy:0.5651,  loss:0.5907,  val_accuracy:0.6890,  val_binary_crossentropy:0.5798,  val_loss:0.6055,  
....................................................................................................
Epoch: 600, accuracy:0.6945,  binary_crossentropy:0.5610,  loss:0.5864,  val_accuracy:0.6820,  val_binary_crossentropy:0.5772,  val_loss:0.6026,  
..........................................................

l2(0.001) signifie que chaque coefficient dans la matrice de pondération de la couche ajoutera 0.001 * weight_coefficient_value**2 à la perte totale du réseau.

C'est pourquoi nous surveillons directement le binary_crossentropy . Parce qu'il n'a pas ce composant de régularisation mélangé.

Ainsi, ce même modèle "Large" avec une pénalité de régularisation L2 fonctionne beaucoup mieux :

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

png

Comme vous pouvez le constater, le modèle régularisé "L2" est désormais beaucoup plus compétitif que le modèle "Tiny" . Ce modèle "L2" est également beaucoup plus résistant au surajustement que le modèle "Large" sur lequel il était basé malgré le même nombre de paramètres.

Plus d'informations

Il y a deux choses importantes à noter à propos de ce type de régularisation.

Premièrement : si vous écrivez votre propre boucle d'entraînement, vous devez vous assurer de demander au modèle ses pertes de régularisation.

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

Deuxièmement : cette implémentation fonctionne en ajoutant les pénalités de poids à la perte du modèle, puis en appliquant une procédure d'optimisation standard par la suite.

Il existe une deuxième approche qui, à la place, n'exécute l'optimiseur que sur la perte brute, puis, tout en appliquant l'étape calculée, l'optimiseur applique également une certaine décroissance du poids. Cette "décroissance de poids découplée" est observée dans les optimiseurs comme les optimizers.FTRL et les optimizers.AdamW .

Ajouter un abandon

L'abandon est l'une des techniques de régularisation les plus efficaces et les plus couramment utilisées pour les réseaux de neurones, développée par Hinton et ses étudiants de l'Université de Toronto.

L'explication intuitive de l'abandon est que, comme les nœuds individuels du réseau ne peuvent pas compter sur la sortie des autres, chaque nœud doit produire des fonctionnalités qui sont utiles par elles-mêmes.

L'abandon, appliqué à une couche, consiste à « abandonner » aléatoirement (c'est-à-dire mettre à zéro) un certain nombre d'entités de sortie de la couche pendant l'apprentissage. Supposons qu'une couche donnée aurait normalement renvoyé un vecteur [0,2, 0,5, 1,3, 0,8, 1,1] pour un échantillon d'entrée donné pendant l'apprentissage ; après l'application de l'abandon, ce vecteur aura quelques entrées nulles distribuées au hasard, par exemple [0, 0,5, 1,3, 0, 1,1].

Le "taux d'abandon" est la fraction des fonctionnalités qui sont mises à zéro ; il est généralement fixé entre 0,2 et 0,5. Au moment du test, aucune unité n'est abandonnée et, à la place, les valeurs de sortie de la couche sont réduites d'un facteur égal au taux d'abandon, afin de tenir compte du fait que plus d'unités sont actives qu'au moment de la formation.

Dans tf.keras , vous pouvez introduire un abandon dans un réseau via la couche Dropout, qui est appliquée à la sortie de la couche juste avant.

Ajoutons deux couches Dropout dans notre réseau pour voir dans quelle mesure elles réduisent le surajustement :

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
_________________________________________________________________

Epoch: 0, accuracy:0.4961,  binary_crossentropy:0.8110,  loss:0.8110,  val_accuracy:0.5330,  val_binary_crossentropy:0.6900,  val_loss:0.6900,  
....................................................................................................
Epoch: 100, accuracy:0.6557,  binary_crossentropy:0.5961,  loss:0.5961,  val_accuracy:0.6710,  val_binary_crossentropy:0.5788,  val_loss:0.5788,  
....................................................................................................
Epoch: 200, accuracy:0.6871,  binary_crossentropy:0.5622,  loss:0.5622,  val_accuracy:0.6860,  val_binary_crossentropy:0.5856,  val_loss:0.5856,  
....................................................................................................
Epoch: 300, accuracy:0.7246,  binary_crossentropy:0.5121,  loss:0.5121,  val_accuracy:0.6820,  val_binary_crossentropy:0.5927,  val_loss:0.5927,  
............
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])
(0.5, 0.7)

png

Il ressort clairement de ce graphique que ces deux approches de régularisation améliorent le comportement du modèle "Large" . Mais cela ne bat toujours pas même la ligne de base "Tiny" .

Ensuite, essayez-les tous les deux, ensemble, et voyez si cela fonctionne mieux.

Combiné L2 + abandon

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
_________________________________________________________________

Epoch: 0, accuracy:0.5090,  binary_crossentropy:0.8064,  loss:0.9648,  val_accuracy:0.4660,  val_binary_crossentropy:0.6877,  val_loss:0.8454,  
....................................................................................................
Epoch: 100, accuracy:0.6445,  binary_crossentropy:0.6050,  loss:0.6350,  val_accuracy:0.6630,  val_binary_crossentropy:0.5871,  val_loss:0.6169,  
....................................................................................................
Epoch: 200, accuracy:0.6660,  binary_crossentropy:0.5932,  loss:0.6186,  val_accuracy:0.6880,  val_binary_crossentropy:0.5722,  val_loss:0.5975,  
....................................................................................................
Epoch: 300, accuracy:0.6697,  binary_crossentropy:0.5818,  loss:0.6100,  val_accuracy:0.6900,  val_binary_crossentropy:0.5614,  val_loss:0.5895,  
....................................................................................................
Epoch: 400, accuracy:0.6749,  binary_crossentropy:0.5742,  loss:0.6046,  val_accuracy:0.6870,  val_binary_crossentropy:0.5576,  val_loss:0.5881,  
....................................................................................................
Epoch: 500, accuracy:0.6854,  binary_crossentropy:0.5703,  loss:0.6029,  val_accuracy:0.6970,  val_binary_crossentropy:0.5458,  val_loss:0.5784,  
....................................................................................................
Epoch: 600, accuracy:0.6806,  binary_crossentropy:0.5673,  loss:0.6015,  val_accuracy:0.6980,  val_binary_crossentropy:0.5453,  val_loss:0.5795,  
....................................................................................................
Epoch: 700, accuracy:0.6937,  binary_crossentropy:0.5583,  loss:0.5938,  val_accuracy:0.6870,  val_binary_crossentropy:0.5477,  val_loss:0.5832,  
....................................................................................................
Epoch: 800, accuracy:0.6911,  binary_crossentropy:0.5576,  loss:0.5947,  val_accuracy:0.7000,  val_binary_crossentropy:0.5446,  val_loss:0.5817,  
.......................
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])
(0.5, 0.7)

png

Ce modèle avec la régularisation "Combined" est évidemment le meilleur jusqu'à présent.

Afficher dans TensorBoard

Ces modèles ont également enregistré des journaux TensorBoard.

Pour ouvrir une visionneuse Tensorboard intégrée dans un notebook, copiez ce qui suit dans une cellule de code :

%tensorboard --logdir {logdir}/regularizers

Vous pouvez afficher les résultats d'une exécution précédente de ce notebook sur TensorDoard.dev .

Il est également inclus dans un <iframe> pour plus de commodité :

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

Ceci a été téléchargé avec :

tensorboard dev upload --logdir  {logdir}/regularizers

conclusion

Pour récapituler : voici les moyens les plus courants d'éviter le surajustement dans les réseaux de neurones :

  • Obtenez plus de données d'entraînement.
  • Réduire la capacité du réseau.
  • Ajouter la régularisation du poids.
  • Ajouter un décrochage.

Deux approches importantes non couvertes dans ce guide sont :

  • augmentation de données
  • normalisation par lots

Rappelez-vous que chaque méthode peut aider seule, mais souvent les combiner peut être encore plus 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.