news 2026/7/8 11:46:02

多模态-复现OpenAI的CLIP模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多模态-复现OpenAI的CLIP模型

基于up主【【多模态】复现OpenAI的CLIP模型】 https://www.bilibili.com/video/BV13K421v7Ar/?share_source=copy_web&vd_source=62e4f3fd48d4ea21c7731322b8e5cb53 的代码 进行了修改 作出如下的学习笔记

一、代码展示与修改

1.CLIP 模型核心原理回顾

CLIP(Contrastive Language-Image Pre-training)是 OpenAI 提出的跨模态预训练模型,核心是通过「对比学习」让图像编码器和文本编码器的输出在特征空间中对齐——即“相似内容的图像和文本,其特征向量距离更近;不相似的则更远”。

2.项目文件作用解析(按功能分类)

(1) 数据层:dataset.py

作用:定义 MNIST 数据集的加载、预处理逻辑。

# 1. 导入 PyTorch 的数据集基类,自定义数据集必须继承它 from torch.utils.data import Dataset # 2. 导入 torchvision 中的图像转换工具 # PILToTensor: 将 PIL 图片转换为 Tensor (张量) # Compose: 用于将多个转换步骤串联起来 from torchvision.transforms.v2 import PILToTensor, Compose # 3. 导入 torchvision 库,主要用于获取内置的 MNIST 数据集 import torchvision # 4. 定义一个名为 MNIST 的类,继承自 Dataset # 这是 PyTorch 加载数据的标准方式 class MNIST(Dataset): # 5. 初始化方法 # is_train=True 表示默认加载训练集,设为 False 则加载测试集 def __init__(self, is_train=True): # 6. 调用父类的初始化方法(标准写法) super().__init__() # 7. 加载真正的 MNIST 数据 # root='./mnist/': 数据下载保存的路径 # train=is_train: 根据参数决定是下载训练集还是测试集 # download=True: 如果本地没有数据,自动从网上下载 self.ds = torchvision.datasets.MNIST('./mnist/', train=is_train, download=True) # 8. 定义图像预处理流程 # 这里只包含一步:将图片转为 Tensor self.img_convert = Compose([ PILToTensor(), ]) # 9. 返回数据集的总长度(即图片的总数量,MNIST 训练集通常是 60000 张) def __len__(self): return len(self.ds) # 10. 核心方法:根据索引 index 获取某一张图片和对应的标签 def __getitem__(self, index): # 11. 从原始数据集中取出第 index 个样本 # img 是 PIL 格式的图片对象,label 是整数(0-9) img, label = self.ds[index] # 12. 处理并返回数据 # self.img_convert(img): 将 PIL 图片转为 Tensor (数值范围 0-255) # / 255.0: 进行归一化,将数值范围压缩到 0.0 - 1.0 之间,利于模型训练 # 返回处理后的图片和原始标签 return self.img_convert(img) / 255.0, label # 13. 主程序入口,只有直接运行此脚本时才会执行以下代码 if __name__ == '__main__': # 14. 导入绘图库 matplotlib import matplotlib.pyplot as plt # 15. 实例化我们定义的 MNIST 数据集类 ds = MNIST() # 16. 获取数据集中的第 0 张图片及其标签 img, label = ds[0] # 17. 在控制台打印这张图片的标签(例如打印出 5) print(label) # 18. 使用 matplotlib 显示图片 # img.permute(1,2,0): 调整维度顺序。 # Tensor 默认是 (C, H, W) 即 (通道, 高, 宽),而 plt.imshow 需要 (H, W, C) plt.imshow(img.permute(1, 2, 0)) # 19. 弹出窗口展示图片 plt.show()

(2) 模型层:图像编码器 & 文本编码器

CLIP 由双编码器组成,项目中分别用独立文件实现:
img_encoder.py:图像编码器
作用:将输入图像映射为固定维度的特征向量(嵌入)。

# 1. 导入 PyTorch 的神经网络模块,包含构建模型所需的各种层(如卷积、线性层等) from torch import nn # 2. 导入 PyTorch 主库,主要用于张量操作和随机数生成(用于测试) import torch # 3. 导入 PyTorch 的函数式接口,主要使用其中的激活函数(如 ReLU) import torch.nn.functional as F # 4. 定义残差块类,继承自 nn.Module # 这是 ResNet 的核心组件,用于解决深层网络梯度消失问题 class ResidualBlock(nn.Module): # 5. 初始化残差块的参数 def __init__(self, in_channels, out_channels, stride): super().__init__() # 6. 第一个卷积层:负责主要的特征提取和下采样(如果 stride > 1) # kernel_size=3, padding=1: 保持空间尺寸不变(不考虑 stride) self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=1, stride=stride) # 7. 第一个批归一化层:加速收敛,稳定训练 self.bn1 = nn.BatchNorm2d(out_channels) # 8. 第二个卷积层:进一步提取特征,stride 固定为 1,不改变尺寸 self.conv2 = nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, padding=1, stride=1) # 9. 第二个批归一化层 self.bn2 = nn.BatchNorm2d(out_channels) # 10. 捷径连接(Shortcut/Projection)卷积层 # 当输入输出维度不一致时(通道数改变或尺寸改变),用 1x1 卷积进行匹配 self.conv3 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, padding=0, stride=stride) # 11. 定义前向传播逻辑 def forward(self, x): # 12. 主路径:卷积 -> 归一化 -> 激活 -> 卷积 -> 归一化 # 注意:这里先做 BN 再做 ReLU,是常见的变体写法 y = F.relu(self.bn1(self.conv1(x))) y = self.bn2(self.conv2(y)) # 13. 捷径路径:直接通过 1x1 卷积调整输入 x 的维度,使其能与 y 相加 z = self.conv3(x) # 14. 残差连接:将主路径输出 y 与捷径输出 z 相加,最后再过一次激活函数 return F.relu(y + z) # 15. 定义图像编码器类,用于将 MNIST 图像转换为特征向量 class ImgEncoder(nn.Module): def __init__(self): super().__init__() # 16. 第一个残差块:输入 1 通道(灰度图),输出 16 通道,步长 2(尺寸减半) # 输入 (28x28) -> 输出 (14x14) self.res_block1 = ResidualBlock(in_channels=1, out_channels=16, stride=2) # 17. 第二个残差块:输入 16 通道,输出 4 通道,步长 2 # 输入 (14x14) -> 输出 (7x7) self.res_block2 = ResidualBlock(in_channels=16, out_channels=4, stride=2) # 18. 第三个残差块:输入 4 通道,输出 1 通道,步长 2 # 输入 (7x7) -> 输出 (4x4) self.res_block3 = ResidualBlock(in_channels=4, out_channels=1, stride=2) # 19. 全连接层(线性层):将展平后的特征映射到最终的嵌入维度 # 此时特征图大小为 4x4x1 = 16,将其映射为 8 维向量 self.wi = nn.Linear(in_features=16, out_features=8) # 20. 层归一化:对最终的 8 维向量进行归一化,常用于 CLIP 模型以计算余弦相似度 self.ln = nn.LayerNorm(8) # 21. 定义前向传播 def forward(self, x): # 22. 依次通过三个残差块进行特征提取和下采样 x = self.res_block1(x) x = self.res_block2(x) x = self.res_block3(x) # 23. 展平操作:将 (Batch, 1, 4, 4) 变为 (Batch, 16) # 然后送入全连接层得到 8 维特征 x = self.wi(x.view(x.size(0), -1)) # 24. 层归一化后返回最终结果 x = self.ln(x) return x # 25. 主程序入口,用于简单测试模型是否能跑通 if __name__ == '__main__': # 26. 实例化图像编码器 img_encoder = ImgEncoder() # 27. 创建一个模拟的输入张量:(Batch=1, Channel=1, Height=28, Width=28) # 对应一张 28x28 的灰度图 out = img_encoder(torch.randn(1, 1, 28, 28)) # 28. 打印输出形状,预期应为 torch.Size([1, 8]) print(out.shape)



text_encoder.py:文本编码器
作用:将文本标签(如“数字3”)映射为与图像编码器同维度的特征向量。

# 1. 导入 PyTorch 的神经网络模块,用于构建模型的各种层 from torch import nn # 2. 导入 PyTorch 主库,用于张量操作(如创建测试用的 tensor) import torch # 3. 导入 PyTorch 的函数式接口,这里主要用到 ReLU 激活函数 import torch.nn.functional as F # 4. 定义文本编码器类,继承自 nn.Module # 作用是将 MNIST 数据集的文本标签(0-9)映射为与图像编码器维度相同的特征向量 class TextEncoder(nn.Module): # 5. 初始化方法 def __init__(self): super().__init__() # 6. 词嵌入层(Embedding) # num_embeddings=10: 词表大小为 10,对应 MNIST 的 10 个数字类别(0-9) # embedding_dim=16: 将每个数字映射为一个 16 维的稠密向量 self.emb = nn.Embedding(num_embeddings=10, embedding_dim=16) # 7. 第一个全连接层:将 16 维向量映射到 64 维,用于提取更丰富的文本特征 self.dense1 = nn.Linear(in_features=16, out_features=64) # 8. 第二个全连接层:将 64 维向量降维回 16 维 self.dense2 = nn.Linear(in_features=64, out_features=16) # 9. 投影层(Projection):将文本特征从 16 维映射到最终的 8 维空间 # 这里的 8 维与 ImgEncoder 输出的 8 维完全一致,这是 CLIP 跨模态对齐的关键 self.wt = nn.Linear(in_features=16, out_features=8) # 10. 层归一化(LayerNorm):对最终的 8 维向量进行归一化 # 确保文本特征和图像特征在同一个特征空间内,便于后续计算余弦相似度 self.ln = nn.LayerNorm(8) # 11. 定义前向传播逻辑 def forward(self, x): # 12. 输入整数标签(如 3),通过 Embedding 层转换为 16 维向量 x = self.emb(x) # 13. 通过第一个全连接层,并使用 ReLU 激活函数引入非线性 x = F.relu(self.dense1(x)) # 14. 通过第二个全连接层,同样使用 ReLU 激活 x = F.relu(self.dense2(x)) # 15. 通过投影层,将特征维度从 16 压缩到 8 x = self.wt(x) # 16. 进行层归一化,返回最终的 8 维文本特征向量 x = self.ln(x) return x # 17. 主程序入口,用于测试文本编码器是否能正常运行 if __name__ == '__main__': # 18. 实例化文本编码器 text_encoder = TextEncoder() # 19. 创建一个模拟的输入张量,包含 0-9 的 10 个数字标签 x = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) # 20. 将 10 个标签送入文本编码器,得到 10 个对应的 8 维特征向量 y = text_encoder(x) # 21. 打印输出形状,预期结果应为 torch.Size([10, 8]) print(y.shape)



(3)训练层:train.py


作用:定义 CLIP 的训练流程,核心是对比损失(Contrastive Loss)。

# 1. 导入 PyTorch 核心库 import torch # 2. 导入之前自定义的 MNIST 数据集类 from dataset import MNIST # 3. 导入之前定义的 CLIP 模型类(包含图像编码器和文本编码器) from clip import CLIP # 4. 导入 PyTorch 的函数式接口,这里主要用到交叉熵损失函数 import torch.nn.functional as F # 5. 导入数据加载器,用于批量加载数据 from torch.utils.data import DataLoader # 6. 导入操作系统模块,用于文件的重命名(安全保存模型) import os # 7. 设置运行设备:如果有 NVIDIA GPU 则使用 'cuda',否则使用 'cpu' DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' # 8. 实例化 MNIST 数据集 dataset = MNIST() # 9. 实例化 CLIP 模型,并将其移动到指定设备(GPU/CPU)上 model = CLIP().to(DEVICE) # 10. 尝试加载之前保存的模型权重(断点续训) try: # 从 'model.pth' 文件中读取状态字典并加载到模型中 model.load_state_dict(torch.load('model.pth')) except: # 如果文件不存在或加载失败,则忽略错误,从头开始训练 pass # 11. 定义 Adam 优化器,用于更新模型参数,学习率设为 0.001 optimzer = torch.optim.Adam(model.parameters(), lr=1e-3) ''' 训练模型主循环 ''' # 12. 定义总迭代次数:训练 10 万个 batch ITER_BATCH_COUNT = 100000 # 13. 定义批次大小:每次从数据集中抽取 64 个样本 BATCH_SIZE = 64 # 14. 定义目标类别数:MNIST 共有 10 种数字(0-9) TARGET_COUNT = 10 # 15. 创建数据加载器,开启 shuffle 打乱数据,num_workers=0 表示在主进程中加载数据 dataloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0) # 16. 开始迭代训练循环 for i in range(ITER_BATCH_COUNT): # 17. 内部死循环:不断抽取 batch,直到抽到一个包含全部 10 种数字的 batch 为止 while True: # 从数据加载器中获取下一个 batch 的图片和标签 imgs, labels = next(iter(dataloader)) # 统计当前 batch 中不重复的标签数量,如果小于 10,说明没凑齐所有数字 if torch.unique(labels).shape[0] < TARGET_COUNT: continue # 继续抽取下一个 batch # 凑齐了 10 种数字后,开始从中挑选出刚好 10 个不重复的样本 target = set() # 用集合记录已经选中的数字 indexes = [] # 记录选中样本的索引 # 遍历当前 batch 的 64 个样本 for j in range(BATCH_SIZE): # 如果当前数字已经被选中过,则跳过 if labels[j].item() in target: continue # 将新数字加入集合,记录其索引 target.add(labels[j].item()) indexes.append(j) # 如果已经选够了 10 个不同的数字,则跳出循环 if len(target) == TARGET_COUNT: break # 根据选出的索引,提取出包含 10 个不同数字的图片和标签 imgs = imgs[indexes] labels = labels[indexes] break # 成功凑齐 10 个样本,跳出 while True 循环 # 18. 将挑选好的 10 个图片和标签送入 CLIP 模型,得到相似度矩阵 (logits) logits = model(imgs.to(DEVICE), labels.to(DEVICE)) # 19. 构造真实标签(Ground Truth):对角线 [0, 1, 2, ..., 9] # 表示第 0 张图对应第 0 个文本,第 1 张图对应第 1 个文本,以此类推 targets = torch.arange(0, TARGET_COUNT).to(DEVICE) # 20. 计算图像到文本方向的交叉熵损失 loss_i = F.cross_entropy(logits, targets) # 21. 计算文本到图像方向的交叉熵损失 # permute(1,0) 将矩阵转置,实现双向对齐 loss_t = F.cross_entropy(logits.permute(1, 0), targets) # 22. 计算最终的对比损失:取双向损失的平均值 loss = (loss_i + loss_t) / 2 # 23. 清空上一步累积的梯度(PyTorch 默认会累加梯度) optimzer.zero_grad() # 24. 反向传播,计算当前 loss 对模型参数的梯度 loss.backward() # 25. 优化器根据梯度更新模型参数 optimzer.step() # 26. 每训练 1000 次迭代,打印一次当前的迭代次数和损失值 if i % 1000 == 0: print('iter:{},loss:{}'.format(i, loss)) # 27. 安全保存模型权重 # 先保存到临时文件 '.model.pth' torch.save(model.state_dict(), '.model.pth') # 28. 将临时文件替换为正式的 'model.pth' # 这样做是为了防止在保存过程中程序崩溃导致原模型文件损坏 os.replace('.model.pth', 'model.pth')



(4)推理层:inference.py


作用:利用训练好的模型做跨模态检索/分类。

# 1. 导入之前自定义的 MNIST 数据集类 from dataset import MNIST # 2. 导入绘图库,用于可视化展示图片和相似度结果 import matplotlib.pyplot as plt # 3. 导入 PyTorch 核心库 import torch # 4. 导入之前定义的 CLIP 模型类 from clip import CLIP # 5. 导入 PyTorch 的函数式接口(本脚本中虽未直接使用,但属于常规导入) import torch.nn.functional as F # 6. 设置运行设备:优先使用 GPU,否则使用 CPU DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' # 7. 实例化 MNIST 数据集 dataset = MNIST() # 8. 实例化 CLIP 模型并移动到指定设备 model = CLIP().to(DEVICE) # 9. 加载训练好的模型权重文件 model.load_state_dict(torch.load('model.pth')) # 10. 将模型切换到评估模式(eval) # 这会关闭 Dropout 和 BatchNorm 的训练行为,确保推理结果稳定 model.eval() ''' 1、对图片分类(Zero-Shot Classification) ''' # 11. 从数据集中取出第 0 张图片及其真实标签 image, label = dataset[0] # 12. 打印这张图片的真实分类(例如 5) print('正确分类:', label) # 13. 显示这张原始图片 # permute(1,2,0) 将 Tensor 维度从 (C,H,W) 转换为 (H,W,C) 以适应 matplotlib plt.imshow(image.permute(1, 2, 0)) plt.show() # 14. 构造包含 0-9 所有类别的文本标签张量 targets = torch.arange(0, 10) # 15. 将 1 张图片和 10 个文本标签一起送入 CLIP 模型 # unsqueeze(0) 在 batch 维度增加一维,变成 (1, 1, 28, 28) # 模型会输出一个 (1, 10) 的相似度矩阵(logits) logits = model(image.unsqueeze(0).to(DEVICE), targets.to(DEVICE)) # 16. 打印模型输出的原始相似度分数 print(logits) # 17. 找出相似度分数最高的类别索引,作为 CLIP 的预测结果 # argmax(-1) 在最后一个维度(10个类别)上找最大值 print('CLIP分类:', logits.argmax(-1).item()) ''' 2、图像相似度检索(Image-to-Image Retrieval) ''' # 18. 准备用于检索的候选图片列表和对应的标签列表 other_images = [] other_labels = [] # 19. 从数据集中遍历第 1 到第 100 张图片,作为候选图库 for i in range(1, 101): other_image, other_label = dataset[i] other_images.append(other_image) other_labels.append(other_label) # 20. 将 100 张候选图片堆叠成一个张量,送入图像编码器提取特征 # torch.stack 将列表变为 (100, 1, 28, 28) 的张量 # 输出 other_img_embs 的形状为 (100, 8) other_img_embs = model.img_enc(torch.stack(other_images, dim=0).to(DEVICE)) # 21. 将当前测试图片也送入图像编码器提取特征 # 输出 img_emb 的形状为 (1, 8) img_emb = model.img_enc(image.unsqueeze(0).to(DEVICE)) # 22. 通过矩阵乘法计算当前图片与 100 张候选图片的余弦相似度(点积) # (1, 8) @ (8, 100) = (1, 100) # 注:代码中变量名拼写为 logtis,应为 logits logtis = img_emb @ other_img_embs.T # 23. 找出相似度最高的前 5 个候选图片 # values 是前 5 高的相似度分数,indexs 是对应的图片索引 values, indexs = logtis[0].topk(5) # 24. 创建一个 15x15 英寸的大画布,用于展示检索结果 plt.figure(figsize=(15, 15)) # 25. 遍历最相似的 5 张图片并依次绘制 for i, img_idx in enumerate(indexs): # 26. 创建 1 行 5 列的子图,当前是第 i+1 个 plt.subplot(1, 5, i + 1) # 27. 显示这张最相似的图片 plt.imshow(other_images[img_idx].permute(1, 2, 0)) # 28. 在图片上方添加标题,显示其真实标签 plt.title(other_labels[img_idx]) # 29. 关闭坐标轴显示,使图片更美观 plt.axis('off') # 30. 弹出窗口展示最终的 5 张最相似图片 plt.show()



(5)辅助文件


clip.py: CLIP 模型的整体封装(整合图像/文本编码器,定义前向传播逻辑)。
refs:存放参考文献、技术文档(如 CLIP 原论文链接)。
clip-model.png / similar_image.png / image.png:可视化素材(模型架构图、相似图像示例、测试用图像)。
resnet-block.svg:ResNet 残差块的示意图(若图像编码器用了 ResNet 结构,用于解释模块设计)。
README.md:项目说明(环境配置、运行步骤、功能介绍)。

3.代码修改

修改后的代码在资源绑定中,修改train.py中的两个地方

dataloader=DataLoader(dataset,batch_size=BATCH_SIZE,shuffle=True,num_workers=0) # 数据加载器

在这里,将 num_workers 改为 0,并将 persistent_workers 改为 False (或者直接删掉 persistent_workers 这个参数)

原因:
多进程冲突:DataLoader 默认会启动多个子进程(num_workers > 0)来并行加载数据。将 num_workers 设置为 0(最快速有效)这是最简单直接的解决方法。它会禁用多进程,让数据加载在主进程中完成。虽然速度会慢一些,但能立即排除多进程相关的所有问题。

既然你选择了不使用 worker(数量为 0),那就没有进程可以“保持存活”了。PyTorch 检测到这个逻辑冲突,所以抛出了 ValueError。

二、运行结果

1.运行train.py后 终端显示

程序正在正常运行(Loss 在不断下降,说明模型正在学习)

2.运行inference.py后,显示

识别正确:终端底部显示正确分类: 5,说明系统准确判断出这是一张数字5

三、注意事项

1.检查torchvision版本,太旧不能运行

2.代码修改后CTRL+s保存,否则依旧报错

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/8 11:45:51

3步构建完美游戏体验:ViGEmBus虚拟手柄驱动完全指南

3步构建完美游戏体验&#xff1a;ViGEmBus虚拟手柄驱动完全指南 【免费下载链接】ViGEmBus Windows kernel-mode driver emulating well-known USB game controllers. 项目地址: https://gitcode.com/gh_mirrors/vi/ViGEmBus 在Windows游戏世界中&#xff0c;你是否曾因…

作者头像 李华
网站建设 2026/7/8 11:45:20

三轴运动追踪:WSEN-ISDS与TM4C1299NCZAD的硬件设计与算法实现

1. 项目概述&#xff1a;三轴运动追踪的核心需求 在工业自动化、无人机导航和可穿戴设备领域&#xff0c;精确测量物体在三维空间中的角运动和线性运动一直是核心技术挑战。WSEN-ISDS&#xff08;型号2536030320001&#xff09;这款MEMS传感器&#xff0c;配合TI的TM4C1299NCZA…

作者头像 李华
网站建设 2026/7/8 11:39:44

MTKClient:你的联发科设备终极“起死回生”工具箱

MTKClient&#xff1a;你的联发科设备终极“起死回生”工具箱 【免费下载链接】mtkclient MTK reverse engineering and flash tool 项目地址: https://gitcode.com/gh_mirrors/mt/mtkclient 当你的手机突然变成一块“砖头”&#xff0c;屏幕漆黑一片&#xff0c;无论怎…

作者头像 李华
网站建设 2026/7/8 11:37:02

为什么客户洞察之后,还需要一张业务场景地图?

在复杂ToB出海业务中&#xff0c;客户洞察已经成为企业开发海外市场的重要基础。通过公开信息、多语种资料和企业数据&#xff0c;团队可以了解目标客户的业务布局、经营动态、组织变化与潜在合作方向。但很多企业很快会遇到新的问题&#xff1a;客户资料已经足够丰富&#xff…

作者头像 李华
网站建设 2026/7/8 11:32:47

CVE-2022-32300 漏洞防御:为 YoudianCMS 9.5.0 添加 2 层 SQL 注入防护方案

CVE-2022-32300 漏洞防御&#xff1a;为 YoudianCMS 9.5.0 构建双层防护体系当安全团队在凌晨三点收到漏洞告警时&#xff0c;往往面临两难选择&#xff1a;是紧急升级到可能存在兼容性问题的新版本&#xff0c;还是为现有系统打上临时补丁&#xff1f;本文将以YoudianCMS 9.5.…

作者头像 李华