يسعدني معرفة أن هذا الرمز قد تم استخدامه واستشهد به في الأوراق التالية:
دومينو: اكتشاف أخطاء منهجية مع تضمينات عبر الوسائط بواسطة eyuboglu et. آل. في ICLR 2022
GSCLIP: إطار لشرح تحولات التوزيع في اللغة الطبيعية بواسطة Zhu et. آل. في ICML 2022
UIC-NLP في Semeval-2022 المهمة 5: استكشاف التعلم التباين للكشف متعدد الوسائط للميمات الكراهية للنساء بواسطة Cuervo et. آل. في Semeval-2022
CDSBERT - تمديد نماذج لغة البروتين مع الوعي بالكونه من قبل Hallee et. آل. من جامعة ديلاوير (سبتمبر 2023)
Enigma-51: نحو فهم دقيق للتفاعلات البشرية في السيناريوهات الصناعية بواسطة Ragusa et. آل. (نوفمبر 2023)
يمكنك العثور على معلومات الاقتباس في القسم الصحيح من صفحة Github Repo هذه المسمى: استشهد بهذا المستودع أو استخدم معلومات الاقتباس أدناه.
@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 نموذجه الجديد الذي يسمى المقطع ، للحصول على تدريب على صورة اللغة المتناقضة . باختصار ، يتعلم هذا النموذج العلاقة بين جملة كاملة والصورة التي يصفها ؛ بمعنى أنه عندما يتم تدريب النموذج ، بالنظر إلى جملة الإدخال ، سيكون بإمكانه استرداد الصور الأكثر ارتباطًا المقابلة لتلك الجملة. الشيء المهم هنا هو أنه يتم تدريبه على جمل كاملة بدلاً من فصول واحدة مثل السيارة والكلب ، وما إلى ذلك. الحدس هو أنه عند تدريبه على جمل كاملة ، يمكن للنموذج أن يتعلم الكثير من الأشياء ويجد بعض الأنماط بين الصور والنصوص. يوضحون أيضًا أنه عندما يتم تدريب هذا النموذج على مجموعة بيانات ضخمة من الصور ونصوصها المقابلة ، يمكن أن يكون أيضًا بمثابة مصنف أيضًا. أشجعك على دراسة الورقة لمعرفة المزيد حول هذا النموذج المثير ونتائجها المذهلة على مجموعات البيانات القياسية. لذكر واحد فقط ، يصنف نموذج مقطع مدرب مع هذه الاستراتيجية 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 ملاحظة على Config و 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 كمشفر نصنا ؛ لذلك ، نحتاج إلى تميز الجمل (التسميات التوضيحية) باستخدام Tokenizer Distilbert ثم إطعام معرفات الرمز المميز (input_ids) وأقنعة الانتباه إلى Distilbert. لذلك ، تحتاج مجموعة البيانات إلى رعاية الرمز المميز أيضًا. أدناه يمكنك رؤية رمز مجموعة البيانات. أدناه سأشرح أهم الأشياء التي تحدث في الكود.
في __init__ ، نتلقى كائنًا من الرمز المميز وهو في الواقع tokinzer luggingface ؛ سيتم تحميل هذا الرمز المميز عند تشغيل النموذج. نحن نقوم بالحشو ونقطع التسميات التوضيحية إلى Max_Length محدد. في __getItem__ ، سنقوم أولاً بتحميل التسمية التوضيحية المشفرة وهي عبارة عن قاموس مع مفاتيح input_ids و entertain_mask ، ونجعل التكرار من قيمه ، وبعد ذلك سنقوم بتحميل الصورة المقابلة ، ونحولها وزيادةها (إذا كان هناك أي!) ثم نجعلها توترًا ونضعها في القول مع الصورة "كطاقة". أخيرًا ، وضعنا النص الخام للتعليق باستخدام "التسمية التوضيحية" المفتاح في القاموس فقط لأغراض التصور.
لم أستخدم زيادة البيانات الإضافية ولكن يمكنك إضافتها إذا كنت ترغب في تحسين أداء النموذج.
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 Image Models (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 ونأمل أن يلتقط هذا التمثيل المعنى العام للجملة (التسمية التوضيحية). التفكير في ذلك بهذه الطريقة ، يشبه ما فعلناه للصور وتحويلها إلى متجه ثابت الحجم.
في حالة Distilbert (وكذلك 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 للنص) ، نحتاج إلى نقل (Project) إلى عالم جديد (!) مع أبعاد مماثلة لكل من الصور والنصوص من أجل أن تكون قادرة على مقارنتها ودفعها إلى الصورة غير ذات الصلة ونصوصها معًا. لذلك ، سوف يجلب الكود التالي 2048 و 768 ناقلات الأبعاد إلى عالم أبعاد 256 (Profince_Dim) ، حيث يمكننا مقارنتها .
"inmbeddding_dim" هو حجم متجه الإدخال (2048 للصور و 768 للنصوص) و "Projection_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) و sext_embeddings مع الشكل (batch_size ، 256). سهل بما فيه الكفاية! وهذا يعني أن لدينا مجموعتان من المتجهات بدلاً من متجهين واحد. كيف نقيس مدى مشابهة مجموعتين من المتجهات (مصفوفتان) لبعضهما البعض؟ مرة أخرى ، مع منتج DOT (@ المشغل في Pytorch يقوم بمنتج DOT أو مضاعفة المصفوفة في هذه الحالة). لتكون قادرًا على مضاعفة هذين المصفوفتين معًا ، نقوم بتنفيذ الثانية. حسنًا ، نحصل على مصفوفة ذات شكل (batch_size ، batch_size) والتي سنقوم بالاتصال بها. (درجة الحرارة تساوي 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 هي نفسها لأنها تصف أشياء مماثلة. دعونا نفكر الآن: إذا حدث هذا ، كيف سيكون شكل مصفوفة Logits؟ دعونا نرى مع مثال بسيط!
# 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.0s في قطري (مصفوفة هوية للاتصال بها بكلمات فاخرة!). نظرًا لأن وظيفة وظيفة الخسارة هي جعل تنبؤات النموذج مشابهة للأهداف (على الأقل في معظم الحالات!) ، نريد مصفوفة مثل هدفنا. هذا هو السبب في أننا نحسب المصفوفات Images_Similarity و Sexts_Similarity في كتلة الكود أعلاه.
الآن بعد أن حصلنا على مصفوفة أهدافنا ، سنستخدم إنتروبيا بسيطة لحساب الخسارة الفعلية. لقد كتبت شكل المصفوفة الكامل من الانتروبيا المتقاطعة كدالة يمكنك رؤيتها في أسفل كتلة الكود. تمام! لقد انتهينا! ألم يكن الأمر بسيطًا؟! حسنًا ، يمكنك تجاهل الفقرة التالية ولكن إذا كنت فضوليًا ، فهناك ملاحظة مهمة في ذلك.
إليك سبب عدم استخدام نهج أبسط : أحتاج إلى الاعتراف بأن هناك طريقة أبسط لحساب هذه الخسارة في Pytorch ؛ من خلال القيام بذلك: nn.crossentropyloss () (logits ، torch.arange (batch_size)). لماذا لم أستخدمها هنا؟ لأسباب 2. 1- مجموعة البيانات التي نستخدمها لها تعليقات متعددة لصورة واحدة ؛ لذلك ، هناك احتمال وجود صورتين متطابقين مع تسميات توضيحية مماثلة في دفعة (إنه أمر نادر الحدوث ولكن يمكن أن يحدث). إن أخذ هذه الطريقة بهذه الطريقة أسهل سيتجاهل هذا الاحتمال ويتعلم النموذج أن يفصل تمثيلين (افترضهما مختلفا) وهما هو نفسه في الواقع. من الواضح أننا لا نريد أن يحدث هذا ، لذا قمت بحساب المصفوفة المستهدفة بأكملها بطريقة تعتني بحالات الحافة هذه. 2- القيام بذلك بالطريقة التي فعلت بها ، أعطاني فهمًا أفضل لما يحدث في وظيفة الخسارة هذه ؛ لذلك ، اعتقدت أنه سيمنحك حدس أفضل أيضًا!
فيما يلي بعض الترح لمساعدتنا على تحميل القطار ورسائل البيانات الصالحة ، ونموذجنا ثم تدريب وتقييم نموذجنا على هؤلاء. لا يحدث الكثير هنا ؛ مجرد حلقة تدريب بسيطة ووظائف المنفعة
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 )تشغيل الخلية التالية ابدأ في تدريب النموذج. ضع kernel على وضع GPU. يجب أن يستغرق كل فترة حوالي 24 دقيقة على GPU (حتى حقبة واحدة كافية!). قد يستغرق الأمر دقيقة واحدة قبل أن يبدأ التدريب بالفعل لأننا سنقوم بتشفير جميع التسميات التوضيحية مرة واحدة في القطار ومجموعة البيانات الصالحة ، لذا يرجى عدم إيقافها! كل شيء يعمل بشكل جيد.
main ()تمام! لقد انتهينا من تدريب النموذج. الآن ، نحتاج إلى القيام بالاستدلال الذي سيعطيه في حالتنا النموذج نصًا من النص ويريد أن يسترجع الصور الأكثر صلة من مجموعة (أو اختبار) غير مرئي.
في هذه الوظيفة ، نقوم بتحميل النموذج الذي نقوم بحفظه بعد التدريب ، وتغذية الصور في مجموعة التحقق من الصحة وإعادة صورة _embeddings بالشكل (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 )
أتمنى أن تكون قد استمتعت بهذا المقال. كان تنفيذ هذه الورقة تجربة مثيرة للاهتمام حقًا بالنسبة لي. أود أن أشكر خالد سالاما على مثال رمز كيراس العظيم الذي قدمه لي الذي ألهمني لكتابة شيء مشابه في بيتورش.