Ich freue mich, herauszufinden, dass dieser Code in den folgenden Papieren verwendet und zitiert wurde:
Domino: Entdeckung systematischer Fehler mit quer-modalen Einbettungen von Eyuboglu ET. al. bei ICLR 2022
GSCLIP: Ein Rahmen zur Erklärung der Verteilung der natürlichen Sprache von Zhu ET. al. bei ICML 2022
UIC-NLP bei Semeval-2022 Aufgabe 5: Untersuchung des kontrastiven Lernens für multimodale Nachweis von frauenfeindlichen Memen durch Cuervo ET. al. bei Semeval-2022
CDSBERT - Erweiterung Proteinsprachmodelle mit Codonbewusstsein von Hallee ET. al. von der Universität von Delaware (September 2023)
Enigma-51: Auf dem Weg zu einem feinkörnigen Verständnis von Wechselwirkungen zwischen Menschen und Objekten in industriellen Szenarien von Ragusa et. al. (Nov 2023)
Die Zitierinformationen finden Sie im rechten Abschnitt dieser GitHub -Repo -Seite mit dem Namen: Zitieren Sie dieses Repository oder verwenden Sie die folgenden Zitierinformationen.
@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 }
}Im Januar 2021 kündigte OpenAI zwei neue Modelle an: Dall-E und Clip , beide Multimodalitätsmodelle, die Texte und Bilder in irgendeiner Weise verbinden. In diesem Artikel werden wir in Pytorch das Clip -Modell von Grund auf neu implementieren. OpenAI hat einen Teil des Codes in Bezug auf das Clip-Modell Open-Sourcut-Code, aber ich fand es einschüchternd und es war alles andere als etwas kurzes und Einfaches. Ich bin auch auf ein gutes Tutorial gestoßen, das von Clip -Modell zu Keras -Code -Beispielen inspiriert wurde, und übersetzt einige Teile davon in Pytorch, um dieses Tutorial mit unserem geliebten Pytorch vollständig zu erstellen!
OpenAI lernt über übertragbare visuelle Modelle aus natürlicher Sprache übertragbar und führt ihr neues Modell vor, das als Clip bezeichnet wird, für kontrastives Sprachbild vor dem Training . Kurz gesagt, dieses Modell lernt die Beziehung zwischen einem ganzen Satz und dem Bild, das es beschreibt; In gewissem Sinne, dass das Modell beim Training bei einem Eingangssatz die am häufigsten diesem Satz entsprechenden verwandten Bilder abrufen kann. Das Wichtigste hier ist, dass es in vollständigen Sätzen anstelle von einzelnen Klassen wie Auto, Hund usw. trainiert wird. Die Intuition ist, dass das Modell beim Training in ganzen Sätzen viel mehr Dinge lernen kann und ein Muster zwischen Bildern und Texten findet. Sie zeigen auch, dass dieses Modell, wenn es in einem riesigen Datensatz mit Bildern und ihren entsprechenden Texten trainiert wird, auch als Klassifikator fungieren kann. Ich ermutige Sie, das Papier zu studieren, um mehr über dieses aufregende Modell und ihre erstaunlichen Ergebnisse zu Benchmarking -Datensätzen zu erfahren. Um nur eins zu erwähnen, klassifiziert das mit dieser Strategie trainierte Clipmodell ImageNet besser als die SOTA -Modelle, die auf dem ImageNet selbst trainiert wurden, um die einzige Aufgabe der Klassifizierung zu optimieren!
Lassen Sie uns als Teaser (!) Sehen, was das endgültige Modell, das wir in diesem Artikel von Grund auf erstellen werden, fähig ist: Wenn Sie eine Abfrage (rohen Text) wie "Ein Junge mit Skateboard springen" oder "Ein Mädchen, das aus der Swing springt", wird das Modell die relevantesten Bilder abgerufen:

Lassen Sie uns noch einige Ausgänge sehen:

# !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 Ein Hinweis zu Konfiguration und CFG: Ich habe die Codes mit Python -Skripten geschrieben und sie dann in ein Jupyter -Notizbuch konvertiert. Im Falle von Python -Skripten ist Config eine normale Python -Datei, in der ich alle Hyperparameter einsetzte, und im Fall von Jupyter Notebook ist eine Klasse am Anfang des Notizbuchs definiert, um alle Hyperparameter zu halten.
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" ]Wie Sie im Tittle -Bild dieses Artikels sehen können, müssen wir beide Bilder und deren beschriebene Texte codieren. Der Datensatz muss also sowohl Bilder als auch Texte zurückgeben . Natürlich werden wir keinen rohen Text an unseren Text -Encoder füttern! Wir werden das Destilbert -Modell (das kleiner als Bert ist, aber fast so gut wie Bert ausreicht) von der Umarmungsface -Bibliothek als Textcodierer. Wir müssen also die Sätze (Bildunterschriften) mit Distilbert -Tokenizer token und dann die Token -IDs (input_ids) und die Aufmerksamkeitsmasken für Distilbert füttern. Daher muss der Datensatz auch die Tokenisierung betreuen. Unten finden Sie den Code des Datensatzes. Im Folgenden werde ich die wichtigsten Dinge erklären, die im Code geschehen.
Im __init__ erhalten wir ein Tokenizer -Objekt, das eigentlich ein Umarmungsface -Tokinzer ist. Dieser Tokenizer wird beim Ausführen des Modells geladen. Wir platten und schneiden die Bildunterschriften auf eine bestimmte max_length ab. In der __GetItem__ laden wir zuerst eine codierte Bildunterschrift, die ein Wörterbuch mit Tasten input_ids und activität_mask ist, Tensoren aus seinen Werten herausnehmen. Danach laden wir das entsprechende Bild, transformieren und erweitern es (falls es zu einem!). Schließlich setzen wir den rohen Text der Bildunterschrift mit dem Schlüssel "Bildunterschrift" im Wörterbuch nur für Visualisierungszwecke ein.
Ich habe keine zusätzlichen Datenvergrößerungen verwendet, aber Sie können sie hinzufügen, wenn Sie die Leistung des Modells verbessern möchten.
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 ),
]
)Der Image -Encoder -Code ist einfach. Ich verwende hier die Pytorch Image Models Library (TIMM), wodurch viele verschiedene Bildmodelle von ResNETs zu effizienten und vielen anderen verfügbar sind. Hier verwenden wir einen ResNet50 als unseren Image -Encoder. Sie können die Torchvision -Bibliothek problemlos verwenden, um ResNets zu verwenden, wenn Sie keine neue Bibliothek installieren möchten.
Der Code codiert jedes Bild mit der Größe der Ausgangskanäle des Modells an einen Vektor der festen Größe (bei RESNET50 beträgt die Vektorgröße 2048 ). Dies ist die Ausgabe nach der Nn.adaptiveAVGGpool2D () -Schicht.
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 )Wie ich bereits erwähnt habe, werde ich Distilbert als Textcodierer verwenden. Wie sein größerer Bruder Bert werden zwei spezielle Token zu den tatsächlichen Eingabestellen hinzugefügt: CLS und SEP , die den Start und Ende eines Satzes markieren. Um die gesamte Darstellung eines Satzes zu erfassen (wie die verwandten Papiere von Bert und Distilbert betonen), verwenden wir die endgültigen Darstellungen des CLS -Tokens und hoffen, dass diese Darstellung die Gesamtbedeutung des Satzes (Bildunterschrift) erfasst. Als ich es auf diese Weise dachte, ähnelt es dem, was wir Bildern angetan und sie in einen Vektor mit fester Größen umgewandelt haben.
Im Fall von Distilbert (und auch Bert) ist die versteckte Darstellung für jeden Token ein Vektor mit Größe 768 . Die gesamte Bildunterschrift wird also in der CLS -Token -Darstellung codiert, deren Größe 768 ist.
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 , :]Ich habe die Implementierung von Projektionskopf mit Keras -Code -Beispiel verwendet, um Folgendes in Pytorch zu schreiben. Jetzt, da wir sowohl unsere Bilder als auch unsere Texte in Vektoren der festen Größe (2048 für Bild und 768 für Text) codiert haben, müssen wir sie (projizieren) in eine neue Welt (!) Mit ähnlichen Dimensionen für Bilder und Texte bringen, um sie vergleichen zu können, das nicht relevante Bild und die nicht relevanten Bild und die Texte zusammenzusetzen und diejenigen zusammenzuziehen, die passen. Der folgende Code bringt also die dimensionalen Vektoren von 2048 und 768 in eine dimensionale Welt von 256 (projection_dim), in der wir sie vergleichen können.
"Embedding_dim" ist die Größe des Eingangsvektors (2048 für Bilder und 768 für Texte) und "projection_dim" ist die Größe des Ausgangsvektors, der für unseren Fall 256 beträgt. Um die Details dieses Teils zu verstehen, können Sie sich auf das Clippapier beziehen.
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 In diesem Teil passiert der ganze Spaß! Ich werde hier auch über die Verlustfunktion sprechen. Ich habe einen Teil des Codes aus dem Keras -Code -Beispiele in Pytorch übersetzt, um diesen Teil zu schreiben. Schauen Sie sich den Code an und lesen Sie dann die Erläuterung unter diesem Codeblock.
Hier werden wir die vorherigen Module verwenden, die wir zur Implementierung des Hauptmodells erstellt haben. Die __init__-Funktion ist selbsterklärend. In der Vorwärtsfunktion codieren wir zunächst die Bilder und Texte getrennt in Vektoren der festen Größe (mit unterschiedlichen Dimensionalitäten). Danach projizieren wir mit separaten Projektionsmodulen sie in diese gemeinsame Welt (Raum), über die ich zuvor gesprochen habe. Hier werden die Codierungen ähnlicher Form (256 in unserem Fall). Danach werden wir den Verlust berechnen. Ich empfehle erneut, Clippapier zu lesen, um es besser zu machen, aber ich werde mein Bestes geben, um diesen Teil zu erklären.
In der linearen Algebra besteht eine häufige Möglichkeit, um zu messen, ob zwei Vektoren ähnliche Eigenschaften haben (sie sind wie einander), ihr Punktprodukt zu berechnen (multiplizieren die passenden Einträge und nehmen Sie die Summe von ihnen an). Wenn die endgültige Zahl groß ist, sind sie gleich und wenn sie klein ist, sind sie (relativ gesehen) nicht!
Okay! Was ich gerade gesagt habe, ist das Wichtigste, um diese Verlustfunktion zu verstehen. Lass uns weitermachen. Wir haben über zwei Vektoren gesprochen, aber was haben wir hier? Wir haben Image_embeding, eine Matrix mit Form (batch_size, 256) und text_embeddings mit Form (batch_size, 256). Einfach genug! Dies bedeutet, dass wir zwei Gruppen von Vektoren anstelle von zwei Einzelvektoren haben. Wie messen wir, wie ähnliche zwei Gruppen von Vektoren (zwei Matrizen) zueinander sind? Wieder mit DOT -Produkt (@ -Operator in Pytorch führt in diesem Fall das DOT -Produkt oder die Matrixmultiplikation). Um diese beiden Matrizen miteinander zu multiplizieren, transponieren wir den zweiten. Okay, wir erhalten eine Matrix mit Form (batch_size, batch_size), die wir Protokolls aufrufen werden. (Die Temperatur entspricht 1,0 in unserem Fall. Sie macht also keinen Unterschied. Sie können damit spielen und sehen, welchen Unterschied es macht. Sehen Sie sich auch das Papier an, um zu sehen, warum es hier ist!).
Ich hoffe du bist immer noch bei mir! Wenn nicht, überprüfen Sie einfach den Code und überprüfen Sie ihre Formen. Jetzt, da wir unsere Protokolls haben, brauchen wir Ziele. Ich muss sagen, dass es einen geringen Weg gibt, Ziele zu erhalten, aber ich musste dies für unseren Fall tun (ich werde darüber sprechen, warum in einem nächsten Absatz).
Überlegen wir, was wir hoffen, dass dieses Modell lernt: Wir möchten, dass es "ähnliche Darstellungen (Vektoren)" für ein bestimmtes Bild und die Beschriftung, die es beschreibt, lernt. Das heißt, entweder geben wir ihm ein Bild oder den Text, der es beschreibt, wir möchten, dass es für beide gleiche 256 -Größe -Vektoren erzeugt.
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 ()Im besten Szenario sollten Text_Embeddings und Image_embedding -Matricies gleich sein, da sie ähnliche Dinge beschreiben. Lassen Sie uns jetzt nachdenken: Wenn dies passiert, wie wäre die Matrix der Logits dann? Lassen Sie uns mit einem einfachen Beispiel sehen!
# 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 ist also im besten Fall eine Matrix, die, wenn wir seinen Softmax einnehmen, 1,0S in der Diagonale haben (eine Identitätsmatrix, um es mit ausgefallenen Wörtern zu nennen!). Da die Aufgabe der Verlustfunktion darin besteht, die Vorhersagen des Modells den Zielen ähnlich zu machen (zumindest in den meisten Fällen!), Sie möchten eine solche Matrix wie unser Ziel. Das ist der Grund, warum wir im obigen Codeblock im Codeblock im Codeblock im Codeblock BIGS_SIMHILITY UND STEXTS_SIMHILICELITY -Matrizen berechnet werden.
Nachdem wir unsere Zielmatrix haben, werden wir eine einfache Kreuzentropie verwenden, um den tatsächlichen Verlust zu berechnen. Ich habe die vollständige Matrixform der Kreuzentropie als eine Funktion geschrieben, die Sie unten im Codeblock sehen können. Okay! Wir sind fertig! War es nicht einfach ?! Okay, Sie können den nächsten Absatz ignorieren, aber wenn Sie neugierig sind, gibt es darin eine wichtige Notiz.
Hier ist der Grund, warum ich keinen einfacheren Ansatz verwendet habe : Ich muss zugeben, dass es eine einfachere Möglichkeit gibt, diesen Verlust in Pytorch zu berechnen. Auf diese Weise: nn.crossentropyloss () (logits, fackel.arange (batch_size)). Warum habe ich es hier nicht benutzt? Aus 2 Gründen. 1- Der Datensatz, den wir verwenden, hat mehrere Bildunterschriften für ein einzelnes Bild. Es besteht also die Möglichkeit, dass zwei identische Bilder mit ihren ähnlichen Bildunterschriften in einer Charge existieren (es ist selten, aber es kann passieren). Wenn Sie den Verlust mit dieser leichteren Methode nehmen, ignorieren diese Möglichkeit und das Modell lernt, zwei Darstellungen (Annahme von unterschiedlichem), die tatsächlich gleich sind. Offensichtlich wollen wir nicht, dass dies geschieht, also habe ich die gesamte Zielmatrix auf eine Weise berechnet, die sich um diese Kantenfälle kümmert. 2- Es gab mir ein besseres Verständnis dafür, was in dieser Verlustfunktion passiert. Also dachte ich, es würde dir auch eine bessere Intuition geben!
Hier sind einige Funktionen, die uns helfen, Zug und gültige Dataloader, unser Modell zu laden und dann unser Modell zu trainieren und zu bewerten. Hier ist nicht viel los; Nur einfache Trainingsschleife und Versorgungsfunktionen
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 dataloaderHier ist eine praktische Funktion, um unser Modell zu trainieren. Hier passiert nicht viel; Laden Sie einfach die Chargen, füttern Sie sie an das Modell und treten Sie den Optimierer und 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 )Ausführen der nächsten Zelle Beginnen Sie mit dem Training des Modells. Setzen Sie den Kernel in den GPU -Modus. Jede Epoche sollte bei der GPU ungefähr 24 Minuten dauern (sogar eine Epoche reicht aus!). Es kann eine Minute dauern, bis das Training tatsächlich beginnt, weil wir alle Bildunterschriften einmal im Zug und einen gültigen Datensatz codieren werden. Bitte stoppen Sie es also nicht! Alles funktioniert gut.
main ()Okay! Wir sind mit dem Training des Modells fertig. Jetzt müssen wir Schlussfolgerung durchführen, die in unserem Fall dem Modell einen Textstück geben und es den relevantesten Bildern aus einem unsichtbaren Validierungssatz (oder Test) abrufen soll.
In dieser Funktion laden wir das Modell, das wir nach dem Training gespeichert haben, die Bilder im Validierungssatz füttern und die Image_embeding mit Form (valid_set_size, 256) und das Modell selbst zurückgeben.
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" )Diese Funktion erledigt die endgültige Aufgabe, für die wir uns gewünscht haben, unser Modell würde in der Lage sein: Es erhält das Modell, die Image_embeddings und eine Textabfrage. Es wird die relevantesten Bilder aus dem Validierungssatz angezeigt! Ist es nicht unglaublich? Mal sehen, wie es doch funktioniert!
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 ()So verwenden wir diese Funktion. Aaaannnndddd Die Ergebnisse:
find_matches ( model ,
image_embeddings ,
query = "a group of people dancing in a party" ,
image_filenames = valid_df [ 'image' ]. values ,
n = 9 )
Ich hoffe, Sie haben diesen Artikel genossen. Die Implementierung dieses Papiers war für mich eine wirklich interessante Erfahrung. Ich möchte Khalid Salama für das große Keras -Code -Beispiel danken, das er zur Verfügung gestellt hat, was mich dazu inspirierte, etwas Ähnliches in Pytorch zu schreiben.