Overfit e underfit

Veja no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Como sempre, o código neste exemplo usará a API tf.keras , sobre a qual você pode saber mais no guia TensorFlow Keras .

Em ambos os exemplos anteriores - classificação de texto e previsão de eficiência de combustível - vimos que a precisão do nosso modelo nos dados de validação atingiria o pico após o treinamento por várias épocas e então estagnaria ou começaria a diminuir.

Em outras palavras, nosso modelo se ajustaria aos dados de treinamento. Aprender a lidar com o overfitting é importante. Embora muitas vezes seja possível obter alta precisão no conjunto de treinamento , o que realmente queremos é desenvolver modelos que generalizem bem para um conjunto de teste (ou dados que eles não viram antes).

O oposto de overfitting é underfitting . O underfitting ocorre quando ainda há espaço para melhorias nos dados do trem. Isso pode acontecer por vários motivos: se o modelo não for poderoso o suficiente, estiver excessivamente regularizado ou simplesmente não tiver sido treinado por tempo suficiente. Isso significa que a rede não aprendeu os padrões relevantes nos dados de treinamento.

No entanto, se você treinar por muito tempo, o modelo começará a se ajustar demais e aprenderá padrões dos dados de treinamento que não generalizam para os dados de teste. Precisamos encontrar um equilíbrio. Entender como treinar para um número apropriado de épocas, como exploraremos abaixo, é uma habilidade útil.

Para evitar o overfitting, a melhor solução é usar dados de treinamento mais completos. O conjunto de dados deve cobrir toda a gama de entradas que o modelo deve manipular. Dados adicionais só podem ser úteis se abrangerem casos novos e interessantes.

Um modelo treinado em dados mais completos naturalmente generalizará melhor. Quando isso não for mais possível, a próxima melhor solução é usar técnicas como a regularização. Isso impõe restrições à quantidade e ao tipo de informação que seu modelo pode armazenar. Se uma rede só puder memorizar um pequeno número de padrões, o processo de otimização a forçará a se concentrar nos padrões mais proeminentes, que têm mais chances de generalizar bem.

Neste caderno, exploraremos várias técnicas comuns de regularização e as usaremos para aprimorar um modelo de classificação.

Configurar

Antes de começar, importe os pacotes necessários:

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)

O conjunto de dados de Higgs

O objetivo deste tutorial não é fazer física de partículas, portanto, não se detenha nos detalhes do conjunto de dados. Ele contém 11.000.000 exemplos, cada um com 28 recursos e um rótulo de classe binária.

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

A classe tf.data.experimental.CsvDataset pode ser usada para ler registros csv diretamente de um arquivo gzip sem etapa de descompactação intermediária.

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

Essa classe de leitor de csv retorna uma lista de escalares para cada registro. A função a seguir reempacota essa lista de escalares em um par (feature_vector, label).

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

O TensorFlow é mais eficiente ao operar em grandes lotes de dados.

Portanto, em vez de reempacotar cada linha individualmente, crie um novo conjunto de Dataset que receba lotes de 10.000 exemplos, aplique a função pack_row a cada lote e, em seguida, divida os lotes em registros individuais:

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

Dê uma olhada em alguns dos registros deste novo packed_ds .

Os recursos não estão perfeitamente normalizados, mas isso é suficiente para este 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

Para manter este tutorial relativamente curto, use apenas as primeiras 1.000 amostras para validação e as próximas 10.000 para treinamento:

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

Os métodos Dataset.skip e Dataset.take facilitam isso.

Ao mesmo tempo, use o método Dataset.cache para garantir que o carregador não precise reler os dados do arquivo em cada época:

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

Esses conjuntos de dados retornam exemplos individuais. Use o método .batch para criar lotes de tamanho apropriado para treinamento. Antes do batching, lembre-se de .shuffle e .repeat o conjunto de treinamento.

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

Demonstrar sobreajuste

A maneira mais simples de evitar o overfitting é começar com um modelo pequeno: um modelo com um pequeno número de parâmetros apreensíveis (que é determinado pelo número de camadas e o número de unidades por camada). No aprendizado profundo, o número de parâmetros que podem ser aprendidos em um modelo é geralmente chamado de "capacidade" do modelo.

Intuitivamente, um modelo com mais parâmetros terá mais "capacidade de memorização" e, portanto, poderá aprender facilmente um mapeamento perfeito do tipo dicionário entre amostras de treinamento e seus alvos, um mapeamento sem nenhum poder de generalização, mas isso seria inútil ao fazer previsões em dados inéditos.

Sempre tenha isso em mente: os modelos de aprendizado profundo tendem a ser bons em se ajustar aos dados de treinamento, mas o verdadeiro desafio é a generalização, não o ajuste.

Por outro lado, se a rede tiver recursos de memorização limitados, ela não conseguirá aprender o mapeamento com tanta facilidade. Para minimizar sua perda, ele terá que aprender representações compactadas que tenham mais poder preditivo. Ao mesmo tempo, se você tornar seu modelo muito pequeno, ele terá dificuldade em se ajustar aos dados de treinamento. Há um equilíbrio entre "capacidade demais" e "capacidade insuficiente".

Infelizmente, não existe uma fórmula mágica para determinar o tamanho certo ou a arquitetura do seu modelo (em termos de número de camadas ou o tamanho certo para cada camada). Você terá que experimentar usando uma série de arquiteturas diferentes.

Para encontrar um tamanho de modelo apropriado, é melhor começar com relativamente poucas camadas e parâmetros e, em seguida, começar a aumentar o tamanho das camadas ou adicionar novas camadas até ver retornos decrescentes na perda de validação.

Comece com um modelo simples usando apenas layers.Dense como linha de base, depois crie versões maiores e compare-as.

Procedimento de treinamento

Muitos modelos treinam melhor se você reduzir gradualmente a taxa de aprendizado durante o treinamento. Use optimizers.schedules para reduzir a taxa de aprendizado ao longo do tempo:

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)

O código acima define um schedules.InverseTimeDecay para diminuir hiperbolicamente a taxa de aprendizado para 1/2 da taxa básica em 1.000 épocas, 1/3 em 2.000 épocas e assim por diante.

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

Cada modelo neste tutorial usará a mesma configuração de treinamento. Portanto, configure-os de maneira reutilizável, começando com a lista de retornos de chamada.

O treinamento para este tutorial é executado por muitas épocas curtas. Para reduzir o ruído de log, use o tfdocs.EpochDots que simplesmente imprime um arquivo . para cada época e um conjunto completo de métricas a cada 100 épocas.

Em seguida, inclua callbacks.EarlyStopping para evitar tempos de treinamento longos e desnecessários. Observe que esse retorno de chamada é definido para monitorar o val_binary_crossentropy , não o val_loss . Essa diferença será importante mais tarde.

Use callbacks.TensorBoard para gerar logs do TensorBoard para o treinamento.

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

Da mesma forma, cada modelo usará as mesmas configurações Model.compile e 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

Modelo minúsculo

Comece treinando um modelo:

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

Agora confira como ficou o modelo:

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

Modelo pequeno

Para ver se você consegue superar o desempenho do modelo pequeno, treine progressivamente alguns modelos maiores.

Experimente duas camadas ocultas com 16 unidades cada:

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

Modelo médio

Agora tente 3 camadas ocultas com 64 unidades cada:

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

E treine o modelo usando os mesmos dados:

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

Modelo grande

Como exercício, você pode criar um modelo ainda maior e ver a rapidez com que ele começa a se ajustar demais. Em seguida, vamos adicionar a este benchmark uma rede que tem muito mais capacidade, muito mais do que o problema justificaria:

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, novamente, treine o modelo usando os mesmos dados:

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

Plote as perdas de treinamento e validação

As linhas sólidas mostram a perda de treinamento e as linhas tracejadas mostram a perda de validação (lembre-se: uma perda de validação menor indica um modelo melhor).

Embora a construção de um modelo maior lhe dê mais poder, se esse poder não for restringido de alguma forma, ele pode facilmente se ajustar ao conjunto de treinamento.

Neste exemplo, normalmente, apenas o modelo "Tiny" consegue evitar completamente o overfitting, e cada um dos modelos maiores superajusta os dados mais rapidamente. Isso se torna tão grave para o modelo "large" que você precisa mudar o gráfico para uma escala logarítmica para realmente ver o que está acontecendo.

Isso fica aparente se você plotar e comparar as métricas de validação com as métricas de treinamento.

  • É normal que haja uma pequena diferença.
  • Se ambas as métricas estão se movendo na mesma direção, está tudo bem.
  • Se a métrica de validação começar a estagnar enquanto a métrica de treinamento continua a melhorar, você provavelmente está perto do overfitting.
  • Se a métrica de validação estiver indo na direção errada, o modelo está claramente superajustado.
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

Ver no TensorBoard

Todos esses modelos gravaram logs do TensorBoard durante o treinamento.

Abra um visualizador TensorBoard incorporado em um notebook:

#docs_infra: no_execute

# Load the TensorBoard notebook extension
%load_ext tensorboard

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

Você pode ver os resultados de uma execução anterior deste notebook em TensorBoard.dev .

O TensorBoard.dev é uma experiência gerenciada para hospedar, rastrear e compartilhar experimentos de ML com todos.

Também está incluído em um <iframe> por conveniência:

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

Se você quiser compartilhar os resultados do TensorBoard, faça upload dos logs para o TensorBoard.dev copiando o seguinte em uma célula de código.

tensorboard dev upload --logdir  {logdir}/sizes

Estratégias para evitar o overfitting

Antes de entrar no conteúdo desta seção, copie os logs de treinamento do modelo "Tiny" acima, para usar como linha de base para comparação.

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

Adicionar regularização de peso

Você pode estar familiarizado com o princípio da Navalha de Occam: dadas duas explicações para algo, a explicação mais provável de ser correta é a "mais simples", aquela que faz a menor quantidade de suposições. Isso também se aplica aos modelos aprendidos pelas redes neurais: dados alguns dados de treinamento e uma arquitetura de rede, existem vários conjuntos de valores de pesos (múltiplos modelos) que podem explicar os dados, e modelos mais simples são menos propensos a sobreajustar do que os complexos.

Um "modelo simples" neste contexto é um modelo onde a distribuição dos valores dos parâmetros tem menos entropia (ou um modelo com menos parâmetros, como vimos na seção acima). Assim, uma maneira comum de mitigar o overfitting é colocar restrições na complexidade de uma rede, forçando seus pesos apenas a assumir valores pequenos, o que torna a distribuição de valores de peso mais "regular". Isso é chamado de "regularização de peso", e é feito adicionando à função de perda da rede um custo associado a ter grandes pesos. Este custo vem em dois sabores:

  • Regularização L1 , onde o custo adicionado é proporcional ao valor absoluto dos coeficientes dos pesos (ou seja, ao que é chamado de "norma L1" dos pesos).

  • Regularização L2 , onde o custo adicionado é proporcional ao quadrado do valor dos coeficientes dos pesos (ou seja, ao que é chamado de quadrado da "norma L2" dos pesos). A regularização L2 também é chamada de decaimento de peso no contexto de redes neurais. Não deixe que o nome diferente o confunda: a redução de peso é matematicamente igual à regularização L2.

A regularização L1 empurra os pesos para exatamente zero, incentivando um modelo esparso. A regularização de L2 penalizará os parâmetros de pesos sem torná-los esparsos, já que a penalidade vai para zero para pesos pequenos - uma razão pela qual L2 é mais comum.

Em tf.keras , a regularização de peso é adicionada passando instâncias do regularizador de peso para camadas como argumentos de palavras-chave. Vamos adicionar a regularização de peso L2 agora.

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) significa que cada coeficiente na matriz de peso da camada adicionará 0.001 * weight_coefficient_value**2 à perda total da rede.

É por isso que estamos monitorando o binary_crossentropy diretamente. Porque não tem esse componente de regularização misturado.

Portanto, esse mesmo modelo "Large" com uma penalidade de regularização L2 tem um desempenho muito melhor:

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

png

Como você pode ver, o modelo regularizado "L2" agora é muito mais competitivo com o modelo "Tiny" . Este modelo "L2" também é muito mais resistente ao overfitting do que o modelo "Large" em que foi baseado, apesar de ter o mesmo número de parâmetros.

Mais informações

Há duas coisas importantes a serem observadas sobre esse tipo de regularização.

Primeiro: se você estiver escrevendo seu próprio loop de treinamento, precisará perguntar ao modelo por suas perdas de regularização.

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

Segundo: Esta implementação funciona adicionando as penalidades de peso à perda do modelo e, em seguida, aplicando um procedimento de otimização padrão depois disso.

Há uma segunda abordagem que, em vez disso, apenas executa o otimizador na perda bruta e, ao aplicar a etapa calculada, o otimizador também aplica alguma redução de peso. Este "Decoupled Weight Decay" é visto em otimizadores como optimizers.FTRL e optimizers.AdamW .

Adicionar desistência

Dropout é uma das técnicas de regularização mais eficazes e mais utilizadas para redes neurais, desenvolvida por Hinton e seus alunos da Universidade de Toronto.

A explicação intuitiva para o dropout é que, como os nós individuais na rede não podem contar com a saída dos outros, cada nó deve produzir recursos que sejam úteis por conta própria.

O dropout, aplicado a uma camada, consiste em "descartar" aleatoriamente (ou seja, definir como zero) um número de recursos de saída da camada durante o treinamento. Digamos que uma determinada camada normalmente retornaria um vetor [0,2, 0,5, 1,3, 0,8, 1,1] para uma determinada amostra de entrada durante o treinamento; após aplicar dropout, este vetor terá algumas entradas zero distribuídas aleatoriamente, por exemplo [0, 0,5, 1,3, 0, 1,1].

A "taxa de abandono" é a fração dos recursos que estão sendo zerados; geralmente é definido entre 0,2 e 0,5. No momento do teste, nenhuma unidade é descartada e, em vez disso, os valores de saída da camada são reduzidos por um fator igual à taxa de abandono, de modo a equilibrar o fato de que mais unidades estão ativas do que no tempo de treinamento.

Em tf.keras você pode introduzir dropout em uma rede através da camada Dropout, que é aplicada à saída da camada imediatamente anterior.

Vamos adicionar duas camadas Dropout em nossa rede para ver como elas se saem na redução do overfitting:

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

Fica claro neste gráfico que ambas as abordagens de regularização melhoram o comportamento do modelo "Large" . Mas isso ainda não supera nem a linha de base "Tiny" .

Em seguida, tente os dois, juntos, e veja se isso melhora.

L2 combinado + desistência

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

Este modelo com a regularização "Combined" é obviamente o melhor até agora.

Ver no TensorBoard

Esses modelos também registraram logs do TensorBoard.

Para abrir um visualizador de tensorboard incorporado em um notebook, copie o seguinte em uma célula de código:

%tensorboard --logdir {logdir}/regularizers

Você pode ver os resultados de uma execução anterior deste notebook em TensorDoard.dev .

Também está incluído em um <iframe> por conveniência:

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

Isso foi carregado com:

tensorboard dev upload --logdir  {logdir}/regularizers

Conclusões

Para recapitular: aqui estão as maneiras mais comuns de evitar o overfitting em redes neurais:

  • Obtenha mais dados de treinamento.
  • Reduza a capacidade da rede.
  • Adicione regularização de peso.
  • Adicionar desistência.

Duas abordagens importantes não abordadas neste guia são:

  • aumento de dados
  • normalização de lote

Lembre-se de que cada método pode ajudar sozinho, mas muitas vezes combiná-los pode ser ainda mais eficaz.

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