ฉันมีความสุขที่ได้พบว่ารหัสนี้ถูกใช้และอ้างถึงในเอกสารต่อไปนี้:
Domino: การค้นพบข้อผิดพลาดอย่างเป็นระบบด้วยการฝังตัวข้ามโมดอลโดย Eyuboglu และ อัล ที่ ICLR 2022
GSCLIP: กรอบการทำงานสำหรับการอธิบายการกระจายการเปลี่ยนแปลงในภาษาธรรมชาติโดย Zhu et อัล ที่ ICML 2022
UIC-NLP ที่ Semeval-20122 ภารกิจที่ 5: การสำรวจการเรียนรู้ที่แตกต่างกันสำหรับการตรวจจับ multimodal ของ memes misogynistic โดย Cuervo et อัล ที่ Semeval-20122
Cdsbert - ขยายโมเดลภาษาโปรตีนด้วยการรับรู้ Codon โดย Hallee และ อัล จาก University of Delaware (ก.ย. 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 แนะนำรูปแบบใหม่ของพวกเขาซึ่งเรียกว่า คลิป สำหรับ การฝึกอบรมภาพภาษาที่แตกต่างกัน สรุปโมเดลนี้เรียนรู้ความสัมพันธ์ระหว่างประโยคทั้งหมดและภาพที่อธิบาย ในแง่ที่ว่าเมื่อโมเดลได้รับการฝึกฝนให้ได้รับประโยคอินพุตมันจะสามารถดึงภาพที่เกี่ยวข้องมากที่สุดที่สอดคล้องกับประโยคนั้น สิ่งสำคัญที่นี่คือมันได้รับการฝึกฝนในประโยคเต็มแทนที่จะเป็นชั้นเรียนเดียวเช่นรถยนต์สุนัข ฯลฯ สัญชาตญาณคือเมื่อได้รับการฝึกฝนเกี่ยวกับประโยคทั้งหมดโมเดลสามารถเรียนรู้สิ่งต่าง ๆ ได้มากขึ้นและพบรูปแบบระหว่างภาพและข้อความ พวกเขายังแสดงให้เห็นว่าเมื่อโมเดลนี้ได้รับการฝึกฝนในชุดข้อมูลขนาดใหญ่ของรูปภาพและข้อความที่สอดคล้องกันก็สามารถทำหน้าที่เป็นตัวจําแนกได้เช่นกัน ฉันขอแนะนำให้คุณศึกษาบทความเพื่อเรียนรู้เพิ่มเติมเกี่ยวกับโมเดลที่น่าตื่นเต้นนี้และผลลัพธ์ที่น่าอัศจรรย์ของพวกเขาในชุดข้อมูลการเปรียบเทียบ ที่จะพูดถึงเพียงหนึ่งโมเดลคลิปที่ได้รับการฝึกฝนด้วยกลยุทธ์นี้จำแนก 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 ปกติที่ฉันใส่ hyperparameters ทั้งหมดและในกรณีของ Jupyter Notebook มันเป็นคลาสที่กำหนดไว้ในจุดเริ่มต้นของสมุดบันทึกเพื่อเก็บพารามิเตอร์ทั้งหมด
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" ]อย่างที่คุณเห็นในภาพที่น่าเบื่อของบทความนี้เราจำเป็นต้องเข้ารหัสทั้งภาพและข้อความที่อธิบาย ดังนั้นชุดข้อมูลจำเป็นต้อง ส่งคืนทั้งภาพและข้อความ แน่นอนว่าเราจะไม่ป้อนข้อความดิบให้กับตัวเข้ารหัสข้อความของเรา! เราจะใช้โมเดล Distilbert (ซึ่งเล็กกว่าเบิร์ต แต่ทำงานได้เกือบและเบิร์ต) จาก HuggingFace Library เป็นตัวเข้ารหัสข้อความของเรา ดังนั้นเราจำเป็นต้อง โทเค็น ประโยค (คำบรรยาย) ด้วย distilbert tokenizer จากนั้นป้อนรหัสโทเค็น (input_ids) และหน้ากากความสนใจเพื่อ distilbert ดังนั้นชุดข้อมูลจำเป็นต้องดูแลการโทเค็นเช่นกัน ด้านล่างคุณสามารถดูรหัสของชุดข้อมูลได้ ด้านล่างว่าฉันจะอธิบายสิ่งที่สำคัญที่สุดที่เกิดขึ้นในรหัส
ใน __init__ เราได้รับวัตถุ tokenizer ซึ่งจริง ๆ แล้วเป็น huggingface tokinzer; tokenizer นี้จะถูกโหลดเมื่อเรียกใช้โมเดล เรากำลังขยายและตัดทอนคำอธิบายภาพไปยัง Max_Length ที่ระบุ ใน __getItem__ ก่อนอื่นเราจะโหลดคำบรรยายภาพที่เข้ารหัสซึ่งเป็นพจนานุกรมที่มีคีย์อินพุต _ids และ atternest_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 Library (TIMM) ที่นี่ซึ่งทำให้โมเดลรูปภาพต่าง ๆ มีให้เลือกมากมายตั้งแต่ resnets ไปจนถึง EfficientNets และอื่น ๆ อีกมากมาย ที่นี่เราจะใช้ 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 Papers ชี้ให้เห็น) เราใช้การเป็นตัวแทนขั้นสุดท้ายของโทเค็น 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 สำหรับข้อความ) เราต้องนำ (โครงการ) เข้าสู่ โลกใหม่ (!) ที่มี มิติที่คล้ายกัน ทั้งภาพและข้อความเพื่อให้สามารถเปรียบเทียบและผลักดันภาพและข้อความที่ไม่เกี่ยวข้อง ดังนั้นรหัสต่อไปนี้จะนำเวกเตอร์มิติ 2048 และ 768 เข้าสู่โลกมิติ 256 (projection_dim) ที่เราสามารถ เปรียบเทียบ ได้
"Embedding_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 ในกรณีของเรา) หลังจากนั้นเราจะคำนวณการสูญเสีย อีกครั้งฉันแนะนำให้อ่าน Clip Paper เพื่อให้ดีขึ้น แต่ฉันจะพยายามอย่างเต็มที่เพื่ออธิบายส่วนนี้
ใน พีชคณิตเชิงเส้น วิธีหนึ่งทั่วไปในการวัดว่าเวกเตอร์สองตัวมีลักษณะคล้ายกัน (พวกเขาเป็นเหมือนกัน) คือการคำนวณ ผลิตภัณฑ์ DOT ของพวกเขา (การคูณรายการที่ตรงกันและรับผลรวมของพวกเขา); หากหมายเลขสุดท้ายมีขนาดใหญ่พวกเขาจะเหมือนกันและถ้ามันมีขนาดเล็กพวกเขาก็ไม่ได้ (ค่อนข้างพูด)!
ตกลง! สิ่งที่ฉันเพิ่งพูดเป็นสิ่งที่สำคัญที่สุดที่ต้องคำนึงถึงเพื่อทำความเข้าใจฟังก์ชั่นการสูญเสียนี้ มาต่อกันเถอะ เราพูดคุยเกี่ยวกับเวกเตอร์สองตัว แต่เรามีอะไรที่นี่? เรามี image_embeddings เมทริกซ์ที่มีรูปร่าง (batch_size, 256) และ text_embeddings ที่มีรูปร่าง (batch_size, 256) ง่ายพอ! หมายความว่าเรามีเวกเตอร์สองกลุ่มแทนที่จะเป็นเวกเตอร์สองตัว เราจะวัดได้อย่างไรว่าเวกเตอร์สองกลุ่มที่คล้ายกัน (เมทริกซ์สองตัว) มีต่อกันอย่างไร? อีกครั้งด้วยผลิตภัณฑ์ DOT (@ Operator ใน Pytorch ทำผลิตภัณฑ์ DOT หรือการคูณเมทริกซ์ในกรณีนี้) เพื่อให้สามารถคูณเมทริกซ์ทั้งสองนี้เข้าด้วยกันได้เราจะเปลี่ยนอันที่สอง โอเคเราได้รับเมทริกซ์ที่มีรูปร่าง (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 และ matricies 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 ))ดังนั้นในกรณีที่ดีที่สุดจะเป็นเมทริกซ์ที่ถ้าเราใช้ softmax ของมันจะมี 1.0s ในแนวทแยง (เมทริกซ์ตัวตนที่จะเรียกมันด้วยคำแฟนซี!) เนื่องจากหน้าที่ของฟังก์ชั่นการสูญเสียคือการคาดการณ์ของโมเดลคล้ายกับเป้าหมาย (อย่างน้อยที่สุดในกรณีส่วนใหญ่!) เราต้องการเมทริกซ์เช่นเป้าหมายของเรา นั่นคือเหตุผลว่าทำไมเราจึงคำนวณภาพ Matrices images_similarity และ texts_similarity ในบล็อกรหัสด้านบน
ตอนนี้เรามีเมทริกซ์เป้าหมายของเราแล้วเราจะใช้ Simple Cross Entropy เพื่อคำนวณการสูญเสียจริง ฉันได้เขียนรูปแบบเมทริกซ์เต็มรูปแบบของเอนโทรปีข้ามเป็นฟังก์ชั่นที่คุณสามารถเห็นได้ที่ด้านล่างของบล็อกรหัส ตกลง! เราเสร็จแล้ว! มันไม่ง่ายเลยเหรอ! เอาล่ะคุณสามารถเพิกเฉยต่อย่อหน้าถัดไป แต่ถ้าคุณอยากรู้อยากเห็นมีโน้ตสำคัญในเรื่องนั้น
นี่คือเหตุผลที่ฉันไม่ได้ใช้วิธีที่ง่ายกว่า : ฉันต้องยอมรับว่ามีวิธีที่ง่ายกว่าในการคำนวณการสูญเสียนี้ใน 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 นาทีใน GPU (แม้แต่ยุคเดียวก็เพียงพอแล้ว!) อาจใช้เวลาหนึ่งนาทีก่อนที่การฝึกอบรมจะเริ่มจริงเพราะเราจะเข้ารหัสคำบรรยายทั้งหมดหนึ่งครั้งในรถไฟและชุดข้อมูลที่ถูกต้องดังนั้นโปรดอย่าหยุด! ทุกสิ่งทำงานได้ดี
main ()ตกลง! เราทำกับการฝึกอบรมแบบจำลอง ตอนนี้เราจำเป็นต้องทำการอนุมานซึ่งในกรณีของเราจะให้ข้อความแบบจำลองและต้องการดึงภาพที่เกี่ยวข้องมากที่สุดจากชุดการตรวจสอบที่มองไม่เห็น (หรือทดสอบ)
ในฟังก์ชั่นนี้เรากำลังโหลดโมเดลที่เราบันทึกไว้หลังจากการฝึกอบรมให้อาหารในชุดตรวจสอบความถูกต้องและส่งคืน image_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 )
ฉันหวังว่าคุณจะสนุกกับบทความนี้ การใช้บทความนี้เป็นประสบการณ์ที่น่าสนใจสำหรับฉัน ฉันอยากจะขอบคุณ Khalid Salama สำหรับตัวอย่างรหัส Keras ที่ยิ่งใหญ่ที่เขาให้ซึ่งเป็นแรงบันดาลใจให้ฉันเขียนสิ่งที่คล้ายกันใน Pytorch