Aprendizaje profundo práctico para visión por computadora con Python - Clasificación de imágenes con aprendizaje de transferencia - Creación de modelos CNN de vanguardia

DeepDream con TensorFlow/Keras Detección de puntos clave con Detectron2 Subtítulos de imágenes con KerasNLP Transformadores y ConvNets Segmentación semántica con DeepLabV...

Con frecuencia, se lanzan nuevos modelos y se comparan con conjuntos de datos aceptados por la comunidad, y mantenerse al día con todos ellos es cada vez más difícil.

La mayoría de estos modelos son de código abierto y también puede implementarlos usted mismo.

Esto significa que el entusiasta promedio puede cargar y jugar con los modelos de vanguardia en su hogar, en máquinas muy promedio, no solo para obtener una comprensión y apreciación más profundas del oficio, sino también para contribuir al discurso científico y publicar sus propias mejoras cada vez que se realizan.

En esta lección, aprenderá a utilizar modelos de aprendizaje profundo de última generación previamente entrenados para la clasificación de imágenes y a reutilizarlos para su propia aplicación específica. De esta manera, está aprovechando sus arquitecturas ingeniosas y de alto rendimiento y el tiempo de capacitación de otra persona, ¡mientras aplica estos modelos a su propio dominio!

Transferencia de aprendizaje para visión artificial y redes neuronales convolucionales {#transferencia de aprendizaje para visión artificial y redes neuronales convolucionales}

Conocimiento y representaciones de conocimiento son muy universales. Un modelo de visión por computadora entrenado en un conjunto de datos aprende a reconocer patrones que pueden ser muy frecuentes en muchos otros conjuntos de datos.

En particular, en "Deep Learning for the Life Sciences", de Bharath Ramsundar, Peter Eastman, Patrick Walters y Vijay Pande, se señala que:

"Se han realizado múltiples estudios que investigan el uso de algoritmos de sistemas de recomendación para su uso en la predicción de enlaces moleculares. Las arquitecturas de aprendizaje automático utilizadas en un campo tienden a trasladarse a otros campos, por lo que es importante conservar la flexibilidad necesaria para el trabajo innovador. "

Por ejemplo, las líneas rectas y curvas, que normalmente se aprenden en un nivel más bajo de una jerarquía de CNN, seguramente estarán presentes en prácticamente todos los conjuntos de datos. Algunas características de alto nivel, como las que distinguen a una abeja de una hormiga, se representarán y aprenderán mucho más arriba en la jerarquía:

¡La "línea fina" entre estos es lo que puedes reutilizar! Según el nivel de similitud entre su conjunto de datos y el modelo en el que se ha entrenado previamente, es posible que pueda reutilizar una pequeña o gran parte de él.

Un modelo que clasifica estructuras creadas por humanos (entrenadas en un conjunto de datos como Places365) y un modelo que clasifica imágenes generales (entrenadas en un conjunto de datos como ImageNet) tienen algunos patrones compartidos, aunque no mucho.

Es posible que desee entrenar un modelo para distinguir, por ejemplo, autobuses y automóviles para el sistema de visión de un automóvil autónomo. También puede optar razonablemente por utilizar una arquitectura de alto rendimiento que ha demostrado funcionar bien en conjuntos de datos similares a los suyos. Luego, comienza el largo proceso de capacitación, ¡y terminas teniendo un modelo propio!

Sin embargo, si es probable que otro modelo tenga representaciones similares en niveles de abstracción más bajos y más altos, no hay necesidad de volver a entrenar un modelo desde cero. Puede decidir usar algunos de los pesos ya entrenados previamente, que son tan aplicables a su propia aplicación del modelo como lo fueron al creador de la arquitectura original. Estaría transfiriendo parte del conocimiento de un modelo ya existente a uno nuevo, y esto se conoce como Transferir aprendizaje. En mi opinión, se subestima la importancia y la versatilidad del aprendizaje por transferencia. A menudo se pone a un lado, o se menciona brevemente al final de las lecciones y conferencias, y a menudo es el último concepto que se cubre al aprender sobre las CNN.

Siempre que esté leyendo sobre la aplicación de la visión por computadora a un problema específico, lo más probable es que se trate de aprendizaje de transferencia en segundo plano. Si pasa sus tardes como yo leyendo documentos de investigación en varios campos (en los que apenas tengo conocimiento), notará cuán comúnmente se usa el aprendizaje por transferencia, incluso cuando no se menciona con ese nombre. Es tan frecuente que prácticamente se supone que se utiliza el aprendizaje por transferencia. Con modelos precargados y conocimiento transferido, casi cualquiera puede utilizar el poder del aprendizaje profundo para avanzar en un campo.

  • Los médicos pueden usar modelos de visión por computadora para diagnosticar imágenes (rayos X, histología, retinoscopia, etc.)
  • Las ciudades pueden usar la visión artificial para detectar peatones y automóviles en las calles y adaptar los semáforos para optimizar el flujo de transporte.
  • Los centros comerciales pueden usar la visión por computadora para detectar la ocupación del estacionamiento
  • Los biólogos marinos pueden usar la visión artificial para detectar arrecifes de coral en peligro de extinción (Competencia de la Gran Barrera de Coral de TensorFlow)
  • Los fabricantes pueden usar la visión por computadora para detectar defectos en las líneas de producción (como píldoras faltantes en medicamentos)
  • Los medios de comunicación pueden usar la visión por computadora para digitalizar números de periódicos antiguos
  • Las plantas agrícolas pueden usar la visión artificial para detectar el rendimiento y la salud de los cultivos (e insectos/otras plagas)

Desde optimizar los flujos de trabajo y las inversiones hasta salvar vidas humanas, la visión artificial es muy aplicable. Sin embargo, lea la lista nuevamente. ¿Quiénes son las personas que utilizan estas tecnologías? Médicos, biólogos, agricultores, urbanistas. Es posible que no tengan una amplia experiencia en informática/ciencia de datos o un hardware potente necesario para entrenar redes grandes, pero pueden ver los beneficios de esas redes incluso si no están optimizadas. A través de modelos democratizados, no necesitan una formación en ciencia de datos. A través de proveedores de capacitación en la nube gratuitos y económicos y redes preentrenadas, no necesitan hardware potente.

El entrenamiento con arquitecturas prediseñadas y pesas descargables se ha simplificado tanto que un niño con una conexión a Internet lenta y una computadora que apenas funciona puede crear modelos más precisos que los equipos y profesionales de primera línea hace una o dos décadas.

El beneficio del aprendizaje por transferencia no se limita a acortar la capacitación. Si no tiene muchos datos, una red no podrá aprender algunas de las distinciones desde el principio. Si lo entrena extensamente en un conjunto de datos y luego lo transfiere a otro, muchas de las representaciones ya están aprendidas y se pueden ajustar en el nuevo conjunto de datos. En el caso de CIFAR100 con el que hemos trabajado en la última lección, muchas de las imágenes se pueden encontrar (en tamaños más grandes) en conjuntos de datos como ImageNet, y muchas podrían haberse transferido con un modelo previamente entrenado. Esto sería, en efecto, lo que pretendía ser el aumento de datos: expandir el conjunto de datos (aunque indirectamente), con instancias de datos de otro conjunto de datos. Si bien realmente no expande el nuevo conjunto de datos, el conocimiento codificado en el modelo que se está ajustando se transfiere entre ellos.

Cuanto más cerca esté el conjunto de datos de un modelo previamente entrenado del suyo, más podrá transferir. Cuanto más pueda transferir, más de su propio tiempo y cálculo podrá ahorrar. Vale la pena recordar que entrenar redes neuronales tiene una huella de carbono, ¡así que no solo estás ahorrando tiempo!

Por lo general, el aprendizaje de transferencia se realiza cargando un modelo previamente entrenado y congelando sus capas. En muchos casos, puede simplemente cortar la capa de clasificación (las capas finales, o cabeza/capa densamente conectada) y simplemente volver a entrenar la parte superior nueva, mientras mantiene todo el resto capas de abstracción intactas. Esto es primordial para usar la base convolucional como un extractor de características, y simplemente vuelve a entrenar el clasificador que contiene todo el conocimiento del dominio (los bloques convolucionales son mucho más genéricos). En otros casos, puede decidir volver a entrenar varias capas en la jerarquía convolucional junto con la parte superior, y esto generalmente se hace cuando los conjuntos de datos contienen puntos de datos lo suficientemente diferentes como para volver a entrenar varias capas. También puede decidir volver a entrenar la totalidad del modelo para ajustar todas las capas.

Estos dos enfoques se pueden resumir en:

  • Uso de la red convolucional como extractor de características
  • Ajuste fino de la red convolucional

En el primero, usa el modelo subyacente como un extractor de características fijas y simplemente entrena una red densa en la parte superior para discernir entre estas características. En este último, ajusta toda la red convolucional (o parte de ella), si aún no tiene mapas de características representativos para algún otro conjunto de datos más específico, mientras también confía en los mapas de características ya entrenados y simplemente los actualiza. para adaptarse también a sus propias necesidades.

Aquí hay una representación visual de cómo funciona Transfer Learning:

Modelos de clasificación de imágenes establecidos y de vanguardia {#modelos de clasificación de imágenes establecidos y de vanguardia}

Existen muchos modelos, y para conjuntos de datos conocidos, es probable que encuentre cientos de modelos de buen rendimiento publicados en repositorios y documentos en línea. Una buena visión holística de los modelos entrenados en el conjunto de datos de ImageNet se puede ver en PapelesConCódigo.

Algunas de las arquitecturas publicadas conocidas que posteriormente se trasladaron a muchos marcos de aprendizaje profundo incluyen:

  • EfficientNet
  • Factura
  • Origen y Xcepción
  • ResNet
  • VGGNet

{.icon aria-hidden=“true”}

Nota: Ser conocido no significa que una arquitectura vaya a rendir al más alto nivel. Por ejemplo, probablemente no desee utilizar VGGNet para el aprendizaje de transferencia, porque se han adaptado y preentrenado arquitecturas más nuevas, más robustas y más eficientes.

La lista de modelos en PapersWithCode se actualiza constantemente, y no debe colgar la posición de estos modelos allí. Muchos de los nuevos modelos que ocupan los primeros lugares en realidad se basan en los que se describen en la lista anterior.

Desafortunadamente, algunos de los modelos más nuevos no se portan como modelos preentrenados dentro de marcos como Tensorflow y PyTorch, aunque los equipos son bastante diligentes en portarlos con pesos preentrenados. No es como si estuvieras perdiendo mucho del rendimiento, por lo que ir con cualquiera de los bien establecidos no está nada mal.

Transferencia de aprendizaje con Keras: adaptación de modelos existentes

Con Keras, los modelos preentrenados están disponibles en el módulo tensorflow.keras.applications. Cada modelo tiene su propio submódulo y clase. Al cargar un modelo, puede establecer un par de argumentos opcionales para controlar cómo se cargan los modelos.

{.icon aria-hidden=“true”}

Nota: Puede encontrar los modelos portados en Keras.io, pero la lista no incluye los modelos más nuevos y experimentales. Para obtener una lista más actualizada, visite Documentos de TensorFlow.

Por ejemplo, el argumento “pesos”, si está presente, define qué pesos preentrenados se utilizarán. Si se omite, solo se cargará la arquitectura (red no entrenada). Si proporciona un argumento 'imagenet', se devolverá una red preentrenada para ese conjunto de datos. Alternativamente, puede proporcionar una ruta a un archivo con los pesos que desea cargar (siempre que sea exactamente la misma arquitectura).

Además, dado que lo más probable es que elimine la(s) capa(s) superior(es) para Transfer Learning, el argumento include_top se utiliza para definir si la(s) capa(s) superior(es) debe(n) estar presente(s) o no.

1
2
3
4
5
6
7
8
9
import tensorflow.keras.applications as models

# 98 MB
resnet = models.resnet50.ResNet50(weights='imagenet', include_top=False)
# 528MB
vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=False)
# 23MB
nnm = models.NASNetMobile(weights='imagenet', include_top=False)
# etc...

{.icon aria-hidden=“true”}

Nota: Si nunca ha cargado modelos previamente entrenados, se descargarán a través de una conexión a Internet. Esto puede demorar entre unos segundos y un par de minutos, según la velocidad de Internet y el tamaño de los modelos. El tamaño de los modelos preentrenados va desde tan solo 14 MB (generalmente más bajo para los modelos Mobile) hasta 549 MB.

EfficientNet es una familia de redes que son bastante eficientes, escalables y, bueno. Se hicieron teniendo en cuenta la reducción de los parámetros de aprendizaje, por lo que solo tienen 4 millones de parámetros para entrenar. Considere que VGG19, por ejemplo, tiene 139M. ¡En una configuración casera, esto también ayuda significativamente con los tiempos de entrenamiento!

Carguemos uno de los miembros de la familia EfficientNet - EfficientNetB0:

1
2
effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=False)
effnet.summary()

Esto resulta en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Model: "efficientnetb0"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_2 (InputLayer)           [(None, None, None,  0           []                               
                                 3)]                                                              
                                                                                                  
 rescaling_1 (Rescaling)        (None, None, None,   0           ['input_2[0][0]']                
                                3)                    
                                                                            
...
...

 block7a_project_bn (BatchNorma  (None, None, None,   1280       ['block7a_project_conv[0][0]']   
 lization)                      320)                                                              
                                                                                                  
 top_conv (Conv2D)              (None, None, None,   409600      ['block7a_project_bn[0][0]']     
                                1280)                                                             
                                                                                                  
 top_bn (BatchNormalization)    (None, None, None,   5120        ['top_conv[0][0]']               
                                1280)                                                             
                                                                                                  
 top_activation (Activation)    (None, None, None,   0           ['top_bn[0][0]']                 
                                1280)                                                             
                                                                                                  
==================================================================================================
Total params: 4,049,571
Trainable params: 4,007,548
Non-trainable params: 42,023
__________________________________________________________________________________________________

Por otro lado, si tuviéramos que cargar en EfficientNetB0 con la parte superior incluida, también tendríamos algunas capas nuevas al final, que fueron entrenadas para clasificar los datos para ImageNet. Esta es la parte superior del modelo que estaremos entrenando para nuestra propia aplicación:

1
2
effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
effnet.summary()

Esto incluiría las capas superiores finales, con un clasificador ‘Dense’ al final:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Model: "efficientnetb0"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_1 (InputLayer)           [(None, 224, 224, 3  0           []                               
                                )]                                                                
                                                                                                  
 rescaling (Rescaling)          (None, 224, 224, 3)  0           ['input_1[0][0]']      
           
...
...

block7a_project_bn (BatchNorma  (None, 7, 7, 320)   1280        ['block7a_project_conv[0][0]']   
 lization)                                                                                        
                                                                                                  
 top_conv (Conv2D)              (None, 7, 7, 1280)   409600      ['block7a_project_bn[0][0]']     
                                                                                                  
 top_bn (BatchNormalization)    (None, 7, 7, 1280)   5120        ['top_conv[0][0]']               
                                                                                                  
 top_activation (Activation)    (None, 7, 7, 1280)   0           ['top_bn[0][0]']                 
                                                                                                  
 avg_pool (GlobalAveragePooling  (None, 1280)        0           ['top_activation[0][0]']         
 2D)                                                                                              
                                                                                                  
 top_dropout (Dropout)          (None, 1280)         0           ['avg_pool[0][0]']               
                                                                                                  
 predictions (Dense)            (None, 1000)         1281000     ['top_dropout[0][0]']            
                                                                                                  
==================================================================================================
Total params: 5,330,571
Trainable params: 5,288,548
Non-trainable params: 42,023
__________________________________________________________________________________________________

Sus nombres comienzan con top_ para anotar el hecho de que pertenecen al clasificador superior.

{.icon aria-hidden=“true”}

Nota: Esta estructura puede cambiar con el tiempo. En una versión anterior de Keras, top_conv, top_bn y top_activation no se cargaban si el argumento include_top se establecía en False, mientras que en la versión más reciente, lo hacen (y sus nombres todavía tienen el prefijo top_, lo que lo hace un poco más confuso. Siempre verifica cuál es el "top" en un modelo, antes de definir el tuyo propio, ya sea que esté inspirado en el implementación original o no.

No usaremos las capas superiores, ya que agregaremos nuestra propia capa superior al modelo EfficientNet y volveremos a entrenar solo las que agreguemos en la parte superior (antes de ajustar la base convolucional). ¡Sin embargo, vale la pena señalar lo que la arquitectura está usando originalmente para la parte superior! Parece que están usando un GlobalAveragePooling2D y Dropout antes de la capa final de clasificación Dense. Estas partes superiores generalmente están optimizadas para redes, por lo que es aconsejable reutilizar la estructura al menos para la línea de base.

Preprocesamiento de entrada para modelos preentrenados {#preprocesamiento de entrada para modelos preentrenados}

{.icon aria-hidden=“true”}

Nota: El preprocesamiento de datos juega un papel crucial en el entrenamiento de modelos, y la mayoría de los modelos tendrán diferentes canalizaciones de preprocesamiento. ¡No tienes que realizar conjeturas aquí! Cuando corresponda, un modelo viene con su propia función preprocess_input().

La función preprocess_input() aplica los mismos pasos de preprocesamiento a la entrada que se aplicaron durante el entrenamiento. Puede importar la función desde el módulo respectivo del modelo, si un modelo reside en su propio módulo. Por ejemplo, ResNets tiene su propia función preprocess_input:

1
from keras.applications.resnet50 import preprocess_input

Dicho esto, cargar un modelo, preprocesar la entrada y predecir un resultado en Keras es tan fácil como:

1
2
3
4
5
6
7
8
import tensorflow.keras.applications as models
from keras.applications.resnet50 import preprocess_input

resnet50 = models.ResNet50(weights='imagenet', include_top=True)

img = # get data
img = preprocess_input(img)
pred = resnet50.predict(img)

{.icon aria-hidden=“true”}

Nota: No todos los modelos tienen una función preprocess_input() dedicada, porque el preprocesamiento se realiza dentro del propio modelo. Por ejemplo, EfficientNet que usaremos no tiene su propia función de preprocesamiento dedicada, ya que las capas de preprocesamiento dentro del modelo se encargan de eso. Esto es cada vez más común.

¡Eso es todo! Ahora, dado que la matriz pred realmente no contiene datos legibles por humanos, también puede importar la función decode_predictions() junto con la función preprocess_input() desde un módulo. Alternativamente, puede importar la función genérica decode_predictions() que también se aplica a los modelos que no tienen sus módulos dedicados:

1
2
3
4
5
from keras.applications.model_name import preprocess_input, decode_predictions
# OR
from keras.applications.imagenet_utils import decode_predictions
# ...
print(decode_predictions(pred))

Uniendo esto, obtengamos una imagen de un oso negro a través de urllib, guarde ese archivo en un tamaño de destino adecuado para EfficientNet (la capa de entrada espera una forma de (batch_size, 224, 224, 3)) y clasificarlo con el modelo pre-entrenado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from tensorflow import keras
from keras.applications.family_name import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image

import urllib.request
import matplotlib.pyplot as plt
import numpy as np

# Public domain image
url = 'https://upload.wikimedia.org/wikipedia/commons/0/02/Black_bear_large.jpg'
urllib.request.urlretrieve(url, 'bear.jpg')

# Load image and resize (doesn't keep aspect ratio)
img = image.load_img('bear.jpg', target_size=(224, 224))
# Turn to array of shape (224, 224, 3)
img = image.img_to_array(img)
# Expand array into (1, 224, 224, 3)
img = np.expand_dims(img, 0)
# Preprocess for models that have specific preprocess_input() function
# img_preprocessed = preprocess_input(img)

# Load model and run prediction
effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
pred = effnet.predict(img)
print(decode_predictions(pred))

Sin embargo, obtuvimos la imagen de una URL: puede obtener la imagen de un dispositivo móvil, una llamada API REST o cualquier otra fuente y clasificarla. Realmente, usar un clasificador pre-entrenado es tan fácil como importarlo, alimentarlo con una imagen y decodificar los resultados. ¡Puede servir un modelo de visión por computadora a un usuario final con solo unas pocas líneas de código! Esto resulta en:

1
2
3
4
5
6
7
[[
('n02133161', 'American_black_bear', 0.6024658),
('n02132136', 'brown_bear', 0.1457715),
('n02134418', 'sloth_bear', 0.09819221),
('n02510455', 'giant_panda', 0.0069221947),
('n02509815', 'lesser_panda', 0.005077324)
]]

Es bastante seguro que la imagen es una imagen de un oso negro americano, ¡lo cual es correcto! Cuando se preprocesa con una función de preprocesamiento, la imagen puede cambiar significativamente. Por ejemplo, la función de preprocesamiento de ResNet cambiaría el color del pelaje del oso:

¡Se ve mucho más marrón ahora! Si introdujéramos esta imagen en EfficientNet, pensaría que es un oso pardo:

1
2
3
4
5
6
[[
('n02132136', 'brown_bear', 0.7152758), 
('n02133161', 'American_black_bear', 0.15667434), 
('n02134418', 'sloth_bear', 0.012813852), 
('n02134084', 'ice_bear', 0.0067828503), ('n02117135', 'hyena', 0.0050422684)
]]

Es importante no mezclar y combinar funciones de preprocesamiento entre modelos. Por ejemplo, ResNet aprende que lo que vemos como marrón se llama negro, ya que el color cambió a través del preprocesamiento, y solo vio lo que llamamos "marrón" con la etiqueta "negro". Ahora, no fue entrenado para clasificar colores, pero fue entrenado para clasificar entre un oso negro y un oso pardo, y los colores definitivamente están mezclados.

¿Es esto algo bueno o algo malo?

Depende de a quién le preguntes. John Locke, uno de los filósofos más influyentes de todos los tiempos, clasificó las propiedades de los objetos en cualidades primarias y secundarias e hizo una clara distinción entre ellas. Las cualidades primarias son aquellas que existen independientemente de un observador. Un libro es un libro y tiene un tamaño, independientemente de cómo lo vea. Esa es una cualidad primaria. Las cualidades secundarias son aquellas que dependen de un observador (color, sabor, olor), etc. y estas son bastante subjetivas. Desde temprana edad, muchas personas se han preguntado si "mi amarillo" es lo mismo que "tu amarillo". Es posible que veamos diferentes colores, pero se nos enseñó a llamarlo "amarillo". ¡Esto no cambia el hecho de que un libro amarillo es un libro!

Independientemente de si es verdadero o no, es concebible que todos veamos el mundo de una manera ligeramente diferente. No hay una razón clara por la que eso nos impida comunicarnos, construir y comprender el mundo, especialmente porque podemos asignar valores numéricos y ubicuos para explicar las fuentes de la experiencia subjetiva. Esto no es “amarillo”, es una onda electromagnética con una longitud de onda de alrededor de 600 nm. ¡Tus receptores rojos y verdes en el ojo reaccionan y “ves amarillo”! Hoy en día, podemos describir las cualidades secundarias, como el color, también como propiedades indiscutibles. Es cierto que es más fácil proporcionar una imagen sin procesar en un modelo, hacer que el modelo haga el preprocesamiento (como lo hace EfficientNet) en lugar de tener una función separada, ya que entonces no tiene que pensar en el preprocesamiento tanto. Sin embargo, no es objetivamente mejor o peor que ResNet “mezcle” los colores. De hecho, esta diversidad en el conocimiento puede conducir a algunas hermosas visualizaciones en el futuro. Veremos qué implica eso en otra lección cuando cubramos el algoritmo DeepDream.

¡Impresionante! El modelo funciona. Ahora, agreguemos una nueva parte superior y volvamos a entrenar la parte superior para realizar la clasificación de algo fuera del conjunto de ImageNet.

Agregar un nuevo techo a un modelo previamente entrenado {#agregar un nuevo techo a un modelo previamente entrenado}

Al realizar el aprendizaje de transferencia, normalmente cargará modelos sin tapas o los eliminará manualmente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Load without top
# When adding new layers, we also need to define the input_shape
effnet_base = keras.applications.EfficientNetB0(weights='imagenet', 
                                          include_top=False, 
                                          input_shape=((224, 224, 3)))

# Or load the full model
full_effnet = keras.applications.EfficientNetB0(weights='imagenet', 
                                            include_top=True, 
                                            input_shape=((224, 224, 3)))
                                            
# And then remove X layers from the top
trimmed_effnet = keras.Model(inputs=full_effnet.input, outputs=full_effnet.layers[-3].output)

Iremos con la primera opción ya que es más conveniente. Dependiendo de si desea ajustar los bloques convolucionales o no, los congelará o no. Digamos que queremos usar los mapas de características previamente entrenados subyacentes y congelar las capas para que solo volvamos a entrenar las nuevas capas de clasificación en la parte superior:

1
effnet_base.trainable = False

No necesita iterar a través del modelo y configurar cada capa para que sea entrenable o no, aunque puede hacerlo. Si desea desactivar las primeras n capas y permitir que se ajusten algunos mapas de características de nivel superior, pero dejar intactos los de nivel inferior, puede:

1
2
for layer in effnet_base.layers[:-2]:
    layer.trainable = False

Aquí, hemos configurado todas las capas en el modelo base para que no se puedan entrenar, excepto las dos últimas. Si revisamos el modelo, ahora solo hay ~2.5K parámetros entrenables:

1
effnet_base.summary()
1
2
3
4
5
6
# ...                
=========================================================================================
Total params: 4,049,571
Trainable params: 2,560
Non-trainable params: 4,047,011
_________________________________________________________________________________________

Ahora, definamos un modelo ‘secuencial’ que se pondrá encima de esta ’effnet_base’. Afortunadamente, encadenar modelos en Keras es tan fácil como hacer un nuevo modelo y ponerlo encima de otro. Puede aprovechar la API funcional y simplemente encadenar algunas capas nuevas sobre un modelo.

Agreguemos una capa GlobalAveragePooling2D, algo de Dropout y una capa de clasificación densa:

1
2
3
4
5
gap = keras.layers.GlobalAveragePooling2D()(effnet_base.output, training=False)
do = keras.layers.Dropout(0.2)(gap)
output = keras.layers.Dense(100, activation='softmax')(do)

new_model = keras.Model(inputs=effnet_base.input, outputs=output)

{.icon aria-hidden=“true”}

Nota: Al agregar las capas de EfficientNet, configuramos el entrenamiento en Falso. Esto pone a la red en modo de inferencia en lugar de modo de entrenamiento y es un parámetro diferente al entrenable que hemos establecido en Falso anteriormente. La capacidad de entrenamiento con pesas (entrenable) es diferente del modo (entrenamiento) para todas las capas, excepto BatchNormalization. Este es un paso crucial si desea descongelar las capas más adelante, ya que se transferirá el modo de inferencia para BatchNormalization. BatchNormalization calcula las estadísticas de movimiento. Cuando se descongele, comenzará a aplicar actualizaciones a los parámetros nuevamente y “deshacerá” el entrenamiento realizado antes del ajuste. Desde TF 2.0, configurar el entrenable del modelo como False también convierte el entrenamiento en False pero solo para las capas BatchNormalization.

Alternativamente, puede usar la API secuencial y llamar al método add() varias veces, o pasarlo en la lista de capas:

1
2
3
4
5
6
7
8
new_model = keras.Sequential([
    effnet_base,
    keras.layers.GlobalAveragePooling2D(),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(100, activation='softmax')
])

new_model.layers[0].trainable = False

Esto agrega todo el modelo como una capa en sí mismo, por lo que se trata como una sola entidad:

1
2
3
4
Layer: 0, Trainable: False # Entire EfficientNet model
Layer: 1, Trainable: True
Layer: 2, Trainable: True
...

Si un modelo es secuencial, simplemente puede agregarlo como:

1
2
3
4
new_model = keras.Sequential()
new_model.add(base_network.output) # Add unwrapped layers
new_model.add(...layer)
...

Sin embargo, esto falla para los modelos no secuenciales. Se recomienda usar la API funcional para aplicaciones como estas, ya que la API secuencial no ofrece la flexibilidad requerida y no todos los modelos son secuenciales (de hecho, desde TF 2.4.0, todos pre-entrenados). los modelos son funcionales). Además, no puede poner fácilmente la red base en modo de inferencia; no hay un argumento de “entrenamiento”. El hecho de que todo el modelo de EfficientNet sea una capa de caja negra no nos ayuda a trabajar fácilmente con él, por lo que la conveniencia menor de la API secuencial realmente no nos beneficia mucho y tiene varias desventajas.

Volviendo a nuestro modelo: hay 100 neuronas de salida para las clases CIFAR100, con una activación softmax. Echemos un vistazo a las capas entrenables en la red:

1
2
for index, layer in enumerate(new_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))

Esto resulta en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Layer: 0, Trainable: False
Layer: 1, Trainable: False
Layer: 2, Trainable: False
...
Layer: 235, Trainable: False
Layer: 236, Trainable: False
Layer: 237, Trainable: True
Layer: 238, Trainable: True
Layer: 239, Trainable: True
Layer: 240, Trainable: True
Layer: 241, Trainable: True

¡Impresionante! Carguemos el conjunto de datos, preprocesémoslo y volvamos a entrenar las capas de clasificación en él. Usaremos el mismo conjunto de datos CIFAR100 de la última lección, ya que resultó ser difícil entrenar a una CNN. La falta de datos y las limitaciones del aumento de datos dificultaron la creación de un clasificador poderoso. ¡Veamos si podemos emplear el aprendizaje por transferencia para ayudarnos!

Conjuntos de datos de TensorFlow {#conjuntos de datos de TensorFlow}

Estaremos trabajando con el Conjunto de datos CIFAR100, nuevamente. Sin embargo, esta vez, no lo cargaremos como una matriz NumPy desnuda de Keras. ¡Trabajaremos con conjuntos de datos de TensorFlow!

El módulo datasets de Keras contiene algunos conjuntos de datos, pero estos están destinados principalmente a la evaluación comparativa y el aprendizaje y no son demasiado útiles más allá de ese punto. ¡Podemos usar tensorflow_datasets para obtener acceso a un corpus de conjuntos de datos mucho más grande! Además, todos los conjuntos de datos del módulo están estandarizados, por lo que no tiene que molestarse con diferentes pasos de preprocesamiento para cada conjunto de datos en el que está probando sus modelos. Si bien puede sonar como una simple conveniencia, en lugar de un cambio de juego, si entrena muchos modelos, el tiempo que lleva hacer el trabajo por encima de la cabeza se vuelve más que molesto. La biblioteca brinda acceso a conjuntos de datos desde MNIST hasta Google Open Images (11 MB - 565 GB), que abarcan varias categorías:

-Audio

  • D4rl
  • Gráficos
  • Imagen
  • Clasificación de imágenes
  • Detección de objetos
  • Respuesta a preguntas
  • Clasificación
  • Rlds
  • Robomimético
  • Robótica
  • Texto
  • Series de tiempo
  • Simplificación de texto
  • Lenguaje de visión
  • Video
  • Traducir
  • etc...

¡Y la lista crece! A partir de 2022, hay 278 conjuntos de datos disponibles, cuyos nombres puede obtener a través de tfds.list_builders(). Además, TensorFlow Datasets es compatible con conjuntos de datos comunitarios, con más de 700 conjuntos de datos HuggingFace y el generador de conjuntos de datos Kubric. Si está construyendo un sistema inteligente general, es muy probable que haya un conjunto de datos público allí. Para todos los demás propósitos, puede descargar conjuntos de datos públicos y trabajar con ellos, con pasos de preprocesamiento personalizados. Kaggle, HuggingFace y los repositorios académicos son opciones populares.

Además, en un esfuerzo similar, TensorFlow lanzó una increíble herramienta GUI: Conozca sus datos, que aún se encuentra en versión beta (al momento de escribir este artículo) y tiene como objetivo responder preguntas importantes. sobre corrupción de datos (imágenes rotas, malas etiquetas, etc.), sensibilidad de datos (sus datos contienen contenido sensible), brechas de datos (falta obvia de muestras), balance de datos, etc.

Muchos de estos pueden ayudar a evitar sesgos y sesgos de datos, posiblemente una de las cosas más importantes que hacer cuando se trabaja en proyectos que pueden tener un impacto en otros humanos.

Otra característica sorprendente es que los conjuntos de datos provenientes de TensorFlow Datasets son objetos tf.data.Dataset, con los que puede maximizar el rendimiento de su red a través de búsqueda previa, optimización automatizada, transformaciones fáciles, etc.

{.icon aria-hidden=“true”}

Nota: Si no eres fanático de las clases propietarias, como Dataset, puedes volver a convertirlo en una matriz NumPy simple para el agnosticismo del marco. Sin embargo, se recomienda trabajar con tf.data.Datasets.

El módulo se puede instalar a través de:

1
$ pip install tensorflow_datasets

Una vez instalado, puede acceder a la lista de conjuntos de datos disponibles a través de:

1
2
print(tfds.list_builders())
print(f'Number of Datasets: {len(tfds.list_builders())}')
1
2
['abstract_reasoning', 'accentdb', 'aeslc', 'aflw2k3d', ...]
Number of Datasets: 278

Sin embargo, es más probable que utilice las páginas web relevantes en el sitio web de conjuntos de datos de TensorFlow, que ofrece más información, imágenes de muestra, etc. en lugar de esta lista. Para cargar un conjunto de datos, puede usar la función load():

1
2
3
4
5
dataset, info = tfds.load("cifar100", as_supervised=True, with_info=True)
class_names = info.features["label"].names
n_classes = info.features["label"].num_classes
print('Class names:', class_names)
print('Num of classes:', n_classes)

Los conjuntos de datos se pueden importar como no supervisados ​​o supervisados, y con o sin información adicional, como los nombres de las etiquetas y el número de clases. En el fragmento de código anterior, hemos cargado "cifar100" como un conjunto de datos supervisado (con etiquetas) e información:

1
2
Class names: ['apple', 'aquarium_fish', 'baby', ...]
Num of classes: 100

¡La lista info.features["label"].names puede ser útil! Es una lista de etiquetas legibles por humanos que corresponden a las etiquetas numéricas en el conjunto de datos. ¡No tenemos que buscar una lista manualmente en línea o escribirla de esta manera!

Divisiones de entrenamiento, prueba y validación con conjuntos de datos de TensorFlow

Uno de los argumentos opcionales que puedes pasar a la función load() es el argumento split. La nueva API de división le permite definir qué divisiones del conjunto de datos desea dividir. De forma predeterminada, para este conjunto de datos, solo admite una división de 'entrenamiento' y 'prueba': estas son las divisiones \"oficiales\" para *este conjunto de datos*. No hay división válida`.

{.icon aria-hidden=“true”}

Nota: Cada conjunto de datos tiene una división "oficial". Algunos solo tienen la división 'entrenamiento', algunos tienen una división 'entrenamiento' y 'prueba' y algunos incluso incluyen una división 'validación'. Esta es la división prevista y solo si un conjunto de datos admite una división, puede usar el alias de cadena de esa división. Si un conjunto de datos contiene solo una división de 'entrenamiento', puede dividir esos datos de entrenamiento en un conjunto de entrenamiento/prueba/válido sin problemas.

Estos corresponden a las enumeraciones tfds.Split.TRAIN y tfds.Split.TEST y tfds.Split.VALIDATION, que solían estar expuestas a través de la API en una versión anterior.

Realmente puede dividir un ‘Conjunto de datos’ en cualquier número arbitrario de conjuntos, aunque normalmente hacemos tres: ’tren_set’, ’test_set’, ‘valid_set’:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(test_set, valid_set, train_set), info = tfds.load("cifar100", 
                                           split=["test", "train[0%:20%]", "train[20%:]"],
                                           as_supervised=True, with_info=True)

class_names = info.features["label"].names
n_classes = info.features["label"].num_classes
print(f'Class names: {class_names[:10]}...', ) # ['apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle', 'bicycle', 'bottle']...
print('Num of classes:', n_classes) # Num of classes: 100

print("Train set size:", len(train_set)) # Train set size: 40000
print("Test set size:", len(test_set)) # Test set size: 10000
print("Valid set size:", len(valid_set)) # Valid set size: 10000

Hemos tomado la división 'test' y la hemos extraído en test_set. La porción entre el 0% y el 20% de la división 'tren' se asigna al conjunto_válido y todo lo que esté más allá del 25% es el conjunto_tren. Esto también se valida a través de los tamaños de los conjuntos.

En lugar de porcentajes, puede usar valores absolutos o una combinación de porcentaje y valores absolutos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Absolute value split
test_set, valid_set, train_set = tfds.load("cifar100", 
                                           split=["test", "train[0:10000]", "train[10000:]"],
                                           as_supervised=True)

print("Train set size:", len(train_set)) # Train set size: 40000
print("Test set size:", len(test_set)) # Test set size: 10000
print("Valid set size:", len(valid_set)) # Valid set size: 10000


# Mixed notation split
# 5000 - 50% (25000) left unassigned
test_set, valid_set, train_set = tfds.load("cifar100", 
                                           split=["test[:2500]", # First 2500 of 'test' are assigned to `test_set`
                                           "train[0:10000]",    # 0-10000 of 'train' are assigned to `valid_set`
                                           "train[50%:]"],        # 50% - 100% of 'train' (25000) assigned to `train_set`
                                           as_supervised=True)

Además, puede hacer una * unión * de conjuntos, que se usa con menos frecuencia, ya que los conjuntos se intercalan entonces:

1
2
3
4
5
6
train_and_test, half_of_train_and_test = tfds.load("cifar100", 
                                split=['train+test', 'train[:50%]+test'],
                                as_supervised=True)
                                
print("Train+test: ", len(train_and_test))               # Train+test:  60000
print("Train[:50%]+test: ", len(half_of_train_and_test)) # Train[:50%]+test:  35000

Estos dos conjuntos ahora están fuertemente intercalados.

Divisiones pares para N conjuntos

Nuevamente, puede crear cualquier número arbitrario de divisiones, simplemente agregando más divisiones a la lista de divisiones:

1
split=["train[:10%]", "train[10%:20%]", "train[20%:30%]", "train[30%:40%]", ...]

Sin embargo, si está creando muchas divisiones, especialmente si son pares, las cadenas que pasará son muy predecibles. Esto se puede automatizar creando una lista de cadenas, con un intervalo igual dado (como 10 %) en su lugar. Exactamente para este propósito, la función tfds.even_splits() genera una lista de cadenas, dada una cadena de prefijo y el número deseado de divisiones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import tensorflow_datasets as tfds

s1, s2, s3, s4, s5 = tfds.even_splits('train', n=5)
# Each of these elements is just a string
split_list = [s1, s2, s3, s4, s5]
print(f"Type: {type(s1)}, contents: '{s1}'")
# Type: <class 'str'>, contents: 'train[0%:20%]'

for split in split_list:
    test_set = tfds.load("cifar100", 
                                split=split,
                                as_supervised=True)
    print(f"Test set length for Split {split}: ", len(test_set))

Esto resulta en:

1
2
3
4
5
Test set length for Split train[0%:20%]: 10000
Test set length for Split train[20%:40%]: 10000
Test set length for Split train[40%:60%]: 10000
Test set length for Split train[60%:80%]: 10000
Test set length for Split train[80%:100%]: 10000

Alternativamente, puede pasar toda la split_list como el propio argumento split, para construir varios conjuntos de datos divididos fuera de un bucle:

1
2
3
ts1, ts2, ts3, ts4, ts5 = tfds.load("cifar100", 
                                split=split_list,
                                as_supervised=True)

Cargando CIFAR100 y aumento de datos

Con una comprensión funcional de tfds en su haber, carguemos el conjunto de datos CIFAR100 en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import tensorflow_datasets as tfds
import tensorflow as tf

(test_set, valid_set, train_set), info = tfds.load("cifar100", 
                                           split=["test", "train[0%:20%]", "train[20%:]"],
                                           as_supervised=True, with_info=True)

class_names = info.features["label"].names
n_classes = info.features["label"].num_classes
print(f'Class names: {class_names[:10]}...', ) # ['apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle', 'bicycle', 'bottle']...
print('Num of classes:', n_classes) # Num of classes: 100

print("Train set size:", len(train_set)) # Train set size: 40000
print("Test set size:", len(test_set)) # Test set size: 10000
print("Valid set size:", len(valid_set)) # Valid set size: 10000

Tomemos nota de un par de variables relevantes en un diccionario config:

1
2
3
4
config = {
    'TRAIN_SIZE' : len(train_set),
    'BATCH_SIZE' : 32
}

¡Ahora, las imágenes de CIFAR100 son significativamente diferentes de las imágenes de ImageNet! Es decir, las imágenes CIFAR100 son solo de 32x32, mientras que nuestro modelo EfficientNet espera imágenes de 224x224. Querremos cambiar el tamaño de las imágenes en cualquier caso. También es posible que deseemos aplicar algunas funciones de transformación en imágenes duplicadas para expandir artificialmente el tamaño de la muestra por clase, ya que el conjunto de datos no tiene suficientes. Con ImageDataGenerator, hemos visto que tiene un grado de libertad muy amplio cuando se trata de aumento, y el proceso está altamente automatizado. Cuando se trata de conjuntos de datos de TensorFlow, para poder usar cada pequeña optimización que brindan, normalmente usará operaciones tf.image para traducir, rotar, etc. imágenes en la función preprocess_image().

En lugar de una función preprocess_image() dedicada, simplemente puede encadenar varias llamadas map() con funciones lambda, pero este enfoque es significativamente menos legible y no se recomienda para una mayor cantidad de operaciones. Es mejor definir funciones y llamarlas en lugar de usar lambdas.

La desventaja es que tf.image es bastante rudimentario. A diferencia de las operaciones ricas (er) de Keras, sorprendentemente solo hay unas pocas que se pueden usar para el aumento aleatorio, y ofrecen un menor grado de libertad. Esto se debe en parte a que tf.image no está diseñado para usarse tanto para aumento como para operaciones generales de imágenes. Hablaremos más sobre los aumentos de Keras y las bibliotecas externas más adelante.

{.icon aria-hidden=“true”}

Nota: Una excelente alternativa para hacer que su modelo sea más independiente del preprocesamiento es incrustar capas de preprocesamiento en el modelo, como keras.layers.RandomFlip() y keras.layers.RandomRotation(0.2).

Definamos una función de preprocesamiento para cada imagen y su etiqueta asociada:

1
2
3
4
5
6
7
def preprocess_image(image, label):
    resized_image = tf.image.resize(image, [224, 224])
    img = tf.image.random_flip_left_right(resized_image)
    img = tf.image.random_brightness(img, 0.4)
    # Preprocess image with model-specific function if it has one
    # processed_image = preprocess_input(resized_image)
    return img, label

Además, dado que no queremos realizar transformaciones aleatorias en los conjuntos de validación y prueba, definamos una función separada para ellos:

1
2
3
4
5
def preprocess_test_valid(image, label):
    resized_image = tf.image.resize(image, [224, 224])
    # Preprocess image with model-specific function if it has one
    # processed_image = preprocess_input(resized_image)
    return resized_image, label

Y finalmente, ¡vamos a querer aplicar esta función a cada imagen en los conjuntos! Esto se hace fácilmente mediante la función map(). Dado que la entrada en la red también espera lotes ((batch_size, 224, 224, 3) en lugar de (224, 224, 3)) - también ``batch()` los conjuntos de datos después del mapeo:

1
2
3
train_set = train_set.map(preprocess_image).batch(32).repeat().prefetch(tf.data.AUTOTUNE)
test_set = test_set.map(preprocess_test_valid).batch(32).prefetch(tf.data.AUTOTUNE)
valid_set = valid_set.map(preprocess_test_valid).batch(32).prefetch(tf.data.AUTOTUNE)

En este ejemplo, estamos usando tf.data, el módulo integrado para crear canalizaciones de datos y optimizar su uso. No debe confundirse con tfds, que es solo una biblioteca para obtener conjuntos de datos, mientras que tf.data hace el trabajo pesado en el hardware. La función prefetch() es opcional pero ayuda con la eficiencia y la llamada tf.data.AUTOTUNE permite que TensorFlow optimice cómo realizar la captación previa. Como el modelo se está entrenando en un solo lote, la función prefetch() obtiene previamente el siguiente lote para que no se espere cuando finaliza el paso de entrenamiento. De manera similar, podría usar funciones como cache() e interleave() para optimizar aún más la E/S y la extracción de datos, aunque no se deben usar a ciegas. Si se usan en un lugar o momento incorrecto, es probable que hagan que sus tuberías sean más lentas. Dedicaremos una lección a optimizar las canalizaciones de datos más adelante. Por ahora, solo prefetch().

Tenemos una llamada repeat() en train_set, que no está presente en otros conjuntos. Esto es análogo a la clase ImageDataGenerator, que produce un número infinito de muestras de entrenamiento, con transformaciones aleatorias. En cada solicitud, la función preprocess_image() que escribimos transformará aleatoriamente las imágenes entrantes, por lo que tenemos un flujo constante y fresco de datos ligeramente alterados. No queremos hacer esto para los conjuntos de prueba y validación, aparte de hacer que las imágenes tengan el mismo tamaño y aplicar el paso de preprocesamiento común, si lo hay (EfficientNetB0 no tiene una función de preprocesamiento externo).

{.icon aria-hidden=“true”}

Nota: El aumento del tiempo de prueba también es una cosa.

Echemos un vistazo rápido a algunas de las imágenes de cualquiera de los conjuntos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fig = plt.figure(figsize=(10, 10))

i = 1
for entry in test_set.take(25):

    sample_image = np.squeeze(entry[0].numpy()[0])
    sample_label = class_names[entry[1].numpy()[0]]
    ax = fig.add_subplot(5, 5, i)
    
    ax.imshow(np.array(sample_image, np.int32))
    ax.set_title(f"Class: {sample_label}")
    ax.axis('off')
    i = i+1

plt.tight_layout()
plt.show()

Entrenamiento de un modelo con aprendizaje por transferencia {#entrenamiento de un modelo con aprendizaje por transferencia}

Con los datos cargados, preprocesados ​​y divididos en conjuntos adecuados, finalmente podemos entrenar el modelo en ellos.

Dado que estamos haciendo una clasificación dispersa, una pérdida sparse_categorical_crossentropy debería funcionar bien, y el optimizador Adam es un optimizador predeterminado razonable. Compilemos el modelo y entrenémoslo en algunas épocas. ¡Vale la pena recordar que la mayoría de las capas de la red están congeladas! Solo estamos entrenando el nuevo clasificador sobre los mapas de características extraídos.

Solo una vez que entrenamos las capas superiores, podemos decidir descongelar las capas de extracción de características y dejar que se ajusten un poco más. Este paso es opcional pero le permite realmente exprimir lo mejor de un modelo, pero naturalmente requiere más recursos para hacerlo. Una buena regla general es tratar de comparar los conjuntos de datos y estimar qué niveles de la jerarquía puede reutilizar sin volver a entrenar, para evitar volver a entrenar algunos de los niveles que podrían ser redundantes para volver a entrenar si su máquina puede hacerlo. 't computacionalmente manejarlo.

En realidad, es sorprendente lo bien que se transfieren los pesos de ImageNet a la mayoría de los conjuntos de datos, incluso si no parecen tener ninguna conexión remota con el dominio. Veremos esto especialmente en el próximo Proyecto Guiado sobre la clasificación del cáncer de mama a partir de imágenes histológicas. Otra buena regla general es usar siempre el aprendizaje por transferencia cuando puedas.

Vamos a compilar la red y comprobar su estructura:

1
2
3
4
5
6
7
checkpoint = keras.callbacks.ModelCheckpoint(filepath='effnet_transfer_learning.h5', save_best_only=True)

new_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=keras.optimizers.Adam(), 
                  metrics=["accuracy", keras.metrics.SparseTopKCategoricalAccuracy(k=3)])

new_model.summary()

Este es un buen momento para validar si has congelado correctamente las capas:

1
2
3
4
5
6
...
==================================================================================================
Total params: 4,177,671
Trainable params: 128,100
Non-trainable params: 4,049,571
__________________________________________________________________________________________________

¡Solo 128k parámetros entrenables! Naturalmente, tomará más tiempo entrenar esta red que una red de 128k, ya que hay mucho más en marcha: toda la red está ahí, solo que no toda es entrenable. Sin embargo, tomará menos tiempo que entrenar toda la red. Entrenemos la nueva red (en realidad, solo la parte superior) durante 10 épocas:

1
2
3
4
5
history = new_model.fit(train_set, 
                        epochs=10,
                        steps_per_epoch = config['TRAIN_SIZE']/config['BATCH_SIZE'],
                        callbacks=[checkpoint],
                        validation_data=valid_set)

Dado que train_set es infinito, querremos definir steps_per_epoch. Esto puede llevar algún tiempo y lo ideal es hacerlo en una GPU. Dependiendo de qué tan grande sea el modelo y el conjunto de datos que se introduzca en él. Si no tiene acceso a una GPU, se recomienda ejecutar este código en cualquiera de los proveedores de la nube que le dan acceso a una GPU gratuita, como Google Colab, Kaggle Notebooks, etc. Cada época puede tomar desde 60 segundos en GPU más potentes hasta 10 minutos en las más débiles.

¡Este es el punto en el que te sientas y tomas un café (o té)! Después de 10 épocas, el tren y la precisión de la validación se ven bien:

1
2
3
4
5
Epoch 1/10
1250/1250[==============================] - 97s 76ms/step - loss: 1.9179 - accuracy: 0.5196 - sparse_top_k_categorical_accuracy: 0.7216 - val_loss: 1.3436 - val_accuracy: 0.6324 - val_sparse_top_k_categorical_accuracy: 0.8225
...
Epoch 10/10
1250/1250[==============================] - 86s 74ms/step - loss: 0.8610 - accuracy: 0.7481 - sparse_top_k_categorical_accuracy: 0.9015 - val_loss: 1.0820 - val_accuracy: 0.6935 - val_sparse_top_k_categorical_accuracy: 0.8651

Tiene una precisión de validación del 69% y una precisión de validación Top-3 del 86%. Sin embargo, estos están lejos del potencial de la red: la parte superior de la clasificación probablemente haya hecho todo lo posible con los extractores de funciones tal como están ahora. ¡Echemos un vistazo a las curvas de aprendizaje!

Evaluación antes del ajuste fino

Primero probemos este modelo, antes de intentar descongelar todas las capas. Realizaremos una evaluación básica: métricas, curvas de aprendizaje y una matriz de confusión. Comencemos con las métricas:

1
2
new_model.evaluate(test_set)
# 157/157 [==============================] - 10s 65ms/step - loss: 1.0806 - accuracy: 0.6884 - sparse_top_k_categorical_accuracy: 0.8718

~69% en el conjunto de prueba, y cerca de la precisión en el conjunto de validación. Tiene una precisión bastante decente del 87% Top-3. Parece que nuestro modelo se está generalizando bien, pero todavía hay espacio para mejorar. Echemos un vistazo a las curvas de aprendizaje:

Las curvas de entrenamiento son de esperar: son bastante cortas, ya que solo entrenamos durante 10 épocas, pero se estancaron rápidamente, por lo que probablemente no habríamos obtenido un rendimiento mucho mejor con más épocas.

Predigamos el conjunto de prueba y extraigamos las etiquetas de él para generar un informe de clasificación y una matriz de confusión:

1
2
y_pred = new_model.predict(test_set)
labels = tf.concat([y for x, y in test_set], axis=0)

Dado que tenemos 100 clases, tanto el informe de clasificación como la matriz de confusión serán muy grandes y difícilmente legibles:

1
2
from sklearn import metrics
print(metrics.classification_report(labels, np.argmax(y_pred, axis=1)))
1
2
3
4
5
6
7
      precision    recall  f1-score   support

   0       0.89      0.89      0.89        55
   1       0.76      0.78      0.77        49
   2       0.45      0.64      0.53        45
   3       0.45      0.58      0.50        52
...

Solo tenemos alrededor de 50 imágenes por clase en el conjunto de prueba, pero realmente no podemos obtener más que eso. Está claro que algunas clases se aprenden mejor que otras clases, como 0 que tiene un recuerdo y una precisión significativamente más altos que, por ejemplo, la clase 3.

¡Esto es realmente sorprendente, ya que la clase ‘0’ es ‘manzana’ y ‘3’ es ‘oso’! ImageNet tiene imágenes de osos, e incluso clasifica diferentes tipos de osos, por lo que es de esperar que la red generalice bien a los osos, transfiriendo el conocimiento de ImageNet. En todo caso, esto habla de la "receta" que tiene efectivamente esta red, dado lo pequeñas que son las imágenes.

Tracemos la matriz de confusión:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from sklearn.metrics import confusion_matrix
import seaborn as sns

matrix = confusion_matrix(labels, y_pred.argmax(axis=1))

# Plot on heatmap
fig, ax = plt.subplots(figsize=(15, 15))
sns.heatmap(matrix, ax=ax, fmt='g')

# Stylize heatmap
ax.set_xlabel('Predicted labels')
ax.set_ylabel('True labels')
ax.set_title('Confusion Matrix')

# Set ticks
ax.xaxis.set_ticks(np.arange(0, 100, 1))
ax.yaxis.set_ticks(np.arange(0, 100, 1))
ax.xaxis.set_ticklabels(class_names, rotation=90, fontsize=8)
ax.yaxis.set_ticklabels(class_names, rotation=0, fontsize=8)

Esto resulta en:

Nuevamente, la matriz de confusión es bastante grande, ya que tenemos 100 clases. Aunque, en su mayor parte, parece que en realidad se está generalizando bien a las clases, aunque no es lo ideal.

¿Podemos afinar aún más esta red? Hemos reemplazado y vuelto a entrenar las capas superiores relacionadas con la clasificación de mapas de características, ¡pero los mapas de características en sí mismos podrían no ser ideales! Si bien son bastante buenas, estas imágenes son simplemente diferentes de ImageNet, por lo que también vale la pena tomarse el tiempo para actualizar las capas de extracción de características. Intentemos descongelar las capas convolucionales y ajustarlas también.

Desbloqueo de capas: ajuste fino de una red entrenada con transferencia de aprendizaje {#descongelación de capas, ajuste fino de una red entrenada con transferencia de aprendizaje}

Una vez que haya terminado de volver a entrenar las capas superiores, puede cerrar el trato y estar satisfecho con su modelo. Por ejemplo, supongamos que tiene una precisión del 95%; en serio, no necesita ir más allá. Sin embargo, ¿por qué no?

Si puede exprimir un 1% adicional en precisión, puede que no parezca mucho, pero considere el otro extremo del intercambio. Si su modelo tiene una precisión del 95 % en 100 muestras, clasificó incorrectamente 5 muestras. Si aumenta eso al 96% de precisión, clasificó incorrectamente 4 muestras.

El 1% de precisión se traduce en una disminución del 25% en las clasificaciones falsas.

Cualquier cosa que pueda exprimir más de su modelo en realidad puede marcar una diferencia significativa en la cantidad de clasificaciones incorrectas. Nuevamente, las imágenes en CIFAR100 son mucho más pequeñas que las imágenes de ImageNet, y es casi como si alguien con una gran vista de repente obtuviera una receta enorme y solo viera el mundo con ojos borrosos. ¡Los mapas de características tienen que ser al menos algo diferentes!

Guardemos el modelo en un archivo para no perder el progreso, y descongelemos/ajustemos una copia cargada, para que no arruinemos accidentalmente los pesos en el original:

1
2
new_model.save('effnet_transfer_learning.h5')
loaded_model = keras.models.load_model('effnet_transfer_learning.h5')

Ahora, podemos manipular y cambiar el modelo_cargado sin afectar al nuevo_modelo. Para empezar, querremos cambiar el modelo_cargado del modo de inferencia al modo de entrenamiento, es decir, descongelar las capas para que se puedan entrenar de nuevo.

{.icon aria-hidden=“true”}

Nota: Nuevamente, si una red usa BatchNormalization (y la mayoría lo hace), querrá mantenerlos congelados mientras ajusta la red. Dado que ya no estamos congelando toda la red base, simplemente congelaremos las capas BatchNormalization y permitiremos que se modifiquen otras capas.

Desactivemos las capas BatchNormalization para que nuestro entrenamiento no se vaya por el desagüe:

1
2
3
4
5
6
7
8
for layer in loaded_model.layers:
    if isinstance(layer, keras.layers.BatchNormalization):
        layer.trainable = False
    else:
        layer.trainable = True

for index, layer in enumerate(loaded_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))

Vamos a comprobar si eso funcionó:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Layer: 0, Trainable: True
Layer: 1, Trainable: True
Layer: 2, Trainable: True
Layer: 3, Trainable: True
Layer: 4, Trainable: True
Layer: 5, Trainable: False
Layer: 6, Trainable: True
Layer: 7, Trainable: True
Layer: 8, Trainable: False
...

¡Impresionante! Antes de que podamos hacer algo con el modelo, para "solidificar" la entrenabilidad, tenemos que volver a compilarlo. Esta vez, usaremos una tasa de aprendizaje más pequeña, ya que no queremos entrenar la red, sino ajustar lo que ya está ahí:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
checkpoint = keras.callbacks.ModelCheckpoint(filepath='effnet_transfer_learning_finetuned.h5', save_best_only=True)

# Recompile after turning to trainable
loaded_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=keras.optimizers.Adam(learning_rate=3e-6, decay=(1e-6)), 
                  metrics=["accuracy", keras.metrics.SparseTopKCategoricalAccuracy(k=3)])

history = loaded_model.fit(train_set, 
                        epochs=15,
                        steps_per_epoch = config['TRAIN_SIZE']/config['BATCH_SIZE'],
                        callbacks=[checkpoint],
                        validation_data=valid_set)

Nuevamente, esto puede llevar algún tiempo, así que beba otra bebida de su elección (manténgase hidratado) mientras se ejecuta en segundo plano. El tiempo de ajuste depende en gran medida de la arquitectura que elija, pero la mayoría de las arquitecturas de vanguardia llevará algún tiempo en una configuración de nivel doméstico.

Una vez que termine, debería alcanzar hasta alrededor del 80 % en precisión y alrededor del 93 % en la precisión Top-3 en el conjunto de validación:

1
2
3
4
5
Epoch 1/15
1250/1250[==============================] - 384s 322ms/step - loss: 0.6567 - accuracy: 0.8024 - sparse_top_k_categorical_accuracy: 0.9356 - val_loss: 0.8687 - val_accuracy: 0.7520 - val_sparse_top_k_categorical_accuracy: 0.9069
...
Epoch 15/15
1250/1250[==============================] - 377s 322ms/step - loss: 0.3858 - accuracy: 0.8790 - sparse_top_k_categorical_accuracy: 0.9715 - val_loss: 0.7071 - val_accuracy: 0.7971 - val_sparse_top_k_categorical_accuracy: 0.9331

Además, si echa un vistazo a las curvas de aprendizaje, parece que no se han estancado, y probablemente podríamos haber aumentado aún más el rendimiento del modelo si solo lo entrenáramos durante más tiempo:

{.icon aria-hidden=“true”}

Nota: Probablemente podríamos haber visto más aumentos en el rendimiento a través de más entrenamiento. Tenga en cuenta que entrenar por más tiempo, naturalmente, lleva tiempo. Si bien es comparativamente bajo para muchas otras arquitecturas y conjuntos de datos, 100 épocas en este conjunto de datos tardaron más de 10 horas en entrenarse en una GPU doméstica. Es comprensible si está ansioso por esperar tanto tiempo, pero desafortunadamente, 10 h no es demasiado tiempo para esperar a que una red entrene.

Vamos a evaluarlo y visualizar algunas de las predicciones:

1
2
loaded_model.evaluate(test_set)
# 157/157 [==============================] - 10s 61ms/step - loss: 0.7041 - accuracy: 0.7920 - sparse_top_k_categorical_accuracy: 0.9336
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fig = plt.figure(figsize=(10, 10))

i = 1
for entry in test_set.take(25):
    # Predict, get the raw Numpy prediction probabilities
    # Reshape entry to the model's expected input shape
    pred = np.argmax(loaded_model.predict(entry[0].numpy()[0].reshape(1, 224, 224, 3)))

    # Get sample image as numpy array
    sample_image = entry[0].numpy()[0]
    # Get associated label
    sample_label = class_names[entry[1].numpy()[0]]
    # Get human label based on the prediction
    prediction_label = class_names[pred]
    ax = fig.add_subplot(5, 5, i)
    # Plot image and sample_label alongside prediction_label
    ax.imshow(np.array(sample_image, np.int32))
    ax.set_title(f"Actual: {sample_label}\nPred: {prediction_label}")
    ax.axis('off')
    i = i+1

plt.tight_layout()
plt.show()

Un par de errores de clasificación, como cabría esperar de un modelo con una precisión del 80 %. Un mapache fue clasificado como una musaraña, que es un animal parecido a un topo (no demasiado lejos de la verdad). Un chimpancé fue clasificado como una lámpara (yo lo habría clasificado como una botella de cerveza). Un autobús fue clasificado como una camioneta. Este es curioso, ya que la franja azul en el autobús lo hace parecer un poco como una camioneta. Parece que el modelo entendió la franja azul como el final de la cama de una camioneta, en lugar de reconocer la parte superior gris como parte del autobús. Finalmente, una araña fue clasificada como trucha, que es una clase muy diferente, pero la imagen es tan borrosa y pequeña que es totalmente comprensible.

Nuestro modelo anterior, uno personalizado, creado y entrenado en este conjunto de datos tenía una precisión del 66 % entre los 1 primeros, lo que significa que disminuimos la tasa de error en un 39 % (de 33 por 100 imágenes a 20 por 100 imágenes).

Si desea obtener las predicciones Top-K (no solo la más probable), en lugar de usar argmax(), puede utilizar el método top_k() de TensorFlow:

1
2
3
4
5
pred = loaded_model.predict(np.expand_dims(img, 0))
top_probas, top_indices = tf.nn.top_k(pred, k=k)

print(top_probas)  # tf.Tensor([[0.900319   0.07157221 0.00889194]], shape=(1, 3), dtype=float32)
print(top_indices) # tf.Tensor([[66 88 21]], shape=(1, 3), dtype=int32)

Si desea mostrar esta información junto con la entrada y las predicciones, puede trazar la imagen de entrada, junto a un gráfico de barras de la confianza de la red:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
for entry in test_set.take(1):
    img = entry[0][0].numpy().astype('int')
    label = entry[1][1]
    
    # Predict and get top-k classes
    pred = loaded_model.predict(np.expand_dims(img, 0))
    top_probas, top_indices = tf.nn.top_k(pred, k=3)
    # Convert to NumPy, squeeze and convert to list for ease of plotting
    top_probas = top_probas.numpy().squeeze().tolist()
    # Turn indices into classes
    pred_classes = [] 
    for index in top_indices.numpy().squeeze():
        pred_classes.append(class_names[index])
        
    fig, ax = plt.subplots(1, 2, figsize=(16, 4))
    ax[0].imshow(img)
    ax[0].axis('off')
    ax[1].bar(pred_classes, top_probas)
    
plt.tight_layout()

Aquí, la red está bastante segura de que la imagen es una imagen de un mapache. Hay un poco de tigre y chimpancé allí, pero las probabilidades son realmente bajas:

¿Qué hay de la trucha araña de antes?

La red está bastante perdida aquí: todas las probabilidades son bajas y ninguna de ellas es correcta. Si toma la probabilidad máxima al pie de la letra y devuelve esa clase, parece que el modelo estaba muy equivocado, pero cuando inspecciona su confianza, su “línea de razonamiento” puede volverse mucho más clara. En general, y especialmente al devolver resultados a un usuario final, querrá mostrar la confianza del modelo y, potencialmente, otras clases Top-K y sus probabilidades, si la probabilidad más alta no es demasiado alta.

Por ejemplo, si la probabilidad máxima está por debajo del 50 %, por ejemplo, podría devolver varias clases y sus probabilidades, como en la segunda imagen de entrada. Si el modelo es bastante seguro, podría devolver solo la clase superior y su probabilidad.

Finalmente, echemos un vistazo a la matriz de confusión en comparación con la anterior:

Si bien todavía no es perfecto, ¡se ve mucho más limpio!

Conclusión

El aprendizaje por transferencia es el proceso de transferir representaciones de conocimiento ya aprendidas de un modelo a otro, cuando corresponda. Esto concluye la lección sobre el aprendizaje de transferencia para la clasificación de imágenes con Keras y Tensorflow. Comenzamos analizando qué es el aprendizaje por transferencia y cómo se pueden compartir las representaciones del conocimiento entre modelos y arquitecturas.

Luego, echamos un vistazo a algunos de los modelos más populares y de vanguardia para la clasificación de imágenes lanzados públicamente, y nos apoyamos en uno de ellos, EfficientNet, para ayudarnos a clasificar algunos de nuestros propios datos. Hemos echado un vistazo a cómo cargar y examinar modelos pre-entrenados, cómo trabajar con sus capas, predecir con ellas y decodificar los resultados, así como también cómo definir sus propias capas y entrelazarlas con la arquitectura existente.

Esta lección presentó los conjuntos de datos de TensorFlow, los beneficios de usar el módulo y los conceptos básicos para trabajar con él. Finalmente, cargamos y preprocesamos un conjunto de datos, y entrenamos nuestras nuevas capas superiores de clasificación en él, antes de descongelar las capas y ajustarlas aún más a través de varias épocas adicionales. ionales.

Licensed under CC BY-NC-SA 4.0