一文看懂CLIP的文本编码器:一句话文本如何变成512维向量去匹配图片
2026/9/2 10:56:53 网站建设 项目流程

一文看懂CLIP的文本编码器:一句话文本如何变成512维向量去匹配图片

【免费下载链接】CLIPCLIP (Contrastive Language-Image Pretraining), Predict the most relevant text snippet given an image项目地址: https://gitcode.com/GitHub_Trending/cl/CLIP

CLIP(Contrastive Language-Image Pretraining,对比语言-图像预训练)解决的是一个很实际的问题:给一张照片,让它直接和"a photo of a cat"这样的句子比相似度,找出最相关的描述。全程不需要针对具体任务再训练,这就是零样本(zero-shot)分类和图文检索的底层能力。整个过程依赖文本编码器和图像编码器把两种模态压进同一个向量空间,这一篇聚焦文本编码器:从BPE分词到Transformer编码,讲清文本最终如何变成一个512维向量。

跟着读下来,你会拿到一张完整的CLIP文本编码数据流图,能看懂 clip/model.py 里encode_text的每一步在干什么,并且能亲手跑通一个最小示例,验证文本向量的形状和相似度计算。

核心概念,一句话讲透

CLIP的文本编码器本质上是一台"向量翻译机":输入一句英文,输出一个512维向量,这个向量和图像特征在同一个空间里,点积(inner product)越大说明图文越相关。

你可以把它想象成图书馆的编目系统:每本书(文本)先被拆成关键词,每一页盖上页码(位置信息),最后整本书被压缩成一张"主题标签",贴到和图片相同的书架上,按距离找书就行。

数据流向可以简化成一条流水线:

文本编码器是怎么工作的(原理拆解)

整条链路的调用顺序是:clip.tokenize()产出整数矩阵,CLIP.encode_text()完成嵌入、编码、投影。下面按数据流走一遍,每个环节对应 clip/ 里的一个组件。

从字符串到整数序列:BPE分词

tokenize()负责把原始文本变成固定长度77的整数ID矩阵,这是进入神经网络前的最后一步预处理。

它先做清洗和小写化,用正则把文本切成基础词元,再靠字节对编码(Byte Pair Encoding, BPE)把生僻词反复合并成高频子词,最后给每条序列套上起始和结束标记,并右补零到77位。

# 来源:clip/clip.py 的 tokenize() sot_token = _tokenizer.encoder["<|startoftext|>"] # 起始标记 eot_token = _tokenizer.encoder["</w>"] # 结束标记 all_tokens = [[sot_token] + _tokenizer.encode(text) + [eot_token] for text in texts] # ... 根据torch版本选择dtype,省略 ... result = torch.zeros(len(all_tokens), context_length, dtype=torch.int) # 形状 [n, 77] for i, tokens in enumerate(all_tokens): if len(tokens) > context_length: if truncate: tokens = tokens[:context_length] tokens[-1] = eot_token else: raise RuntimeError(f"Input {texts[i]} is too long for context length {context_length}") result[i, :len(tokens)] = torch.tensor(tokens)

BPE词表来自仓库里的 clip/bpe_simple_vocab_16e6.txt.gz,由 clip/simple_tokenizer.py 加载。这段代码看着长,实际只干三件事:加标记、对齐长度、堆成矩阵。

从整数到向量:Token嵌入与位置嵌入

token_embeddingpositional_embedding负责把稀疏的ID变成稠密向量,同时让模型知道每个词在第几个位置。

每个ID到嵌入表里查一个768维向量(ViT-B/32规格);每个位置再叠加一个同形状的"位置向量"。两者相加后,一个词元就同时携带了语义和顺序信息。注意位置向量是nn.Parameter,靠训练学出来的,不是sin/cos公式算的。

# 来源:clip/model.py 的 CLIP.__init__ self.token_embedding = nn.Embedding(vocab_size, transformer_width) self.positional_embedding = nn.Parameter(torch.empty(self.context_length, transformer_width)) # 来源:clip/model.py 的 CLIP.encode_text x = self.token_embedding(text).type(self.dtype) # 形状 [batch, 77, width] x = x + self.positional_embedding.type(self.dtype) # 叠加位置信息

你可以把它理解成"给每页书盖页码":词嵌入是书名,位置嵌入是页码,合起来才知道"第几页说了什么"。

因果注意力:语言理解的中枢

Transformer堆叠负责让每个位置把前文信息聚合进来,是整个编码器的主体。

它由多个ResidualAttentionBlock叠成,每块内部是 LayerNorm → 多头自注意力 → LayerNorm → 前馈网络,全程配残差连接稳住训练。关键在注意力掩码:上三角填-inf,第i个位置只能看到自己和它前面的位置,这正是"从左到右读"的因果顺序。

# 来源:clip/model.py 的 CLIP.build_attention_mask def build_attention_mask(self): # 因果掩码:上三角为-inf,第i位只看得到自己与前文 mask = torch.empty(self.context_length, self.context_length) mask.fill_(float("-inf")) mask.triu_(1) return mask # 来源:clip/model.py 的 ResidualAttentionBlock.forward def forward(self, x: torch.Tensor): x = x + self.attention(self.ln_1(x)) # 自注意力 + 残差 x = x + self.mlp(self.ln_2(x)) # 前馈网络 + 残差 return x

不同规模的模型只是把这块"叠得更深更宽",其余逻辑完全一致:

模型Transformer层数注意力头数隐藏维度
RN50 / RN101 / ViT-B/32 / ViT-B/161212768
ViT-L/14 / ViT-L/14@336px24161024

所有模型的上下文长度都是77,投影输出都是512维。

从向量序列到单一特征:eot提取与投影

最后一步负责从77个位置的输出里挑出"整句总结",并投影到和图像特征同维的共享空间。

编码完成后,取结束标记</w>位置的向量作为整句表示——训练时它学到了聚合全句语义。由于</w>在词表里ID最大,text.argmax一行就能定位。随后乘上text_projection矩阵,把768维映射到512维。

# 来源:clip/model.py 的 CLIP.__init__ self.text_projection = nn.Parameter(torch.empty(transformer_width, embed_dim)) # 来源:clip/model.py 的 CLIP.encode_text 尾部 x = x.permute(1, 0, 2) # LND -> NLD x = self.ln_final(x).type(self.dtype) # 最终层归一化 # 取eot位置特征:词表中</w>的ID最大,argmax直接定位 x = x[torch.arange(x.shape[0]), text.argmax(dim=-1)] @ self.text_projection return x

投影完之后,文本向量就可以和encode_image的输出做矩阵相乘了。model(image, text)里还会先做L2归一化再乘一个可学习温度logit_scale,得到余弦相似度形式的logits。

动手试试(实操指南)

🛠️ 前置条件,一条命令都不多:

  1. 安装 PyTorch 1.7.1 以上版本和 torchvision
  2. 安装依赖:pip install ftfy regex tqdm
  3. 获取仓库:git clone https://gitcode.com/GitHub_Trending/cl/CLIP
  4. 模型权重首次运行会自动下载到~/.cache/clip

最小可运行示例:

import torch import clip model, _ = clip.load("ViT-B/32", device="cpu") # 首次会自动下载权重 texts = ["a photo of a cat", "a picture of a dog", "an image of a bird"] with torch.no_grad(): tokens = clip.tokenize(texts) features = model.encode_text(tokens) print(tokens.shape) # 预期:torch.Size([3, 77]) print(features.shape) # 预期:torch.Size([3, 512]) print((features @ features.T).shape) # 预期:torch.Size([3, 3])

跑通这段代码,你就已经完整走了一遍文本编码流水线:分词得到[3, 77]的ID矩阵,encode_text输出[3, 512]的特征,两者相乘得到[3, 3]的文本间相似度矩阵。如果输出和预期不一样,大概率是设备不一致(tokens没挪到模型所在设备)或PyTorch版本过老,先打印torch.__version__确认一下。

常见疑问 & 避坑提示

  • torch.Size([3, 77])里的77是什么?所有CLIP模型固定的上下文长度,包含起止两个标记,短序列右补零。序列尾部看到一串0是padding,不是bug。

  • 为什么用argmax找结束标记的位置?</w>是词表中追加的最后一个条目,ID全局最大,所以一条序列里最大值只可能是它。如果换成自定义的小ID结束标记,这行代码就不成立了。

  • 如果句子超过77个token会怎样?默认会直接抛RuntimeError。想保留长文本就调用tokenize(texts, truncate=True),它会自动截断到77位并强制把最后一位换成结束标记。

  • 768和512到底是什么关系?768是Transformer隐藏宽度(中间表示),512是text_projection投影后的输出维度,也是和图像特征同空间的那一个。做零样本分类要用512维的输出。

  • 手动调encode_text算相似度要归一化吗?要。model(image, text)内部会自动做L2归一化并乘logit_scale;自己调encode_text的话,记得先features / features.norm(...)再相乘。

延伸资源

  • 官方文档:README.md(含安装步骤与API说明)
  • 模型卡:model-card.md(训练数据与已知局限)
  • 核心源码目录:clip/(clip.py分词与加载、model.py模型结构、simple_tokenizer.pyBPE实现)
  • 交互式教程:notebooks/Interacting_with_CLIP.ipynb
  • 零样本分类提示模板:data/prompts.md
  • 一致性测试:tests/test_consistency.py

文本编码器吃透之后,建议接着看 clip/model.py 里的VisionTransformer,对照图像编码器走完另一半流程,你才会真正明白两个向量的点积为什么能当"相似度"用;想练手零样本分类,从 README.md 里的 Zero-Shot Prediction 示例开始即可。

【免费下载链接】CLIPCLIP (Contrastive Language-Image Pretraining), Predict the most relevant text snippet given an image项目地址: https://gitcode.com/GitHub_Trending/cl/CLIP

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询