4 votos

¿Por qué este autocodificador no puede alcanzar la pérdida cero?

Estoy construyendo un autoencoder y me preguntaba por qué la pérdida no converge a cero después de 500 iteraciones. Así que creé este autocodificador "ilustrativo" con una dimensión de codificación igual a la dimensión de entrada. Para asegurarme de que no había nada malo con los datos, creé una muestra de matriz aleatoria de forma (30000, 100) y la alimenté como entrada y salida (x = y). Se supone que la NN sólo tiene que aprender a mantener las entradas como están. Entonces, ¿por qué no alcanza la pérdida cero?

x_rand = numpy.random.rand(30000, 100)
# this is the size of our encoded representations
encoding_dim = 100

inputs = Input(shape=x_rand.shape[1:])

encoded = Dense(100, activation='relu')(inputs)
encoded = Dense(100, activation='relu')(encoded)

encoded = Dense(encoding_dim, activation='relu')(encoded)

decoded = Dense(100, activation='relu')(encoded)
decoded = Dense(100, activation='relu')(decoded)
decoded = Dense(x_rand.shape[-1], activation='sigmoid')(decoded)

# this model maps an input to its reconstruction
autoencoder = Model(inputs, decoded)

autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
history = autoencoder.fit(x_rand, x_rand, epochs=EPOCHS, batch_size=BATCH_SIZE, verbose=2)

5voto

user777 Puntos 10934

Para responder sucintamente a la pregunta titular: "Este autocodificador no puede alcanzar la pérdida 0 porque hay una mala correspondencia entre las entradas y la función de pérdida. Entrenando el mismo modelo en los mismos datos con una función de pérdida diferente, o entrenando un modelo ligeramente modificado en datos diferentes con la misma función de pérdida se alcanza la pérdida cero muy rápidamente."

Simplifique

Siempre que encuentro un comportamiento desconcertante, me resulta útil reducirlo al problema más básico y resolverlo. Tú has iniciado ese proceso con tu modelo de juguete, pero creo que el modelo puede simplificarse aún más.

La versión más sencilla de este problema es una monocapa red con activaciones de identidad; se trata de un modelo lineal. El codificador es una transformación lineal (matriz de pesos y vector de sesgo) y el decodificador es otra transformación lineal (matriz de pesos y vector de sesgo).

Sin embargo, todos estos modelos mantienen la propiedad de que no hay cuello de botella: la dimensión de incrustación es tan grande como la dimensión de entrada.

Modelo lineal, $x \sim \mathcal{U}(0,1)$

Este modelo tan sencillo tiene el siguiente aspecto

$$ \hat{x} = W_\text{dec}(W_\text{enc}x + b_\text{enc})+b_\text{dec} $$

Utilizando la siguiente configuración, este modelo converge a una pérdida de entrenamiento inferior a $10^{-5}$ en menos de 450 iteraciones:

  • Optimizador Adam con tasa de aprendizaje $10^{-5}$
  • 30.000 muestras de dos características
  • tamaño de lote mínimo de 128
  • Función de pérdida MSE.

Sigmoide, $x \sim \mathcal{U}(0,1)$

Utilizar una activación sigmoidea en la capa final y la pérdida BCE no parece funcionar tan bien. El modelo sigmoide tiene la forma $$ \hat{x} = \sigma\left(W_\text{dec}(W_\text{enc}x + b_\text{enc})+b_\text{dec}\right) $$

  • Optimizador Adam con tasa de aprendizaje $10^{-4}$
  • 30.000 muestras de dos características
  • tamaño de lote mínimo de 128
  • Función de pérdida BCE
  • función de activación sigmoidea

Creo que este modelo no funciona bien con los datos de origen porque los objetivos son uniformes en $[0,1]$ en lugar de concentrarse en 0 y 1. (Recordemos que una forma de justificar el uso de la función de pérdida logarítmica es que surge naturalmente de la probabilidad de Bernoulli). He probado muchas variaciones en la tasa de aprendizaje y la complejidad del modelo, pero este modelo con estos datos no logra una pérdida por debajo de alrededor de 0,5.

Podemos probar mi hipótesis intentando estimar el mismo modelo utilizando entradas binarias .

Sigmoide, $x \sim \text{Bernoulli}(0.5)$

Sin embargo, si cambiamos la forma de construir los datos para que sean aleatorios binario a continuación, utilizando la pérdida BCE con la activación sigmoidea hace convergen.

  • Optimizador Adam con tasa de aprendizaje $10^{-4}$
  • 30.000 muestras de dos características
  • tamaño del mini lote de 128
  • Función de pérdida BCE.
  • activación sigmoidea en la capa final

Con este modelo se consiguen bajas pérdidas muy rápidamente. Si desea presionar para extremadamente valores de pérdida pequeños, mi consejo es calcular la pérdida en la escala logit para evitar problemas de redondeo.

Complicar

Ahora que tenemos una hipótesis de cómo funciona el modelo cuando es muy simple y barato de estimar, podemos aumentar la complejidad del modelo simple y comprobar si la hipótesis que desarrollamos a partir de los modelos más simples sigue siendo válida cuando intentamos modelos más complejos.

He realizado experimentos con modelos más profundos, activaciones no lineales (leaky ReLU), pero repitiendo el mismo diseño experimental utilizado para entrenar los modelos simples: mezclar la elección de la función de pérdida y comparar distribuciones alternativas de los datos de entrada. En estos experimentos con modelos más grandes y no lineales, encuentro que es mejor ajustar el MSE a entradas de valor continuo y la pérdida logarítmica a entradas de valor binario.

Cuellos de botella

Si deseamos entrenar un modelo utilizando una estructura de codificador/decodificador de cuello de botella, es decir, un modelo en el que la salida del codificador tenga una dimensión menor que la dimensión de entrada, debemos considerar si nuestros datos de origen están estructurados de modo que sea posible dicha compresión. Todos nuestros experimentos hasta ahora han utilizado valores aleatorios iid, que son los menos compresibles porque los valores de una característica no tienen información sobre los valores de ninguna otra característica por construcción .

Alternativamente, supongamos que los datos de entrada fueran valores completamente redundantes, por lo que un ejemplo podría ser $[1,1,1,1]$ y otro ejemplo es $[2,2,2,2]$ y otra es $[-1.5, -1.5, -1.5, -1.5]$ . Una red de cuello de botella encajaría fácilmente, ya que tres columnas son totalmente redundantes. Este tipo de datos fuente se prestaría más a un autocodificador de cuello de botella.


from __future__ import division

import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

class ToyAutoEncoder(nn.Module):
  def __init__(self, n_features, encoding_dim):
    super(ToyAutoEncoder, self).__init__()
    self.encoder = nn.Sequential(
      nn.Linear(n_features, n_features),
      nn.LeakyReLU(0.1),
      nn.Linear(n_features, encoding_dim),
    )

    self.decoder = nn.Sequential(
      nn.Linear(encoding_dim, n_features),
      nn.LeakyReLU(0.1),
      nn.Linear(n_features, n_features),
      # nn.Sigmoid()
    )

  def forward(self, x):
    x_enc = self.encoder(x)
    x_dec = self.decoder(x_enc)
    return x_dec

def fit(model, data_queue, optimizer, n_epoch, print_freq, tol=1e-5):
  # loss_fn = nn.BCELoss(reduction="mean")
  loss_fn = nn.MSELoss(reduction="mean")
  for epoch_ndx in range(n_epoch):
    model.train()
    loss_epoch = np.zeros(len(queue))
    loss_recent = np.zeros(print_freq)
    for batch_ndx, (x_batch,) in enumerate(data_queue):
      optimizer.zero_grad()
      x_dec = autoencoder(x_batch)
      loss_tensor = loss_fn(input=x_dec, target=x_batch)
      loss_tensor.backward()
      optimizer.step()
      loss_np = loss_tensor.detach().numpy()
      loss_epoch[batch_ndx] = loss_np
      loss_recent[batch_ndx % print_freq] = loss_np
      if batch_ndx % print_freq == 0 and batch_ndx > 0:
        msg_data = (epoch_ndx, batch_ndx, loss_recent.mean(), loss_epoch[:batch_ndx].mean())
        print("Epoch %d Batch %d Recent loss %.4f Cumulative Loss %.4f" % msg_data)
    if loss_epoch.mean() < tol:
      print("Convergence achieved: %s" % loss_epoch.mean())
      break

if __name__ == "__main__":
  N_FEATURES = ENCODING_DIM = 10
  N_EPOCH = 1024
  PRINT_FREQ = 32
  autoencoder = ToyAutoEncoder(n_features=N_FEATURES, encoding_dim=ENCODING_DIM, )

  x_rand = torch.rand((30000, N_FEATURES))
  # x_rand = (torch.rand((30000, N_FEATURES)) - 0.5).sign()
  x_rand = x_rand.clamp(min=0.0)

  optim = torch.optim.Adam(lr=1e-4, params=autoencoder.parameters(),
                           # weight_decay=1e-6,
                           )
  # optim = torch.optim.SGD(lr=1e-3, params=autoencoder.parameters(), momentum=0.9)

  queue = DataLoader(TensorDataset(x_rand), batch_size=128, shuffle=True)
  fit(model=autoencoder, data_queue=queue, optimizer=optim, n_epoch=N_EPOCH, print_freq=PRINT_FREQ)

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X