Fico feliz em descobrir que este código foi usado e citado nos documentos a seguir:
Domino: Descobrindo erros sistemáticos com incorporações cruzadas por Eyuboglu et. al. no ICLR 2022
GSCLIP: Uma estrutura para explicar mudanças de distribuição na linguagem natural por Zhu et. al. no ICML 2022
UIC-NLP na Tarefa Semeval-2022 5: Explorando o aprendizado contrastivo para a detecção multimodal de memes misóginos por Cuervo et. al. em Semeval-2022
CDSBERT - Estendendo modelos de linguagem de proteínas com consciência de códon de Hallee et. al. da Universidade de Delaware (setembro de 2023)
Enigma-51: Rumo a uma compreensão de granulação fina das interações humano-objeto em cenários industriais de Ragusa et. al. (Novembro de 2023)
Você pode encontrar as informações de citação na seção correta desta página de repositório do GitHub denominada: citar este repositório ou usar as informações de citação abaixo.
@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 }
}Foi em janeiro de 2021 que o OpenAI anunciou dois novos modelos: Dall-E e Clip , ambos os modelos de multimodalidade que conectam textos e imagens de alguma forma. Neste artigo, vamos implementar o modelo de clipe do zero em Pytorch . O OpenAI tem de origem aberta parte do código relacionado ao modelo de clipe, mas achei intimidador e estava longe de ser algo curto e simples. Também me deparei com um bom tutorial inspirado no modelo de clipe em exemplos de código Keras e traduzi algumas partes dele em Pytorch para construir este tutorial totalmente com nosso amado Pytorch!
Na aprendizagem de modelos visuais transferíveis do artigo de supervisão de linguagem natural, o OpenAI apresenta seu novo modelo, chamado clipe , para o pré-treinamento contrastante de imagem de linguagem . Em poucas palavras, esse modelo aprende a relação entre uma frase inteira e a imagem que descreve; Em certo sentido, quando o modelo é treinado, dada uma frase de entrada, ele poderá recuperar as imagens mais relacionadas correspondentes a essa frase. O importante aqui é que ele é treinado em frases completas, em vez de classes únicas como carro, cachorro etc. A intuição é que, quando treinadas em frases inteiras, o modelo pode aprender muito mais coisas e encontrar algum padrão entre imagens e textos. Eles também mostram que, quando esse modelo é treinado em um enorme conjunto de dados de imagens e seus textos correspondentes, ele também pode atuar como um classificador. Encorajo você a estudar o artigo para saber mais sobre esse modelo emocionante e seus resultados surpreendentes nos conjuntos de dados de benchmarking. Para mencionar apenas um, o modelo de clipe treinou com essa estratégia classifica o ImageNet melhor do que os modelos SOTA treinados na própria imagem otimizada para a única tarefa de classificação!
Como teaser (!), Vamos ver qual o modelo final que criaremos neste artigo do zero é capaz de: dada uma consulta (texto cru) como "Um garoto pulando com skate" ou "uma garota pulando de swing", o modelo recuperará as imagens mais relevantes:

Vamos ver mais alguns resultados:

# !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 Uma nota no Config e CFG: escrevi os códigos com scripts Python e depois o converti em um notebook Jupyter. Portanto, no caso de scripts Python, o Config é um arquivo python normal, onde eu coloco todos os hiperparâmetros e, no caso do Jupyter Notebook, é uma classe definida no início do notebook para manter todos os hyperparameters.
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 você pode ver na imagem Tittle deste artigo, precisamos codificar as duas imagens e os textos descrevendo. Portanto, o conjunto de dados precisa retornar imagens e textos . Claro que não vamos alimentar texto bruto ao nosso codificador de texto! Usaremos o modelo Distilbert (que é menor que o BERT, mas executa quase tão bem quanto o Bert) da Biblioteca do Huggingface como nosso codificador de texto; Portanto, precisamos tokenizar as frases (legendas) com o Distilbert Tokenizer e, em seguida, alimentar os IDs de token (input_ids) e as máscaras de atenção para destilbert. Portanto, o conjunto de dados também precisa cuidar da tokenização. Abaixo, você pode ver o código do conjunto de dados. Abaixo disso, explicarei as coisas mais importantes que estão acontecendo no código.
No __init__, recebemos um objeto tokenizador que é na verdade um Tokinzer de Huggingface; Este tokenizador será carregado ao executar o modelo. Estamos preenchendo e truncando as legendas para um max_length especificado. No __getItem__, primeiro carregaremos uma legenda codificada, que é um dicionário com chaves input_ids e attion_mask, fará os tensores fora de seus valores e, depois disso, carregaremos a imagem correspondente, transformaremos e aumentaremos (se houver alguma!) E depois o tornaremos um tensor e o colocaremos no dicionário com "imagem" como a chave. Finalmente, colocamos o texto bruto da legenda com a chave "legenda" no dicionário apenas para fins de visualização.
Não usei aumentos adicionais de dados, mas você pode adicioná -los se quiser melhorar o desempenho do 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 ),
]
)O código do codificador de imagem é direto. Estou usando a Biblioteca de Modelos de Imagens Pytorch (Timm) aqui, o que disponibiliza muitos modelos de imagens diferentes de Resnets a EfientNets e muito mais. Aqui usaremos um resnet50 como nosso codificador de imagem. Você pode usar facilmente a Biblioteca Torchvision para usar Resnets se não quiser instalar uma nova biblioteca.
O código codifica cada imagem para um vetor de tamanho fixo com o tamanho dos canais de saída do modelo (no caso de resnet50, o tamanho do vetor será 2048 ). Esta é a saída após a camada 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 mencionei antes, usarei o Distilbert como o codificador de texto. Como seu irmão maior Bert, dois tokens especiais serão adicionados aos tokens de entrada reais: CLS e SEP, que marcam o início e o fim de uma frase. Para agarrar toda a representação de uma frase (como apontam os artigos relacionados a Bert e Distilbert), usamos as representações finais do token CLS e esperamos que essa representação capture o significado geral da frase (legenda). Pensando dessa maneira, é semelhante ao que fizemos com as imagens e as convertemos em um vetor de tamanho fixo.
No caso de destilbert (e também bert), a representação oculta de saída para cada token é um vetor com tamanho 768 . Portanto, toda a legenda será codificada na representação do token CLS, cujo tamanho é 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 , :]Usei o exemplo de código Keras, implementação do chefe de projeção para escrever o seguinte em Pytorch. Agora que codificamos nossas imagens e textos em vetores de tamanho fixo (2048 para imagem e 768 para texto), precisamos trazê-los (projetá-los) para um novo mundo (!) Com dimensões semelhantes para imagens e textos para poder compará-los e separar a imagem e textos não relevantes e reunir aqueles que correspondem. Portanto, o código a seguir trará os vetores dimensionais 2048 e 768 para um mundo dimensional de 256 (projection_dim), onde podemos compará -los.
"Embedding_dim" é o tamanho do vetor de entrada (2048 para imagens e 768 para textos) e "projection_dim" é o tamanho do vetor de saída que será 256 para o nosso caso. Para entender os detalhes desta parte, você pode consultar o papel de clipe.
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 é onde toda a diversão acontece! Também falarei sobre a função de perda aqui. Traduzi parte do código dos exemplos de código Keras em Pytorch para escrever esta parte. Dê uma olhada no código e leia a explicação abaixo deste bloco de código.
Aqui usaremos os módulos anteriores que criamos para implementar o modelo principal. A função __init__ é auto-explicativa. Na função avançada, primeiro codificamos as imagens e textos separadamente em vetores de tamanho fixo (com diferentes dimensões). Depois disso, usando módulos de projeção separados, projetamos -os para esse mundo compartilhado (espaço) sobre o qual falei anteriormente. Aqui, as codificações se tornarão de forma semelhante (256 no nosso caso). Depois disso, calcularemos a perda. Mais uma vez, recomendo a leitura de papel do clipe para melhorar, mas vou tentar o meu melhor para explicar esta parte.
Na álgebra linear , uma maneira comum de medir se dois vetores são de características semelhantes (elas são como uma à outra) é calcular seu produto DOT (multiplicar as entradas correspondentes e tirar a soma deles); Se o número final for grande, eles são iguais e se forem pequenos, eles não são (relativamente falando)!
OK! O que eu acabei de dizer é a coisa mais importante a ter em mente para entender essa função de perda. Vamos continuar. Conversamos sobre dois vetores, mas o que temos aqui? Temos image_embeddings, uma matriz com forma (batch_size, 256) e text_embeddings com forma (batch_size, 256). Fácil o suficiente! Isso significa que temos dois grupos de vetores em vez de dois vetores únicos. Como medimos como dois grupos de vetores (duas matrizes) são semelhantes? Novamente, com o produto DOT (@ operador em Pytorch faz o produto DOT ou a multiplicação da matriz nesse caso). Para poder multiplicar essas duas matrizes, transpõimos o segundo. Ok, temos uma matriz com forma (Batch_size, Batch_size), que chamaremos de logits. (A temperatura é igual a 1,0 no nosso caso, portanto, não faz a diferença. Você pode brincar com ele e ver que diferença faz. Também observe o papel para ver por que está aqui!).
Espero que você ainda esteja comigo! Caso contrário, basta revisar o código e verificar suas formas. Agora que temos nossos logits, precisamos de metas. Preciso dizer que existe uma maneira mais direta de obter metas, mas tive que fazer isso para o nosso caso (falarei sobre por que em um próximo parágrafo).
Vamos considerar o que esperamos que esse modelo aprenda: queremos que ele aprenda "representações semelhantes (vetores)" para uma determinada imagem e a legenda que a descreve. Significando que fornecemos uma imagem ou o texto que a descreve, queremos que ela produza os mesmos vetores de 256 de tamanho 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 ()Portanto, na melhor das hipóteses, o Text_Embeddings e o Image_EMbedding Matricies devem ser os mesmos, porque estão descrevendo coisas semelhantes. Vamos pensar agora: se isso acontecer, como seria a matriz de logits? Vamos ver com um exemplo simples!
# A simple Example
batch_size = 4
dim = 256
embeddings = torch . randn ( batch_size , dim )
out = embeddings @ embeddings . T
print ( F . softmax ( out , dim = - 1 ))Portanto, os logits, na melhor das hipóteses, serão uma matriz de que, se tomarmos seu softmax, terá 1.0s na diagonal (uma matriz de identidade para chamá -la com palavras sofisticadas!). Como o trabalho da função de perda é tornar as previsões do modelo semelhantes às metas (pelo menos na maioria dos casos!), Queremos uma matriz como nosso alvo. Essa é a razão pela qual estamos calculando as matrizes de imagens_similaridade e textos_similaridade no bloco de código acima.
Agora que temos nossa matriz de metas, usaremos uma entropia cruzada simples para calcular a perda real. Eu escrevi a forma completa da Matrix de entropia cruzada como uma função que você pode ver na parte inferior do bloco de código. OK! Terminamos! Não era simples?! Tudo bem, você pode ignorar o próximo parágrafo, mas se você estiver curioso, há uma nota importante nisso.
Eis por que eu não usei uma abordagem mais simples : preciso admitir que há uma maneira mais simples de calcular essa perda em Pytorch; Ao fazer isso: nn.crossentropyloss () (logits, tochas.arange (batch_size)). Por que eu não usei aqui? Por 2 razões. 1- O conjunto de dados que estamos usando tem várias legendas para uma única imagem; Portanto, existe a possibilidade de que duas imagens idênticas com suas legendas existam em um lote (é raro, mas pode acontecer). Tomar a perda com esse método mais fácil ignorará essa possibilidade e o modelo aprenderá a separar duas representações (suponha que sejam diferentes) que são realmente iguais. Obviamente, não queremos que isso aconteça, então calculei toda a matriz de destino de uma maneira que cuida desses casos de borda. 2- Fazer da maneira que fiz, me deu uma melhor compreensão do que está acontecendo nessa função de perda; Então, eu pensei que isso lhe daria uma melhor intuição também!
Aqui estão algumas funções para nos ajudar a carregar o trem e os dados válidos, nosso modelo e, em seguida, treinar e avaliar nosso modelo neles. Não há muita coisa acontecendo aqui; Apenas loop de treinamento simples e funções de utilidade
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 dataloaderAqui está uma função útil para treinar nosso modelo. Não há muita coisa acontecendo aqui; Apenas carregando os lotes, alimentando -os com o modelo e passando o otimizador e 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 )Executar a próxima célula começa a treinar o modelo. Coloque o kernel no modo GPU. Cada época deve levar cerca de 24 minutos na GPU (mesmo uma época é suficiente!). Pode demorar um minuto antes do treinamento começar, porque vamos codificar todas as legendas uma vez no trem e no conjunto de dados válidos, então, por favor, não pare! Tudo está funcionando bem.
main ()OK! Terminamos o treinamento do modelo. Agora, precisamos fazer a inferência que, em nosso caso, estará dando ao modelo uma peça de texto e deseja que ele recupere as imagens mais relevantes de um conjunto de validação (ou teste) invisível.
Nesta função, estamos carregando o modelo que salvamos após o treinamento, alimentando imagens de TI no conjunto de validação e retornando o Image_Embeddings com Shape (Valid_Set_Size, 256) e o próprio modelo.
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 função faz a tarefa final da qual desejamos que nosso modelo fosse capaz: ele recebe o modelo, o image_embeddings e uma consulta de texto. Ele exibirá as imagens mais relevantes do conjunto de validação! Não é incrível? Vamos ver como ele funciona, afinal!
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 ()É assim que usamos essa função. Aaaannnndddd os 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 você tenha gostado deste artigo. Implementar este artigo foi uma experiência realmente interessante para mim. Quero agradecer a Khalid Salama pelo exemplo do Código do Grande Keras que ele forneceu, que me inspirou a escrever algo semelhante em Pytorch.