<jupyter_start><jupyter_text>[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1PtNR7npOVBVixG2sEepX-b1l754jqWGg?usp=sharing) Práctica 8: Transfer learning Pre-requisitos Instalar paquetesSi la práctica requiere algún paquete de Python, habrá que incluir una celda en la que se instalen. Si usamos un paquete que se ha utilizado en prácticas anteriores, podríamos dar por supuesto que está instalado pero no cuesta nada satisfacer todas las dependencias en la propia práctica para reducir las dependencias entre ellas. NOTA: En Google Colab hay que instalar los paquetes EN CADA EJECUCIÓN<jupyter_code># Ejemplo de instalación de tensorflow 2.0
#%tensorflow_version 2.x
# !pip3 install tensorflow  # NECESARIO SOLO SI SE EJECUTA EN LOCAL
import tensorflow as tf

# Hacemos los imports que sean necesarios
import numpy as np
import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>Transfer learning sobre Stanford DogsLo que queremos hacer es entrenar un clasificador para que catalogue las distintas clases de perros almacenadas en el dataset Stanford Dogs. Para ello, lo primero que tenemos que hacer es cargar el dataset.<jupyter_code>import tensorflow_datasets as tfds

(ds_train, ds_test), ds_info = tfds.load(
    'stanford_dogs', split=['train', 'test'], with_info=True, as_supervised=True
)
NUM_CLASSES = ds_info.features['label'].num_classes<jupyter_output><empty_output><jupyter_text>Preprocesado de los datosAntes de nada, vamos a ver el tamaño que tienen las imágenes de entrada.<jupyter_code># ds_train es un iterable con una tupla (imagen, clase). Vamos a ver la resolución de la imagen
for i, element in enumerate(ds_train.take(5)):
    print(element[0].shape)<jupyter_output><empty_output><jupyter_text>Como podemos observar, el tamaño de las imágenes es variable y no coincide con el tamaño que esperamos a la entrada de la red. Nuestro primer cometido será el modificar las imágenes para que tengan el tamaño fijo 224x224x3.**PISTA: puedes usar la función [resize](https://www.tensorflow.org/api_docs/python/tf/image/resize) y la función map, que te permite operar sobre cada elemento de un iterable**<jupyter_code>## TODO: completa el código en las partes donde hay un None
ds_train = ds_train.map(lambda image, label: (None, label))
ds_test = None<jupyter_output><empty_output><jupyter_text>Visualización de los datosUna vez hecho el preprocesado, ya podemos visualizar las imágenes.<jupyter_code>def format_label(label):
    string_label = label_info.int2str(label)
    return string_label.split("-")[1]

label_info = ds_info.features["label"]
for i, (image, label) in enumerate(ds_train.take(9)):
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(image.numpy().astype("uint8"))
    plt.title("{}".format(format_label(label)))
    plt.axis("off")<jupyter_output><empty_output><jupyter_text>A mayores, cada red preentrenada tiene un preprocesado de los datos propio, que hay que realizar antes de pasárselo al modelo. En el caso de DenseNet121, la función de preprocesado se llama ```tf.keras.applications.densenet.preprocess_input```<jupyter_code>## TODO: aplica la función de preprocesado a los conjuntos del dataset
None
None<jupyter_output><empty_output><jupyter_text>La etiqueta es numérica, y hay que convertirla a tipo one_hot con la función [tf.one_hot](https://www.tensorflow.org/api_docs/python/tf/one_hot)**PISTA: el número de clases se ha almacenado anteriormente en la variable NUM_CLASSES**<jupyter_code>## TODO: convierte las etiquetas a tipo one hot.
None
None<jupyter_output><empty_output><jupyter_text>Finalmente, nos queda preparar los datos en batches para pasárselos al entrenamiento. Aquí distinguiremos entre entrenamiento y test, ya que los datos de entrenamiento es mejor hacer los batches de manera aleatoria en cada iteración.<jupyter_code>batch_size = 128
ds_train_batch = ds_train.cache().shuffle(batch_size*5).batch(batch_size)
ds_test_batch = ds_test.batch(batch_size) # En test no es necesario aleatorizar<jupyter_output><empty_output><jupyter_text>Creando el modeloUna vez obtenido un correcto preprocesado de los datos, vamos a crear el modelo. Nuestro modelo constará de tres partes diferenciadas:1. Data augmentation1. DenseNet121 preentrenado1. ClasificadorVamos a ir paso a paso creando cada uno de los puntos.  Data augmentationEl primer paso que vamos a realizar es aumentar los datos de entrenamiento mediante la aplicación de distintas operaciones, que se encuentran en el paquete [tensorflow.keras.layers.experimental.preprocessing](https://www.tensorflow.org/api_docs/python/tf/keras/layers/experimental/preprocessing). En concreto, vamos a realizar las siguientes operaciones:1. Rotaciones aleatorias de $2*\pi*0.15$ grados1. Traslaciones aleatorias del $10\%$ del tamaño de la imagen1. Giros aleatorios de la imagen1. Cambios aleatorios del contraste del $10\%$<jupyter_code>from tensorflow.keras.layers.experimental import preprocessing
from tensorflow.keras.models import Sequential

## TODO: crea el modelo de data augmentation, rellenando los huecos marcados con None
img_augmentation = Sequential(
    [
        None, # rotaciones
        None, # traslaciones
        None, # giros
        None  # contraste
    ],
    name="img_augmentation",
)<jupyter_output><empty_output><jupyter_text>Descargar un modelo preentrenadoLo primero que vamos a hacer es descargar un modelo preentrenado. Por suerte, en Tensorflow tenemos una serie de modelos ya preentrenados. Se encuentran en el paquete **tensorflow.keras.applications** ([Doc](https://keras.io/api/applications/)). En concreto, vamos a utilizar la red DenseNet121, cuyo esquema podemos ver en la siguiente imagen:![](https://drive.google.com/uc?export=view&id=1cINnKtOxOtZGxCiZwSXfhku495KHsSvk)En esencia, es una variante del bloque residual, en donde la salida de una capa se suma a todas las salidas de las capas siguientes.Para cargar el modelo haremos lo siguiente:<jupyter_code>from tensorflow.keras.applications import DenseNet121

# Cargamos el modelo. 
densenet = DenseNet121(
    weights='imagenet', # El parámetro weights le dice que cargue los datos de la red ya entrenada sobre el dataset ImageNet
    include_top=False, # El parámetro include_top le dice que cargue la última capa con el clasificador
    input_shape=(224, 224, 3), # Este parámetro será necesario para hacer transfer learning, 
                      # pues necesitaremos especificar el tamaño de los datos de entrada
)

# Comprobamos el tamaño de entrada de la red
print('Input shape :', densenet.input_shape)

# Comprobamos el tamaño de salida de la red
print('Output shape :', densenet.output_shape)<jupyter_output><empty_output><jupyter_text>En la [documentación](https://keras.io/api/applications/densenet/densenet121-function) dodemos ver que esta red admite, como entrada, imágenes de tamaño 224x224x3. Aunque podemos utilizar cualquier tamaño de entrada (siempre que sea mayor de 32x32x3), vamos a seguir utilizando ese tamaño. Crear el clasificadorPara crear el clasificador, lo que vamos a hacer es crear una última capa que será un clasificador lineal (capa [Dense](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense)). Sin embargo, podemos observar que la salida de la red DenseNet tiene 4 dimensiones, no 2 que es lo necesario para crear el clasificador. Aquí tenemos dos opciones para realizar la conversión:1. Usar [Flatten](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Flatten), en la que la dimensión 2 será de tamaño la multiplicación de las dimensiones 2, 3 y 4.1. Usar GlobalPooling, colapsando las dimensiones 2 y 3, siendo por tanto la dimensión 2 de salida igual a la dimensión 4 de entrada.Aquí podemos ver una imagen ilustrando la diferencia:![](https://drive.google.com/uc?export=view&id=1I3a2v7cuEjAUVdlLENtCaIq-etCXriGT)En nuestro caso, vamos a usar un GlobalPooling, en concreto [GlobalAveragePooling2D](https://www.tensorflow.org/api_docs/python/tf/keras/layers/GlobalAveragePooling2D). A mayores, añadiremos alguna regularización extra como [Dropout](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dropout) o [Batch Normalization](https://www.tensorflow.org/api_docs/python/tf/keras/layers/BatchNormalization).<jupyter_code>from tensorflow.keras import layers

## TODO: crea el clasificador, rellenando los huecos marcados por None
classifier = Sequential(
    [
        None, # GlobalAveragePooling2D
        None, # BatchNormalization
        None, # Dropout con probabilidad 0.2
        None, # Dense con salida el número de clases del dataset
        None, # Softmax activation
    ]
)<jupyter_output><empty_output><jupyter_text>Combinando todos los elementosUna vez creados todos los elementos, sólo nos falta combinarlos. Hay que recordar que la red preentrenada no se modifica, por lo que hay que indicarle que los pesos no son entrenables.<jupyter_code>## TODO: Crea el modelo final combinando los 3 modelos creados anteriormente

input = None  # capa de input, teniendo en cuenta el tamaño de las imágenes 

input_augmented = None  # aplica el modelo de data augmentation a la capa de entrada

densenet.trainable = False
densenet_output = None  # aplica el modelo densenet

output = None  # aplica el clasificador

model = tf.keras.Model(None, None, name="DenseNet")  # crea el modelo final, indicando la entrada y la salida
optimizer = None  # define el optimizador. Usaremos un Adam
model.compile(
    optimizer=None, loss=None, metrics=[None]  # Compila el modelo, usando el optimizador creado, la función de pérdida para clasificación multiclase y la métrica de precisión
)<jupyter_output><empty_output><jupyter_text>Entrenando el modeloPor último, sólo nos queda entrenar el modelo durante 10 iteraciones (ten paciencia, puede llevar sobre 10 minutos). En test, deberíamos tener una precisión cercana al $80\%$.<jupyter_code>hist = model.fit(
    ds_train_batch, epochs=10, verbose=1  # se podría poner validation_data=ds_test_batch, y así ver en cada iteración como va, pero en este caso consume mucho tiempo
)


def plot_hist(hist):
    plt.plot(hist.history["accuracy"])
    # plt.plot(hist.history["val_accuracy"])
    plt.title("train accuracy")
    plt.ylabel("accuracy")
    plt.xlabel("epoch")
    # plt.legend(["train", "validation"], loc="upper left")
    plt.show()

print('TEST ACCURACY : ', model.evaluate(ds_test_batch)[-1])

plot_hist(hist)<jupyter_output><empty_output><jupyter_text>Fine tunningAl inicio hemos entrenado el modelo sin tocar los pesos de la red DenseNet. Sin embargo, ahora que ya hemos entrenado las capas finales, podemos reentrenar el modelo al completo. Sin embargo, hay que tener en cuenta una serie de consideraciones.- La tasa de aprendizaje tiene que ser muy baja. Un valor alto podría eliminar la calidad del entrenamiento inicial, haciendo que el sistema se comporte de manera similar a entrenarlo desde 0.- Las capas de Batch Normalization se mantienen sin entrenar. Esto es debido a que dificultan el entrenamiento en redes ya entrenadas, y sólo estamos buscando un ajuste fino en el sistema.<jupyter_code>## TODO: completa aquellos puntos marcados con None
batch_size = 32 # Tenemos que bajar el tamaño anterior para no saturar la memoria del sistema
ds_train_batch = ds_train.cache().shuffle(batch_size*5).batch(batch_size)
ds_test_batch = ds_test.batch(batch_size)

def unfreeze_model(model):
    # Descongelamos las últimas 20 capas while, dejando BatchNormalization sin entrenar
    for layer in model.layers[-20:]:
        if not isinstance(layer, None): # Comprueba que la capa no sea de tipo BatchNormalization
            None                        # Marca la capa como entrenable

    optimizer = None                    # Usa Adam con una tasa de aprendizaje de 1e-5
    model.compile(
        None, None, None                # define el compilador, la función de coste y la métrica
    )

None                                    # Llama a la función que has creado sobre nuestro modelo

epochs = 8  
hist = None  # Llama a la función de entrenamiento sobre el modelo

plot_hist(hist)

print('TEST ACCURACY : ', model.evaluate(ds_test_batch)[-1])<jupyter_output><empty_output><jupyter_text>La precisión en entrenamiento ha subido, pero en test ha bajado. ¿Qué puede estar pasando?  ¡ENHORABUENA! Has completado la práctica de Transfer Learning. ¿Deseas saber más?El transfer learning no se limita sólo a entrenar las últimas capas. Se pueden utilizar sólo unas pocas capas preentrenadas, y entrenar el resto. No hay límite sobre lo que se puede hacer. Tampoco se restringe a clasificación únicamente. Las *deep features* de un modelo de clasificación suelen ser útiles para modelos de regresión, segmentación, etc.  Trabajo extraComo trabajo extra, se propone realizar una idea parecida, pero con dificultad añadida.- El dataset a utilizar será [cifar10](https://www.tensorflow.org/datasets/catalog/cifar10).- Se utilizará una entrada de la red de 32x32x3.- La red preentrenada a utilizar será [MobileNetV3Small](https://www.tensorflow.org/api_docs/python/tf/keras/applications/MobileNetV3Small).- Puedes escoger el calsificador que quieras.<jupyter_code># TODO: escribe el código para el trabajo extra sin ayuda. Usa todos los bloques de código que quieras.<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07337589185129553, 'fraction_numerical': 0.010739767179872324, 'mean_word_length': 4.987410071942446, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 61, 'https://': 16, 'lorem ipsum': 0, 'www.': 10, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '194889', 'license': 'MIT', 'n_tokens_mistral': 4505, 'n_tokens_neox': 4349, 'n_words': 1480, 'path': 'lab8/lab8.ipynb', 'repo_name': 'eirasf/GCED-AA2', 'size': '13315'}