Je suis heureux de découvrir que ce code a été utilisé et cité dans les articles suivants:
Domino: Découvrir des erreurs systématiques avec des incorporations intermodales par eyuboglu et. al. à ICLR 2022
GSCLIP: Un cadre pour expliquer les changements de distribution en langage naturel par Zhu et. al. à ICML 2022
UIC-NLP à SEMEVAL-2022 Tâche 5: Exploration de l'apprentissage contrastif pour la détection multimodale de mèmes misogynes par Cuervo et. al. à Semeval-2022
CDSBERT - Extension des modèles de langage protéique avec conscience du codon par Hallee et. al. de l'Université du Delaware (septembre 2023)
Enigma-51: Vers une compréhension à grains fins des interactions humaines-objet dans les scénarios industriels par Ragusa et. al. (Novembre 2023)
Vous pouvez trouver les informations de citation sur la bonne section de cette page GitHub Repo nommée: Citez ce référentiel ou utilisez les informations de citation ci-dessous.
@software { Shariatnia_Simple_CLIP_2021 ,
author = { Shariatnia, M. Moein } ,
doi = { 10.5281/zenodo.6845731 } ,
month = { 4 } ,
title = { {Simple CLIP} } ,
version = { 1.0.0 } ,
year = { 2021 }
}C'est en janvier 2021 qu'Openai a annoncé deux nouveaux modèles: Dall-E et Clip , les deux modèles multimodalités reliant des textes et des images d'une manière ou d'une autre. Dans cet article, nous allons implémenter le modèle de clip à partir de zéro dans Pytorch . OpenAI a open source une partie du code relatif au modèle de clip, mais je l'ai trouvé intimidant et c'était loin d'être court et simple. Je suis également tombé sur un bon tutoriel inspiré du modèle de clip sur des exemples de code Keras et j'ai traduit certaines parties de celui-ci dans Pytorch pour construire ce tutoriel totalement avec notre pytorch bien-aimé!
Dans l'apprentissage des modèles visuels transférables à partir du papier de supervision du langage naturel, OpenAI présente leur nouveau modèle qui est appelé Clip , pour la pré-formation d'image linguistique contrastive . En un mot, ce modèle apprend la relation entre une phrase entière et l'image qu'il décrit; Dans un sens que lorsque le modèle est formé, compte tenu d'une phrase d'entrée, il pourra récupérer les images les plus apparentées correspondant à cette phrase. La chose importante ici est qu'elle est formée à des phrases complètes au lieu de classes uniques comme la voiture, le chien, etc. L'intuition est que lorsqu'elle est formée sur des phrases entières, le modèle peut apprendre beaucoup plus de choses et trouve un modèle entre les images et les textes. Ils montrent également que lorsque ce modèle est formé sur un énorme ensemble de données d'images et leurs textes correspondants, il peut également agir en tant que classificateur. Je vous encourage à étudier l'article pour en savoir plus sur ce modèle passionnant et leurs résultats étonnants sur les ensembles de données d'analyse comparative. Pour en mentionner un seul, le modèle de clip formé avec cette stratégie classe ImageNet mieux que ces modèles Sota formés sur l'imageNet lui-même optimisé pour la seule tâche de classification!
En tant que teaser (!), Voyons ce que le modèle final que nous allons construire dans cet article à partir de zéro est capable: étant donné une requête (texte brut) comme "un garçon sautant avec skateboard" ou "une fille qui sautait du swing", le modèle récupérera les images les plus pertinentes:

Voyons quelques sorties supplémentaires:

# !pip install timm
# !pip install transformers import os
import cv2
import gc
import numpy as np
import pandas as pd
import itertools
from tqdm . autonotebook import tqdm
import albumentations as A
import torch
from torch import nn
import torch . nn . functional as F
import timm
from transformers import DistilBertModel , DistilBertConfig , DistilBertTokenizer Une note sur Config et CFG: J'ai écrit les codes avec des scripts Python, puis je l'ai converti en un cahier Jupyter. Ainsi, dans le cas des scripts Python, la configuration est un fichier python normal où je mets tous les hyperparamètres et dans le cas de Jupyter Notebook, c'est une classe définie au début du cahier pour garder tous les hyperparamètres.
class CFG :
debug = False
image_path = "C:/Moein/AI/Datasets/Flicker-8k/Images"
captions_path = "C:/Moein/AI/Datasets/Flicker-8k"
batch_size = 32
num_workers = 4
head_lr = 1e-3
image_encoder_lr = 1e-4
text_encoder_lr = 1e-5
weight_decay = 1e-3
patience = 1
factor = 0.8
epochs = 4
device = torch . device ( "cuda" if torch . cuda . is_available () else "cpu" )
model_name = 'resnet50'
image_embedding = 2048
text_encoder_model = "distilbert-base-uncased"
text_embedding = 768
text_tokenizer = "distilbert-base-uncased"
max_length = 200
pretrained = True # for both image encoder and text encoder
trainable = True # for both image encoder and text encoder
temperature = 1.0
# image size
size = 224
# for projection head; used for both image and text encoders
num_projection_layers = 1
projection_dim = 256
dropout = 0.1 class AvgMeter :
def __init__ ( self , name = "Metric" ):
self . name = name
self . reset ()
def reset ( self ):
self . avg , self . sum , self . count = [ 0 ] * 3
def update ( self , val , count = 1 ):
self . count += count
self . sum += val * count
self . avg = self . sum / self . count
def __repr__ ( self ):
text = f" { self . name } : { self . avg :.4f } "
return text
def get_lr ( optimizer ):
for param_group in optimizer . param_groups :
return param_group [ "lr" ]Comme vous pouvez le voir dans l'image Tittle de cet article, nous devons coder les images et leurs textes décrits. Ainsi, l'ensemble de données doit renvoyer à la fois les images et les textes . Bien sûr, nous n'allons pas nourrir du texte brut à notre encodeur de texte! Nous utiliserons le modèle Distilbert (qui est plus petit que Bert mais qui fonctionne presque aussi bien que Bert) de la bibliothèque HuggingFace comme codeur de texte; Nous devons donc tokeniser les phrases (légendes) avec Distilbert Tokenizer, puis nourrir les ID de jeton (Input_ids) et les masques d'attention à Distilbert. Par conséquent, l'ensemble de données doit également s'occuper de la tokenisation. Vous trouverez ci-dessous le code de l'ensemble de données. Ci-dessous, je vais expliquer les choses les plus importantes qui se produisent dans le code.
Dans le __init__ , nous recevons un objet de tokenzer qui est en fait un Tinkerzer en étreinte; Ce tokenzer sera chargé lors de l'exécution du modèle. Nous remboursons et tronçons les légendes d'un max_length spécifié. Dans le __getItem__, nous chargerons d'abord une légende codée qui est un dictionnaire avec Keys Input_ids et Attention_mask, faire des tenseurs à partir de ses valeurs et après cela, nous chargerons l'image correspondante, la transformer et l'augmenter (s'il y en a!) Et ensuite nous en faisons un tenseur et le mettrons dans le dictionnaire avec "l'image" comme clé. Enfin, nous mettons le texte brut de la légende avec la «légende» clé dans le dictionnaire uniquement à des fins de visualisation.
Je n'ai pas utilisé d'augmentations de données supplémentaires, mais vous pouvez les ajouter si vous souhaitez améliorer les performances du modèle.
class CLIPDataset ( torch . utils . data . Dataset ):
def __init__ ( self , image_filenames , captions , tokenizer , transforms ):
"""
image_filenames and cpations must have the same length; so, if there are
multiple captions for each image, the image_filenames must have repetitive
file names
"""
self . image_filenames = image_filenames
self . captions = list ( captions )
self . encoded_captions = tokenizer (
list ( captions ), padding = True , truncation = True , max_length = CFG . max_length
)
self . transforms = transforms
def __getitem__ ( self , idx ):
item = {
key : torch . tensor ( values [ idx ])
for key , values in self . encoded_captions . items ()
}
image = cv2 . imread ( f" { CFG . image_path } / { self . image_filenames [ idx ] } " )
image = cv2 . cvtColor ( image , cv2 . COLOR_BGR2RGB )
image = self . transforms ( image = image )[ 'image' ]
item [ 'image' ] = torch . tensor ( image ). permute ( 2 , 0 , 1 ). float ()
item [ 'caption' ] = self . captions [ idx ]
return item
def __len__ ( self ):
return len ( self . captions )
def get_transforms ( mode = "train" ):
if mode == "train" :
return A . Compose (
[
A . Resize ( CFG . size , CFG . size , always_apply = True ),
A . Normalize ( max_pixel_value = 255.0 , always_apply = True ),
]
)
else :
return A . Compose (
[
A . Resize ( CFG . size , CFG . size , always_apply = True ),
A . Normalize ( max_pixel_value = 255.0 , always_apply = True ),
]
)Le code d'encodeur d'image est simple. J'utilise ici la bibliothèque des modèles d'images Pytorch (TIMM), ce qui rend beaucoup de modèles d'images différents disponibles à partir de ResNet à EfficientNet et bien d'autres. Ici, nous utiliserons un RESNET50 comme encodeur d'image. Vous pouvez facilement utiliser la bibliothèque TorchVision pour utiliser des resnets si vous ne souhaitez pas installer une nouvelle bibliothèque.
Le code code pour chaque image à un vecteur de taille fixe avec la taille des canaux de sortie du modèle (en cas de RESNET50, la taille du vecteur sera 2048 ). Il s'agit de la sortie après la couche NN.AdaptiveAvgPool2d ().
class ImageEncoder ( nn . Module ):
"""
Encode images to a fixed size vector
"""
def __init__ (
self , model_name = CFG . model_name , pretrained = CFG . pretrained , trainable = CFG . trainable
):
super (). __init__ ()
self . model = timm . create_model (
model_name , pretrained , num_classes = 0 , global_pool = "avg"
)
for p in self . model . parameters ():
p . requires_grad = trainable
def forward ( self , x ):
return self . model ( x )Comme je l'ai mentionné précédemment, j'utiliserai Distilbert comme encodeur de texte. Comme son plus grand frère Bert, deux jetons spéciaux seront ajoutés aux jetons d'entrée réels: CLS et SEP qui marquent le début et la fin d'une phrase. Pour saisir toute la représentation d'une phrase (comme le soulignent les papiers Bert et Distilbert associés), nous utilisons les représentations finales du jeton CLS et nous espérons que cette représentation capture la signification globale de la phrase (légende). En pensant que de cette manière, il est similaire à ce que nous avons fait aux images et les a convertis en un vecteur de taille fixe.
Dans le cas de Distilbert (et également Bert), la représentation cachée de sortie pour chaque jeton est un vecteur de taille 768 . Ainsi, toute la légende sera codée dans la représentation du jeton CLS dont la taille est 768.
class TextEncoder ( nn . Module ):
def __init__ ( self , model_name = CFG . text_encoder_model , pretrained = CFG . pretrained , trainable = CFG . trainable ):
super (). __init__ ()
if pretrained :
self . model = DistilBertModel . from_pretrained ( model_name )
else :
self . model = DistilBertModel ( config = DistilBertConfig ())
for p in self . model . parameters ():
p . requires_grad = trainable
# we are using the CLS token hidden representation as the sentence's embedding
self . target_token_idx = 0
def forward ( self , input_ids , attention_mask ):
output = self . model ( input_ids = input_ids , attention_mask = attention_mask )
last_hidden_state = output . last_hidden_state
return last_hidden_state [:, self . target_token_idx , :]J'ai utilisé l'exemple de code Keras Implémentation de Projection Head pour écrire ce qui suit dans Pytorch. Maintenant que nous avons encodé nos images et nos textes en vecteurs de taille fixe (2048 pour l'image et 768 pour le texte), nous devons les amener (projeter) dans un nouveau monde (!) Avec des dimensions similaires pour les images et les textes afin de pouvoir les comparer et de séparer l'image non retenue et les textes et les réprimer ceux qui correspondent. Ainsi, le code suivant apportera les vecteurs dimensionnels 2048 et 768 dans un monde dimensionnel 256 (projection_dim), où nous pouvons les comparer .
"Embedding_dim" est la taille du vecteur d'entrée (2048 pour les images et 768 pour les textes) et "projection_dim" est la taille du vecteur de sortie qui sera 256 pour notre cas. Pour comprendre les détails de cette partie, vous pouvez vous référer au papier clip.
class ProjectionHead ( nn . Module ):
def __init__ (
self ,
embedding_dim ,
projection_dim = CFG . projection_dim ,
dropout = CFG . dropout
):
super (). __init__ ()
self . projection = nn . Linear ( embedding_dim , projection_dim )
self . gelu = nn . GELU ()
self . fc = nn . Linear ( projection_dim , projection_dim )
self . dropout = nn . Dropout ( dropout )
self . layer_norm = nn . LayerNorm ( projection_dim )
def forward ( self , x ):
projected = self . projection ( x )
x = self . gelu ( projected )
x = self . fc ( x )
x = self . dropout ( x )
x = x + projected
x = self . layer_norm ( x )
return x Cette partie est l'endroit où tout le plaisir se passe! Je vais également parler de la fonction de perte ici. J'ai traduit une partie du code des exemples de code Keras dans Pytorch pour écrire cette pièce. Jetez un œil au code, puis lisez l'explication sous ce bloc de code.
Ici, nous utiliserons les modules précédents que nous avons construits pour implémenter le modèle principal. La fonction __init__ est explicite. Dans la fonction avant, nous codant pour d'abord les images et les textes séparément en vecteurs de taille fixe (avec des dimensions différentes). Après cela, en utilisant des modules de projection séparés, nous les projetons dans ce monde (espace) partagé dont j'ai parlé précédemment. Ici, les encodages deviendront d'une forme similaire (256 dans notre cas). Après cela, nous calculerons la perte. Encore une fois, je recommande de lire Clip Paper pour mieux l'améliorer, mais je ferai de mon mieux pour expliquer cette partie.
Dans l'algèbre linéaire , une façon courante de mesurer si deux vecteurs sont de caractéristiques similaires (ils sont comme les uns les autres) est de calculer leur produit DOT (multipliant les entrées correspondantes et en prendre la somme); Si le numéro final est grand, ils se ressemblent et s'ils sont petits, ils ne le sont pas (relativement parlant)!
D'accord! Ce que je viens de dire, c'est la chose la plus importante à avoir à l'esprit de comprendre cette fonction de perte. Continuons. Nous avons parlé de deux vecteurs, mais qu'avons-nous ici? Nous avons Image_embeddings, une matrice avec forme (Batch_size, 256) et text_embeddings avec forme (Batch_size, 256). Assez facile! Cela signifie que nous avons deux groupes de vecteurs au lieu de deux vecteurs uniques. Comment mesurer à quel point deux groupes de vecteurs (deux matrices) sont similaires les uns aux autres? Encore une fois, avec Dot Product (@ Operator dans Pytorch, le produit DOT ou la multiplication matricielle dans ce cas). Pour pouvoir multiplier ces deux matrices, nous transposons le second. D'accord, nous obtenons une matrice avec une forme (batch_size, batch_size) que nous appellerons les logits. (La température est égale à 1,0 dans notre cas, donc, cela ne fait pas de différence. Vous pouvez jouer avec elle et voir quelle différence il fait. Regardez également le papier pour voir pourquoi il est ici!).
J'espère que tu es toujours avec moi! Si ce n'est pas bon, passez en revue le code et vérifiez leurs formes. Maintenant que nous avons nos logits, nous avons besoin de cibles. Je dois dire qu'il existe un moyen plus simple d'obtenir des cibles mais j'ai dû le faire pour notre cas (je vais expliquer pourquoi dans un paragraphe suivant).
Considérons ce que nous espérons que ce modèle apprend: nous voulons qu'il apprenne "des représentations similaires (vecteurs)" pour une image donnée et la légende la décrivant. Ce qui signifie que nous lui donnons une image, soit le texte le décrivant, nous voulons qu'il produise les mêmes vecteurs de 256 tailles pour les deux.
class CLIPModel ( nn . Module ):
def __init__ (
self ,
temperature = CFG . temperature ,
image_embedding = CFG . image_embedding ,
text_embedding = CFG . text_embedding ,
):
super (). __init__ ()
self . image_encoder = ImageEncoder ()
self . text_encoder = TextEncoder ()
self . image_projection = ProjectionHead ( embedding_dim = image_embedding )
self . text_projection = ProjectionHead ( embedding_dim = text_embedding )
self . temperature = temperature
def forward ( self , batch ):
# Getting Image and Text Features
image_features = self . image_encoder ( batch [ "image" ])
text_features = self . text_encoder (
input_ids = batch [ "input_ids" ], attention_mask = batch [ "attention_mask" ]
)
# Getting Image and Text Embeddings (with same dimension)
image_embeddings = self . image_projection ( image_features )
text_embeddings = self . text_projection ( text_features )
# Calculating the Loss
logits = ( text_embeddings @ image_embeddings . T ) / self . temperature
images_similarity = image_embeddings @ image_embeddings . T
texts_similarity = text_embeddings @ text_embeddings . T
targets = F . softmax (
( images_similarity + texts_similarity ) / 2 * self . temperature , dim = - 1
)
texts_loss = cross_entropy ( logits , targets , reduction = 'none' )
images_loss = cross_entropy ( logits . T , targets . T , reduction = 'none' )
loss = ( images_loss + texts_loss ) / 2.0 # shape: (batch_size)
return loss . mean ()
def cross_entropy ( preds , targets , reduction = 'none' ):
log_softmax = nn . LogSoftmax ( dim = - 1 )
loss = ( - targets * log_softmax ( preds )). sum ( 1 )
if reduction == "none" :
return loss
elif reduction == "mean" :
return loss . mean ()Ainsi, dans le meilleur des cas, Text_Embeddings et Image_embedding Maticies devraient être les mêmes car ils décrivent des choses similaires. Réfléchissons maintenant: si cela se produit, à quoi ressemblerait la matrice Logits? Voyons avec un exemple simple!
# A simple Example
batch_size = 4
dim = 256
embeddings = torch . randn ( batch_size , dim )
out = embeddings @ embeddings . T
print ( F . softmax ( out , dim = - 1 ))Les logits, dans le meilleur cas, seront donc une matrice qui, si nous prenons son softmax, aura 1,0 s en diagonale (une matrice d'identité pour l'appeler avec des mots fantaisistes!). Comme le travail de la fonction de perte consiste à faire des prédictions du modèle similaires aux cibles (au moins dans la plupart des cas!), Nous voulons une telle matrice que notre cible. C'est la raison pour laquelle nous calculons les matrices d'images_similarity et textes_similarité dans le bloc de code ci-dessus.
Maintenant que nous avons notre matrice cible, nous utiliserons une entropie croisée simple pour calculer la perte réelle. J'ai écrit la forme matricielle complète de l'entropie croisée comme une fonction que vous pouvez voir en bas du bloc de code. D'accord! Nous avons fini! N'était-ce pas simple ?! D'accord, vous pouvez ignorer le paragraphe suivant, mais si vous êtes curieux, il y a une note importante à ce sujet.
Voici pourquoi je n'ai pas utilisé une approche plus simple : je dois admettre qu'il existe un moyen plus simple de calculer cette perte en pytorch; En faisant cela: nn.crossentropyloss () (Logits, torch.arange (batch_size)). Pourquoi je ne l'ai pas utilisé ici? Pour 2 raisons. 1- L'ensemble de données que nous utilisons a plusieurs légendes pour une seule image; Il est donc possible que deux images identiques avec leurs légendes similaires existent dans un lot (c'est rare mais cela peut arriver). Prendre la perte avec cette méthode plus facile ignorera cette possibilité et le modèle apprend à séparer deux représentations (supposons-les différentes) qui sont en fait les mêmes. De toute évidence, nous ne voulons pas que cela se produise, j'ai donc calculé toute la matrice cible d'une manière qui s'occupe de ces cas de bord. 2- Le faisant comme je l'ai fait, m'a donné une meilleure compréhension de ce qui se passe dans cette fonction de perte; Donc, je pensais que cela vous donnerait aussi une meilleure intuition!
Voici quelques fonds pour nous aider à charger des dataloaders de train et valides, notre modèle, puis à former et à évaluer notre modèle sur ceux-ci. Il ne se passe pas grand-chose ici; Juste des fonctions de boucle de formation simple et utilitaire
def make_train_valid_dfs ():
dataframe = pd . read_csv ( f" { CFG . captions_path } /captions.csv" )
max_id = dataframe [ "id" ]. max () + 1 if not CFG . debug else 100
image_ids = np . arange ( 0 , max_id )
np . random . seed ( 42 )
valid_ids = np . random . choice (
image_ids , size = int ( 0.2 * len ( image_ids )), replace = False
)
train_ids = [ id_ for id_ in image_ids if id_ not in valid_ids ]
train_dataframe = dataframe [ dataframe [ "id" ]. isin ( train_ids )]. reset_index ( drop = True )
valid_dataframe = dataframe [ dataframe [ "id" ]. isin ( valid_ids )]. reset_index ( drop = True )
return train_dataframe , valid_dataframe
def build_loaders ( dataframe , tokenizer , mode ):
transforms = get_transforms ( mode = mode )
dataset = CLIPDataset (
dataframe [ "image" ]. values ,
dataframe [ "caption" ]. values ,
tokenizer = tokenizer ,
transforms = transforms ,
)
dataloader = torch . utils . data . DataLoader (
dataset ,
batch_size = CFG . batch_size ,
num_workers = CFG . num_workers ,
shuffle = True if mode == "train" else False ,
)
return dataloaderVoici une fonction pratique pour former notre modèle. Il ne se passe pas grand-chose ici; Il suffit de charger les lots, de les nourrir au modèle et de faire un pas de l'optimiseur et de LR_SCheDuler.
def train_epoch ( model , train_loader , optimizer , lr_scheduler , step ):
loss_meter = AvgMeter ()
tqdm_object = tqdm ( train_loader , total = len ( train_loader ))
for batch in tqdm_object :
batch = { k : v . to ( CFG . device ) for k , v in batch . items () if k != "caption" }
loss = model ( batch )
optimizer . zero_grad ()
loss . backward ()
optimizer . step ()
if step == "batch" :
lr_scheduler . step ()
count = batch [ "image" ]. size ( 0 )
loss_meter . update ( loss . item (), count )
tqdm_object . set_postfix ( train_loss = loss_meter . avg , lr = get_lr ( optimizer ))
return loss_meter
def valid_epoch ( model , valid_loader ):
loss_meter = AvgMeter ()
tqdm_object = tqdm ( valid_loader , total = len ( valid_loader ))
for batch in tqdm_object :
batch = { k : v . to ( CFG . device ) for k , v in batch . items () if k != "caption" }
loss = model ( batch )
count = batch [ "image" ]. size ( 0 )
loss_meter . update ( loss . item (), count )
tqdm_object . set_postfix ( valid_loss = loss_meter . avg )
return loss_meter
def main ():
train_df , valid_df = make_train_valid_dfs ()
tokenizer = DistilBertTokenizer . from_pretrained ( CFG . text_tokenizer )
train_loader = build_loaders ( train_df , tokenizer , mode = "train" )
valid_loader = build_loaders ( valid_df , tokenizer , mode = "valid" )
model = CLIPModel (). to ( CFG . device )
params = [
{ "params" : model . image_encoder . parameters (), "lr" : CFG . image_encoder_lr },
{ "params" : model . text_encoder . parameters (), "lr" : CFG . text_encoder_lr },
{ "params" : itertools . chain (
model . image_projection . parameters (), model . text_projection . parameters ()
), "lr" : CFG . head_lr , "weight_decay" : CFG . weight_decay }
]
optimizer = torch . optim . AdamW ( params , weight_decay = 0. )
lr_scheduler = torch . optim . lr_scheduler . ReduceLROnPlateau (
optimizer , mode = "min" , patience = CFG . patience , factor = CFG . factor
)
step = "epoch"
best_loss = float ( 'inf' )
for epoch in range ( CFG . epochs ):
print ( f"Epoch: { epoch + 1 } " )
model . train ()
train_loss = train_epoch ( model , train_loader , optimizer , lr_scheduler , step )
model . eval ()
with torch . no_grad ():
valid_loss = valid_epoch ( model , valid_loader )
if valid_loss . avg < best_loss :
best_loss = valid_loss . avg
torch . save ( model . state_dict (), "best.pt" )
print ( "Saved Best Model!" )
lr_scheduler . step ( valid_loss . avg )L'exécution de la prochaine cellule commence à former le modèle. Mettez le noyau sur le mode GPU. Chaque époque devrait prendre environ 24 minutes sur GPU (même une époque suffit!). Cela peut prendre une minute avant le début de l'entraînement, car nous allons encoder toutes les légendes une fois dans le train et un ensemble de données valides, alors ne l'arrêtez pas! Tout fonctionne bien.
main ()D'accord! Nous avons terminé avec la formation du modèle. Maintenant, nous devons faire une inférence qui, dans notre cas, donnera au modèle un morceau de texte et voudra qu'il récupérera les images les plus pertinentes à partir d'un ensemble de validation (ou de test) invisible.
Dans cette fonction, nous chargeons le modèle que nous avons enregistré après l'entraînement, l'alimentant des images dans un ensemble de validation et le renvoi de l'image_embeddings avec une forme (valid_set_size, 256) et le modèle lui-même.
def get_image_embeddings ( valid_df , model_path ):
tokenizer = DistilBertTokenizer . from_pretrained ( CFG . text_tokenizer )
valid_loader = build_loaders ( valid_df , tokenizer , mode = "valid" )
model = CLIPModel (). to ( CFG . device )
model . load_state_dict ( torch . load ( model_path , map_location = CFG . device ))
model . eval ()
valid_image_embeddings = []
with torch . no_grad ():
for batch in tqdm ( valid_loader ):
image_features = model . image_encoder ( batch [ "image" ]. to ( CFG . device ))
image_embeddings = model . image_projection ( image_features )
valid_image_embeddings . append ( image_embeddings )
return model , torch . cat ( valid_image_embeddings ) _ , valid_df = make_train_valid_dfs ()
model , image_embeddings = get_image_embeddings ( valid_df , "best.pt" )Cette fonction fait la tâche finale dont nous souhaitons que notre modèle serait capable: il obtient le modèle, l'image_embeddings et une requête de texte. Il affichera les images les plus pertinentes de l'ensemble de validation! N'est-ce pas incroyable? Voyons comment cela fonctionne après tout!
def find_matches ( model , image_embeddings , query , image_filenames , n = 9 ):
tokenizer = DistilBertTokenizer . from_pretrained ( CFG . text_tokenizer )
encoded_query = tokenizer ([ query ])
batch = {
key : torch . tensor ( values ). to ( CFG . device )
for key , values in encoded_query . items ()
}
with torch . no_grad ():
text_features = model . text_encoder (
input_ids = batch [ "input_ids" ], attention_mask = batch [ "attention_mask" ]
)
text_embeddings = model . text_projection ( text_features )
image_embeddings_n = F . normalize ( image_embeddings , p = 2 , dim = - 1 )
text_embeddings_n = F . normalize ( text_embeddings , p = 2 , dim = - 1 )
dot_similarity = text_embeddings_n @ image_embeddings_n . T
values , indices = torch . topk ( dot_similarity . squeeze ( 0 ), n * 5 )
matches = [ image_filenames [ idx ] for idx in indices [:: 5 ]]
_ , axes = plt . subplots ( 3 , 3 , figsize = ( 10 , 10 ))
for match , ax in zip ( matches , axes . flatten ()):
image = cv2 . imread ( f" { CFG . image_path } / { match } " )
image = cv2 . cvtColor ( image , cv2 . COLOR_BGR2RGB )
ax . imshow ( image )
ax . axis ( "off" )
plt . show ()C'est ainsi que nous utilisons cette fonction. Aaaannnndddd les résultats:
find_matches ( model ,
image_embeddings ,
query = "a group of people dancing in a party" ,
image_filenames = valid_df [ 'image' ]. values ,
n = 9 )
J'espère que vous avez apprécié cet article. La mise en œuvre de ce document a été une expérience vraiment intéressante pour moi. Je tiens à remercier Khalid Salama pour l'exemple du Great Keras Code qu'il a fourni, ce qui m'a inspiré à écrire quelque chose de similaire à Pytorch.