Keras y las convnets modernas en TPU

1. Descripción general

En este lab, aprenderás a compilar, entrenar y ajustar tus propias redes neuronales convolucionales desde cero con Keras y TensorFlow 2. Ahora, esto se puede hacer en minutos con la potencia de las TPU. También explorarás varios enfoques, desde el aprendizaje por transferencia muy simple hasta las arquitecturas convolucionales modernas, como Squeezenet. Este lab incluye explicaciones teóricas sobre las redes neuronales y es un buen punto de partida para los desarrolladores que aprenden sobre el aprendizaje profundo.

Leer artículos sobre aprendizaje profundo puede ser difícil y confuso. Veamos de cerca las arquitecturas de redes neuronales convolucionales modernas.

ca8cc21f6838eccc.png

Qué aprenderás

  • Usar Keras y las unidades de procesamiento tensorial (TPU) para compilar tus modelos personalizados más rápido
  • Usar la API de tf.data.Dataset y el formato TFRecord para cargar datos de entrenamiento de forma eficiente
  • Para hacer trampa 😈, usa el aprendizaje por transferencia en lugar de crear tus propios modelos.
  • Usar los estilos de modelos secuenciales y funcionales de Keras
  • Para crear tu propio clasificador de Keras con una capa softmax y una pérdida de entropía cruzada
  • Para ajustar tu modelo con una buena elección de capas convolucionales
  • Explorar ideas de arquitecturas de redes neuronales convolucionales modernas, como módulos, reducción de promedio global, etcétera
  • Compilar una convnet moderna simple con la arquitectura de Squeezenet

Comentarios

Si ves algo incorrecto en este codelab, infórmanos. Los comentarios se pueden proporcionar a través de los problemas de GitHub [vínculo de comentarios].

2. Guía de inicio rápido de Google Colaboratory

En este lab, se usa Google Colaboratory y no se requiere ninguna configuración de tu parte. Puedes ejecutarlo desde una Chromebook. Abre el siguiente archivo y ejecuta las celdas para familiarizarte con los notebooks de Colab.

c3df49e90e5a654f.png Welcome to Colab.ipynb

Selecciona un backend de TPU

8832c6208c99687d.png

En el menú de Colab, selecciona Entorno de ejecución > Cambiar tipo de entorno de ejecución y, luego, selecciona TPU. En este lab de código, usarás una potente TPU (unidad de procesamiento tensorial) compatible con el entrenamiento acelerado por hardware. La conexión con el tiempo de ejecución se realizará automáticamente en la primera ejecución, o bien puedes usar el botón "Conectar" en la esquina superior derecha.

Ejecución de notebooks

76d05caa8b4db6da.png

Ejecuta las celdas una por una haciendo clic en una celda y usando Mayúsculas + INTRO. También puedes ejecutar todo el notebook con Runtime > Run all.

Índice

429f106990037ec4.png

Todos los notebooks tienen un índice. Puedes abrirlo con la flecha negra que se encuentra a la izquierda.

Celdas ocultas

edc3dba45d26f12a.png

Algunas celdas solo mostrarán su título. Esta es una función del notebook específica de Colab. Puedes hacer doble clic en ellos para ver el código que contienen, pero, por lo general, no es muy interesante. Por lo general, son funciones de asistencia o visualización. Aún debes ejecutar estas celdas para que se definan las funciones que contienen.

Autenticación

cdd4b41413100543.png

Colab puede acceder a tus buckets privados de Google Cloud Storage si te autenticas con una cuenta autorizada. El fragmento de código anterior activará un proceso de autenticación.

3. [INFO] ¿Qué son las unidades de procesamiento tensorial (TPUs)?

En pocas palabras

f88cf6facfc70166.png

El código para entrenar un modelo en TPU con Keras (y recurrir a la GPU o la CPU si no hay una TPU disponible) es el siguiente:

try: # detect TPUs
    tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect()
    strategy = tf.distribute.TPUStrategy(tpu)
except ValueError: # detect GPUs
    strategy = tf.distribute.MirroredStrategy() # for CPU/GPU or multi-GPU machines

# use TPUStrategy scope to define model
with strategy.scope():
  model = tf.keras.Sequential( ... )
  model.compile( ... )

# train model normally on a tf.data.Dataset
model.fit(training_dataset, epochs=EPOCHS, steps_per_epoch=...)

Hoy usaremos las TPU para compilar y optimizar un clasificador de flores a velocidades interactivas (minutos por ejecución de entrenamiento).

688858c21e3beff2.png

¿Por qué usar TPUs?

Las GPUs modernas se organizan en torno a "núcleos" programables, una arquitectura muy flexible que les permite manejar una variedad de tareas, como la renderización 3D, el aprendizaje profundo, las simulaciones físicas, etcétera. Por otro lado, las TPU combinan un procesador de vectores clásico con una unidad de multiplicación de matrices dedicada y se destacan en cualquier tarea en la que predominan las multiplicaciones de matrices grandes, como las redes neuronales.

8eb3e718b8e2ed08.png

Ilustración: Una capa de red neuronal densa como una multiplicación de matrices, con un lote de ocho imágenes procesadas a través de la red neuronal a la vez. Realiza una multiplicación de una línea por una columna para verificar que, de hecho, se realiza una suma ponderada de todos los valores de píxeles de una imagen. Las capas convolucionales también se pueden representar como multiplicaciones de matrices, aunque es un poco más complicado ( explicación aquí, en la sección 1).

El hardware

MXU y VPU

Un núcleo de TPU v2 está compuesto por una unidad de multiplicación de matrices (MXU) que ejecuta multiplicaciones de matrices y una unidad de procesamiento vectorial (VPU) para todas las demás tareas, como activaciones, softmax, etcétera. La VPU controla los cálculos de float32 y int32. Por otro lado, la MXU opera en un formato de punto flotante de precisión mixta de 16 a 32 bits.

7d68944718f76b18.png

Punto flotante de precisión mixta y bfloat16

La MXU calcula multiplicaciones de matrices con entradas bfloat16 y salidas float32. Las acumulaciones intermedias se realizan con una precisión de float32.

19c5fc432840c714.png

Por lo general, el entrenamiento de redes neuronales es resistente al ruido que introduce una precisión de punto flotante reducida. Hay casos en los que el ruido incluso ayuda al optimizador a converger. La precisión de punto flotante de 16 bits se usó tradicionalmente para acelerar los cálculos, pero los formatos float16 y float32 tienen rangos muy diferentes. Por lo general, reducir la precisión de float32 a float16 genera desbordamientos superiores e inferiores. Existen soluciones, pero, por lo general, se requiere trabajo adicional para que float16 funcione.

Por eso, Google introdujo el formato bfloat16 en las TPU. bfloat16 es un float32 truncado con exactamente los mismos bits y rango de exponente que float32. Esto, sumado al hecho de que las TPU calculan las multiplicaciones de matrices en precisión mixta con entradas de bfloat16, pero salidas de float32, significa que, por lo general, no es necesario realizar cambios en el código para aprovechar las mejoras de rendimiento de la precisión reducida.

Matriz sistólica

La MXU implementa multiplicaciones de matrices en hardware con una arquitectura denominada "array sistólico", en la que los elementos de datos fluyen a través de un array de unidades de procesamiento de hardware. (En medicina, “sistólica” se refiere a las contracciones cardíacas y al flujo sanguíneo, aquí al flujo de datos).

El elemento básico de una multiplicación de matrices es un producto escalar entre una fila de una matriz y una columna de la otra matriz (consulta la ilustración en la parte superior de esta sección). Para una multiplicación de matrices Y=X*W, un elemento del resultado sería el siguiente:

Y[2,0] = X[2,0]*W[0,0] + X[2,1]*W[1,0] + X[2,2]*W[2,0] + ... + X[2,n]*W[n,0]

En una GPU, se programaría este producto escalar en un "núcleo" de la GPU y, luego, se ejecutaría en tantos "núcleos" como estén disponibles en paralelo para intentar calcular cada valor de la matriz resultante de una sola vez. Si la matriz resultante es de 128 x 128, se requerirían 128 x 128=16 000 "núcleos" disponibles, lo que no suele ser posible. Las GPU más grandes tienen alrededor de 4,000 núcleos. Por otro lado, una TPU usa el mínimo hardware necesario para las unidades de procesamiento en la MXU: solo bfloat16 x bfloat16 => float32 acumuladores de multiplicación, nada más. Son tan pequeñas que una TPU puede implementar 16,000 de ellas en una MXU de 128 x 128 y procesar esta multiplicación de matrices de una sola vez.

f1b283fc45966717.gif

Ilustración: El array sistólico de la MXU. Los elementos de procesamiento son acumuladores multiplicadores. Los valores de una matriz se cargan en el array (puntos rojos). Los valores de la otra matriz fluyen a través del array (puntos grises). Las líneas verticales propagan los valores hacia arriba. Las líneas horizontales propagan las sumas parciales. El usuario debe verificar que, a medida que los datos fluyen a través del array, se obtiene el resultado de la multiplicación de matrices en el lado derecho.

Además, mientras se calculan los productos punto en una MXU, las sumas intermedias simplemente fluyen entre las unidades de procesamiento adyacentes. No es necesario almacenarlos ni recuperarlos de la memoria o incluso de un archivo de registro. El resultado final es que la arquitectura de la matriz sistólica de la TPU tiene una ventaja significativa en cuanto a densidad y potencia, así como una ventaja de velocidad no despreciable sobre una GPU, cuando se calculan multiplicaciones de matrices.

Cloud TPU

Cuando solicitas una "Cloud TPU v2" en Google Cloud Platform, obtienes una máquina virtual (VM) que tiene una placa de TPU conectada a PCI. La placa de TPU tiene cuatro chips de TPU de doble núcleo. Cada núcleo de TPU incluye una VPU (unidad de procesamiento vectorial) y una MXU (unidad de multiplicación de matrices) de 128 x 128. Luego, esta "Cloud TPU" suele conectarse a través de la red a la VM que la solicitó. Por lo tanto, la imagen completa se ve de la siguiente manera:

dfce5522ed644ece.png

Ilustración: Tu VM con un acelerador "Cloud TPU" conectado a la red. La "Cloud TPU" en sí está compuesta por una VM con una placa de TPU conectada a PCI que tiene cuatro chips de TPU de doble núcleo.

Pods de TPU

En los centros de datos de Google, las TPU están conectadas a una interconexión de computación de alto rendimiento (HPC) que puede hacer que parezcan un acelerador muy grande. Google los llama pods y pueden abarcar hasta 512 núcleos de TPU v2 o 2,048 núcleos de TPU v3.

2ec1e0d341e7fc34.jpeg

Ilustración: Un pod de TPU v3. Paneles y racks de TPU conectados a través de interconexión de HPC.

Durante el entrenamiento, los gradientes se intercambian entre los núcleos de TPU con el algoritmo de reducción total ( aquí se encuentra una buena explicación de la reducción total). El modelo que se entrena puede aprovechar el hardware entrenando con tamaños de lote grandes.

d97b9cc5d40fdb1d.gif

Ilustración: Sincronización de gradientes durante el entrenamiento con el algoritmo de reducción total en la red de HPC de malla toroidal bidimensional de las TPU de Google.

El software

Entrenamiento con un tamaño de lote grande

El tamaño de lote ideal para las TPU es de 128 elementos de datos por núcleo de TPU, pero el hardware ya puede mostrar una buena utilización a partir de 8 elementos de datos por núcleo de TPU. Recuerda que una Cloud TPU tiene 8 núcleos.

En este codelab, usaremos la API de Keras. En Keras, el lote que especificas es el tamaño de lote global para toda la TPU. Tus lotes se dividirán automáticamente en 8 y se ejecutarán en los 8 núcleos de la TPU.

da534407825f01e3.png

Para obtener más sugerencias sobre el rendimiento, consulta la Guía de rendimiento de las TPU. Para tamaños de lote muy grandes, es posible que se necesite un cuidado especial en algunos modelos. Consulta LARSOptimizer para obtener más detalles.

Detrás de escena: XLA

Los programas de TensorFlow definen grafos de procesamiento. La TPU no ejecuta directamente el código de Python, sino el grafo de procesamiento definido por tu programa de TensorFlow. En segundo plano, un compilador llamado XLA (compilador de álgebra lineal acelerada) transforma el grafo de nodos de procesamiento de TensorFlow en código máquina de la TPU. Este compilador también realiza muchas optimizaciones avanzadas en tu código y en el diseño de la memoria. La compilación se realiza automáticamente a medida que se envía trabajo a la TPU. No es necesario que incluyas XLA en tu cadena de compilación de forma explícita.

edce61112cd57972.png

Ilustración: Para ejecutarse en la TPU, el grafo de procesamiento definido por tu programa de TensorFlow primero se traduce a una representación de XLA (compilador de álgebra lineal acelerada) y, luego, XLA lo compila en código máquina de la TPU.

Usa TPU en Keras

Las TPU se admiten a través de la API de Keras a partir de TensorFlow 2.1. La compatibilidad con Keras funciona en las TPU y los pods de TPU. Este es un ejemplo que funciona en TPU, GPU y CPU:

try: # detect TPUs
    tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect()
    strategy = tf.distribute.TPUStrategy(tpu)
except ValueError: # detect GPUs
    strategy = tf.distribute.MirroredStrategy() # for CPU/GPU or multi-GPU machines

# use TPUStrategy scope to define model
with strategy.scope():
  model = tf.keras.Sequential( ... )
  model.compile( ... )

# train model normally on a tf.data.Dataset
model.fit(training_dataset, epochs=EPOCHS, steps_per_epoch=...)

En este fragmento de código:

  • TPUClusterResolver().connect() encuentra la TPU en la red. Funciona sin parámetros en la mayoría de los sistemas de Google Cloud (trabajos de AI Platform, Colaboratory, Kubeflow, VMs de aprendizaje profundo creadas a través de la utilidad "ctpu up"). Estos sistemas saben dónde está su TPU gracias a una variable de entorno TPU_NAME. Si creas una TPU de forma manual, configura la variable de entorno TPU_NAME en la VM desde la que la usas o llama a TPUClusterResolver con parámetros explícitos: TPUClusterResolver(tp_uname, zone, project)
  • TPUStrategy es la parte que implementa la distribución y el algoritmo de sincronización de gradientes "all-reduce".
  • La estrategia se aplica a través de un alcance. El modelo debe definirse dentro del método scope() de la estrategia.
  • La función tpu_model.fit espera un objeto tf.data.Dataset como entrada para el entrenamiento en TPU.

Tareas comunes de adaptación de TPU

  • Si bien hay muchas formas de cargar datos en un modelo de TensorFlow, para las TPU se requiere el uso de la API de tf.data.Dataset.
  • Las TPU son muy rápidas, y la transferencia de datos suele convertirse en el cuello de botella cuando se ejecutan en ellas. En la Guía de rendimiento de TPU, encontrarás herramientas que puedes usar para detectar cuellos de botella en los datos y otras sugerencias de rendimiento.
  • Los números int8 o int16 se tratan como int32. La TPU no tiene hardware de números enteros que opere con menos de 32 bits.
  • No se admiten algunas operaciones de TensorFlow. La lista está aquí. La buena noticia es que esta limitación solo se aplica al código de entrenamiento, es decir, el pase hacia adelante y hacia atrás a través de tu modelo. Aún puedes usar todas las operaciones de TensorFlow en tu canalización de entrada de datos, ya que se ejecutará en la CPU.
  • tf.py_func no es compatible con las TPU.

4. Cargar datos

c0ecb860e4cad0a9.jpeg cc4781a7739c49ae.jpeg 81236b00f8bbf39e.jpeg 961e2228974076bb.jpeg 7517dc163bdffcd5.jpeg 96392df4767f566d.png

Trabajaremos con un conjunto de datos de imágenes de flores. El objetivo es aprender a clasificarlas en 5 tipos de flores. La carga de datos se realiza con la API de tf.data.Dataset. Primero, conozcamos la API.

Práctica

Abre el siguiente notebook, ejecuta las celdas (MAYÚSCULAS + INTRO) y sigue las instrucciones donde veas la etiqueta "WORK REQUIRED".

c3df49e90e5a654f.png Fun with tf.data.Dataset (playground).ipynb

Información adicional

Acerca del conjunto de datos "flores"

El conjunto de datos se organiza en 5 carpetas. Cada carpeta contiene flores de un solo tipo. Las carpetas se llaman sunflowers, daisy, dandelion, tulips y roses. Los datos se alojan en un bucket público de Google Cloud Storage. Extracto:

gs://flowers-public/sunflowers/5139971615_434ff8ed8b_n.jpg
gs://flowers-public/daisy/8094774544_35465c1c64.jpg
gs://flowers-public/sunflowers/9309473873_9d62b9082e.jpg
gs://flowers-public/dandelion/19551343954_83bb52f310_m.jpg
gs://flowers-public/dandelion/14199664556_188b37e51e.jpg
gs://flowers-public/tulips/4290566894_c7f061583d_m.jpg
gs://flowers-public/roses/3065719996_c16ecd5551.jpg
gs://flowers-public/dandelion/8168031302_6e36f39d87.jpg
gs://flowers-public/sunflowers/9564240106_0577e919da_n.jpg
gs://flowers-public/daisy/14167543177_cd36b54ac6_n.jpg

¿Por qué tf.data.Dataset?

Keras y TensorFlow aceptan conjuntos de datos en todas sus funciones de entrenamiento y evaluación. Una vez que cargas datos en un Dataset, la API ofrece todas las funcionalidades comunes que son útiles para los datos de entrenamiento de redes neuronales:

dataset = ... # load something (see below)
dataset = dataset.shuffle(1000) # shuffle the dataset with a buffer of 1000
dataset = dataset.cache() # cache the dataset in RAM or on disk
dataset = dataset.repeat() # repeat the dataset indefinitely
dataset = dataset.batch(128) # batch data elements together in batches of 128
AUTOTUNE = tf.data.AUTOTUNE
dataset = dataset.prefetch(AUTOTUNE) # prefetch next batch(es) while training

Puedes encontrar sugerencias de rendimiento y prácticas recomendadas para los conjuntos de datos en este artículo. La documentación de referencia se encuentra aquí.

Conceptos básicos de tf.data.Dataset

Por lo general, los datos se presentan en varios archivos, como imágenes en este caso. Puedes crear un conjunto de datos de nombres de archivos llamando a:

filenames_dataset = tf.data.Dataset.list_files('gs://flowers-public/*/*.jpg')
# The parameter is a "glob" pattern that supports the * and ? wildcards.

Luego, "asignas" una función a cada nombre de archivo que, por lo general, cargará y decodificará el archivo en datos reales en la memoria:

def decode_jpeg(filename):
  bits = tf.io.read_file(filename)
  image = tf.io.decode_jpeg(bits)
  return image

image_dataset = filenames_dataset.map(decode_jpeg)
# this is now a dataset of decoded images (uint8 RGB format)

Para iterar en un Dataset, haz lo siguiente:

for data in my_dataset:
  print(data)

Conjuntos de datos de tuplas

En el aprendizaje supervisado, un conjunto de datos de entrenamiento suele estar compuesto por pares de datos de entrenamiento y respuestas correctas. Para permitir esto, la función de decodificación puede devolver tuplas. Luego, tendrás un conjunto de datos de tuplas, y se devolverán tuplas cuando iteres sobre él. Los valores que se muestran son tensores de TensorFlow listos para que los use tu modelo. Puedes llamar a .numpy() en ellos para ver los valores sin procesar:

def decode_jpeg_and_label(filename):
  bits = tf.read_file(filename)
  image = tf.io.decode_jpeg(bits)
  label = ... # extract flower name from folder name
  return image, label

image_dataset = filenames_dataset.map(decode_jpeg_and_label)
# this is now a dataset of (image, label) pairs 

for image, label in dataset:
  print(image.numpy().shape, label.numpy())

Conclusión:Cargar imágenes una por una es lento.

A medida que iteres en este conjunto de datos, verás que puedes cargar entre 1 y 2 imágenes por segundo. ¡Es demasiado lento! Los aceleradores de hardware que usaremos para el entrenamiento pueden mantener muchas veces esta tasa. Ve a la siguiente sección para ver cómo lo lograremos.

Solución

Aquí está el notebook de solución. Puedes usarlo si no puedes avanzar.

c3df49e90e5a654f.png Fun with tf.data.Dataset (solution).ipynb

Temas abordados

  • 🤔 tf.data.Dataset.list_files
  • 🤔 tf.data.Dataset.map
  • 🤔 Conjuntos de datos de tuplas
  • 😀 Iterar a través de conjuntos de datos

Tómate un momento para revisar esta lista de tareas mentalmente.

5. Carga de datos rápida

Los aceleradores de hardware de la unidad de procesamiento tensorial (TPU) que usaremos en este lab son muy rápidos. El desafío suele ser proporcionarles datos con la suficiente rapidez para mantenerlos ocupados. Google Cloud Storage (GCS) puede mantener un rendimiento muy alto, pero, como sucede con todos los sistemas de almacenamiento en la nube, iniciar una conexión genera un costo de ida y vuelta de la red. Por lo tanto, no es ideal tener nuestros datos almacenados como miles de archivos individuales. Los agruparemos en lotes en una cantidad menor de archivos y usaremos la potencia de tf.data.Dataset para leer desde varios archivos en paralelo.

Lectura

El código que carga archivos de imágenes, los cambia de tamaño a un tamaño común y, luego, los almacena en 16 archivos TFRecord se encuentra en el siguiente notebook. Léelo rápidamente. No es necesario ejecutarlo, ya que se proporcionarán datos con el formato TFRecord adecuado para el resto del codelab.

c3df49e90e5a654f.png Flower pictures to TFRecords.ipynb

Diseño de datos ideal para un rendimiento óptimo de GCS

El formato de archivo TFRecord

El formato de archivo preferido de TensorFlow para almacenar datos es el formato TFRecord basado en protobuf. Otros formatos de serialización también funcionarían, pero puedes cargar un conjunto de datos desde archivos TFRecord directamente escribiendo lo siguiente:

filenames = tf.io.gfile.glob(FILENAME_PATTERN)
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...) # do the TFRecord decoding here - see below

Para obtener un rendimiento óptimo, se recomienda usar el siguiente código más complejo para leer varios archivos TFRecord a la vez. Este código leerá N archivos en paralelo y no tendrá en cuenta el orden de los datos en favor de la velocidad de lectura.

AUTOTUNE = tf.data.AUTOTUNE
ignore_order = tf.data.Options()
ignore_order.experimental_deterministic = False

filenames = tf.io.gfile.glob(FILENAME_PATTERN)
dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads=AUTOTUNE)
dataset = dataset.with_options(ignore_order)
dataset = dataset.map(...) # do the TFRecord decoding here - see below

Hoja de referencia de TFRecord

En los TFRecords, se pueden almacenar tres tipos de datos: cadenas de bytes (lista de bytes), números enteros de 64 bits y números de punto flotante de 32 bits. Siempre se almacenan como listas, y un solo elemento de datos será una lista de tamaño 1. Puedes usar las siguientes funciones auxiliares para almacenar datos en TFRecords.

escribir cadenas de bytes

# warning, the input is a list of byte strings, which are themselves lists of bytes
def _bytestring_feature(list_of_bytestrings):
  return tf.train.Feature(bytes_list=tf.train.BytesList(value=list_of_bytestrings))

escribir números enteros

def _int_feature(list_of_ints): # int64
  return tf.train.Feature(int64_list=tf.train.Int64List(value=list_of_ints))

Flotantes de escritura

def _float_feature(list_of_floats): # float32
  return tf.train.Feature(float_list=tf.train.FloatList(value=list_of_floats))

Escribir un TFRecord con los asistentes anteriores

# input data in my_img_bytes, my_class, my_height, my_width, my_floats
with tf.python_io.TFRecordWriter(filename) as out_file:
  feature = {
    "image": _bytestring_feature([my_img_bytes]), # one image in the list
    "class": _int_feature([my_class]),            # one class in the list
    "size": _int_feature([my_height, my_width]),  # fixed length (2) list of ints
    "float_data": _float_feature(my_floats)       # variable length  list of floats
  }
  tf_record = tf.train.Example(features=tf.train.Features(feature=feature))
  out_file.write(tf_record.SerializeToString())

Para leer datos de TFRecords, primero debes declarar el diseño de los registros que almacenaste. En la declaración, puedes acceder a cualquier campo con nombre como una lista de longitud fija o una lista de longitud variable:

lectura de TFRecords

def read_tfrecord(data):
  features = {
    # tf.string = byte string (not text string)
    "image": tf.io.FixedLenFeature([], tf.string), # shape [] means scalar, here, a single byte string
    "class": tf.io.FixedLenFeature([], tf.int64),  # shape [] means scalar, i.e. a single item
    "size": tf.io.FixedLenFeature([2], tf.int64),  # two integers
    "float_data": tf.io.VarLenFeature(tf.float32)  # a variable number of floats
  }

  # decode the TFRecord
  tf_record = tf.io.parse_single_example(data, features)

  # FixedLenFeature fields are now ready to use
  sz = tf_record['size']

  # Typical code for decoding compressed images
  image = tf.io.decode_jpeg(tf_record['image'], channels=3)

  # VarLenFeature fields require additional sparse.to_dense decoding
  float_data = tf.sparse.to_dense(tf_record['float_data'])

  return image, sz, float_data

# decoding a tf.data.TFRecordDataset
dataset = dataset.map(read_tfrecord)
# now a dataset of triplets (image, sz, float_data)

Fragmentos de código útiles:

lectura de elementos de datos individuales

tf.io.FixedLenFeature([], tf.string)   # for one byte string
tf.io.FixedLenFeature([], tf.int64)    # for one int
tf.io.FixedLenFeature([], tf.float32)  # for one float

leer listas de elementos de tamaño fijo

tf.io.FixedLenFeature([N], tf.string)   # list of N byte strings
tf.io.FixedLenFeature([N], tf.int64)    # list of N ints
tf.io.FixedLenFeature([N], tf.float32)  # list of N floats

leer una cantidad variable de elementos de datos

tf.io.VarLenFeature(tf.string)   # list of byte strings
tf.io.VarLenFeature(tf.int64)    # list of ints
tf.io.VarLenFeature(tf.float32)  # list of floats

Un VarLenFeature devuelve un vector disperso, y se requiere un paso adicional después de decodificar el TFRecord:

dense_data = tf.sparse.to_dense(tf_record['my_var_len_feature'])

También es posible tener campos opcionales en los registros de TF. Si especificas un valor predeterminado cuando lees un campo, se devolverá el valor predeterminado en lugar de un error si falta el campo.

tf.io.FixedLenFeature([], tf.int64, default_value=0) # this field is optional

Temas abordados

  • 🤔 Cómo fragmentar archivos de datos para un acceso rápido desde GCS
  • 😓 cómo escribir TFRecords (¿Ya olvidaste la sintaxis? No hay problema, guarda esta página como hoja de referencia).
  • 🤔 Cómo cargar un Dataset desde TFRecords con TFRecordDataset

Tómate un momento para revisar esta lista de tareas mentalmente.

6. [INFO] Clasificador de redes neuronales 101

En pocas palabras

Si ya conoces todos los términos en negrita del siguiente párrafo, puedes pasar al siguiente ejercicio. Si recién comienzas a aprender sobre el aprendizaje profundo, bienvenido y sigue leyendo.

Para los modelos creados como una secuencia de capas, Keras ofrece la API secuencial. Por ejemplo, un clasificador de imágenes que usa tres capas densas se puede escribir en Keras de la siguiente manera:

model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=[192, 192, 3]),
    tf.keras.layers.Dense(500, activation="relu"),
    tf.keras.layers.Dense(50, activation="relu"),
    tf.keras.layers.Dense(5, activation='softmax') # classifying into 5 classes
])

# this configures the training of the model. Keras calls it "compiling" the model.
model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy']) # % of correct answers

# train the model
model.fit(dataset, ... )

688858c21e3beff2.png

Red neuronal densa

Esta es la red neuronal más simple para clasificar imágenes. Está compuesta por "neuronas" dispuestas en capas. La primera capa procesa los datos de entrada y envía sus resultados a otras capas. Se llama "densa" porque cada neurona está conectada a todas las neuronas de la capa anterior.

c21bae6dade487bc.png

Puedes ingresar una imagen en una red de este tipo aplanando los valores RGB de todos sus píxeles en un vector largo y usándolo como entrada. No es la mejor técnica para el reconocimiento de imágenes, pero la mejoraremos más adelante.

Neuronas, activaciones y ReLU

Una "neurona" calcula una suma ponderada de todas sus entradas, agrega un valor llamado "sesgo" y alimenta el resultado a través de una "función de activación". Al principio, se desconocen los pesos y el sesgo. Se inicializarán de forma aleatoria y se "aprenderán" entrenando la red neuronal con muchos datos conocidos.

644f4213a4ee70e5.png

La función de activación más popular se llama ReLU, que significa unidad lineal rectificada. Es una función muy simple, como puedes ver en el gráfico anterior.

Activación Softmax

La red anterior termina con una capa de 5 neuronas porque clasificamos las flores en 5 categorías (rosa, tulipán, diente de león, margarita y girasol). Las neuronas de las capas intermedias se activan con la función de activación ReLU clásica. Sin embargo, en la última capa, queremos calcular números entre 0 y 1 que representen la probabilidad de que esta flor sea una rosa, un tulipán, etcétera. Para ello, usaremos una función de activación llamada "softmax".

Para aplicar softmax a un vector, se toma el exponencial de cada elemento y, luego, se normaliza el vector, por lo general, con la norma L1 (suma de valores absolutos) para que los valores sumen 1 y se puedan interpretar como probabilidades.

ef0d98c0952c262d.png d51252f75894479e.gif

Pérdida de entropía cruzada

Ahora que nuestra red neuronal produce predicciones a partir de imágenes de entrada, debemos medir qué tan buenas son, es decir, la distancia entre lo que nos dice la red y las respuestas correctas, que a menudo se denominan "etiquetas". Recuerda que tenemos etiquetas correctas para todas las imágenes del conjunto de datos.

Cualquier distancia funcionaría, pero para los problemas de clasificación, la llamada "distancia de entropía cruzada" es la más eficaz. Llamaremos a esta nuestra función de error o "pérdida":

7bdf8753d20617fb.png

Descenso de gradientes

"Entrenar" la red neuronal en realidad significa usar imágenes y etiquetas de entrenamiento para ajustar los pesos y los sesgos de modo que se minimice la función de pérdida de entropía cruzada. Así es como funciona.

La entropía cruzada es una función de los pesos, las tendencias, los píxeles de la imagen de entrenamiento y su clase conocida.

Si calculamos las derivadas parciales de la entropía cruzada en relación con todos los pesos y todos los sesgos, obtenemos un "gradiente" que se calcula para una imagen, una etiqueta y un valor presente de pesos y sesgos determinados. Recuerda que podemos tener millones de pesos y sesgos, por lo que calcular el gradiente parece una tarea muy laboriosa. Afortunadamente, TensorFlow lo hace por nosotros. La propiedad matemática de un gradiente es que apunta "hacia arriba". Como queremos ir hacia donde la entropía cruzada es baja, vamos en la dirección opuesta. Actualizamos los pesos y las bias con una fracción del gradiente. Luego, repetimos el mismo proceso una y otra vez con los siguientes lotes de imágenes y etiquetas de entrenamiento, en un bucle de entrenamiento. Esperamos que esto converja en un punto en el que la entropía cruzada sea mínima, aunque nada garantiza que este mínimo sea único.

gradient descent2.png

Minilotes y momento

Puedes calcular tu gradiente en una sola imagen de ejemplo y actualizar los pesos y las tendencias de inmediato, pero hacerlo en un lote de, por ejemplo, 128 imágenes proporciona un gradiente que representa mejor las restricciones impuestas por diferentes imágenes de ejemplo y, por lo tanto, es probable que converja hacia la solución más rápido. El tamaño del minilote es un parámetro ajustable.

Esta técnica, a veces denominada "descenso de gradiente estocástico", tiene otro beneficio más pragmático: trabajar con lotes también significa trabajar con matrices más grandes, y estas suelen ser más fáciles de optimizar en las GPU y las TPU.

Sin embargo, la convergencia puede ser un poco caótica y hasta detenerse si el vector de gradiente es todo ceros. ¿Eso significa que encontramos un mínimo? No en todos los casos. Un componente de gradiente puede ser cero en un mínimo o un máximo. Con un vector de gradiente con millones de elementos, si todos son ceros, la probabilidad de que cada cero corresponda a un mínimo y ninguno a un punto máximo es bastante baja. En un espacio de muchas dimensiones, los puntos de silla son bastante comunes y no queremos detenernos en ellos.

52e824fe4716c4a0.png

Ilustración: Un punto de silla. El gradiente es 0, pero no es un mínimo en todas las direcciones. (Atribución de la imagen: Wikimedia: De Nicoguaro - Trabajo propio, CC BY 3.0)

La solución es agregar algo de impulso al algoritmo de optimización para que pueda superar los puntos de silla sin detenerse.

Glosario

Lote o minilote: El entrenamiento siempre se realiza en lotes de datos y etiquetas de entrenamiento. Esto ayuda a que el algoritmo converja. La dimensión "lote" suele ser la primera dimensión de los tensores de datos. Por ejemplo, un tensor con la forma [100, 192, 192, 3] contiene 100 imágenes de 192 x 192 píxeles con tres valores por píxel (RGB).

Pérdida de entropía cruzada: Es una función de pérdida especial que se usa a menudo en los clasificadores.

Capa densa: Es una capa de neuronas en la que cada neurona está conectada a todas las neuronas de la capa anterior.

Atributos: Las entradas de una red neuronal a veces se denominan "atributos". El arte de determinar qué partes de un conjunto de datos (o combinaciones de partes) se deben ingresar en una red neuronal para obtener buenas predicciones se denomina "ingeniería de funciones".

etiquetas: Otro nombre para "clases" o respuestas correctas en un problema de clasificación supervisada

Tasa de aprendizaje: Es la fracción del gradiente por la que se actualizan los pesos y las tendencias en cada iteración del ciclo de entrenamiento.

logits: Los resultados de una capa de neuronas antes de que se aplique la función de activación se denominan "logits". El término proviene de la "función logística", también conocida como "función sigmoidea", que solía ser la función de activación más popular. "Salidas de neuronas antes de la función logística" se abrevió a "logits".

loss: Es la función de error que compara los resultados de la red neuronal con las respuestas correctas.

Neurona: Calcula la suma ponderada de sus entradas, agrega un sesgo y alimenta el resultado a través de una función de activación.

Codificación one-hot: La clase 3 de 5 se codifica como un vector de 5 elementos, todos ceros, excepto el 3º, que es 1.

relu: Unidad lineal rectificada. Es una función de activación popular para las neuronas.

sigmoid: Otra función de activación que solía ser popular y que sigue siendo útil en casos especiales.

softmax: Es una función de activación especial que actúa sobre un vector, aumenta la diferencia entre el componente más grande y todos los demás, y también normaliza el vector para que tenga una suma de 1 y se pueda interpretar como un vector de probabilidades. Se usa como el último paso en los clasificadores.

tensor: Un "tensor" es como una matriz, pero con una cantidad arbitraria de dimensiones. Un tensor unidimensional es un vector. Un tensor de 2 dimensiones es una matriz. Luego, puedes tener tensores con 3, 4, 5 o más dimensiones.

7. Aprendizaje por transferencia

Para un problema de clasificación de imágenes, es probable que las capas densas no sean suficientes. Tenemos que aprender sobre las capas convolucionales y las muchas formas en que puedes organizarlas.

Pero también podemos tomar un atajo. Hay redes neuronales convolucionales completamente entrenadas disponibles para descargar. Es posible cortar su última capa, el encabezado de clasificación softmax, y reemplazarlo por el tuyo. Todos los pesos y sesgos entrenados permanecen como están, solo vuelves a entrenar la capa softmax que agregas. Esta técnica se denomina aprendizaje por transferencia y, sorprendentemente, funciona siempre y cuando el conjunto de datos con el que se preentrena la red neuronal sea "lo suficientemente similar" al tuyo.

Práctico

Abre el siguiente notebook, ejecuta las celdas (MAYÚSCULAS + INTRO) y sigue las instrucciones donde veas la etiqueta "WORK REQUIRED".

c3df49e90e5a654f.png Keras Flowers transfer learning (playground).ipynb

Información adicional

Con el aprendizaje por transferencia, te beneficias de las arquitecturas avanzadas de redes neuronales convolucionales desarrolladas por los mejores investigadores y del entrenamiento previo en un enorme conjunto de datos de imágenes. En nuestro caso, realizaremos una transferencia del aprendizaje a partir de una red entrenada en ImageNet, una base de datos de imágenes que contiene muchas plantas y escenas al aire libre, lo que es lo suficientemente cercano a las flores.

b8fc1efd2001f072.png

Ilustración: Se usa una red neuronal convolucional compleja, ya entrenada, como una caja negra, y se vuelve a entrenar solo el encabezado de clasificación. Esto es aprendizaje por transferencia. Más adelante, veremos cómo funcionan estos complicados arreglos de capas convolucionales. Por ahora, es problema de otra persona.

Aprendizaje por transferencia en Keras

En Keras, puedes crear una instancia de un modelo previamente entrenado desde la colección tf.keras.applications.*. Por ejemplo, MobileNet V2 es una muy buena arquitectura convolucional que mantiene un tamaño razonable. Si seleccionas include_top=False, obtendrás el modelo entrenado previamente sin su capa softmax final para que puedas agregar la tuya:

pretrained_model = tf.keras.applications.MobileNetV2(input_shape=[*IMAGE_SIZE, 3], include_top=False)
pretrained_model.trainable = False

model = tf.keras.Sequential([
    pretrained_model,
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(5, activation='softmax')
])

También observa el parámetro de configuración pretrained_model.trainable = False. Inmoviliza los pesos y las tendencias del modelo previamente entrenado para que solo entrenes tu capa softmax. Por lo general, esto implica relativamente pocos pesos y se puede hacer rápidamente sin necesidad de un conjunto de datos muy grande. Sin embargo, si tienes muchos datos, el aprendizaje por transferencia puede funcionar aún mejor con pretrained_model.trainable = True. Luego, los pesos previamente entrenados proporcionan excelentes valores iniciales y el entrenamiento los puede ajustar para que se adapten mejor a tu problema.

Por último, observa la capa Flatten() que se insertó antes de la capa softmax densa. Las capas densas funcionan con vectores de datos planos, pero no sabemos si eso es lo que devuelve el modelo previamente entrenado. Por eso, debemos aplanar la curva. En el próximo capítulo, a medida que nos adentremos en las arquitecturas convolucionales, explicaremos el formato de datos que devuelven las capas convolucionales.

Con este enfoque, deberías alcanzar una precisión cercana al 75%.

Solución

Aquí está el notebook de solución. Puedes usarlo si no puedes avanzar.

c3df49e90e5a654f.png Keras Flowers transfer learning (solution).ipynb

Temas abordados

  • 🤔 Cómo escribir un clasificador en Keras
  • 🤓 configurado con una última capa de softmax y pérdida de entropía cruzada
  • 😈 Aprendizaje por transferencia
  • 🤔 Entrena tu primer modelo
  • 🧐 Seguimiento de la pérdida y la exactitud durante el entrenamiento

Tómate un momento para revisar esta lista de tareas mentalmente.

8. [INFO] Redes neuronales convolucionales

En pocas palabras

Si ya conoces todos los términos en negrita del siguiente párrafo, puedes pasar al siguiente ejercicio. Si recién comienzas a usar redes neuronales convolucionales, sigue leyendo.

convolutional.gif

Ilustración: Filtrado de una imagen con dos filtros sucesivos compuestos por 48 pesos entrenables cada uno (4 × 4 × 3=48).

Así se ve una red neuronal convolucional simple en Keras:

model = tf.keras.Sequential([
  # input: images of size 192x192x3 pixels (the three stands for RGB channels)
  tf.keras.layers.Conv2D(kernel_size=3, filters=24, padding='same', activation='relu', input_shape=[192, 192, 3]),
  tf.keras.layers.Conv2D(kernel_size=3, filters=24, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=12, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=6, padding='same', activation='relu'),
  tf.keras.layers.Flatten(),
  # classifying into 5 categories
  tf.keras.layers.Dense(5, activation='softmax')
])

model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy'])

688858c21e3beff2.png

Introducción a las redes neuronales convolucionales

En una capa de una red convolucional, una "neurona" realiza una suma ponderada de los píxeles que se encuentran justo arriba de ella, solo en una pequeña región de la imagen. Agrega un sesgo y alimenta la suma a través de una función de activación, tal como lo haría una neurona en una capa densa normal. Luego, esta operación se repite en toda la imagen con los mismos pesos. Recuerda que, en las capas densas, cada neurona tenía sus propios pesos. Aquí, un solo "parche" de pesos se desliza por la imagen en ambas direcciones (una "convolución"). El resultado tiene tantos valores como píxeles hay en la imagen (aunque es necesario agregar relleno en los bordes). Es una operación de filtrado que usa un filtro de 48 pesos (4 x 4 x 3).

Sin embargo, 48 pesos no serán suficientes. Para agregar más grados de libertad, repetimos la misma operación con un nuevo conjunto de pesos. Esto genera un nuevo conjunto de resultados del filtro. Por analogía con los canales R,G y B de la imagen de entrada,llamémoslo "canal" de salidas.

Screen Shot 2016-07-29 at 16.02.37.png

Los dos (o más) conjuntos de pesos se pueden sumar como un tensor agregando una nueva dimensión. Esto nos da la forma genérica del tensor de pesos para una capa convolucional. Dado que la cantidad de canales de entrada y salida son parámetros, podemos comenzar a apilar y encadenar capas convolucionales.

d1b557707bcd1cb9.png

Ilustración: Una red neuronal convolucional transforma "cubos" de datos en otros "cubos" de datos.

Convoluciones con stride, reducción máxima

Si realizamos las convoluciones con un paso de 2 o 3, también podemos reducir el cubo de datos resultante en sus dimensiones horizontales. Existen dos formas comunes de hacerlo:

  • Convolución con stride: Un filtro deslizante como el anterior, pero con un stride >1
  • Reducción máxima: Una ventana deslizante que aplica la operación MAX (generalmente en parches de 2×2, repetidos cada 2 píxeles)

2b2d4263bb8470b.gif

Ilustración: Deslizar la ventana de cálculo en 3 píxeles genera menos valores de salida. Las convoluciones con segmentos o la reducción máxima (máximo en una ventana de 2 x 2 que se desliza con un segmento de 2) son una forma de reducir el cubo de datos en las dimensiones horizontales.

Clasificador convolucional

Por último, conectamos un encabezado de clasificación aplanando el último cubo de datos y pasándolo por una capa densa activada por softmax. Un clasificador convolucional típico puede verse de la siguiente manera:

4a61aaffb6cba3d1.png

Ilustración: Un clasificador de imágenes que usa capas convolucionales y softmax. Utiliza filtros de 3x3 y 1x1. Las capas de maxpool toman el máximo de grupos de puntos de datos de 2x2. El encabezado de clasificación se implementa con una capa densa con activación softmax.

En Keras

La pila convolucional ilustrada anteriormente se puede escribir en Keras de la siguiente manera:

model = tf.keras.Sequential([
  # input: images of size 192x192x3 pixels (the three stands for RGB channels)    
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu', input_shape=[192, 192, 3]),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=16, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=8, padding='same', activation='relu'),
  tf.keras.layers.Flatten(),
  # classifying into 5 categories
  tf.keras.layers.Dense(5, activation='softmax')
])

model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy'])

9. Tu red neuronal convolucional personalizada

Práctico

Construyamos y entrenemos una red neuronal convolucional desde cero. Usar una TPU nos permitirá iterar muy rápido. Abre el siguiente notebook, ejecuta las celdas (MAYÚSCULAS + INTRO) y sigue las instrucciones donde veas la etiqueta "WORK REQUIRED".

c3df49e90e5a654f.png Keras_Flowers_TPU (playground).ipynb

El objetivo es superar la precisión del 75% del modelo de aprendizaje por transferencia. Ese modelo tenía una ventaja, ya que se había entrenado previamente con un conjunto de datos de millones de imágenes, mientras que aquí solo tenemos 3,670 imágenes. ¿Al menos puedes igualarlo?

Información adicional

¿Cuántas capas y qué tamaño tienen?

Seleccionar los tamaños de las capas es más un arte que una ciencia. Debes encontrar el equilibrio adecuado entre tener muy pocos y demasiados parámetros (pesos y sesgos). Si hay muy pocos pesos, la red neuronal no puede representar la complejidad de las formas de las flores. Si hay demasiadas, puede ser propensa al "sobreajuste", es decir, a especializarse en las imágenes de entrenamiento y no poder generalizar. Con muchos parámetros, el modelo también será lento para entrenar. En Keras, la función model.summary() muestra la estructura y el recuento de parámetros de tu modelo:

Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 192, 192, 16)      448       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 192, 192, 30)      4350      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 96, 96, 30)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 96, 96, 60)        16260     
_________________________________________________________________
 ... 
_________________________________________________________________
global_average_pooling2d (Gl (None, 130)               0         
_________________________________________________________________
dense (Dense)                (None, 90)                11790     
_________________________________________________________________
dense_1 (Dense)              (None, 5)                 455       
=================================================================
Total params: 300,033
Trainable params: 300,033
Non-trainable params: 0
_________________________________________________________________

Algunas sugerencias:

  • Tener varias capas es lo que hace que las redes neuronales "profundas" sean eficaces. Para este problema simple de reconocimiento de flores, entre 5 y 10 capas son suficientes.
  • Usa filtros pequeños. Por lo general, los filtros de 3x3 son adecuados en cualquier lugar.
  • También se pueden usar filtros de 1 x 1, que son económicos. En realidad, no "filtran" nada, sino que calculan combinaciones lineales de canales. Alterna los filtros reales. (Encontrarás más información sobre las "convoluciones 1 x 1" en la siguiente sección).
  • Para un problema de clasificación como este, realiza un muestreo inferior con frecuencia con capas de max-pooling (o convoluciones con stride > 1). No te importa dónde está la flor, solo que sea una rosa o un diente de león, por lo que perder la información de X e Y no es importante y filtrar áreas más pequeñas es más económico.
  • La cantidad de filtros suele ser similar a la cantidad de clases al final de la red (¿por qué? Consulta el truco de "agrupación promedio global" a continuación). Si clasificas en cientos de clases, aumenta el recuento de filtros de forma progresiva en las capas consecutivas. Para el conjunto de datos de flores con 5 clases, no sería suficiente filtrar con solo 5 filtros. Puedes usar el mismo recuento de filtros en la mayoría de las capas, por ejemplo, 32, y disminuirlo hacia el final.
  • Las capas densas finales son costosas. Puede tener más pesos que todas las capas convolucionales combinadas. Por ejemplo, incluso con un resultado muy razonable del último cubo de datos de 24 x 24 x 10 puntos de datos, una capa densa de 100 neuronas costaría 24 x 24 x 10 x 100=576,000 pesos. Intenta ser reflexivo o usa la agrupación global del promedio (consulta a continuación).

Agrupación global del promedio

En lugar de usar una capa densa costosa al final de una red neuronal convolucional, puedes dividir el "cubo" de datos entrantes en tantas partes como tengas clases, calcular el promedio de sus valores y pasarlos a través de una función de activación softmax. Esta forma de crear el encabezado de clasificación no requiere ningún peso. En Keras, la sintaxis es tf.keras.layers.GlobalAveragePooling2D()..

93240029f59df7c2.png

Solución

Aquí está el notebook de solución. Puedes usarlo si no puedes avanzar.

c3df49e90e5a654f.png Keras_Flowers_TPU (solution).ipynb

Temas abordados

  • 🤔 Jugó con capas convolucionales
  • 🤓 Experimenté con la agrupación máxima, los pasos, la agrupación promedio global, etc.
  • 😀 Iteramos rápidamente en un modelo del mundo real en la TPU.

Tómate un momento para revisar esta lista de tareas mentalmente.

10. [INFO] Arquitecturas convolucionales modernas

En pocas palabras

7968830b57b708c0.png

Ilustración: Un "módulo" convolucional. ¿Qué es lo mejor en este punto? ¿Una capa de max-pooling seguida de una capa convolucional de 1 x 1 o una combinación diferente de capas? Prueba todas las opciones, concatena los resultados y deja que la red decida. A la derecha, se muestra la arquitectura convolucional " inception" que usa esos módulos.

En Keras, para crear modelos en los que el flujo de datos puede ramificarse, debes usar el estilo de modelo "funcional". A continuación, se muestra un ejemplo:

l = tf.keras.layers # syntax shortcut

y = l.Conv2D(filters=32, kernel_size=3, padding='same',
             activation='relu', input_shape=[192, 192, 3])(x) # x=input image

# module start: branch out
y1 = l.Conv2D(filters=32, kernel_size=1, padding='same', activation='relu')(y)
y3 = l.Conv2D(filters=32, kernel_size=3, padding='same', activation='relu')(y)
y = l.concatenate([y1, y3]) # output now has 64 channels
# module end: concatenation

# many more layers ...

# Create the model by specifying the input and output tensors.
# Keras layers track their connections automatically so that's all that's needed.
z = l.Dense(5, activation='softmax')(y)
model = tf.keras.Model(x, z)

688858c21e3beff2.png

Otros trucos baratos

Filtros pequeños de 3 x 3

40a7b15fb7dbe75c.png

En esta ilustración, se muestra el resultado de dos filtros de 3x3 consecutivos. Intenta rastrear qué puntos de datos contribuyeron al resultado: Estos dos filtros consecutivos de 3x3 calculan alguna combinación de una región de 5x5. No es exactamente la misma combinación que calcularía un filtro de 5 x 5, pero vale la pena intentarlo porque dos filtros consecutivos de 3 x 3 son más económicos que un solo filtro de 5 x 5.

¿Convoluciones de 1x1?

fd7cac16f8ecb423.png

En términos matemáticos, una convolución "1x1" es una multiplicación por una constante, un concepto no muy útil. Sin embargo, en las redes neuronales convolucionales, recuerda que el filtro se aplica a un cubo de datos, no solo a una imagen 2D. Por lo tanto, un filtro "1x1" calcula una suma ponderada de una columna de datos de 1x1 (consulta la ilustración) y, a medida que lo deslizas por los datos, obtendrás una combinación lineal de los canales de la entrada. Esto es muy útil. Si piensas en los canales como los resultados de operaciones de filtrado individuales, por ejemplo, un filtro para "orejas puntiagudas", otro para "bigotes" y un tercero para "ojos rasgados", una capa convolucional "1x1" calculará varias combinaciones lineales posibles de estas características, lo que podría ser útil cuando se busca un "gato". Además, las capas de 1 x 1 usan menos pesos.

11. Squeezenet

En el artículo"Squeezenet", se muestra una forma sencilla de combinar estas ideas. Los autores sugieren un diseño de módulo convolucional muy simple, que solo utiliza capas convolucionales de 1 x 1 y 3 x 3.

1730ac375379269b.png

Ilustración: Arquitectura de SqueezeNet basada en "módulos de incendio". Alternan una capa de 1 x 1 que "comprime" los datos entrantes en la dimensión vertical, seguida de dos capas convolucionales paralelas de 1 x 1 y 3 x 3 que "expanden" la profundidad de los datos nuevamente.

Práctico

Continúa en tu notebook anterior y crea una red neuronal convolucional inspirada en SqueezeNet. Deberás cambiar el código del modelo al "estilo funcional" de Keras.

c3df49e90e5a654f.png Keras_Flowers_TPU (playground).ipynb

Información adicional

Será útil para este ejercicio definir una función auxiliar para un módulo de SqueezeNet:

def fire(x, squeeze, expand):
  y = l.Conv2D(filters=squeeze, kernel_size=1, padding='same', activation='relu')(x)
  y1 = l.Conv2D(filters=expand//2, kernel_size=1, padding='same', activation='relu')(y)
  y3 = l.Conv2D(filters=expand//2, kernel_size=3, padding='same', activation='relu')(y)
  return tf.keras.layers.concatenate([y1, y3])

# this is to make it behave similarly to other Keras layers
def fire_module(squeeze, expand):
  return lambda x: fire(x, squeeze, expand)

# usage:
x = l.Input(shape=[192, 192, 3])
y = fire_module(squeeze=24, expand=48)(x) # typically, squeeze is less than expand
y = fire_module(squeeze=32, expand=64)(y)
...
model = tf.keras.Model(x, y)

Esta vez, el objetivo es alcanzar una precisión del 80%.

Qué puedes probar

Comienza con una sola capa convolucional y, luego, continúa con "fire_modules", alternando con capas MaxPooling2D(pool_size=2). Puedes experimentar con 2 a 4 capas de max pooling en la red y también con 1, 2 o 3 módulos Fire consecutivos entre las capas de max pooling.

En los módulos de activación, el parámetro de "compresión" suele ser más pequeño que el parámetro de "expansión". En realidad, estos parámetros son cantidades de filtros. Por lo general, pueden variar de 8 a 196. Puedes experimentar con arquitecturas en las que la cantidad de filtros aumenta gradualmente a través de la red o con arquitecturas sencillas en las que todos los módulos de Fire tienen la misma cantidad de filtros.

A continuación, se muestra un ejemplo:

x = tf.keras.layers.Input(shape=[*IMAGE_SIZE, 3]) # input is 192x192 pixels RGB

y = tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu')(x)
y = fire_module(24, 48)(y)
y = tf.keras.layers.MaxPooling2D(pool_size=2)(y)
y = fire_module(24, 48)(y)
y = tf.keras.layers.MaxPooling2D(pool_size=2)(y)
y = fire_module(24, 48)(y)
y = tf.keras.layers.GlobalAveragePooling2D()(y)
y = tf.keras.layers.Dense(5, activation='softmax')(y)

model = tf.keras.Model(x, y)

En este punto, es posible que notes que tus experimentos no están funcionando tan bien y que el objetivo de precisión del 80% parece lejano. Es hora de ver algunos trucos más baratos.

Normalización por lotes

La normalización por lotes te ayudará con los problemas de convergencia que estás experimentando. En el próximo taller, se brindarán explicaciones detalladas sobre esta técnica. Por ahora, úsala como un asistente "mágico" de caja negra agregando esta línea después de cada capa convolucional en tu red, incluidas las capas dentro de tu función fire_module:

y = tf.keras.layers.BatchNormalization(momentum=0.9)(y)
# please adapt the input and output "y"s to whatever is appropriate in your context

El parámetro de momento debe disminuirse de su valor predeterminado de 0.99 a 0.9 porque nuestro conjunto de datos es pequeño. Por ahora, no te preocupes por este detalle.

Aumento de datos

Obtendrás algunos puntos porcentuales más si aumentas los datos con transformaciones sencillas, como inversiones de izquierda a derecha de los cambios de saturación:

4ed2958e09b487ca.png

ad795b70334e0d6b.png

Esto es muy fácil de hacer en TensorFlow con la API de tf.data.Dataset. Define una nueva función de transformación para tus datos:

def data_augment(image, label):
    image = tf.image.random_flip_left_right(image)
    image = tf.image.random_saturation(image, lower=0, upper=2)
    return image, label

Luego, úsalo en la transformación de datos final (celda "Conjuntos de datos de entrenamiento y validación", función "get_batched_dataset"):

dataset = dataset.repeat() # existing line
# insert this
if augment_data:
  dataset = dataset.map(data_augment, num_parallel_calls=AUTO)
dataset = dataset.shuffle(2048) # existing line

No olvides que la magnificación de datos debe ser opcional y que debes agregar el código necesario para asegurarte de que solo se aumente el conjunto de datos de entrenamiento. No tiene sentido aumentar el conjunto de datos de validación.

Ahora deberías poder alcanzar el 80% de precisión en 35 ciclos de entrenamiento.

Solución

Aquí está el notebook de solución. Puedes usarlo si no puedes avanzar.

c3df49e90e5a654f.png Keras_Flowers_TPU_squeezenet.ipynb

Temas abordados

  • 🤔 Modelos de "estilo funcional" de Keras
  • 🤓 Arquitectura de SqueezeNet
  • 🤓 Aumentación de datos con tf.data.Dataset

Tómate un momento para revisar esta lista de tareas mentalmente.

12. Xception ajustado

Convoluciones separables

Recientemente, se popularizó una forma diferente de implementar capas convolucionales: las convoluciones separables en profundidad. Lo sé, es un trabalenguas, pero el concepto es bastante simple. Se implementan en TensorFlow y Keras como tf.keras.layers.SeparableConv2D.

Una convolución separable también ejecuta un filtro en la imagen, pero usa un conjunto distinto de pesos para cada canal de la imagen de entrada. Luego, se aplica una "convolución 1 x 1", una serie de productos escalares que dan como resultado una suma ponderada de los canales filtrados. Con pesos nuevos cada vez, se calculan tantas recombinaciones ponderadas de los canales como sea necesario.

615720b803bf8dda.gif

Ilustración: Convoluciones separables. Fase 1: Convoluciones con un filtro independiente para cada canal. Fase 2: Recombinaciones lineales de canales Se repite con un nuevo conjunto de pesos hasta que se alcanza la cantidad deseada de canales de salida. La fase 1 también se puede repetir, con pesos nuevos cada vez, pero en la práctica rara vez es así.

Las convoluciones separables se usan en las arquitecturas de redes convolucionales más recientes: MobileNetV2, Xception y EfficientNet. Por cierto, MobileNetV2 es lo que usaste para el aprendizaje por transferencia anteriormente.

Son más económicas que las convoluciones normales y se ha demostrado que son igual de eficaces en la práctica. A continuación, se muestra el recuento de pesos para el ejemplo ilustrado anteriormente:

Capa convolucional: 4 x 4 x 3 x 5 = 240

Capa convolucional separable: 4 x 4 x 3 + 3 x 5 = 48 + 15 = 63

Se deja como ejercicio para el lector calcular la cantidad de multiplicaciones necesarias para aplicar cada estilo de capa convolucional, que se ajusta de manera similar. Las convoluciones separables son más pequeñas y mucho más eficaces desde el punto de vista computacional.

Práctico

Reinicia el notebook de la zona de pruebas "Aprendizaje por transferencia", pero esta vez selecciona Xception como el modelo previamente entrenado. Xception solo usa convoluciones separables. Dejar todos los pesos entrenables Ajustaremos los pesos previamente entrenados en nuestros datos en lugar de usar las capas previamente entrenadas como tales.

c3df49e90e5a654f.png Keras Flowers transfer learning (playground).ipynb

Objetivo: Precisión > 95% (en serio, es posible)

Como este es el ejercicio final, requiere un poco más de código y trabajo de ciencia de datos.

Información adicional sobre el ajuste fino

Xception está disponible en los modelos previamente entrenados estándar en tf.keras.application.* No olvides dejar todos los pesos entrenables esta vez.

pretrained_model = tf.keras.applications.Xception(input_shape=[*IMAGE_SIZE, 3],
                                                  include_top=False)
pretrained_model.trainable = True

Para obtener buenos resultados cuando ajustes un modelo, deberás prestar atención a la tasa de aprendizaje y usar un programa de la tasa de aprendizaje con un período de aumento. Para ello, puedes escribir lo siguiente:

9b1af213b2b36d47.png

Comenzar con una tasa de aprendizaje estándar interrumpiría los pesos previamente entrenados del modelo. Comenzar de forma progresiva los conserva hasta que el modelo se haya ajustado a tus datos y pueda modificarlos de manera lógica. Después de la aceleración, puedes continuar con una tasa de aprendizaje constante o con una tasa de aprendizaje que disminuya de forma exponencial.

En Keras, la tasa de aprendizaje se especifica a través de una devolución de llamada en la que puedes calcular la tasa de aprendizaje adecuada para cada época. Keras pasará la tasa de aprendizaje correcta al optimizador para cada época.

def lr_fn(epoch):
  lr = ...
  return lr

lr_callback = tf.keras.callbacks.LearningRateScheduler(lr_fn, verbose=True)

model.fit(..., callbacks=[lr_callback])

Solución

Aquí está el notebook de solución. Puedes usarlo si no puedes avanzar.

c3df49e90e5a654f.png 07_Keras_Flowers_TPU_xception_fine_tuned_best.ipynb

Temas abordados

  • 🤔 Convolución separable en profundidad
  • 🤓 Programas de la tasa de aprendizaje
  • 😈 Ajustar un modelo entrenado previamente

Tómate un momento para revisar esta lista de tareas mentalmente.

13. ¡Felicitaciones!

Creaste tu primera red neuronal convolucional moderna y la entrenaste para que alcance una precisión superior al 90%, con iteraciones en ejecuciones de entrenamiento sucesivas en solo minutos gracias a las TPU.

TPUs en la práctica

Las TPU y las GPU están disponibles en Vertex AI de Google Cloud:

Por último, nos encanta recibir comentarios. Avísanos si ves algo incorrecto en este lab o si crees que se debería mejorar. Los comentarios se pueden proporcionar a través de los problemas de GitHub [vínculo de comentarios].

HR.png

Martin Görner ID small.jpg
El autor: Martin Görner
Twitter: @martin_gorner