Я рад узнать, что этот код использовался и цитируется в следующих документах:
Domino: Обнаружение систематических ошибок с межмодальными встроениями Eyuboglu et. ал. в ICLR 2022
GSClip: структура для объяснения сдвигов распределения на естественном языке от Zhu et. ал. в ICML 2022
UIC-NLP в Semeval-2022 Задача 5: Изучение контрастного обучения для мультимодального обнаружения женоненавистнических мемов Cuervo et. ал. в Semeval-2012
CDSBERT - Расширение моделей белкового языка с осознанием кодона Hallee et. ал. из Университета штата Делавэр (сентябрь 2023 г.)
Enigma-51: На пути к мелкозернистому пониманию взаимодействий с человеком в промышленных сценариях Ragusa et. ал. (Ноябрь 2023 г.)
Вы можете найти информацию о цитировании в правом разделе этой страницы Repo GitHub с именем: цитируйте этот репозиторий или используйте приведенную ниже информацию о цитировании.
@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 }
}В январе 2021 года OpenAI анонсировала две новые модели: Dall-E и Clip , обе модели с несколькими модальными, которые каким-то образом подключали тексты и изображения . В этой статье мы собираемся реализовать модель клипа с нуля в Pytorch . OpenAI открыл некоторые из кодов, относящихся к модели клипа, но я нашел его пугающим, и он был далеко не чем-то коротким и простым. Я также наткнулся на хорошее руководство, вдохновленное моделью клипа на примерах кода Keras, и я перевел некоторые его части в Pytorch, чтобы полностью построить этот учебник с нашим любимым Pytorch!
В обучении переносимых визуальных моделей из бумаги по надзору за естественным языком OpenAI представляет свою новую модель, которая называется CLIP , для контрастного предварительного обучения языкового изображения . Короче говоря, эта модель изучает взаимосвязь между целым предложением и изображением, которое он описывает; В некотором смысле, что когда модель обучена, учитывая входное предложение, она сможет получить наиболее связанные изображения, соответствующие этому предложению. Здесь важна то, что он обучается полным предложениям вместо отдельных классов, таких как автомобиль, собака и т. Д. Интуиция заключается в том, что при обучении на целых предложениях модель может выучить гораздо больше вещей и находит некоторый шаблон между изображениями и текстами. Они также показывают, что когда эта модель обучена огромному набору данных изображений и их соответствующих текстов, она также может действовать как классификатор. Я призываю вас изучить статью, чтобы узнать больше об этой захватывающей модели и их удивительных результатах на наборах данных сравнительного анализа. Чтобы упомянуть только одну, модель клипа, обученная этой стратегией, классифицирует ImageNet лучше, чем те модели SOTA, обученные самому ImageNet, оптимизированной для единственной задачи классификации!
Как тизер (!), Давайте посмотрим, на что мы построим в этой статье в этой статье с нуля: Учитывая запрос (сырой текст), как «мальчик, прыгающий со скейтбордом» или «Девушка, прыгающая от свинга», модель будет извлекать самые релевантные изображения:

Посмотрим еще несколько выходов:

# !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 Примечание на конфигурации и CFG: я написал коды с помощью сценариев Python, а затем преобразовал в ноутбук Jupyter. Таким образом, в случае сценариев Python Config - это обычный файл Python, в котором я помещаю все гиперпараметры и в случае ноутбука Jupyter, это класс, определенный в начале ноутбука, чтобы сохранить все гиперпараметра.
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" ]Как вы можете видеть на изображении Tittle этой статьи, нам нужно кодировать как изображения, так и их описание текстов. Таким образом, набор данных должен вернуть как изображения, так и тексты . Конечно, мы не собираемся подавать необработанный текст в наш текстовый энкодер! Мы будем использовать модель Distilbert (которая меньше BERT, но работает почти так же, как BERT) из библиотеки HuggingFace , как наш текстовый кодер; Таким образом, нам нужно токенизировать предложения (подписи) с токенизатором дистильберта, а затем подавать идентификаторы токена (input_ids) и маски внимания для Дисмилберта. Поэтому набор данных также должен позаботиться о токенизации. Ниже вы можете увидеть код набора данных. Ниже я объясню самые важные вещи, которые происходят в коде.
В __init__ мы получаем объект -токенизатор, который на самом деле является объявлением Tokinzer; Этот токенизатор будет загружен при запуске модели. Мы накладываем накладку и усекаем подписи к указанному max_length. В __getItem__ мы сначала загрузим закодированную подпись, которая представляет собой словарь с ключами input_ids и внимания, изготовление тензоров из его значений, а после этого мы загрузим соответствующее изображение, преобразовать и увеличить его (если есть!), А затем мы сделаем его тензором и поместим его в слоем с «изображением». Наконец, мы поместили необработанный текст заголовка с ключом «Подпись» в словаре только для целей визуализации.
Я не использовал дополнительные дополнения данных, но вы можете добавить их, если хотите улучшить производительность модели.
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 ),
]
)Код энкодера изображения прост. Я использую здесь библиотеку моделей изображений Pytorch (TIMM), которая предоставляет множество различных моделей изображений, доступных от Resnets, до эффективной сети и многих других. Здесь мы будем использовать Resnet50 в качестве нашего энкодера изображения. Вы можете легко использовать библиотеку Torchvision для использования Resnets, если вы не хотите устанавливать новую библиотеку.
Код кодирует каждое изображение вектору фиксированного размера с размером выходных каналов модели (в случае RESNET50 размер вектора будет 2048 ). Это выход после слоя 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 )Как я упоминал ранее, я буду использовать Distilbert в качестве текстового кодера. Как и его более крупный брат Берт, два специальных токена будут добавлены в фактические входные токены: CLS и SEP , которые отмечают начало и конец предложения. Чтобы получить все представление предложения (как указывают связанные документы Bert и Distilbert), мы используем окончательные представления токена CLS, и мы надеемся, что это представление отражает общее значение предложения (заголовок). Думая об этом таким образом, это похоже на то, что мы сделали с изображениями, и преобразовали их в вектор с фиксированным размером.
В случае Дистильберта (а также BERT) выходное скрытое представление для каждого токена представляет собой вектор с размером 768 . Таким образом, весь заголовок будет кодироваться в представлении токена CLS, размер которого составляет 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 , :]Я использовал пример примера кода Keras, чтобы написать следующее в Pytorch. Теперь, когда мы закодировали как наши изображения, так и тексты в векторы фиксированного размера (2048 для изображения и 768 для текста), нам нужно принести (проецировать) их в новый мир (!) С одинаковыми размерами как для изображений, так и для текстов, чтобы сравнить их и разделить не связанные с ними изображения и тексты, а также поддерживать те, которые соответствуют. Таким образом, следующий код принесет размеры 2048 и 768 размерных векторов в мир 256 (Protection_dim), где мы можем сравнить их.
«Embedding_dim» - это размер входного вектора (2048 для изображений и 768 для текстов), а «Protection_dim» - это размер выходного вектора, который будет 256 для нашего случая. Для понимания деталей этой части вы можете обратиться к бумаге клипа.
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 Эта часть, где происходит все удовольствие! Я также расскажу о функции потери здесь. Я перевел некоторые из кодов из примеров кода Keras в Pytorch для написания этой части. Посмотрите на код, а затем прочитайте объяснение ниже этого блока кода.
Здесь мы будем использовать предыдущие модули, которые мы создали для реализации основной модели. Функция __init__ является самоэкспланирующей. В форвардной функции мы сначала кодируем изображения и тексты отдельно в векторы фиксированного размера (с разными размерами). После этого, используя отдельные проекционные модули, мы проецируем их в тот общий мир (пространство), о котором я говорил ранее. Здесь кодировки станут аналогичной формой (256 в нашем случае). После этого мы будем вычислить потерю. Я снова рекомендую читать бумагу для клипа, чтобы получить ее лучше, но я стараюсь изо всех сил объяснить эту часть.
В линейной алгебре один общий способ измерить, имеют ли два вектора сходные характеристики (они похожи друг на друга), - это рассчитать их точечный продукт (умножение соответствующих записей и взять их сумму); Если окончательное число большое, они одинаковы, и если оно мало, они не являются (относительно говорящие)!
Хорошо! То, что я только что сказал, является наиболее важной вещью, чтобы понять эту функцию потери. Давайте продолжим. Мы говорили о двух векторах, но что у нас здесь? У нас есть Image_embeddings, матрица с формой (batch_size, 256) и Text_embeddings с формой (batch_size, 256). Достаточно легко! Это означает, что у нас есть две группы векторов вместо двух одиночных векторов. Как мы измеряем, насколько похожи две группы векторов (две матрицы) друг для друга? Опять же, с точечным продуктом (@ Оператор в Pytorch делает точечный продукт или умножение матрицы в этом случае). Чтобы иметь возможность умножить эти две матрицы вместе, мы транспонируем второе. Хорошо, мы получаем матрицу с формой (batch_size, batch_size), которую мы будем называть Logits. (Температура равна 1,0 в нашем случае, поэтому она не имеет значения. Вы можете играть с ней и посмотреть, какую разницу это имеет. Также посмотрите на бумагу, чтобы понять, почему она здесь!).
Надеюсь, ты все еще со мной! Если нет, это нормально, просто просмотрите код и проверьте их формы. Теперь, когда у нас есть наши логиты, нам нужны цели. Я должен сказать, что есть более простой способ получить цели, но я должен был сделать это для нашего дела (я расскажу о том, почему в следующем абзаце).
Давайте рассмотрим, что мы надеемся, что эта модель узнает: мы хотим, чтобы она изучала «аналогичные представления (векторы)» для данного изображения и подписи, описывающей его. Это означает, что либо мы даем ему изображение, либо текст, описывающий его, мы хотим, чтобы он производил одинаковые векторы размером 256 для обоих.
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 ()Таким образом, в лучшем случае сценария, матрицы Text_embeddings и Image_embedding должны быть одинаковыми, потому что они описывают похожие вещи. Давайте подумаем сейчас: если это произойдет, какая бы была матрица логитов? Посмотрим с простым примером!
# A simple Example
batch_size = 4
dim = 256
embeddings = torch . randn ( batch_size , dim )
out = embeddings @ embeddings . T
print ( F . softmax ( out , dim = - 1 ))Таким образом, Logits, в лучшем случае, будет матрицей, которая, если мы возьмем его Softmax, будет иметь 1.0 -е в диагонали (матрица идентификации, чтобы назвать его причудливыми словами!). Поскольку задача функции потерь состоит в том, чтобы сделать прогнозы модели аналогичными целям (по крайней мере, в большинстве случаев!), Мы хотим, чтобы такая матрица, как наша цель. Вот почему мы рассчитываем матрицы Images_simiLaity и Texts_similality в приведенном выше блоке кода.
Теперь, когда у нас есть матрица нашей целей, мы будем использовать простую поперечную энтропию для расчета фактической потери. Я написал полную матричную форму поперечной энтропии как функцию, которую вы можете увидеть в нижней части блока кода. Хорошо! Мы закончили! Разве это не просто?! Хорошо, вы можете игнорировать следующий абзац, но если вам любопытно, в этом есть важная нота.
Вот почему я не использовал более простой подход : мне нужно признать, что есть более простой способ рассчитать эту потерю в Pytorch; Делая это: nn.crossentropyloss () (logits, torch.arange (batch_size)). Почему я здесь не использовал? По 2 причинам. 1- используемый набор данных имеет несколько подписей для одного изображения; Таким образом, существует вероятность того, что два идентичных изображения с их аналогичными подписями существуют в партии (это редко, но это может произойти). Принимая потерю с этим более простым методом, проигнорирует эту возможность, и модель учится разбирать два представления (предполагайте, что они на самом деле одинаковы. Очевидно, мы не хотим, чтобы это произошло, поэтому я рассчитал всю целевую матрицу таким образом, чтобы позаботиться об этих краевых случаях. 2- Делая это так, как я, дал мне лучшее понимание того, что происходит в этой функции потери; Итак, я подумал, что это даст вам лучшую интуицию!
Вот несколько функций, которые помогут нам загрузить поезд и действительные DataLoaders, нашу модель, а затем тренировать и оценивать нашу модель на них. Здесь не так много; Просто простая тренировочная петля и функции утилиты
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 dataloaderВот удобная функция для обучения нашей модели. Здесь мало что происходит; Просто загружая партии, подав их в модель и поступив оптимизатор и 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 )Запуск следующей ячейки начинает обучать модель. Поместите ядро в режим GPU. Каждая эпоха должна занять около 24 минут на графическом процессоре (даже одной эпохи достаточно!). Это может занять одну минуту до того, как тренировка на самом деле начнется, потому что мы собираемся кодировать все подписи один раз в поезде и действительный набор данных, поэтому, пожалуйста, не останавливайтесь! Все работает нормально.
main ()Хорошо! Мы закончили с обучением модели. Теперь нам необходимо сделать вывод, который в нашем случае будет предоставлять модели кусок текста и хотим, чтобы она извлекла наиболее соответствующие изображения из невидимой проверки (или теста).
В этой функции мы загружаем модель, которую мы сохранили после обучения, подарив ИТ -изображения в наборе валидации и возвращаем изображение_ембеддингс с формой (valid_set_size, 256) и самой модели.
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" )Эта функция выполняет окончательную задачу, которую мы хотели бы, чтобы наша модель была способна: она получает модель, image_embeddings и текстовый запрос. Он отобразит наиболее релевантные изображения из набора валидации! Разве это не удивительно? Посмотрим, как это работает в конце концов!
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 ()Вот как мы используем эту функцию. AAAANNNNDDDD Результаты:
find_matches ( model ,
image_embeddings ,
query = "a group of people dancing in a party" ,
image_filenames = valid_df [ 'image' ]. values ,
n = 9 )
Надеюсь, вам понравилась эта статья. Реализация этой статьи была для меня действительно интересным опытом. Я хочу поблагодарить Халида Саламу за пример великого кода Керас, который он дал, что вдохновило меня написать что -то подобное в Pytorch.