Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/es-419/guide/keras/transfer_learning.ipynb
25118 views
Kernel: Python 3
#@title Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.

Aprendizaje por transferencia y ajuste

Preparación

import numpy as np import tensorflow as tf from tensorflow import keras

Introducción

El aprendizaje por transferencia consiste en tomar características aprendidas en un problema y aprovecharlas en un nuevo problema similar. Por ejemplo, las características de un modelo que aprendió a identificar mapaches pueden ser útiles para poner en marcha un modelo destinado a identificar tanukis.

El aprendizaje por transferencia suele realizarse para tareas en las que el conjunto de datos es demasiado escaso para entrenar un modelo completo desde cero.

La forma más común de aplicar el aprendizaje por transferencia en el contexto del deep learning es el siguiente flujo de trabajo:

  1. Tomar capas de un modelo previamente entrenado.

  2. Congélelas para no destruir la información que contienen en futuras rondas de entrenamiento.

  3. Agregue algunas capas nuevas, entrenables, sobre las capas congeladas. Aprenderán a convertir las características antiguas en predicciones sobre un nuevo conjunto de datos.

  4. Entrene las nuevas capas en su conjunto de datos.

Un último paso opcional es el ajuste fino, que consiste en descongelar todo el modelo obtenido anteriormente (o parte de él) y volver a entrenarlo con los nuevos datos a un ritmo de aprendizaje muy bajo. De este modo se pueden conseguir mejoras significativas, adaptando de forma gradual las características preentrenadas a los nuevos datos.

En primer lugar, repasaremos en detalle la API trainable de Keras, que subyace en la mayoría de los flujos de trabajo de aprendizaje por transferencia y ajuste fino.

A continuación, demostraremos el flujo de trabajo típico tomando un modelo preentrenado en el conjunto de datos ImageNet y reentrenándolo en el conjunto de datos de clasificación "gatos contra perros" de Kaggle.

Esto es una adaptación de Deep Learning con Python y la entrada de blog del 2016 "building powerful image classification models using very little data".

Congelar capas: cómo entender el atributo trainable

Las capas y los modelos tienen tres atributos de peso:

  • weights es la lista de todas las variables de pesos de la capa.

  • trainable_weights es la lista de los que están destinados a ser actualizados (mediante descenso del gradiente) para minimizar la pérdida durante el entrenamiento.

  • non_trainable_weights es la lista de los que no están destinados a ser entrenados. Normalmente son actualizados por el modelo durante el siguiente paso.

Ejemplo: la capa Dense tiene 2 pesos entrenables (kernel & bias)

layer = keras.layers.Dense(3) layer.build((None, 4)) # Create the weights print("weights:", len(layer.weights)) print("trainable_weights:", len(layer.trainable_weights)) print("non_trainable_weights:", len(layer.non_trainable_weights))

En general, todos los pesos son entrenables. La única capa incorporada que tiene pesos no entrenables es la capa BatchNormalization. Utiliza pesos no entrenables para realizar un seguimiento de la media y la varianza de sus entradas durante el entrenamiento. Para aprender a utilizar pesos no entrenables en sus propias capas personalizadas, consulte la guía para escribir nuevas capas desde cero.

Ejemplo: la capa BatchNormalization tiene 2 pesos entrenables y 2 pesos no entrenables.

layer = keras.layers.BatchNormalization() layer.build((None, 4)) # Create the weights print("weights:", len(layer.weights)) print("trainable_weights:", len(layer.trainable_weights)) print("non_trainable_weights:", len(layer.non_trainable_weights))

Las capas y modelos también cuentan con un atributo booleano trainable. Su valor puede cambiarse. Establecer layer.trainable a False mueve todos los pesos de la capa de entrenable a no entrenable. Esto se denomina "congelar" la capa: el estado de una capa congelada no se actualizará durante el entrenamiento (ya sea cuando se entrene con fit() o cuando se entrene con cualquier bucle personalizado que dependa de trainable_weights para aplicar actualizaciones del gradiente).

Ejemplo: establecer trainable en False

layer = keras.layers.Dense(3) layer.build((None, 4)) # Create the weights layer.trainable = False # Freeze the layer print("weights:", len(layer.weights)) print("trainable_weights:", len(layer.trainable_weights)) print("non_trainable_weights:", len(layer.non_trainable_weights))

Cuando un peso entrenable se convierte en uno no entrenable, su valor deja de actualizarse durante el entrenamiento.

# Make a model with 2 layers layer1 = keras.layers.Dense(3, activation="relu") layer2 = keras.layers.Dense(3, activation="sigmoid") model = keras.Sequential([keras.Input(shape=(3,)), layer1, layer2]) # Freeze the first layer layer1.trainable = False # Keep a copy of the weights of layer1 for later reference initial_layer1_weights_values = layer1.get_weights() # Train the model model.compile(optimizer="adam", loss="mse") model.fit(np.random.random((2, 3)), np.random.random((2, 3))) # Check that the weights of layer1 have not changed during training final_layer1_weights_values = layer1.get_weights() np.testing.assert_allclose( initial_layer1_weights_values[0], final_layer1_weights_values[0] ) np.testing.assert_allclose( initial_layer1_weights_values[1], final_layer1_weights_values[1] )

No confunda el atributo layer.trainable con el argumento training en layer.__call__() (que controla si la capa debe ejecutar su siguiente paso en modo de inferencia o en modo de entrenamiento). Para obtener más información, consulte Keras FAQ.

Establecer de forma recursiva el atributo trainable

Si establece trainable = False en un modelo o en cualquier capa que tenga subcapas, todas las capas hijas se convertirán también en no entrenables.

Ejemplo:

inner_model = keras.Sequential( [ keras.Input(shape=(3,)), keras.layers.Dense(3, activation="relu"), keras.layers.Dense(3, activation="relu"), ] ) model = keras.Sequential( [keras.Input(shape=(3,)), inner_model, keras.layers.Dense(3, activation="sigmoid"),] ) model.trainable = False # Freeze the outer model assert inner_model.trainable == False # All layers in `model` are now frozen assert inner_model.layers[0].trainable == False # `trainable` is propagated recursively

El flujo de trabajo típico del aprendizaje por transferencia

Esto nos lleva a cómo se puede implementar un flujo de trabajo típico de aprendizaje por transferencia en Keras:

  1. Crear instancias de un modelo base y cargar en él los pesos preentrenados.

  2. Congele todas las capas del modelo base estableciendo trainable = False.

  3. Cree un nuevo modelo sobre la salida de una (o varias) capas del modelo base.

  4. Entrene su nuevo modelo en su nuevo conjunto de datos.

Tenga en cuenta que un flujo de trabajo alternativo, más ligero, también podría ser:

  1. Crear instancias de un modelo base y cargar en él los pesos preentrenados.

  2. Ejecute su nuevo conjunto de datos mediante él y registre la salida de una (o varias) capas del modelo base. Esto se denomina extracción de características.

  3. Utilice ese resultado como datos de entrada para un nuevo modelo más pequeño.

Una ventaja clave de este segundo flujo de trabajo es que solo se ejecuta el modelo base una vez en los datos, en vez de una vez por cada época de entrenamiento. Así que es mucho más rápido y barato.

Sin embargo, el problema de este segundo flujo de trabajo es que no permite modificar dinámicamente los datos de entrada del nuevo modelo durante el entrenamiento, lo que es necesario, por ejemplo, al aumentar los datos. El aprendizaje por transferencia se utiliza normalmente para realizar tareas en las que el nuevo conjunto de datos tiene muy pocos datos para entrenar un modelo completo desde cero, y en estos casos el aumento de datos es muy importante. Por lo tanto, a continuación nos centraremos en el primer flujo de trabajo.

Este es el aspecto del primer flujo de trabajo en Keras:

En primer lugar, cree instancias de un modelo base con pesos preentrenados.

base_model = keras.applications.Xception( weights='imagenet', # Load weights pre-trained on ImageNet. input_shape=(150, 150, 3), include_top=False) # Do not include the ImageNet classifier at the top.

Después, congele el modelo base.

base_model.trainable = False

Cree un nuevo modelo encima.

inputs = keras.Input(shape=(150, 150, 3)) # We make sure that the base_model is running in inference mode here, # by passing `training=False`. This is important for fine-tuning, as you will # learn in a few paragraphs. x = base_model(inputs, training=False) # Convert features of shape `base_model.output_shape[1:]` to vectors x = keras.layers.GlobalAveragePooling2D()(x) # A Dense classifier with a single unit (binary classification) outputs = keras.layers.Dense(1)(x) model = keras.Model(inputs, outputs)

Entrene el modelo con nuevos datos.

model.compile(optimizer=keras.optimizers.Adam(), loss=keras.losses.BinaryCrossentropy(from_logits=True), metrics=[keras.metrics.BinaryAccuracy()]) model.fit(new_dataset, epochs=20, callbacks=..., validation_data=...)

Ajuste fino

Una vez que su modelo haya convergido con los nuevos datos, puede intentar descongelar todo o parte del modelo base y volver a entrenar todo el modelo de principio a fin con una tasa de aprendizaje muy baja.

Se trata de un último paso opcional que puede proporcionarle posibles mejoras incrementales. También puede dar lugar a un sobreajuste rápido, así que téngalo en cuenta.

Es fundamental realizar este paso después de que el modelo con capas congeladas haya sido entrenado hasta la convergencia. Si mezcla capas entrenables inicializadas aleatoriamente con capas entrenables que contienen características pre-entrenadas, las capas inicializadas aleatoriamente causarán actualizaciones de gradiente muy grandes durante el entrenamiento, lo que destruirá sus características pre-entrenadas.

También es fundamental utilizar una tasa de aprendizaje muy baja en esta fase, porque se está entrenando un modelo mucho mayor que en la primera ronda de entrenamiento, en un conjunto de datos que suele ser muy pequeño. Como resultado, corre el riesgo de sobreajustarse muy rápidamente si aplica grandes actualizaciones de pesos. En este caso, lo único que se desea es readaptar los pesos preentrenados de forma gradual.

Así se aplica el ajuste fino de todo el modelo base:

# Unfreeze the base model base_model.trainable = True # It's important to recompile your model after you make any changes # to the `trainable` attribute of any inner layer, so that your changes # are take into account model.compile(optimizer=keras.optimizers.Adam(1e-5), # Very low learning rate loss=keras.losses.BinaryCrossentropy(from_logits=True), metrics=[keras.metrics.BinaryAccuracy()]) # Train end-to-end. Be careful to stop before you overfit! model.fit(new_dataset, epochs=10, callbacks=..., validation_data=...)

Nota importante sobre compile() y trainable

Llamar a compile() en un modelo es "congelar" el comportamiento de ese modelo. Esto implica que los valores de los atributos trainable en el momento en que se compila el modelo deben conservarse durante toda la vida del modelo, hasta que se vuelva a llamar a compile. Por lo tanto, si cambia algún valor de trainable, asegúrese de volver a llamar a compile() en el modelo para que se tengan en cuenta los cambios.

Notas importantes sobre la capa BatchNormalization

Muchos modelos de imágenes contienen capas BatchNormalization. Esa capa es un caso especial en todos los aspectos imaginables. Aquí hay algunas cosas que debe tener en cuenta.

  • BatchNormalization contiene 2 pesos no entrenables que se actualizan durante el entrenamiento. Estas son las variables de seguimiento de la media y la varianza de las entradas.

  • Cuando se establece bn_layer.trainable = False, la capa BatchNormalization se ejecutará en modo de inferencia, y no se actualizarán sus estadísticas de media y varianza. Este no es el caso de otras capas en general, ya que la entrenabilidad del peso y los modos de inferencia/entrenamiento son dos conceptos ortogonales. Pero ambos están relacionados en el caso de la capa BatchNormalization.

  • Cuando se descongela un modelo que contiene capas BatchNormalization para realizar un ajuste fino, se deben mantener las capas BatchNormalization en el modo de inferencia pasando training=False al llamar al modelo base. De lo contrario, las actualizaciones aplicadas a los pesos no entrenables destruirán repentinamente lo que el modelo ha aprendido.

Verá este patrón en acción en el ejemplo de extremo a extremo al final de esta guía.

Aprendizaje por transferencia y ajuste con un bucle de entrenamiento personalizado

Si en lugar de fit(), utiliza su propio bucle de entrenamiento de bajo nivel, el flujo de trabajo seguirá siendo esencialmente el mismo. Debe tener cuidado de sólo tener en cuenta la lista model.trainable_weights al aplicar actualizaciones del gradiente:

# Create base model base_model = keras.applications.Xception( weights='imagenet', input_shape=(150, 150, 3), include_top=False) # Freeze base model base_model.trainable = False # Create new model on top. inputs = keras.Input(shape=(150, 150, 3)) x = base_model(inputs, training=False) x = keras.layers.GlobalAveragePooling2D()(x) outputs = keras.layers.Dense(1)(x) model = keras.Model(inputs, outputs) loss_fn = keras.losses.BinaryCrossentropy(from_logits=True) optimizer = keras.optimizers.Adam() # Iterate over the batches of a dataset. for inputs, targets in new_dataset: # Open a GradientTape. with tf.GradientTape() as tape: # Forward pass. predictions = model(inputs) # Compute the loss value for this batch. loss_value = loss_fn(targets, predictions) # Get gradients of loss wrt the *trainable* weights. gradients = tape.gradient(loss_value, model.trainable_weights) # Update the weights of the model. optimizer.apply_gradients(zip(gradients, model.trainable_weights))

Lo mismo ocurre con el ajuste fino.

Un ejemplo de extremo a extremo: puesta a punto de un modelo de clasificación de imágenes en un conjunto de datos de gatos frente a perros

Para consolidar estos conceptos, veamos un ejemplo concreto de aprendizaje por transferencia y ajuste de extremo a extremo. Cargaremos el modelo Xception, preentrenado en ImageNet, y lo utilizaremos en el conjunto de datos de clasificación "gatos frente a perros" de Kaggle.

Obtener los datos

En primer lugar, vamos a obtener el conjunto de datos de gatos frente a perros utilizando TFDS. Si tiene su propio conjunto de datos, probablemente querrá utilizar la utilidad tf.keras.preprocessing.image_dataset_from_directory para generar objetos a partir de un conjunto de imágenes archivadas en carpetas específicas para las clases.

El aprendizaje por transferencia es más útil cuando se trabaja con conjuntos de datos muy pequeños. Para que nuestro conjunto de datos sea pequeño, utilizaremos el 40% de los datos de entrenamiento originales (25,000 imágenes) para el entrenamiento, el 10% para la validación y el 10% para las pruebas.

import tensorflow_datasets as tfds tfds.disable_progress_bar() train_ds, validation_ds, test_ds = tfds.load( "cats_vs_dogs", # Reserve 10% for validation and 10% for test split=["train[:40%]", "train[40%:50%]", "train[50%:60%]"], as_supervised=True, # Include labels ) print("Number of training samples: %d" % tf.data.experimental.cardinality(train_ds)) print( "Number of validation samples: %d" % tf.data.experimental.cardinality(validation_ds) ) print("Number of test samples: %d" % tf.data.experimental.cardinality(test_ds))

Estas son las 9 primeras imágenes del conjunto de datos de entrenamiento: como puede ver, todas tienen tamaños diferentes.

import matplotlib.pyplot as plt plt.figure(figsize=(10, 10)) for i, (image, label) in enumerate(train_ds.take(9)): ax = plt.subplot(3, 3, i + 1) plt.imshow(image) plt.title(int(label)) plt.axis("off")

También podemos ver que la etiqueta 1 es "perro" y la etiqueta 0 es "gato".

Normalización de los datos

Nuestras imágenes sin procesar tienen diversos tamaños. Además, cada pixel consiste en 3 valores enteros entre 0 y 255 (valores de nivel RGB). Esto no es muy adecuado para alimentar una red neuronal. Tenemos que hacer 2 cosas:

  • Estandarizar a un tamaño de imagen fijo. Elegimos 150x150.

  • Normalizar los valores de los pixeles entre -1 y 1. Haremos esto usando una capa Normalization como parte del propio modelo.

En general, es una práctica recomendada desarrollar modelos que tomen datos sin procesar como entrada, en vez de modelos que tomen datos ya preprocesados. La razón es que, si su modelo espera datos preprocesados, cada vez que exporte su modelo para utilizarlo en otro lugar (en un navegador web, en una aplicación móvil), tendrá que volver a implementar exactamente el mismo proceso de preprocesamiento. Esto se complica rápidamente. Así que debemos hacer la menor cantidad posible de preprocesamiento antes de golpear el modelo.

En este caso, cambiaremos el tamaño de la imagen en la canalización de datos (porque una red neuronal profunda solo puede procesar lotes contiguos de datos), y haremos el escalado del valor de entrada como parte del modelo, cuando lo creemos.

Redimensionemos las imágenes a 150x150:

size = (150, 150) train_ds = train_ds.map(lambda x, y: (tf.image.resize(x, size), y)) validation_ds = validation_ds.map(lambda x, y: (tf.image.resize(x, size), y)) test_ds = test_ds.map(lambda x, y: (tf.image.resize(x, size), y))

Además, procesemos los datos por lotes y utilicemos el almacenamiento en caché y la preextracción para optimizar la velocidad de carga.

batch_size = 32 train_ds = train_ds.cache().batch(batch_size).prefetch(buffer_size=10) validation_ds = validation_ds.cache().batch(batch_size).prefetch(buffer_size=10) test_ds = test_ds.cache().batch(batch_size).prefetch(buffer_size=10)

Utilizar el aumento aleatorio de datos

Cuando no se dispone de un gran conjunto de datos de imágenes, es una práctica recomendada introducir artificialmente una diversidad de muestras aplicando transformaciones aleatorias pero realistas a las imágenes de entrenamiento, como volteos horizontales aleatorios o pequeñas rotaciones aleatorias. Esto ayuda a exponer el modelo a diferentes aspectos de los datos de entrenamiento y ralentiza el sobreajuste.

from tensorflow import keras from tensorflow.keras import layers data_augmentation = keras.Sequential( [layers.RandomFlip("horizontal"), layers.RandomRotation(0.1),] )

Visualicemos el aspecto de la primera imagen del primer lote después de varias transformaciones aleatorias:

import numpy as np for images, labels in train_ds.take(1): plt.figure(figsize=(10, 10)) first_image = images[0] for i in range(9): ax = plt.subplot(3, 3, i + 1) augmented_image = data_augmentation( tf.expand_dims(first_image, 0), training=True ) plt.imshow(augmented_image[0].numpy().astype("int32")) plt.title(int(labels[0])) plt.axis("off")

Cómo construir un modelo

Ahora vamos a construir un modelo que siga el esquema que hemos explicado anteriormente.

Tenga en cuenta que:

  • Agregamos una capa Rescaling para escalar los valores de entrada (inicialmente en el rango [0, 255]) al rango [-1, 1].

  • Agregamos una capa Dropout antes de la capa de clasificación, para realizar la regularización.

  • Nos aseguramos de pasar training=False al llamar al modelo base, para que se ejecute en modo de inferencia, de modo que las estadísticas batchnorm no se actualicen incluso después de descongelar el modelo base para el ajuste fino.

base_model = keras.applications.Xception( weights="imagenet", # Load weights pre-trained on ImageNet. input_shape=(150, 150, 3), include_top=False, ) # Do not include the ImageNet classifier at the top. # Freeze the base_model base_model.trainable = False # Create new model on top inputs = keras.Input(shape=(150, 150, 3)) x = data_augmentation(inputs) # Apply random data augmentation # Pre-trained Xception weights requires that input be scaled # from (0, 255) to a range of (-1., +1.), the rescaling layer # outputs: `(inputs * scale) + offset` scale_layer = keras.layers.Rescaling(scale=1 / 127.5, offset=-1) x = scale_layer(x) # The base model contains batchnorm layers. We want to keep them in inference mode # when we unfreeze the base model for fine-tuning, so we make sure that the # base_model is running in inference mode here. x = base_model(x, training=False) x = keras.layers.GlobalAveragePooling2D()(x) x = keras.layers.Dropout(0.2)(x) # Regularize with dropout outputs = keras.layers.Dense(1)(x) model = keras.Model(inputs, outputs) model.summary()

Entrene la capa superior

model.compile( optimizer=keras.optimizers.Adam(), loss=keras.losses.BinaryCrossentropy(from_logits=True), metrics=[keras.metrics.BinaryAccuracy()], ) epochs = 20 model.fit(train_ds, epochs=epochs, validation_data=validation_ds)

Realice una ronda de ajuste de todo el modelo

Por último, descongelemos el modelo base y entrenemos todo el modelo de principio a fin con una tasa de aprendizaje baja.

Es importante destacar que, aunque el modelo base se convierte en entrenable, todavía se está ejecutando en modo de inferencia ya que pasamos training=False al llamarlo cuando construimos el modelo. Esto significa que las capas de normalización de lotes no actualizarán sus estadísticas de lotes. Si lo hicieran, causarían estragos en las representaciones aprendidas por el modelo como hasta ahora.

# Unfreeze the base_model. Note that it keeps running in inference mode # since we passed `training=False` when calling it. This means that # the batchnorm layers will not update their batch statistics. # This prevents the batchnorm layers from undoing all the training # we've done so far. base_model.trainable = True model.summary() model.compile( optimizer=keras.optimizers.Adam(1e-5), # Low learning rate loss=keras.losses.BinaryCrossentropy(from_logits=True), metrics=[keras.metrics.BinaryAccuracy()], ) epochs = 10 model.fit(train_ds, epochs=epochs, validation_data=validation_ds)

Después de 10 épocas, el ajuste fino nos proporcionará una buena mejora.