Keras et des réseaux de neurones convolutifs modernes sur des TPU

1. Présentation

Dans cet atelier, vous allez apprendre à créer, entraîner et régler vos propres réseaux de neurones convolutifs en partant de zéro avec Keras et TensorFlow 2. Cela peut désormais se faire en quelques minutes grâce à la puissance des TPU. Vous explorerez également plusieurs approches, du simple apprentissage par transfert aux architectures convolutives modernes telles que Squeezenet. Cet atelier comprend des explications théoriques sur les réseaux de neurones et constitue un bon point de départ pour les développeurs qui découvrent le deep learning.

La lecture d'articles sur le deep learning peut être difficile et déroutante. Voyons comment fonctionnent les architectures de réseaux de neurones convolutifs modernes.

ca8cc21f6838eccc.png

Points abordés

  • Utiliser Keras et des Tensor Processing Units (TPU) pour créer vos modèles personnalisés plus rapidement
  • Utiliser l'API tf.data.Dataset et le format TFRecord pour charger efficacement les données d'entraînement
  • Pour tricher 🇦, en utilisant l'apprentissage par transfert au lieu de créer vos propres modèles.
  • Utiliser des styles de modèles Keras séquentiels et fonctionnels
  • Créer votre propre classificateur Keras avec une couche softmax et une perte de cross-entropy
  • Pour affiner votre modèle avec un bon choix de couches convolutives.
  • Pour explorer des idées d'architecture de réseau de neurones convolutif moderne telles que les modules, le pooling moyen global, etc.
  • Créer un convnet moderne simple à l'aide de l'architecture Squeezenet

Commentaires

Si vous remarquez quelque chose d'anormal dans cet atelier de programmation, veuillez nous en informer. Vous pouvez envoyer vos commentaires via les problèmes GitHub [ lien de commentaires].

2. Guide de démarrage rapide de Google Colaboratory

Cet atelier utilise Google Colaboratory et ne nécessite aucune configuration de votre part. Vous pouvez l'exécuter depuis un Chromebook. Veuillez ouvrir le fichier ci-dessous et exécuter les cellules pour vous familiariser avec les notebooks Colab.

c3df49e90e5a654f.png Welcome to Colab.ipynb

Sélectionner un backend TPU

8832c6208c99687d.png

Dans le menu Colab, sélectionnez Environnement d'exécution > Modifier le type d'exécution, puis sélectionnez "TPU". Dans cet atelier de programmation, vous allez utiliser un TPU (Tensor Processing Unit) puissant compatible avec l'entraînement accéléré par matériel. La connexion à l'environnement d'exécution se fait automatiquement lors de la première exécution. Vous pouvez également utiliser le bouton "Connect" (Connecter) en haut à droite.

Exécution du notebook

76d05caa8b4db6da.png

Exécutez les cellules une par une en cliquant sur une cellule et en appuyant sur Maj+Entrée. Vous pouvez également exécuter l'intégralité du notebook en sélectionnant Environnement d'exécution > Tout exécuter.

Sommaire

429f106990037ec4.png

Tous les notebooks comportent une table des matières. Vous pouvez l'ouvrir à l'aide de la flèche noire située à gauche.

Cellules masquées

edc3dba45d26f12a.png

Certaines cellules n'affichent que leur titre. Il s'agit d'une fonctionnalité de notebook propre à Colab. Vous pouvez double-cliquer dessus pour voir le code à l'intérieur, mais ce n'est généralement pas très intéressant. Elles sont généralement associées à des fonctions d'assistance ou de visualisation. Vous devez quand même exécuter ces cellules pour que les fonctions qu'elles contiennent soient définies.

Authentification

cdd4b41413100543.png

Colab peut accéder à vos buckets Google Cloud Storage privés à condition que vous vous authentifiiez avec un compte autorisé. L'extrait de code ci-dessus déclenche un processus d'authentification.

3. [INFO] What are Tensor Processing Units (TPUs) ?

En résumé

f88cf6facfc70166.png

Le code pour entraîner un modèle sur TPU dans Keras (et utiliser le GPU ou le processeur si aucun TPU n'est disponible):

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=...)

Nous allons aujourd'hui utiliser des TPU pour créer et optimiser un classificateur de fleurs à des vitesses interactives (minutes par exécution d'entraînement).

688858c21e3beff2.png

Pourquoi utiliser des TPU ?

Les GPU modernes sont organisés autour de "noyaux" programmables, une architecture très flexible qui leur permet de gérer diverses tâches telles que le rendu 3D, le deep learning, les simulations physiques, etc. À l'inverse, les TPU associent un processeur vectoriel classique à une unité de multiplication matricielle dédiée et excellent dans toute tâche où de grandes multiplications de matrices dominent, comme les réseaux de neurones.

8eb3e718b8e2ed08.png

Illustration : une couche de réseau de neurones dense en tant que multiplication matricielle, avec un lot de huit images traitées par le réseau de neurones en même temps. Veuillez effectuer une multiplication ligne x colonne pour vérifier qu'il s'agit bien d'une somme pondérée de toutes les valeurs de pixels d'une image. Les couches convolutives peuvent également être représentées par des multiplications matricielles, bien que cela soit un peu plus compliqué ( explication ici, section 1).

Le matériel

MXU et VPU

Un cœur de TPU v2 est composé d'une unité matricielle (MXU, Matrix Multiply Unit) qui exécute les multiplications matricielles et d'une unité de traitement vectoriel (VPU) pour toutes les autres tâches telles que les activations, softmax, etc. Le VPU gère les calculs float32 et int32. Les unités matricielles, quant à elles, fonctionnent dans un format à virgule flottante 16-32 bits de précision mixte.

7d68944718f76b18.png

Précision mixte à virgule flottante et bfloat16

La MXU calcule les multiplications matricielles à l'aide d'entrées bfloat16 et de sorties float32. Les accumulations intermédiaires sont effectuées avec une précision de type float32.

19c5fc432840c714.png

L'entraînement des réseaux de neurones est généralement résistant au bruit introduit par une précision réduite à virgule flottante. Dans certains cas, le bruit aide même l'optimiseur à converger. La précision à virgule flottante 16 bits est traditionnellement utilisée pour accélérer les calculs, mais les formats float16 et float32 ont des plages très différentes. Réduire la précision de float32 à float16 se traduit généralement par des sur-débits et des dépassements de capacité insuffisants. Des solutions existent, mais un travail supplémentaire est généralement nécessaire pour que float16 fonctionne.

C'est pourquoi Google a introduit le format bfloat16 dans les TPU. bfloat16 est une valeur float32 tronquée avec exactement les mêmes bits d'exposant et la même plage que float32. En plus du fait que les TPU calculent les multiplications matricielles en précision mixte avec des entrées bfloat16 mais en sorties float32, aucune modification du code n'est généralement nécessaire pour bénéficier des gains de performances liés à une précision réduite.

Tableau systolique

L'unité matricielle implémente les multiplications matricielles dans le matériel à l'aide d'une architecture dite "tableau systolique" dans laquelle les éléments de données circulent à travers un tableau d'unités de calcul matérielles. (En médecine, le terme « systolique » fait référence aux contractions cardiaques et au flux sanguin, ici au flux de données.)

L'élément de base d'une multiplication matricielle est un produit scalaire entre une ligne d'une matrice et une colonne de l'autre matrice (voir l'illustration en haut de cette section). Pour une multiplication matricielle Y=X*W, un élément du résultat serait :

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]

Sur un GPU, on programme ce produit scalaire dans un "cœur" de GPU, puis on l'exécute en parallèle sur autant de "cœurs" que possible pour essayer de calculer toutes les valeurs de la matrice résultante en même temps. Si la matrice obtenue a une taille de 128 x 128, il faudrait disposer de 128 x 128=16 000 cœurs pour être disponible, ce qui n'est généralement pas possible. Les GPU les plus grands comptent environ 4 000 cœurs. En revanche, un TPU utilise le strict minimum de matériel pour les unités de calcul de l'unité matricielle: seulement bfloat16 x bfloat16 => float32 accumulateurs de multiplication, rien d'autre. Elles sont si petites qu'un TPU peut en implémenter 16K dans une unité matricielle de 128 x 128 et traiter cette multiplication matricielle en une seule fois.

f1b283fc45966717.gif

Illustration : tableau systolique de l'unité matricielle. Les éléments de calcul sont des multiplicateurs-accumulateurs. Les valeurs d'une matrice sont chargées dans le tableau (points rouges). Les valeurs de l'autre matrice circulent dans le tableau (points gris). Les lignes verticales propagent les valeurs vers le haut. Les lignes horizontales propagent les sommes partielles. Il revient à l'utilisateur de vérifier qu'au fur et à mesure que les données circulent dans le tableau, vous obtenez le résultat de la multiplication matricielle depuis le côté droit.

De plus, pendant que les produits scalaires sont calculés dans une unité matricielle, les sommes intermédiaires circulent simplement entre les unités de calcul adjacentes. Ils n'ont pas besoin d'être stockés et récupérés à partir de la mémoire ni même d'un fichier de registre. Au final, l'architecture de tableau systolique des TPU présente un avantage significatif en termes de densité et de puissance, ainsi qu'un avantage non négligeable en termes de vitesse par rapport à un GPU lors du calcul des multiplications matricielles.

Cloud TPU

Lorsque vous demandez un Cloud TPU v2 sur Google Cloud Platform, vous obtenez une machine virtuelle (VM) dotée d'une carte TPU PCI. La carte TPU comporte quatre puces TPU à deux cœurs. Chaque cœur de TPU comporte un VPU (Vector Processing Unit) et une unité de multiplication matriX de 128 x 128 MXU. Ce "Cloud TPU" est ensuite généralement connecté via le réseau à la VM qui l'a demandé. La vue d'ensemble ressemble donc à ceci:

dfce5522ed644ece.png

Illustration : votre VM avec un accélérateur "Cloud TPU" connecté au réseau. "The Cloud TPU" est lui-même composé d'une VM avec une carte TPU PCI et quatre puces TPU à deux cœurs.

Pods TPU

Dans les centres de données de Google, les TPU sont connectés à une interconnexion de calcul hautes performances (HPC, High Performance Computing), qui peut les faire apparaître comme un accélérateur très important. Ils sont appelés "pods", et peuvent englober jusqu'à 512 cœurs de TPU v2 ou 2 048 cœurs de TPU v3.

2ec1e0d341e7fc34.jpeg

Illustration: un pod TPU v3. Racks et cartes TPU connectés via une interconnexion HPC.

Pendant l'entraînement, les gradients sont échangés entre les cœurs de TPU à l'aide de l'algorithme all-reduce ( une bonne explication ici). Le modèle en cours d'entraînement peut exploiter le matériel en s'appuyant sur de grands lots.

d97b9cc5d40fdb1d.gif

Illustration: synchronisation des gradients lors de l'entraînement à l'aide de l'algorithme all-reduce sur le réseau HPC de maillage toroïdal 2D de Google TPU

Logiciel

Entraînement de lots de grande taille

La taille de lot idéale pour les TPU est de 128 éléments de données par cœur de TPU, mais le matériel peut déjà afficher une bonne utilisation à partir de 8 éléments de données par cœur de TPU. N'oubliez pas qu'un Cloud TPU comporte huit cœurs.

Dans cet atelier de programmation, nous allons utiliser l'API Keras. Dans Keras, le lot que vous spécifiez correspond à la taille de lot globale pour l'ensemble du TPU. Vos lots seront automatiquement divisés en huit et exécutés sur les huit cœurs du TPU.

da534407825f01e3.png

Pour obtenir d'autres conseils sur les performances, consultez le Guide sur les performances des TPU. Pour les très grandes tailles de lot, une attention particulière peut être nécessaire dans certains modèles. Pour en savoir plus, consultez LARSOptimizer.

En arrière-plan : XLA

Les programmes TensorFlow définissent des graphes de calcul. Le TPU n'exécute pas directement le code Python, mais le graphe de calcul défini par votre programme TensorFlow. Sous le capot, un compilateur appelé XLA (compilateur d'algèbre linéaire accélérée) transforme le graphe TensorFlow des nœuds de calcul en code machine TPU. Ce compilateur effectue également de nombreuses optimisations avancées sur votre code et votre mise en page de la mémoire. La compilation se produit automatiquement lorsque le travail est envoyé au TPU. Vous n'avez pas besoin d'inclure explicitement XLA dans votre chaîne de compilation.

edce61112cd57972.png

Illustration : pour s'exécuter sur un TPU, le graphe de calcul défini par votre programme TensorFlow est d'abord converti en représentation XLA (compilateur d'algèbre linéaire accélérée), puis compilé par XLA en code machine TPU.

Utiliser des TPU dans Keras

Les TPU sont compatibles avec l'API Keras depuis TensorFlow 2.1. La compatibilité avec Keras fonctionne sur les TPU et les pods TPU. Voici un exemple qui fonctionne sur les TPU, les GPU et les processeurs:

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=...)

Dans cet extrait de code:

  • TPUClusterResolver().connect() trouve le TPU sur le réseau. Il fonctionne sans paramètres sur la plupart des systèmes Google Cloud (jobs AI Platform, Colaboratory, Kubeflow, VM de deep learning créées via l'utilitaire "ctpu up"). Ces systèmes savent où se trouve leur TPU grâce à une variable d'environnement TPU_NAME. Si vous créez un TPU manuellement, définissez la variable d'environnement TPU_NAME sur la VM à partir de laquelle vous l'utilisez ou appelez TPUClusterResolver avec des paramètres explicites : TPUClusterResolver(tp_uname, zone, project)
  • TPUStrategy est la partie qui implémente l'algorithme de synchronisation de gradient "all-reduce" ;
  • La stratégie est appliquée via une portée. Le modèle doit être défini dans le champ d'application de la stratégie (scope()).
  • La fonction tpu_model.fit attend un objet tf.data.Dataset en entrée pour l'entraînement TPU.

Tâches de portage TPU courantes

  • Bien qu'il existe de nombreuses façons de charger des données dans un modèle TensorFlow, pour les TPU, l'utilisation de l'API tf.data.Dataset est requise.
  • Les TPU sont très rapides et l'ingestion de données devient souvent le goulot d'étranglement lors de leur exécution. Le guide sur les performances TPU propose des outils permettant de détecter les goulots d'étranglement des données, ainsi que d'autres conseils de performances.
  • Les nombres int8 ou int16 sont traités comme des int32. Le TPU ne dispose pas de matériel entier fonctionnant sur moins de 32 bits.
  • Certaines opérations TensorFlow ne sont pas compatibles. Cliquez ici pour consulter la liste. La bonne nouvelle est que cette limitation ne s'applique qu'au code d'entraînement, c'est-à-dire à la propagation avant et arrière de votre modèle. Vous pouvez toujours utiliser toutes les opérations Tensorflow dans votre pipeline d'entrée de données, car elles seront exécutées sur le processeur.
  • tf.py_func n'est pas compatible avec TPU.

4. Chargement des données…

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

Nous allons travailler avec un ensemble de données de photos de fleurs. L'objectif est d'apprendre à les classer en cinq types de fleurs. Le chargement des données est effectué à l'aide de l'API tf.data.Dataset. Tout d'abord, familiarisez-vous avec l'API.

Pratique

Veuillez ouvrir le notebook suivant, exécuter les cellules (Maj+Entrée) et suivre les instructions chaque fois que le libellé "TRAVAIL REQUIS" s'affiche.

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

Informations supplémentaires

À propos de l'ensemble de données "flowers"

Le jeu de données est organisé en cinq dossiers. Chaque dossier contient des fleurs d'un même type. Les dossiers sont appelés "tournesols", "marguerite", "pissenlit", "tulipes" et "roses". Les données sont hébergées dans un bucket public sur Google Cloud Storage. Extrait:

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

Pourquoi tf.data.Dataset ?

Keras et TensorFlow acceptent les ensembles de données dans toutes leurs fonctions d'entraînement et d'évaluation. Une fois que vous avez chargé des données dans un ensemble de données, l'API offre toutes les fonctionnalités courantes utiles pour les données d'entraînement de réseaux de neurones:

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

Vous trouverez des conseils sur les performances et les bonnes pratiques concernant les ensembles de données dans cet article. Cliquez ici pour accéder à la documentation de référence.

Principes de base de tf.data.Dataset

Les données sont généralement fournies dans plusieurs fichiers, ici des images. Vous pouvez créer un ensemble de données de noms de fichiers en appelant :

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

Vous "mappez" ensuite une fonction sur chaque nom de fichier, qui chargera et décodera généralement le fichier en données réelles dans la mémoire :

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)

Pour itérer sur un ensemble de données:

for data in my_dataset:
  print(data)

Ensembles de données de tupels

Dans l'apprentissage supervisé, un ensemble de données d'entraînement est généralement constitué de paires de données d'entraînement et de réponses correctes. Pour ce faire, la fonction de décodage peut renvoyer des tuples. Vous disposerez alors d'un ensemble de données composé de tuples. Les tuples seront renvoyés lorsque vous effectuerez une itération sur celui-ci. Les valeurs renvoyées sont des Tensors TensorFlow prêts à être utilisés par votre modèle. Vous pouvez appeler .numpy() dessus pour afficher les valeurs brutes :

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())

Conclusion : Le chargement d'images une à une est lent.

À mesure que vous itérez sur cet ensemble de données, vous verrez que vous pouvez charger quelque chose comme 1 à 2 images par seconde. C'est trop lent ! Les accélérateurs matériels que nous utiliserons pour l'entraînement peuvent atteindre plusieurs fois ce taux. Passez à la section suivante pour découvrir comment procéder.

Solution

Voici le notebook de la solution. Vous pouvez l'utiliser si vous êtes bloqué.

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

Points abordés

  • 🤔 tf.data.Dataset.list_files
  • 🤔 tf.data.Dataset.map
  • 🤔 Ensembles de données de tuples
  • 😀 l'itération sur les ensembles de données

Veuillez prendre quelques instants pour passer en revue cette checklist.

5. Chargement rapide des données

Les accélérateurs matériels TPU (Tensor Processing Unit) que nous utiliserons dans cet atelier sont très rapides. Le défi consiste souvent à leur fournir des données suffisamment rapidement pour les occuper. Google Cloud Storage (GCS) peut assurer un débit très élevé, mais comme pour tous les systèmes de stockage cloud, l'établissement d'une connexion entraîne des échanges réseau. Par conséquent, stocker nos données sous la forme de milliers de fichiers individuels n'est pas idéal. Nous allons les regrouper dans un plus petit nombre de fichiers et utiliser la puissance de tf.data.Dataset pour lire plusieurs fichiers en parallèle.

Lecture

Le code qui charge les fichiers image, les redimensionne à une taille commune, puis les stocke dans 16 fichiers TFRecord se trouve dans le notebook suivant. Veuillez le lire rapidement. Il n'est pas nécessaire de l'exécuter, car des données correctement mises en forme au format TFRecord seront fournies pour le reste de l'atelier de programmation.

c3df49e90e5a654f.png Flower pictures to TFRecords.ipynb

Disposition de données idéale pour un débit GCS optimal

Format de fichier TFRecord

Le format de fichier préféré de TensorFlow pour stocker des données est le format TFRecord basé sur protobuf. D'autres formats de sérialisation fonctionneraient également, mais vous pouvez charger un jeu de données directement à partir de fichiers TFRecord en écrivant:

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

Pour des performances optimales, nous vous recommandons d'utiliser le code plus complexe suivant pour lire plusieurs fichiers TFRecord à la fois. Ce code lit les N fichiers en parallèle et ignore l'ordre des données afin d'améliorer la vitesse de lecture.

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

Aide-mémoire TFRecord

Trois types de données peuvent être stockés dans des TFRecords : des chaînes d'octets (liste d'octets), des entiers 64 bits et des flottants 32 bits. Elles sont toujours stockées sous forme de listes. Un seul élément de données correspond à une liste de taille 1. Vous pouvez utiliser les fonctions d'assistance suivantes pour stocker des données dans des TFRecords.

écrire des chaînes d'octets

# 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))

écrire des entiers

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

écrire des floats

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

écrire un TFRecord à l'aide des outils d'assistance ci-dessus ;

# 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())

Pour lire des données à partir de TFRecords, vous devez d'abord déclarer la mise en page des enregistrements que vous avez stockés. Dans la déclaration, vous pouvez accéder à n'importe quel champ nommé en tant que liste à longueur fixe ou à longueur variable :

lecture à partir 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)

Extraits de code utiles:

lire des éléments de données individuels ;

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

lire des listes d'éléments à taille fixe

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

Lire un nombre variable d'éléments de données

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 renvoie un vecteur creux et une étape supplémentaire est requise après le décodage du TFRecord:

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

Il est également possible d'avoir des champs facultatifs dans les fichiers TFRecord. Si vous spécifiez une valeur par défaut lors de la lecture d'un champ, la valeur par défaut est renvoyée au lieu d'une erreur si le champ est manquant.

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

Points abordés

  • 🤔 Diviser les fichiers de données en plusieurs parties pour un accès rapide depuis GCS
  • 😓 Comment écrire des TFRecords. (Vous avez déjà oublié la syntaxe ? Pas de problème, ajoutez cette page à vos favoris pour vous en souvenir.)
  • 🤔 Charger un ensemble de données à partir de TFRecords avec TFRecordDataset

Veuillez prendre un moment pour passer en revue cette liste de contrôle.

6. [INFO] Principes de base du classificateur de réseaux de neurones

En résumé

Si vous connaissez déjà tous les termes en gras dans le paragraphe suivant, vous pouvez passer à l'exercice suivant. Si vous débutez dans le deep learning, bienvenue. Veuillez poursuivre la lecture.

Pour les modèles créés sous la forme d'une séquence de couches, Keras propose l'API Sequential. Par exemple, un classificateur d'images utilisant trois couches denses peut être écrit en Keras comme suit:

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

Réseau de neurones dense

Il s'agit du réseau de neurones le plus simple pour classer des images. Elle est constituée de "neurones" disposés en couches. La première couche traite les données d'entrée et transmet ses sorties à d'autres couches. Elle est appelée "dense", car chaque neurone est connecté à tous les neurones de la couche précédente.

c21bae6dade487bc.png

Vous pouvez alimenter un tel réseau en aplatissant les valeurs RVB de tous ses pixels dans un vecteur long et en les utilisant comme entrées. Ce n'est pas la meilleure technique de reconnaissance d'image, mais nous l'améliorerons plus tard.

Neurones, activations, ReLU

Un "neurone" calcule une somme pondérée de toutes ses entrées, ajoute une valeur appelée "biais" et alimente le résultat via une "fonction d'activation". Les pondérations et le biais sont inconnus au début. Ils seront initialisés de manière aléatoire et "appris" en entraînant le réseau de neurones sur de nombreuses données connues.

644f4213a4ee70e5.png

La fonction d'activation la plus populaire est appelée RELU, ou unité de rectification linéaire. Il s'agit d'une fonction très simple, comme vous pouvez le voir sur le graphique ci-dessus.

Activation Softmax

Le réseau ci-dessus se termine par une couche à cinq neurones, car nous classons les fleurs en cinq catégories (rose, tulipe, pissenlit, marguerite, tournesol). Les neurones des couches intermédiaires sont activés à l'aide de la fonction d'activation RELU classique. Dans la dernière couche, nous voulons cependant calculer des nombres compris entre 0 et 1 représentant la probabilité que cette fleur soit une rose, une tulipe, etc. Pour cela, nous allons utiliser une fonction d'activation appelée "softmax".

Pour appliquer la fonction softmax à un vecteur, vous devez prendre l'exponentielle de chaque élément, puis normaliser le vecteur, généralement à l'aide de la norme L1 (somme des valeurs absolues) afin que les valeurs totalisent 1 et puissent être interprétées comme des probabilités.

ef0d98c0952c262d.png d51252f75894479e.gif

Perte d'entropie croisée

Maintenant que notre réseau de neurones produit des prédictions à partir des images d'entrée, nous devons mesurer leur qualité, c'est-à-dire la distance entre ce que le réseau nous dit et les bonnes réponses, souvent appelées "libellés". N'oubliez pas que nous disposons d'étiquettes correctes pour toutes les images de l'ensemble de données.

N'importe quelle distance conviendrait, mais pour les problèmes de classification, la "distance d'entropie croisée" est la plus efficace. Nous appellerons cela notre fonction d'erreur ou "loss" :

7bdf8753d20617fb.png

Descente de gradient

"Entraîner" le réseau de neurones consiste en fait à utiliser des images et des étiquettes d'entraînement pour ajuster les pondérations et les biais afin de minimiser la fonction de perte d'entropie croisée. Voici comment cela fonctionne.

L'entropie croisée est une fonction des pondérations, des biais, des pixels de l'image d'entraînement et de sa classe connue.

Si nous calculons les dérivées partielles de l'entropie croisée par rapport à tous les poids et à tous les biais, nous obtenons un "gradient", calculé pour une image, un libellé et une valeur actuelle donnés des poids et des biais. N'oubliez pas que nous pouvons avoir des millions de poids et de biais. Le calcul du gradient semble donc être une tâche ardue. Heureusement, Tensorflow s'en charge pour nous. La propriété mathématique d'un dégradé est qu'il pointe "vers le haut". Comme nous voulons aller là où l'entropie croisée est faible, nous allons dans la direction opposée. Nous mettons à jour les pondérations et les biais selon une fraction du gradient. Nous répétons ensuite la même chose à l'aide des prochains lots d'images et de libellés d'entraînement, dans une boucle d'entraînement. Espérons que cela converge vers un point où l'entropie croisée est minimale, même si rien ne garantit que ce minimum est unique.

gradient descent2.png

Mini-lot et momentum

Vous pouvez calculer votre gradient sur un seul exemple d'image et mettre à jour les poids et les biais immédiatement, mais le faire sur un lot de 128 images, par exemple, donne un gradient qui représente mieux les contraintes imposées par les différents exemples d'images et est donc susceptible de converger vers la solution plus rapidement. La taille du mini-lot est un paramètre ajustable.

Cette technique, parfois appelée "descente de gradient stochastique", présente un autre avantage plus pragmatique: travailler avec des lots implique également de travailler avec des matrices plus grandes, qui sont généralement plus faciles à optimiser sur les GPU et les TPU.

La convergence peut toutefois être un peu chaotique et peut même s'arrêter si le vecteur de gradient est entièrement nul. Cela signifie-t-il que nous avons trouvé un minimum ? Non. Un composant de dégradé peut être nul à un minimum ou à un maximum. Avec un vecteur de gradient contenant des millions d'éléments, si tous sont des zéros, la probabilité que chaque zéro corresponde à un minimum et qu'aucun ne corresponde à un point maximal est assez faible. Dans un espace comportant de nombreuses dimensions, les points d'arrêt sont assez courants et nous ne voulons pas nous en arrêter là.

52e824fe4716c4a0.png

Illustration: un point de selle. Le dégradé est de 0, mais ce n'est pas un minimum dans toutes les directions. (Attribution de l'image : Wikimedia : par Nicoguaro - Propriété, CC BY 3.0)

La solution consiste à donner une dynamique à l'algorithme d'optimisation afin qu'il puisse passer en selle sans s'arrêter.

Glossaire

lot ou mini-lot: l'entraînement est toujours effectué sur des lots de données d'entraînement et d'étiquettes. Cela permet à l'algorithme de converger. La dimension "lot" est généralement la première dimension des tenseurs de données. Par exemple, un Tensor de forme [100, 192, 192, 3] contient 100 images de 192 x 192 pixels avec trois valeurs par pixel (RVB).

perte entropie croisée : fonction de perte spéciale souvent utilisée dans les classificateurs.

couche dense: couche de neurones où chaque neurone est connecté à tous les neurones de la couche précédente.

caractéristiques : les entrées d'un réseau de neurones sont parfois appelées "caractéristiques". L'art de déterminer quelles parties d'un ensemble de données (ou combinaisons de parties) doivent être transmises à un réseau de neurones pour obtenir de bonnes prédictions s'appelle l'ingénierie des caractéristiques.

Libellés : autre nom des "classes" ou des réponses correctes dans un problème de classification supervisée

Taux d'apprentissage: fraction du gradient par laquelle les pondérations et les biais sont mis à jour à chaque itération de la boucle d'entraînement.

Logits : les sorties d'une couche de neurones avant l'application de la fonction d'activation sont appelées "logits". Ce terme vient de "fonction logistique" ou "fonction sigmoïde" qui était autrefois la fonction d'activation la plus populaire. L'expression "sorties de neurones avant la fonction logistique" a été raccourcie en "logits".

perte : fonction d'erreur qui compare les sorties du réseau de neurones aux bonnes réponses

neurone : calcule la somme pondérée de ses entrées, ajoute un biais et transmet le résultat via une fonction d'activation.

Encodage one-hot: la classe 3 sur 5 est encodée sous la forme d'un vecteur de cinq éléments, tous des zéros à l'exception du troisième, qui est égal à 1.

relu : unité de rectification linéaire. Fonction d'activation populaire pour les neurones.

sigmoïde: autre fonction d'activation populaire et qui reste utile dans des cas particuliers.

softmax: fonction d'activation spéciale qui agit sur un vecteur, augmente la différence entre la composante la plus grande et les autres, et normalise également le vecteur pour obtenir une somme égale à 1, de sorte qu'il puisse être interprété comme un vecteur de probabilités. Utilisé comme dernière étape des classificateurs.

tensor : un "tensor" est comme une matrice, mais avec un nombre arbitraire de dimensions. Un Tensor unidimensionnel est un vecteur. Un tenseur à deux dimensions est une matrice. Vous pouvez aussi avoir des Tensors ayant au moins 3, 4, 5 dimensions ou plus.

7. Apprentissage par transfert

Pour un problème de classification d'images, les couches denses ne suffiront probablement pas. Nous devons nous familiariser avec les couches convolutives et les nombreuses façons de les organiser.

Mais nous pouvons aussi prendre un raccourci. Des réseaux de neurones convolutifs entièrement entraînés sont disponibles en téléchargement. Vous pouvez supprimer la dernière couche, la tête de classification softmax, et la remplacer par la vôtre. Tous les biais et pondérations entraînés restent inchangés. Vous ne réentraînez que la couche softmax que vous ajoutez. C'est ce qu'on appelle l'apprentissage par transfert. Étonnamment, elle fonctionne tant que l'ensemble de données sur lequel le réseau de neurones est pré-entraîné est "suffisamment proche" du vôtre.

Pratique

Veuillez ouvrir le notebook suivant, exécuter les cellules (Maj+Entrée) et suivre les instructions chaque fois que le libellé "TRAVAIL REQUIS" s'affiche.

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

Informations supplémentaires

L'apprentissage par transfert vous permet de bénéficier à la fois d'architectures avancées de réseaux de neurones convolutifs développées par des chercheurs de renom et d'un pré-entraînement sur un énorme ensemble de données d'images. Dans notre cas, nous allons effectuer un transfert d'apprentissage à partir d'un réseau entraîné sur ImageNet, une base de données d'images contenant de nombreuses plantes et scènes extérieures, ce qui est assez proche des fleurs.

b8fc1efd2001f072.png

Illustration: utilisation d'un réseau de neurones convolutif complexe, déjà entraîné, sous la forme d'une boîte noire, pour ne réentraîner que la tête de classification. Il s'agit de l'apprentissage par transfert. Nous verrons plus tard comment fonctionnent ces dispositions complexes de couches convolutives. Pour l'instant, c'est le problème de quelqu'un d'autre.

Apprentissage par transfert dans Keras

Dans Keras, vous pouvez instancier un modèle pré-entraîné à partir de la collection tf.keras.applications.*. MobileNet V2, par exemple, est une très bonne architecture convolutive dont la taille reste raisonnable. En sélectionnant include_top=False, vous obtenez le modèle pré-entrainé sans sa dernière couche softmax afin de pouvoir ajouter la vôtre :

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')
])

Notez également le paramètre pretrained_model.trainable = False. Il fige les poids et les biais du modèle pré-entraîné afin que vous n'entraînez que votre couche softmax. Cela implique généralement relativement peu de poids et peut être effectué rapidement et sans nécessiter un très grand ensemble de données. Toutefois, si vous avez beaucoup de données, l'apprentissage par transfert peut encore mieux fonctionner avec pretrained_model.trainable = True. Les poids pré-entraînés fournissent alors d'excellentes valeurs initiales et peuvent toujours être ajustés par l'entraînement pour mieux s'adapter à votre problème.

Enfin, notez que la couche Flatten() s'est insérée avant la couche dense softmax. Les couches denses fonctionnent sur des vecteurs de données plats, mais nous ne savons pas si c'est ce que le modèle pré-entraîné renvoie. C'est pourquoi nous devons aplatir. Dans le chapitre suivant, nous allons nous intéresser aux architectures convolutives et expliquer le format de données renvoyé par les couches convolutives.

Avec cette approche, vous devriez obtenir une précision proche de 75 %.

Solution

Voici le notebook de la solution. Vous pouvez l'utiliser si vous êtes bloqué.

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

Points abordés

  • 🤔 Comment écrire un classificateur dans Keras
  • 🤓 configuré avec une dernière couche softmax et une perte d'entropie croisée
  • 😈 Apprentissage par transfert
  • 🤔 Entraîner votre premier modèle
  • 🧐 Suivre sa perte et sa justesse pendant l'entraînement

Veuillez prendre quelques instants pour passer en revue cette checklist.

8. [INFO] Réseaux de neurones convolutifs

En résumé

Si vous connaissez déjà tous les termes en gras du paragraphe suivant, vous pouvez passer à l'exercice suivant. Si vous débutez avec les réseaux de neurones convolutifs, poursuivez votre lecture.

convolutional.gif

Illustration: filtrer une image avec deux filtres successifs composés chacun de 4x4x3=48 pondérations apprises.

Voici à quoi ressemble un réseau de neurones convolutif simple dans 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

Introduction aux réseaux de neurones convolutifs

Dans une couche d'un réseau convolutif, un "neurone" effectue une somme pondérée des pixels juste au-dessus, sur une petite zone de l'image uniquement. Elle ajoute un biais et alimente la somme via une fonction d'activation, tout comme le ferait un neurone d'une couche dense standard. Cette opération est ensuite répétée sur l'ensemble de l'image à l'aide des mêmes poids. N'oubliez pas que dans les couches denses, chaque neurone avait ses propres poids. Ici, un seul "bloc" de pondérations glisse sur l'image dans les deux sens (une "convolution"). La sortie comporte autant de valeurs qu'il y a de pixels dans l'image (un remplissage est toutefois nécessaire au niveau des bords). Il s'agit d'une opération de filtrage utilisant un filtre de 4x4x3=48 pondérations.

Toutefois, 48 poids ne suffiront pas. Pour ajouter d'autres degrés de liberté, nous répétons la même opération avec un nouvel ensemble de poids. Vous obtenez ainsi un nouvel ensemble de sorties de filtre. Appelons-le "canal" de sorties par analogie avec les canaux R, G et B de l'image d'entrée.

Screen Shot 2016-07-29 at 16.02.37.png

Les deux (ou plusieurs) ensembles de poids peuvent être résumés en un seul tenseur en ajoutant une nouvelle dimension. Cela nous donne la forme générique du tenseur de poids pour une couche convolutive. Le nombre de canaux d'entrée et de sortie étant des paramètres, nous pouvons commencer à empiler et à chaîner des couches convolutives.

d1b557707bcd1cb9.png

Illustration : un réseau de neurones convolutifs transforme des "cubes" de données en d'autres "cubes" de données.

Convolutions avec pas, pooling max

En effectuant les convolutions avec un pas de 2 ou 3, nous pouvons également réduire le cube de données obtenu dans ses dimensions horizontales. Vous disposez pour cela de deux méthodes courantes:

  • Convolusion avec pas: filtre coulissant comme ci-dessus, mais avec un pas supérieur à 1
  • Max pooling: fenêtre mobile qui applique l'opération MAX (généralement sur des patches 2x2, répétés tous les deux pixels)

2b2d4263bb8470b.gif

Illustration: le fait de faire glisser la fenêtre de calcul de 3 pixels réduit le nombre de valeurs de sortie. Les convolutions stylisées ou le pooling maximal (max. sur une fenêtre de 2x2 glissant d'un pas de 2) sont un moyen de réduire le cube de données dans les dimensions horizontales.

Classificateur cvolutif

Enfin, nous associons une tête de classification en aplatissant le dernier cube de données et en l'alimentant via une couche dense activée par softmax. Un classificateur convolutif type peut se présenter comme suit:

4a61aaffb6cba3d1.png

Illustration: un classificateur d'images utilisant des couches convolutives et softmax. Elle utilise les filtres 3x3 et 1x1. Les couches "maxpool" prennent le nombre maximal de groupes de 2 x 2 points de données. La tête de classification est implémentée avec une couche dense avec activation softmax.

Dans Keras

La pile convolutive illustrée ci-dessus peut être écrite dans Keras comme suit:

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. Votre réseau de neurones convolutif personnalisé

Pratique

Créons et entraînons un réseau de neurones convolutif de A à Z. L'utilisation d'un TPU nous permettra d'itérer très rapidement. Veuillez ouvrir le notebook suivant, exécuter les cellules (Maj+Entrée) et suivre les instructions chaque fois que le libellé "TRAVAIL REQUIS" s'affiche.

c3df49e90e5a654f.png Keras_Flowers_TPU (playground).ipynb

L'objectif est de battre la justesse de 75% du modèle d'apprentissage par transfert. Ce modèle avait un avantage, car il avait été pré-entraîné sur un ensemble de données de millions d'images, alors que nous n'en avons que 3 670 ici. Pouvez-vous au moins l'égaler ?

Informations supplémentaires

Combien de couches, quelle taille ?

La sélection des tailles de calques relève plus de l'art que de la science. Vous devez trouver le bon équilibre entre un nombre insuffisant et un nombre excessif de paramètres (pondérations et biais). Avec un poids insuffisant, le réseau de neurones ne peut pas représenter la complexité des formes de fleurs. S'il y en a trop, le modèle peut être sujet à une "suradaptation", c'est-à-dire qu'il se spécialise sur les images d'entraînement et ne peut pas généraliser. Avec un grand nombre de paramètres, l'entraînement du modèle sera également lent. Dans Keras, la fonction model.summary() affiche le nombre de structures et de paramètres de votre modèle:

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
_________________________________________________________________

Voici quelques conseils:

  • C'est la présence de plusieurs couches qui rend les réseaux de neurones "profonds" efficaces. Pour ce problème simple de reconnaissance des fleurs, il est judicieux d'utiliser 5 à 10 couches.
  • Utilisez des filtres de petite taille. En général, les filtres 3x3 sont efficaces partout.
  • Vous pouvez également utiliser des filtres 1x1, qui sont peu coûteux. Ils ne "filtrent" rien, mais calculent des combinaisons linéaires de canaux. Alternez-les avec des filtres réels. (Plus d'informations sur les "convolutions 1x1" dans la section suivante.)
  • Pour un problème de classification comme celui-ci, sous-échantillonnez fréquemment avec des couches de pooling maximal (ou des convolutions avec un pas supérieur à 1). Vous ne vous souciez pas de l'emplacement de la fleur, mais seulement du fait qu'il s'agit d'une rose ou d'un pissenlit. Il n'est donc pas important de perdre les informations X et Y, et le filtrage de petites zones est moins coûteux.
  • Le nombre de filtres devient généralement similaire au nombre de classes à la fin du réseau (pourquoi ? voir l'astuce "pooling moyen global" ci-dessous). Si vous classez vos données en plusieurs centaines de classes, augmentez progressivement le nombre de filtres par couches consécutives. Pour l'ensemble de données sur les fleurs à 5 classes, un filtrage avec seulement 5 filtres ne suffira pas. Vous pouvez utiliser le même nombre de filtres dans la plupart des calques (par exemple, 32) et le réduire vers la fin.
  • La ou les couches denses finales sont coûteuses. Ils peuvent avoir plus de poids que toutes les couches convolutives combinées. Par exemple, même avec un résultat très raisonnable provenant du dernier cube de données de 24 x 24 x 10 points de données, une couche dense de 100 neurones coûte 24 x 24 x 10 x 100=576 000 pondérations. Essayez de réfléchir, ou essayez le pooling moyen global (voir ci-dessous).

Pooling moyen au niveau mondial

Au lieu d'utiliser une couche dense coûteuse à la fin d'un réseau de neurones convolutifs, vous pouvez diviser le "cube" de données entrantes en autant de parties que vous avez de classes, calculer la moyenne de leurs valeurs et les transmettre via une fonction d'activation softmax. Cette méthode de création de la section de classification ne nécessite aucun poids. Dans Keras, la syntaxe est tf.keras.layers.GlobalAveragePooling2D()..

93240029f59df7c2.png

Solution

Voici le notebook de la solution. Vous pouvez l'utiliser si vous êtes bloqué.

c3df49e90e5a654f.png Keras_Flowers_TPU (solution).ipynb

Points abordés

  • 🤔 Jeux avec des couches convolutives
  • 🤓 J'ai expérimenté le pooling maximal, les pas, le pooling moyen global, etc.
  • des itérations rapides sur un modèle réel, sur TPU

Veuillez prendre un moment pour passer en revue cette liste de contrôle.

10. [INFO] Architectures convolutives modernes

En résumé

7968830b57b708c0.png

Illustration : "module" convolutif Quelle est la meilleure option à ce stade ? Une couche "max-pool" suivie d'une couche convolutive 1x1 ou d'une autre combinaison de couches ? Essayez-les tous, concatenatez les résultats et laissez le réseau décider. À droite: l'architecture convolutive " inception" utilisant ces modules.

Dans Keras, pour créer des modèles dans lesquels le flux de données peut se ramifier, vous devez utiliser le style de modèle "fonctionnel". Voici un exemple :

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

Autres astuces

Petits filtres 3x3

40a7b15fb7dbe75c.png

Dans cette illustration, vous voyez le résultat de deux filtres 3 x 3 consécutifs. Essayez de déterminer les points de données qui ont contribué au résultat: ces deux filtres 3 x 3 consécutifs calculent une combinaison d'une région 5 x 5. Il ne s'agit pas exactement de la même combinaison qu'un filtre 5x5, mais cela vaut la peine d'essayer, car deux filtres 3x3 consécutifs sont moins chers qu'un seul filtre 5x5.

des convolutions 1 x 1 ?

fd7cac16f8ecb423.png

En termes mathématiques, une convolution "1x1" est une multiplication par une constante, ce n'est pas un concept très utile. Cependant, dans les réseaux de neurones convolutifs, n'oubliez pas que le filtre est appliqué à un cube de données, et non seulement à une image 2D. Par conséquent, un filtre "1x1" calcule la somme pondérée d'une colonne de données 1x1 (voir l'illustration) et, lorsque vous la faites glisser sur les données, vous obtenez une combinaison linéaire des canaux de l'entrée. C'est vraiment utile. Si vous considérez les canaux comme le résultat d'opérations de filtrage individuelles, par exemple un filtre pour les "oreilles pointues", un autre pour les "moustaches" et un troisième pour les "yeux fendus", une couche convolutive "1x1" calculera plusieurs combinaisons linéaires possibles de ces caractéristiques, ce qui peut être utile lorsque vous recherchez un "chat". De plus, les couches 1x1 utilisent moins de poids.

11. Pression

L'article Squeezenet offre une méthode simple pour assembler ces idées. Les auteurs suggèrent une conception de module convolutif très simple, utilisant uniquement des couches convolutives 1x1 et 3x3.

1730ac375379269b.png

Illustration: architecture squeezenet basée sur des modules de déclenchement. Elles alternent une couche 1x1 qui "compresse" les données entrantes dans la dimension verticale, suivie de deux couches convolutives 1x1 et 3x3 parallèles qui "étendent" à nouveau la profondeur des données.

Pratique

Continuez dans votre notebook précédent et créez un réseau de neurones convolutif inspiré de la compression. Vous devrez remplacer le code du modèle par le "style fonctionnel" Keras.

c3df49e90e5a654f.png Keras_Flowers_TPU (playground).ipynb

Informations supplémentaires

Dans cet exercice, il sera utile de définir une fonction d'assistance pour un module de compression:

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)

L'objectif cette fois est d'atteindre une précision de 80 %.

À essayer

Commencez par une seule couche convolutive, puis ajoutez "fire_modules", en alternant avec des couches MaxPooling2D(pool_size=2). Vous pouvez tester de deux à quatre couches de pooling max dans le réseau, ainsi que de un, deux ou trois modules de déclenchement consécutifs entre les couches de pooling max.

Dans les modules Fire, la valeur du paramètre "squeeze" doit généralement être inférieure à celle du paramètre "expand". Ces paramètres sont en fait des nombres de filtres. Elles peuvent généralement aller de 8 à 196. Vous pouvez tester des architectures dans lesquelles le nombre de filtres augmente progressivement dans le réseau, ou des architectures simples dans lesquelles tous les modules de déclenchement ont le même nombre de filtres.

Voici un exemple :

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)

À ce stade, vous remarquerez peut-être que vos tests ne se déroulent pas très bien et que l'objectif de précision de 80% semble lointain. Il est temps de découvrir quelques autres astuces.

Normalisation par lot

La norme de traitement par lot vous aidera à résoudre les problèmes de convergence que vous rencontrez. Vous trouverez des explications détaillées sur cette technique dans le prochain atelier. Pour l'instant, veuillez l'utiliser comme assistant "magique" de boîte noire en ajoutant cette ligne après chaque couche convolutive de votre réseau, y compris les couches de votre fonction "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

La valeur par défaut du paramètre d'élan est de 0,99. Nous devons la réduire à 0,9, car notre ensemble de données est petit. Peu importe ce détail pour l'instant.

Augmentation des données

Vous obtiendrez quelques points de pourcentage supplémentaires en augmentant les données avec des transformations simples, comme des inversions gauche-droite des modifications de saturation:

4ed2958e09b487ca.png

ad795b70334e0d6b.png

Cette opération est très simple dans TensorFlow avec l'API tf.data.Dataset. Définissez une nouvelle fonction de transformation pour vos données:

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

Utilisez-les ensuite dans votre transformation finale des données (cellule "ensembles de données d'entraînement et de validation", fonction "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

N'oubliez pas de rendre l'augmentation des données facultative et d'ajouter le code nécessaire pour vous assurer que seul l'ensemble de données d'entraînement est augmenté. Il n'a aucun sens d'augmenter l'ensemble de données de validation.

Une précision de 80% en 35 époques devrait désormais être à votre portée.

Solution

Voici le notebook de la solution. Vous pouvez l'utiliser si vous êtes bloqué.

c3df49e90e5a654f.png Keras_Flowers_TPU_squeezenet.ipynb

Points abordés

  • 🤔 Modèles "style fonctionnel" Keras
  • 🤓 Architecture Squeezenet
  • 🤓 Augmentation des données avec tf.data.datset

Veuillez prendre un moment pour passer en revue cette liste de contrôle.

12. Xception affiné

Convolutions séparables

Une autre méthode d'implémentation des couches convolutives a récemment gagné en popularité : les convolutions séparables en profondeur. Je sais, c'est un peu long, mais le concept est assez simple. Ils sont implémentés dans TensorFlow et Keras sous la forme tf.keras.layers.SeparableConv2D.

Une convolution séparable exécute également un filtre sur l'image, mais utilise un ensemble distinct de pondérations pour chaque canal de l'image d'entrée. Il est suivi d'une "convolution 1x1", une série de produits scalaires qui aboutit à une somme pondérée des canaux filtrés. Avec de nouvelles pondérations à chaque fois, le nombre de recombinaisons pondérées des canaux est calculé autant que nécessaire.

615720b803bf8dda.gif

Illustration: convolutions séparables. Phase 1: convolutions avec un filtre distinct pour chaque canal. Phase 2 : recombinaisons linéaires des canaux. Répété avec un nouvel ensemble de pondérations jusqu'à ce que le nombre souhaité de canaux de sortie soit atteint. La phase 1 peut également être répétée, avec de nouvelles pondérations à chaque fois, mais dans la pratique, c'est rarement le cas.

Les convolutions séparables sont utilisées dans les architectures de réseaux convolutifs les plus récentes: MobileNetV2, Xception et EffectiveNet. Par ailleurs, vous avez utilisé MobileNetV2 pour l'apprentissage par transfert précédemment.

Elles sont moins chères que les convolutions standards et se sont avérées tout aussi efficaces dans la pratique. Voici le nombre de poids pour l'exemple illustré ci-dessus :

Couche convolutive: 4 x 4 x 3 x 5 = 240

Couche convolutive séparable : 4 x 4 x 3 + 3 x 5 = 48 + 15 = 63

Il est laissé au lecteur de calculer le nombre de multiplications requises pour appliquer chaque style de mise à l'échelle de la couche convolutive de manière similaire. Les convolutions séparables sont plus petites et beaucoup plus efficaces en termes de calcul.

Pratiquer

Redémarrez à partir du notebook de simulation "Apprentissage par transfert", mais cette fois, sélectionnez Xception comme modèle pré-entraîné. Xception n'utilise que des convolutions séparables. Laissez toutes les pondérations pouvant être entraînées. Nous allons ajuster les poids pré-entraînés sur nos données au lieu d'utiliser les couches pré-entraînées telles quelles.

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

Objectif: justesse supérieure à 95% (non, sérieusement, c'est possible !)

Il s'agit du dernier exercice, qui nécessite un peu plus de travail en codage et en data science.

Informations supplémentaires sur l'affinage

Xception est disponible dans les modèles pré-entraînés standards de tf.keras.application.* N'oubliez pas de laisser tous les poids en cours d'entraînement cette fois.

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

Pour obtenir de bons résultats lorsque vous affinez un modèle, vous devez prêter attention au taux d'apprentissage et utiliser une programmation de taux d'apprentissage avec une période d'optimisation. Exemple :

9b1af213b2b36d47.png

Commencer avec un taux d'apprentissage standard perturberait les pondérations pré-entraînées du modèle. Le démarrage progressif les conserve jusqu'à ce que le modèle ait saisi vos données et puisse les modifier de manière appropriée. Après la montée en puissance, vous pouvez continuer avec un taux d'apprentissage constant ou exponentiel.

Dans Keras, le taux d'apprentissage est spécifié via un rappel, dans lequel vous pouvez calculer le taux d'apprentissage approprié pour chaque époque. Keras transmet le taux d'apprentissage approprié à l'optimiseur pour chaque époque.

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

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

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

Solution

Voici le notebook de la solution. Vous pouvez l'utiliser si vous êtes bloqué.

c3df49e90e5a654f.png 07_Keras_Flowers_TPU_xception_fine_tuned_best.ipynb

Points abordés

  • 🤔 Convolution séparable en profondeur
  • 🤓 Planifications du taux d'apprentissage
  • 😈 Affiner un modèle pré-entraîné

Veuillez prendre quelques instants pour passer en revue cette checklist.

13. Félicitations !

Vous avez créé votre premier réseau de neurones convolutif moderne et l'avez entraîné à une justesse supérieure à 90 %. Grâce aux TPU, vous effectuez des itérations sur des entraînements successifs en quelques minutes seulement.

TPU en pratique

Les TPU et les GPU sont disponibles dans Vertex AI de Google Cloud :

Enfin, les commentaires sont les bienvenus. N'hésitez pas à nous contacter si vous remarquez quelque chose d'anormal dans cet atelier ou si vous pensez qu'il doit être amélioré. Vous pouvez nous faire part de vos commentaires via GitHub [lien de commentaires].

HR.png

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