Skip to main content

Show and Tell

Ceci est l’implémentation TensorFlow de Show and Tell
Created on February 12|Last edited on February 2
Ceci est une traduction d'un article en anglais qui peut être trouvé ici.



Introduction

Consultez le Kaggle Notebook

Il y a quelques années, si quelqu’un avait affirmé que nous aurions des assistants virtuels qui seraient capable de décrire correctement un paysage qui leur serait présenté, les gens se seraient moqués de lui. Au fur et à mesure que l’apprentissage automatique s’est aventuré dans le deep learning, ouvrant ainsi la porte à des possibilités infinies, des idées auxquelles nous n’aurions jamais rêvées ont commencé à paraître possible. Un de ces idées est étudiées dans Show and Tell :A Neural Image Caption Generator(Montre et Décris : Un Générateur de Légendes d’Images Neuronal) par Vynials et al. Dans ce papier, les auteurs ont suggéré une solution de bout en bout pour obtenir un générateur de légendes d’images. Précédemment, tout ce qui avait été proposé pour cette tâche impliquait de l’optimisation de tâche indépendante (vision et langage naturel) et ensuite, il fallait qu’un ingénieur recoupe ces tâches indépendantes à la main.

Ce papier tire son inspiration de la Traduction Automatique Neuronale Neural Machine Translation où un encodeur s’entraîne sur une séquence dans un langage donné et produit une représentation à longueur fixe pour un décodeur, qui crache une séquence dans un autre langage. À partir de cette idée, les auteurs ont utilisé un extracteur de caractéristiques visuelles comme encodeur et un modèle de séquences comme décodeur.



📜Lecture du papier

Il est assez fascinant de se procurer un papier universitaire, qui, une fois lu, vous fait jurer que vous auriez pu avoir l’idée qui a été proposée. À cet instant précis, on comprend combien une idée peut être simple et pourtant, puissante. Show and Tell: A Neural Image Caption Generator est l’un de ces papiers incroyables qui ont ouvert les portes de la recherche en deep learning pour un générateur de légendes d’images.

Les auteurs ont affirmé avoir été grandement inspirés par les traductions par machine. Cela nous amène à diviser notre article sous la forme d’une feuille de route. En suivant cette feuille de route, le lecteur devrait ressentir le même engouement que nous lorsque nous avons lu le papier.

La feuille de route :

  • Tâche : Où nous examinons les concepts centraux de la tâche à venir.
  • Modules : Où nous examinons les différentes architectures utilisées et nous parlons un peu plus des raisons pour lesquelles on les utilise.
  • Code : Comment construire un tutoriel sans vous montrer le code ?
  • Perte et Résultats : Quelle est la fonction objectif utilisée et comment se comporte-t-elle ?


👨‍🏫Tâche

À partir d’une image donnée, nous avons besoin d’une légende qui décrit cette image. Ce n’est pas un simple problème de classification, où un agent artificiel décide dans quelle catégorie une image doit être placée. Ce n’est pas une tâche de détection, où un agent artificiel trace des boîtes à limite minimum sur des objets qu’il catégorise. Ici, nous avons besoin de déchiffrer les contenus de l’image puis de former une séquence de mots qui décrit la relation entre les contenus de l’image.

L’idée la plus simple qui vient à l’esprit est de diviser notre tâche en deux tâches distinctes.

  1. 👁️ Vision par ordinateur (computer vision) : Cette partie s’occupe de l’image donnée. Elle essaye d’extraire les caractéristiques de l’image, de construire des concepts à partir des caractéristiques hiérarchiques, et de modeler la distribution de données. Un simple Réseau Neuronal Convolutif (Convolutional Neural Network, CNN) serait très bien pour ce dessein. Après l’input d’une image, les kernels CNN relèveraient les caractéristiques de l’image de manière hiérarchique. Ces caractéristiques seraient une représentation compressée du contenu de l’image présentée. image.png

DeepLearning by Goodfellow et. al.

  1. 🗣️ Génération de mots : Dans cette tâche, on nous fournit une image ainsi que la légende de l’image sur laquelle s’entraîner. Cette légende doit être utilisée pour obtenir un modèle. La légende est une séquence de mots qui décrivent l’image. Un Processeur de Langage Naturel (Natural Language Processor) est nécessaire pour modéliser la distribution des données de la légende. Ce modèle doit comprendre la distribution des mots ainsi que leur contexte. Ici, nous pouvons voir une architecture récurrente simple qui peut modéliser les légendes et générer des mots qui sont échantillonnées de près depuis l’espace de données des légendes fourni. image.png

Le plus compliqué, c’est de regrouper ces deux domaines ensemble. Non seulement il nous faudrait le Processeur de Langage Naturel pour générer des mots depuis la distribution des données de légende, mais il faudrait aussi qu’il prenne l’image en considération. La caractéristique de l’image est un facteur important dans le problème de la génération de légendes pour images. Le générateur de légendes a besoin de capter les caractéristiques de l’image et ensuite, avec ce contexte, échantillonner les mots depuis l’espace de légende et fournir une description de l’image. Le recoupement de ces deux domaines est ce qui rend cette tâche aussi intéressante. image.png

Show and Tell : a Neural Image Caption Generator

Une petite approche que j’ai trouvée hautement intéressante est l’utilisation des nombres. Nous autres humains avons inventé ce magnifique langage de communications appelés les Mathématiques. Ici, nous pouvons dépeindre des concepts, des idées, et biens plus avec des nombres et des symboles. Concentrons-nous sur les nombres, pour l’instant. Un modèle de vision par ordinateur extrait les caractéristiques utiles d’images, qui sont essentiellement des nombres (les poids et les biais du modèle). De manière similaire, le langage et les mots peuvent être représentés par des nombres (plongements de mots). C’est l’idée qui, une fois prise en main, peut résoudre notre problème de recouper ensemble les différents domaines de cette tâche. Nous avons besoin de donner des nombres en input, depuis le modèle de vision au modèle de langage, d’une telle manière que la tâche de génération de légende d’image fonctionne.



Data

Pour nos expériences, nous avons utilisé le dataset Flickr30k, qui héberge 30 000 images et de multiples légendes uniques qui correspondent à chaque image. Les données sont prétraitées et hébergées dans Kaggle pour faciliter notre cas d’utilisation. Nous utiliserons donc le dataset Kaggle de Flickr30k.

Le dataset abritait un CSV qui avait des enregistrements d’images qui les reliaient à leurs légendes respectives.

Un aperçu du dataset est comme suit : image.png caption.png

Avant de continuer, nous aimerions faire remarquer au lecteur l’utilisation de <start> et <end>. C’est particulièrement important pour que le modèle sache situer le début et la fin de la légende. Ça ne paraît pas nécessaire lorsqu’on entraîne les données, mais lors des tests, nous devrons donner un token <start> pour que le modèle génère le premier mot de la légende, tandis que le modèle doit s’arrêter de générer des mots après qu’il a produit le token <end>.




Run set
1


🏋️Modèles

Nous avons une certaine compréhension de la tâche, ainsi que des modèles qu’il nous faut pour travailler. L’approche par les nombres sera très utile dans cette section. Commençons avec l’architecture du modèle proposée par les auteurs, avant de plonger dans les fonctionnements. image.png

L’architecture proposée

Ici, nous avons deux modèles distincts pour des tâches distinctes devant nous. Sur le côté gauche se trouve un modèle de vision, et sur le côté droit, un modèle récurrent est utilisé pour la génération de mots. L’idée, ici, est de donner les caractéristiques de l’image au modèle récurrent comme si c’était simplement un autre mot. Les caractéristiques qui sont extraites de l’image sont une collection de nombres (un vecteur). Si nous traçons ce vecteur dans l’espace de plongement des légendes, nous aurons forcément une représentation d’un mot. Cette caractéristique d’image changée en un mot est toute la beauté du processus. Les caractéristiques d’images qui sont tracées dans l’espace de plongement peuvent ne pas représenter un véritable mot du dictionnaire, mais ce mot est juste suffisant pour que le modèle récurrent puisse apprendre dessus. Cet image-word (mot-image) est l’input initial du modèle récurrent. Après une introspection profonde, le lecteur est forcé de reconnaître que cette idée est très simple, mais non moins efficace. Deux images avec la même caractéristique sont proches dans l’espace de plongement, et lorsqu’on fournira ces caractéristiques au modèle récurrent, le modèle générera des légendes qui seront similaires pour les deux images. On peut également penser que cette idée d’image-word est importante parce que désormais, nous pouvons calculer de l’algèbre vectorielle sur l’espace de plongement, et de nouveaux concepts peuvent être appris en ajoutant simplement deux concepts. image_caption.png

Exemple d’image-word (mot-image)

En suivant l’architecture, nous avons utilisé un CNN comme encodeur et une couche empilée de GRU comme décodeur. Nous avons utilisé des Gated Recurrent Units (Réseau récurrent à portes) plutôt que des LSTM (Réseau récurrent à mémoire court et long terme) parce que les GRU sont plus efficients en calcul que les LSTM, et plus efficaces que les simples RNN (Réseaux de Neurones Récurrents). En gardant à l’esprit que nous avons affaire à un énorme dataset, nous avons choisi le GRU pour booster l’efficacité de notre pipeline, en échange de quelques pertes d’efficacités de calculs (problème de dissipation des gradients).

👀Show

La partie "Montrer" de notre modèle agit comme notre encodeur d’informations. Nous utilisons un modèle resnet50qui a été pré-entraîné sur des données de Imagenet. Ici, nous omettons la dernière couche du modèle et nous extrayons l’output de l’avant-dernière couche. Par-dessus l’output resnet, nous ajoutons une Agrégation Moyenne Globale (Global Average Pooling, GAP) et une couche Dense (Dense layer). Avec le GAP, nous prenons une moyenne de l’avant-dernier output kernel, et avec la couche Dense, nous essayons de donner une forme aux caractéristiques d’images, qui soit la même que celle des plongements de mot.

🗣️Tell

La partie "Décrire" de notre modèle agit comme notre décodeur d’informations. Pour expliquer ceci, il faut que nous regardions les racines d’un modèle RNN. Pour ceux d’entre vous qui auraient besoin d’une révision sur les architectures récurrentes, Sous le Capot des RNN serait un bon point de départ. L’output d’une cellule simple est déterminé par son input actuel et son activation d’état caché depuis sa cellule précédente, . Maintenant, si notre première cellule de RNN a les images encodées comme input, l’état caché qu’elle génère sera transporté sur les cellules suivantes. L’état caché agira également comme le lien entre l’input et l’output de la cellule actuelle, et sera répété pour toutes les cellules de cette séquence. Donc, en résumé, l’effet de l’image encodée est essentiellement passé à travers une séquence de cellules pour que chaque mot soit prédit en gardant à l’esprit l’image donnée.

image.png

Depiction of the recurrent formula



🖥️Code

Consultez le Kaggle Notebook

Ici, nous allons parler des principaux éléments de notre code.

Gestion de texte

Dans le bloc de code qui suit, nous faisons de la gestion de texte basique et nous créons notre vocabulaire depuis notre set de légendes disponibles. Nous ajoutons également un token "pad" (rembourrage) pour que nous puissions ensuite faire en sorte que toutes les phrases soient de la même taille, pour nos propres bénéfices.

train_df = df.iloc[:train_size,:]
val_df = df.iloc[train_size+1:train_size+val_size,:]
test_df = df.iloc[train_size+val_size+1:,:]

# Choose the top 5000 words from the vocabulary
top_k = 10000
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=top_k,
                                                  oov_token="<unk>",
                                                  filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~')

# build the vocabulary
tokenizer.fit_on_texts(train_df['comment'])

tokenizer.word_index['<pad>'] = 0
tokenizer.index_word[0] = '<pad>'

Pour vérifier si notre création de vocabulaire de base a été faite correctement, nous créons une fonction d’aide

# This is a sanity check function
def check_vocab(word):
    i = tokenizer.word_index[word]
    print(f"The index of the word: {i}")
    print(f"Index {i} is word {tokenizer.index_word[i]}")
    
check_vocab("pajama")

Nous obtiendrons un output qui ressemblera à ceci :

index.png

Puis, il nous faut créer nos données d’entraînement en accord avec le vocabulaire généré par le tokenizer. Nous rembourrons manuellement toutes les phrases pour qu’elles aient la même longueur. Nous intégrons nos données dans le pipeline tf.data.

# Create the tokenized vectors
train_seqs = tokenizer.texts_to_sequences(train_df['comment'])
val_seqs = tokenizer.texts_to_sequences(val_df['comment'])
test_seqs = tokenizer.texts_to_sequences(test_df['comment'])

# If you do not provide a max_length value, pad_sequences calculates it automatically
train_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
val_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(val_seqs, padding='post')
test_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(test_seqs, padding='post')

train_cap_ds = tf.data.Dataset.from_tensor_slices(train_cap_vector)
val_cap_ds = tf.data.Dataset.from_tensor_slices(val_cap_vector)
test_cap_ds = tf.data.Dataset.from_tensor_slices(test_cap_vector)

Gestion d’images

Maintenant, nous allons créer un pipeline tf.data pour les images dans le dataset flicker30K. Nous faisons des opérations basiques, comme charger l’image, la décoder, convertir des type de données et redimensionner.

@tf.function
def load_img(image_path):
    img = tf.io.read_file(image_path)
    img = tf.image.decode_jpeg(img)
    img = tf.image.convert_image_dtype(img, tf.float32)
    img = tf.image.resize(img, (224, 224))
    return img

train_img_name = train_df['image_name'].values
val_img_name = val_df['image_name'].values
test_img_name = test_df['image_name'].values

train_img_ds = tf.data.Dataset.from_tensor_slices(train_img_name).map(load_img)
val_img_ds = tf.data.Dataset.from_tensor_slices(val_img_name).map(load_img)
test_img_ds = tf.data.Dataset.from_tensor_slices(test_img_name).map(load_img)

Joindre les données

Notre intention est de fusionner les deux pipelines de données créés pour que nous puissions les déverser ensemble dans nos réseaux. Nous prenons nos données en différents lots (batch), puisque le dataset est très grand dans son ensemble.

# prefecth and batch the dataset
AUTOTUNE = tf.data.experimental.AUTOTUNE
BATCH_SIZE = 512

train_ds = tf.data.Dataset.zip((train_img_ds, train_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
val_ds = tf.data.Dataset.zip((val_img_ds, val_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
test_ds = tf.data.Dataset.zip((test_img_ds, test_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)

Model

Show

Comme mentionné plus tôt, Show correspond à la partie encodeur de l’architecture, qui compresse l’image. Un modèle ResNet50 entraîné sur ImageNetagit comme un extracteur de caractéristiques, suivi d’un GAP. Enfin, nous finissons cette partie de notre architecture avec une couche entièrement connectée.

class CNN_Encoder(tf.keras.Model):
    
    def __init__(self, embedding_dim):
        super(CNN_Encoder, self).__init__()
        self.embedding_dim = embedding_dim
        
    def build(self, input_shape):
        self.resnet = tf.keras.applications.ResNet50(include_top=False,
                                                     weights='imagenet')
        self.resnet.trainable=False
        self.gap = GlobalAveragePooling2D()
        self.fc = Dense(units=self.embedding_dim,
                        activation='sigmoid')
        
    def call(self, x):
        x = self.resnet(x)
        x = self.gap(x)
        x = self.fc(x)
        return x

Tell

Comme mentionné plus tôt, Show correspond à la partie encodeur de l’architecture, qui compresse l’image. Un modèle ResNet50 entraîné sur ImageNet agit comme un extracteur de caractéristiques, suivi d’un GAP. Enfin, nous finissons cette partie de notre architecture avec une couche entièrement connectée.

class RNN_Decoder(tf.keras.Model):
    def __init__(self, embedding_dim, units, vocab_size):
        super(RNN_Decoder, self).__init__()
        self.units = units
        self.embedding_dim = embedding_dim
        self.vocab_size = vocab_size
        self.embedding = Embedding(input_dim=self.vocab_size,
                                   output_dim=self.embedding_dim)
    
    def build(self, input_shape):
        self.gru1 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.gru2 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.gru3 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.gru4 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.fc1 = Dense(self.units)
        self.fc2 = Dense(self.vocab_size)

    def call(self, x, initial_zero=False):
        # x, (batch, 512)
        # hidden, (batch, 256)
        if initial_zero:
            initial_state = decoder.reset_state(batch_size=x.shape[0])
            output, state = self.gru1(inputs=x,
                                      initial_state=initial_state)
            output, state = self.gru2(inputs=output,
                                      initial_state=initial_state)
            output, state = self.gru3(inputs=output,
                                      initial_state=initial_state)
            output, state = self.gru4(inputs=output,
                                      initial_state=initial_state)
        else:
            output, state = self.gru1(inputs=x)
            output, state = self.gru2(inputs=output)
            output, state = self.gru3(inputs=output)
            output, state = self.gru4(inputs=output)
        # output, (batch, 256)
        x = self.fc1(output)
        x = self.fc2(x)
        
        return x, state
    
    def embed(self, x):
        return self.embedding(x)
    
    def reset_state(self, batch_size):
        return tf.zeros((batch_size, self.units))

Entraînement

Nous créons la classe objects et nous assignons notre optimiseur sur Adam. La perte est l’entropie croisée catégorique dispersée (Sparse Categorical Cross entropy), parce qu’ici, il serait inefficace d’utiliser des encodeurs one-hot en tant que vérité terrain. Nous utiliserons aussi mask, pour aider à masquer le fait que nous ne laissions pas le modèle de séquence apprendre pour trop correspondre aux mêmes données.

encoder = CNN_Encoder(EMBEDDIN_DIM)
decoder = RNN_Decoder(embedding_dim=EMBEDDIN_DIM,
                      units=UNITS_RNN,
                      vocab_size=VOCAB_SIZE)

optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')

def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    loss_ = loss_object(real, pred)

    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask

    return tf.reduce_mean(loss_)

Ensuite, nous écrivons notre fonction d’étape d’entraînement, qui calculera les gradients par rétropropagation.

@tf.function
def train_step(img_tensor, target):
    # img_tensor (batch, 224,224,3)
    # target     (batch, 80)
    loss = 0
    with tf.GradientTape() as tape:
        features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
        em_words = decoder.embed(target)
        x = tf.concat([features,em_words],axis=1)
        predictions, _ = decoder(x, True)

        loss = loss_function(target[:,1:], predictions[:,1:-1,:])

    trainable_variables = encoder.trainable_variables + decoder.trainable_variables

    gradients = tape.gradient(loss, trainable_variables)

    optimizer.apply_gradients(zip(gradients, trainable_variables))

    return loss

@tf.function
def val_step(img_tensor, target):
    # img_tensor (batch, 224,224,3)
    # target     (batch, 80)
    loss = 0
    features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
    em_words = decoder.embed(target)
    x = tf.concat([features,em_words],axis=1)
    predictions, _ = decoder(x, True)
    loss = loss_function(target[:,1:], predictions[:,1:-1,:])
    return loss


📉Perte et résultats

Consultez le Kaggle Notebook

La fonction objectif est la Log-vraisemblance négative (Negative Log-Likelihood) des mots générés. Pour rendre cela un peu plus intuitif, plongeons dans l’architecture qui est fournie. Une image, lorsqu’elle est donnée dans le CNN, fournit des caractéristiques d’images. La caractéristique est ensuite encodée dans un tenseur de même forme que celle des plongements de mots qui sont fournis. La caractéristique d’image est donnée dans le GRU pour la première étape chronologique. Cette cellule produit ensuite un softmax du vocabulaire entier du mot. L’objectif de notre tâche est d’augmenter la vraisemblance que le mot décrive de près l’image. Nous prenons la Log-vraisemblance négative pour pouvoir minimiser cette mesure et entraîner notre modèle. Dans les prochaines étapes chronologiques, nous fournissons les mots de la légende comme input et essayons de maximiser la probabilité du prochain mot immédiat.




Run set
1


Conclusion

En utilisant des concepts simples appris de la traduction machine, les auteurs ont vraiment élaboré une manière brillante de générer des légendes automatiques pour une image donnée. Ce papier a été l’inspiration pour de nombreux papiers subséquents dans le domaine de la génération automatique de légendes, en particulier pour Show, Attend and Tell(Montre, Fais attention, Décris) qui, comme le laisse à penser son nom, utilise l’attention ainsi que les concepts appris dont nous avons parlé dans ce rapport.

Parlez aux auteurs ::

NomTwitterGitHub
Devjyoti Chakrobarty@Cr0wley_zz@cr0wley-zz
Aritra Roy Gosthipaty@ariG23498@ariG23498

Iterate on AI agents and models faster. Try Weights & Biases today.