CLIP 是 Contrastive Language-Image Pre-training 的缩写,是一个擅长理解文本和图像之间关系的模型,下面是一个简单的介绍:
优点: CLIP 在零样本学习方面特别强大,它可以(用自然语言)给出图像的描述,并在基于该描述对新图像进行分类方面表现良好,例如,您可以将图像描述为“a”。猫的黑白照片”,CLIP 可以准确地对猫的新照片进行分类,即使它以前没有见过这些特定图像。
训练: CLIP 在从互联网收集的大量文本图像对数据集上进行训练,这使得它能够学习视觉概念及其描述之间的联系。
局限性: CLIP 也有缺点,训练的计算成本可能很高,并且在需要非常具体或抽象概念的任务上,或者对于与训练所用的文本描述非常不同的数据时,可能表现不佳。训练可能会将社会偏见引入模型中。
paper:Learning Transferable Visual Models From Natural Language Supervision
本文用CLIP做一个零样本分类,
CLIP训练的时候用的是图片和文本描述对,并没有分类的标签,那如何让CLIP做零样本分类?
我们需要给出标签的文本,让图像和所有的文本标签进行匹配,得分高的就是匹配到的标签文本。
paper中提到预测哪个文本整体与哪个图像配对,而不是该文本的准确单词。
下面通过一个kaggle数据集来具体说明。
这里选用indo fashion dataset, 它有15种印度服饰。
类别如下:
数据集结构:
其中images文件夹下又有train, val, test文件夹。
再看一下json文件,
image_path指的是上面images文件夹下的路径,
product_title是和图片对应的文本描述,训练的时候就是用图片和这个文本进行匹配。
class_label训练的时候不需要,最后验证分类是否正确时会用到。
import需要的库,定义数据集的文件夹,读取json数据
import json
from PIL import Image
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import clip
from transformers import CLIPProcessor,CLIPModel
from tqdm import tqdm
json_path = 'your_path/train_data.json'
image_path = 'your_path/images/train/'
input_data = []
with open(json_path, 'r') as f:
for line in f:
obj = json.loads(line)
input_data.append(obj)
CLIP模型,如果不能download, 手动下载走offline模式。
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
Setting our device to GPU (Cuda) and loading the pre-trained CLIP model.
device = "cuda:0" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-B/32", device=device, jit=False)
定义Dataloader
# Define a custom dataset
class image_title_dataset():
def __init__(self, list_image_path, list_txt):
self.image_path = list_image_path
# Tokenize text using CLIP's tokenizer
self.title = clip.tokenize(list_txt)
def __len__(self):
# Define the length of the dataset
return len(self.title)
def __getitem__(self, idx):
image = preprocess(Image.open(self.image_path[idx]))
title = self.title[idx]
return image, title
这里的dataset需要传入list_image_path和list_txt,
格式是这种:
list_image_path = [‘folder/image1.jpg’,‘folder2/image2.jpg’]
list_txt = [‘description for image1.jpg’ , ‘description for image2.jpg’]
所以要把image_path和product_title都装进list里面。
注意,CLIP的最大序列长度限制在76, 而有些文本描述非常长,需要截掉一部分,
当然截到76长度也有很多种方法,这里简单粗暴就从开头取长度76.
实际代码中,indo数据集不限制长度会报错,而博主觉得这个76可能是text被tokenize之后的token的长度,而不是原文本的长度,
因为把文本截到长度>77也是可以的。
而token的长度是由tokenize的算法决定的。具体最大极限文本长度是多少没测,这里简单地截取到77.
list_image_path = []
list_txt = []
for item in input_data:
img_path = image_path + item['image_path'].split('/')[-1]
caption = item['product_title'][:77]
list_image_path.append(img_path)
list_txt.append(caption)
dataset = image_title_dataset(list_image_path, list_txt)
train_dataloader = DataLoader(dataset, batch_size=100, shuffle=True)
# Function to convert model's parameters to FP32 format
#转精度省内存.
def convert_models_to_fp32(model):
for p in model.parameters():
p.data = p.data.float()
p.grad.data = p.grad.data.float()
if device == "cpu":
model.float() # Convert the model's parameters to float if using CPU
optimizer用Adam,参数按paper中的设置.
不过博主的机器容纳不了这么大的batch_size, 具体batch_size设多少合适,需要自己去验证。
由于数据集比较小,lr设得更小一些。
optimizer = torch.optim.Adam(
model.parameters(), lr=5e-5, betas=(0.9, 0.98), eps=1e-6 ,weight_decay=0.2)
训练
paper中的训练是这样的
for epoch in range(num_epochs):
pbar = tqdm(train_dataloader, total=len(train_dataloader))
for batch in pbar:
optimizer.zero_grad()
images, texts = batch
images = images.to(device)
texts = texts.to(device)
logits_per_image, logits_per_text = model(images, texts)
ground_truth = torch.arange(len(images), dtype=torch.long, device=device)
total_loss = (loss_img(logits_per_image, ground_truth) + loss_txt(logits_per_text, ground_truth)) / 2
total_loss.backward()
if device == "cpu":
optimizer.step()
else:
convert_models_to_fp32(model)
optimizer.step()
clip.model.convert_weights(model)
pbar.set_description(f"Epoch {epoch}/{num_epochs}, Loss: {total_loss.item():.4f}")
if torch.isnan(total_loss).any():
print("epoch {} loss is NaN".format(epoch))
epoch = num_epochs
break
训练中,遇到了这些问题:
loss出现了NaN, 调整batch_size能解决,batch_size不要太小。
loss降不下去了,看看paper中的参数,有哪些需要调整。
训练完之后,找来一张图片测试。
这里又有一些注意事项,
请看paper.
因为训练的时候是图片和一段文本描述匹配的,而不是图片和一个单词。
所以你做零样本分类时,类别文本最好不要只写一个单词,比如只写"Saree"。
你要写"A photo of Saree", 这就成了一个句子,效果就会好一些。
model, preprocess = clip.load("ViT-B/32", device=device)
checkpoint = torch.load("model.pt")
model.load_state_dict(checkpoint['model_state_dict'])
clothing_items = [
"Saree",
"Lehenga",
"Women Kurta",
"Dupatta",
"Gown",
"Nehru Jacket",
"Sherwani",
"Men Kurta",
"Men Mojari",
"Leggings and Salwar",
"Blouse",
"Palazzo",
"Dhoti Pants",
"Petticoat",
"Women Mojari"
]
这里你可能要问,那json文件里面的标签不是这么写的,比如"Women Kurta",json文件的标签是"women_kurta",
为什么不写成"women_kurta"。
这个博主是测试过的,写成json文件里面的标签形式准确率会降低,可能是因为"Women Kurta"更接近自然语言,更贴合训练数据吧。
把15个类别的标签都写成"A photo of {label}" 进行测试。
#你想测的第几张图片
index_ = 500
image_json = input_data[index_]
image_path = os.path.join("indo-fashion-dataset", image_json['image_path'])
image_class = image_json['class_label']
image = preprocess(Image.open(image_path)).unsqueeze(0).to(device)
text = torch.cat([clip.tokenize(f"a photo of a {c}") for c in clothing_items]).to(device)
with torch.no_grad():
# Encode image and text
image_features = model.encode_image(image)
text_features = model.encode_text(text)
# Calculate similarity scores between image and text
logits_per_image, logits_per_text = model(image, text)
probs = logits_per_image.softmax(dim=-1).cpu().numpy()
# Normalize image and text features
image_features /= image_features.norm(dim=-1, keepdim=True)
text_features /= text_features.norm(dim=-1, keepdim=True)
# Calculate similarity scores
similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)
values, indices = similarity[0].topk(5)
# Print the top predictions
print("\nTop predictions:\n")
for value, index in zip(values, indices):
print(f"{clothing_items[index]:>16s}: {100 * value.item():.2f}%")
# Display the image with its class label
plt.imshow(plt.imread(image_path))
plt.title(f"Image for class: {image_class}")
plt.axis('off')
plt.show()
训练中并没有精调参数,也没有训练很多epoch. 效果如下。
统计了一下测试集中7450张图片的top1和top3准确率。
top1: 77.7%, top3: 93.57%
paper中说CLIP 模型的 Top-5 准确率明显高于其 Top-1 准确率, 本文虽测的是top3, 但也是明显高于top1的。
又试了一下这种方法,这里效果并没有变好。