Me complace descubrir que este código ha sido utilizado y citado en los siguientes documentos:
Domino: Descubrir errores sistemáticos con incrustaciones intermodales por Eyuboglu et. Alabama. en ICLR 2022
GSCLIP: un marco para explicar los cambios de distribución en el lenguaje natural por Zhu et. Alabama. en ICML 2022
UIC-NLP en Semeval-2022 Tarea 5: Explorando el aprendizaje contrastante para la detección multimodal de memes misóginos por Cuervo et. Alabama. en Semeval-2022
CDSBERT: extendiendo modelos de lenguaje de proteínas con conciencia de codones de Hallee et. Alabama. de la Universidad de Delaware (septiembre de 2023)
Enigma-51: Hacia una comprensión de grano fino de las interacciones de objeto humano en escenarios industriales por Ragusa et. Alabama. (Nov 2023)
Puede encontrar la información de citas en la sección correcta de esta página de repositorio de GitHub llamada: cita este repositorio o usar la información de cita a continuación.
@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 }
}Fue en enero de 2021 que OpenAI anunció dos nuevos modelos: Dall-E y Clip , ambos modelos multimodalidad que conectan textos e imágenes de alguna manera. En este artículo vamos a implementar el modelo de clip desde cero en Pytorch . OpenAi ha de origen abierto parte del código relacionado con el modelo de clip, pero me pareció intimidante y estaba lejos de ser algo corto y simple. ¡También me encontré con un buen tutorial inspirado en el modelo de clip en los ejemplos de código Keras y traducí algunas partes a Pytorch para construir este tutorial totalmente con nuestro amado Pytorch!
En el aprendizaje de modelos visuales transferibles del papel de supervisión del lenguaje natural, OpenAi presenta su nuevo modelo que se llama clip , para el pre-entrenamiento de imagen de lenguaje contrastante . En pocas palabras, este modelo aprende la relación entre una oración completa y la imagen que describe; En el sentido de que cuando el modelo esté capacitado, dada una oración de entrada, podrá recuperar las imágenes más relacionadas correspondientes a esa oración. Lo importante aquí es que está entrenado en oraciones completas en lugar de clases individuales como automóvil, perro, etc. La intuición es que cuando se entrena en oraciones completas, el modelo puede aprender muchas más cosas y encuentra algún patrón entre imágenes y textos. También muestran que cuando este modelo está entrenado en un gran conjunto de datos de imágenes y sus textos correspondientes, también puede actuar como un clasificador. Le animo a que estudie el documento para que aprenda más sobre este emocionante modelo y sus asombrosos resultados en conjuntos de datos de evaluación comparativa. ¡Para mencionar solo uno, el modelo de clip entrenado con esta estrategia clasifica ImageNet mejor que aquellos modelos SOTA entrenados en el ImageNet en sí optimizado para la única tarea de clasificación!
Como teaser (!), Veamos cuál es el modelo final que construiremos en este artículo desde cero: es capaz de: dada una consulta (texto crudo) como "un niño saltando con skate" o "una niña saltando desde el swing", el modelo recuperará las imágenes más relevantes:

Veamos algunas salidas más:

# !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 Una nota sobre config y cfg: escribí los códigos con scripts de Python y luego lo convertí en un cuaderno Jupyter. Entonces, en el caso de los scripts de Python, la configuración es un archivo de Python normal donde pongo todos los hiperparámetros y, en el caso de Jupyter Notebook, es una clase definida al comienzo del cuaderno para mantener todos los hiperparámetros.
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" ]Como puede ver en la imagen de Tittle de este artículo, necesitamos codificar tanto las imágenes como sus textos de descripción. Entonces, el conjunto de datos necesita devolver imágenes y textos . ¡Por supuesto que no vamos a alimentar el texto crudo a nuestro codificador de texto! Usaremos el modelo Distilbert (que es más pequeño que Bert pero funciona casi tan bien como Bert) de Huggingface Library como nuestro codificador de texto; Por lo tanto, necesitamos tokenizar las oraciones (subtítulos) con Tokenizer Distilbert y luego alimentar las ID de token (Input_IDS) y las máscaras de atención a Distilbert. Por lo tanto, el conjunto de datos también debe ocuparse de la tokenización. A continuación puede ver el código del conjunto de datos. Debajo de eso explicaré las cosas más importantes que están sucediendo en el código.
En el __init__ recibimos un objeto de tokenizador que en realidad es un tokinzer de la cara de abrazo; Este tokenizador se cargará al ejecutar el modelo. Estamos acolchando y truncando los subtítulos a una longitud máxima especificada. En el __getItem__, primero cargaremos una leyenda codificada que es un diccionario con teclas input_ids y atención_mask, haremos tensores con sus valores y después de eso cargaremos la imagen correspondiente, la transformaremos y aumentaremos la (si hay alguno!) Y luego lo hacemos un tensor y lo ponemos en la dicción con la "imagen" como la clave ". Finalmente, colocamos el texto en bruto de la leyenda con el "título" clave en el diccionario solo para fines de visualización.
No utilicé aumentos de datos adicionales, pero puede agregarlos si desea mejorar el rendimiento del modelo.
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 ),
]
)El código del codificador de imagen es sencillo. Aquí estoy usando la Biblioteca de modelos de imagen Pytorch (TIMM), lo que hace que muchos modelos de imagen diferentes estén disponibles desde resnets hasta redes eficientes y muchos más. Aquí usaremos un resnet50 como nuestro codificador de imagen. Puede usar fácilmente la biblioteca TorchVision para usar resnets si no desea instalar una nueva biblioteca.
El código codifica cada imagen a un vector de tamaño fijo con el tamaño de los canales de salida del modelo (en el caso de resnet50, el tamaño del vector será 2048 ). Esta es la salida después de la capa 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 )Como mencioné antes, usaré Distilbert como codificador de texto. Al igual que su hermano mayor Bert, se agregarán dos tokens especiales a los tokens de entrada reales: CLS y SEP que marcan el inicio y el final de una oración. Para obtener toda la representación de una oración (como señalan los documentos relacionados de Bert y Distilbert) usamos las representaciones finales del token CLS y esperamos que esta representación capture el significado general de la oración (subtítulos). Pensando de esta manera, es similar a lo que le hicimos a las imágenes y las convertimos en un vector de tamaño fijo.
En el caso de Distilbert (y también Bert), la representación oculta de salida para cada token es un vector con tamaño 768 . Por lo tanto, todo el título se codificará en la representación de token CLS cuyo tamaño es 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 , :]Utilicé la implementación del ejemplo del código Keras de la cabeza de proyección para escribir lo siguiente en Pytorch. Ahora que hemos codificado nuestras imágenes y textos en vectores de tamaño fijo (2048 para imagen y 768 para texto) necesitamos traerlos (proyectar) a un mundo nuevo (!) Con dimensiones similares para imágenes y textos para poder compararlas y separar la imagen y los textos no relevantes y unir las que coinciden. Por lo tanto, el siguiente código llevará los vectores dimensionales 2048 y 768 a un mundo dimensional 256 (Proyect_Dim), donde podemos compararlos .
"Ingredding_dim" es el tamaño del vector de entrada (2048 para imágenes y 768 para textos) y "Projection_Dim" es el tamaño del vector de salida que será 256 para nuestro caso. Para comprender los detalles de esta parte, puede consultar el papel de 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 ¡Esta parte es donde sucede toda la diversión! También hablaré sobre la función de pérdida aquí. Traducí parte del código de los ejemplos de código Keras a Pytorch para escribir esta parte. Eche un vistazo al código y luego lea la explicación debajo de este bloque de código.
Aquí usaremos los módulos anteriores que creamos para implementar el modelo principal. La función __init__ se explica por sí misma. En la función de avance, primero codificamos las imágenes y los textos por separado en vectores de tamaño fijo (con diferentes dimensiones). Después de eso, utilizando módulos de proyección separados los proyectamos a ese mundo compartido (espacio) del que hablé anteriormente. Aquí las codificaciones tendrán una forma similar (256 en nuestro caso). Después de eso calcularemos la pérdida. Nuevamente recomiendo leer papel clip para mejorar, pero haré todo lo posible para explicar esta parte.
En el álgebra lineal , una forma común de medir si dos vectores son de características similares (son como la otra) es calcular su producto DOT (multiplicar las entradas coincidentes y tomar la suma de ellos); Si el número final es grande, son iguales y si es pequeño, no lo son (relativamente hablando).
¡Bueno! Lo que acabo de decir es lo más importante que debe tener en cuenta para comprender esta función de pérdida. Continuemos. Hablamos de dos vectores, pero, ¿qué tenemos aquí? Tenemos Image_embeddings, una matriz con forma (Batch_Size, 256) y Text_embeddings con forma (Batch_Size, 256). ¡Muy fácil! Significa que tenemos dos grupos de vectores en lugar de dos vectores individuales. ¿Cómo medimos cuán similares son dos grupos de vectores (dos matrices) entre sí? Nuevamente, con el producto DOT (@ operador en Pytorch hace el producto DOT o la multiplicación de matriz en este caso). Para poder multiplicar estas dos matrices juntas, transponemos la segunda. Bien, obtenemos una matriz con forma (Batch_Size, Batch_Size) que llamaremos logits. (La temperatura es igual a 1.0 en nuestro caso, por lo que no hace la diferencia. Puedes jugar con ella y ver qué diferencia hace. ¡También mira el papel para ver por qué está aquí!).
¡Espero que todavía estés conmigo! Si no está bien, simplemente revise el código y verifique sus formas. Ahora que tenemos nuestros logits, necesitamos objetivos. Necesito decir que hay una forma más directa de obtener objetivos, pero tuve que hacer esto para nuestro caso (hablaré sobre por qué en un próximo párrafo).
Consideremos lo que esperamos que este modelo aprenda: queremos que aprenda "representaciones similares (vectores)" para una imagen determinada y la leyenda que lo describe. Lo que significa que o le damos una imagen o el texto que lo describe, queremos que produzca los mismos 256 vectores de tamaño para ambos.
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 ()Entonces, en el mejor de los casos, las matrices Text_embeddings y Image_embedding deberían ser las mismas porque están describiendo cosas similares. Pensemos ahora: si esto sucede, ¿cómo sería la matriz de logits? ¡Veamos con un ejemplo 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 ))Por lo tanto, los logits, en el mejor de los casos, serán una matriz de que si tomamos su softmax, tendrá 1.0 en la diagonal (¡una matriz de identidad para llamarlo con palabras elegantes!). Como el trabajo de la función de pérdida es hacer que las predicciones del modelo sean similares a los objetivos (¡al menos en la mayoría de los casos!), Queremos una matriz como nuestro objetivo. Esa es la razón por la que estamos calculando las matrices de imágenes_similarity y texts_similarity en el bloque de código anterior.
Ahora que tenemos nuestra matriz de objetivos, usaremos entropía cruzada simple para calcular la pérdida real. He escrito la forma de matriz completa de entropía cruzada en función que puede ver en la parte inferior del bloque de código. ¡Bueno! ¡Hemos terminado! ¿No fue simple? Muy bien, puede ignorar el siguiente párrafo, pero si tiene curiosidad, hay una nota importante en eso.
He aquí por qué no utilicé un enfoque más simple : necesito admitir que hay una forma más simple de calcular esta pérdida en Pytorch; Al hacer esto: nn.crossentropyloss () (logits, tortch.arange (batch_size)). ¿Por qué no lo usé aquí? Por 2 razones. 1- El conjunto de datos que estamos utilizando tiene múltiples subtítulos para una sola imagen; Por lo tanto, existe la posibilidad de que existan dos imágenes idénticas con sus subtítulos similares en un lote (es raro pero puede suceder). Tomar la pérdida con este método más fácil ignorará esta posibilidad y el modelo aprende a separar dos representaciones (suponga que son diferentes) que en realidad son los mismos. Obviamente, no queremos que esto suceda, así que calculé toda la matriz objetivo de una manera que se encarga de estos casos de borde. 2- haciéndolo como lo hice, me dio una mejor comprensión de lo que está sucediendo en esta función de pérdida; ¡Entonces, pensé que también te daría una mejor intuición!
Aquí hay algunas funciones para ayudarnos a cargar entrenar y dataloaders válidos, nuestro modelo y luego entrenar y evaluar nuestro modelo en ellos. No está pasando mucho aquí; solo un bucle de entrenamiento simple y funciones de utilidad
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 dataloaderAquí hay una función útil para entrenar nuestro modelo. No hay mucho que suceda aquí; Simplemente cargando los lotes, alimentándolos al modelo y pisando el optimizador y 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 )Ejecutando la siguiente celda inicio de entrenamiento del modelo. Coloque el núcleo en modo GPU. Cada época debe tomar unos 24 minutos en GPU (¡incluso una época es suficiente!). Puede pasar un minuto antes de que comience el entrenamiento porque vamos a codificar todos los subtítulos una vez en el tren y el conjunto de datos válidos, ¡así que no lo detenga! Todo está funcionando bien.
main ()¡Bueno! Hemos terminado con el entrenamiento del modelo. Ahora, debemos hacer una inferencia que en nuestro caso le dará al modelo un texto y queremos que recupere las imágenes más relevantes de un conjunto de validación (o prueba) invisible.
En esta función, estamos cargando el modelo que guardamos después del entrenamiento, alimentándole imágenes en el conjunto de validación y devolviendo el Image_Embeddings con Shape (VALAL_SET_SIZE, 256) y el modelo en sí.
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" )Esta función realiza la tarea final de la que deseamos que nuestro modelo fuera capaz: obtiene el modelo, image_embeddings y una consulta de texto. ¡Mostrará las imágenes más relevantes del conjunto de validación! ¿No es asombroso? ¡Veamos cómo funciona después de todo!
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 ()Así es como usamos esta función. Aaaannndddd los resultados:
find_matches ( model ,
image_embeddings ,
query = "a group of people dancing in a party" ,
image_filenames = valid_df [ 'image' ]. values ,
n = 9 )
Espero que hayas disfrutado este artículo. Implementar este documento fue una experiencia realmente interesante para mí. Quiero agradecer a Khalid Salama por el ejemplo del Código Gran Keras que proporcionó que me inspiró a escribir algo similar en Pytorch.