CH3_01_bge_visualized
import torchfrom visual_bge.visual_bge.modeling import Visualized_BGEvisual_bge库:BAAI(北京智源研究院)开源的多模态检索模型套件,它扩展了传统的 BGE 文本嵌入模型,使其能够同时编码图像和文本。Visualized_BGE类:核心模型类,它融合了:- 视觉编码器(如 ViT):提取图像特征。
- 文本编码器(BGE 系列,如
bge-base-en-v1.5):提取文本特征。 - 对齐映射层:将图像和文本特征映射到同一个向量空间,使得图文可以直接计算相似度。
这个模型可以做:
| 输入类型 | 示例 | 输出 |
|---|---|---|
| 纯文本 | "datawhale开源组织的logo" | 文本向量 |
| 纯图像 | logo.png | 图像向量 |
| 图文混合 | logo.png + "datawhale开源组织的logo" | 融合向量 |
三种输出都在同一个语义空间中,所以你可以:
- 用图像搜图像
- 用文本搜图像
- 用图像搜文本
- 做图文混合检索
model = Visualized_BGE(model_name_bge="BAAI/bge-base-en-v1.5", model_weight="../../models/bge/Visualized_base_en_v1.5.pth")model.eval()输出为:
Visualized_BGE( (bge_encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSdpaSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (bge_embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) (bge_pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ) (model_visual): CustomCLIP( (visual): EVAVisionTransformer( (patch_embed): PatchEmbed( (proj): Conv2d(3, 768, kernel_size=(16, 16), stride=(16, 16)) ) (pos_drop): Dropout(p=0.0, inplace=False) (rope): VisionRotaryEmbeddingFast() (blocks): ModuleList( (0-11): 12 x Block( (norm1): LayerNorm((768,), eps=1e-06, elementwise_affine=True) (attn): Attention( (q_proj): Linear(in_features=768, out_features=768, bias=False) (k_proj): Linear(in_features=768, out_features=768, bias=False) (v_proj): Linear(in_features=768, out_features=768, bias=False) (attn_drop): Dropout(p=0.0, inplace=False) (inner_attn_ln): LayerNorm((768,), eps=1e-06, elementwise_affine=True) (proj): Linear(in_features=768, out_features=768, bias=True) (proj_drop): Dropout(p=0.0, inplace=False) (rope): VisionRotaryEmbeddingFast() ) (drop_path): Identity() (norm2): LayerNorm((768,), eps=1e-06, elementwise_affine=True) (mlp): SwiGLU( (w1): Linear(in_features=768, out_features=2048, bias=True) (w2): Linear(in_features=768, out_features=2048, bias=True) (act): SiLU() (ffn_ln): LayerNorm((2048,), eps=1e-06, elementwise_affine=True) (w3): Linear(in_features=2048, out_features=768, bias=True) (drop): Dropout(p=0.0, inplace=False) ) ) ) (norm): LayerNorm((768,), eps=1e-06, elementwise_affine=True) (head): Linear(in_features=768, out_features=512, bias=True) (patch_dropout): PatchDropout() ) (text): None ) (visual_proj): Linear(in_features=768, out_features=768, bias=True) (cross_entropy): CrossEntropyLoss())1. Visualized_BGE 模型的架构
BGE 模型实际上是 BERT 的衍生,所以我们可以看到在动手写 BERT 系列中与 BERT 模型类似的架构。
1️⃣ bge_embeddings — 文本嵌入层
BertEmbeddings( word_embeddings: Embedding(30522, 768) # 词表大小 30522,每个词映射为 768 维 position_embeddings: Embedding(512, 768) # 位置编码,最长 512 token token_type_embeddings: Embedding(2, 768) # 区分两个句子的类型编码 LayerNorm(768) # 层归一化 Dropout(p=0.1) # 防止过拟合)作用:把输入的文本 token ID 转换为 768 维的向量,并加上位置信息和层归一化。
2️⃣ bge_encoder — 文本编码器(核心)
BertEncoder( layer: ModuleList( (0-11): 12 × BertLayer # 12 层 Transformer ))每层 BertLayer 包含:
| 子模块 | 组件 | 说明 |
|---|---|---|
| attention | BertSdpaSelfAttention | 自注意力 (Scaled Dot-Product Attention) |
query/Key/Value: Linear(768→768) | Q、K、V 线性投影 | |
BertSelfOutput | 残差连接 + LayerNorm + Dropout | |
| intermediate | Linear(768→3072) + GELU | 前馈网络放大 4 倍 |
| output | Linear(3072→768) | 缩回 768 维 |
作用:这 12 层 Transformer 对文本进行上下文建模,每个 token 都能”看到”句子中的其他 token。
这里可以看到,BGE 模型的隐藏层架构与 BERT 是一致的,都是第 0 层 embedding 层 + 12 层 Transformer 层。
3️⃣ bge_pooler — 池化层
BertPooler( dense: Linear(768→768) activation: Tanh())作用:取出 [CLS] 位置的向量,经过一个全连接层 + Tanh 激活,得到整句话的句向量。
4️⃣ model_visual — 图像编码器(Visual 分支)
这是 EVA-CLIP 的视觉 Transformer(ViT),具体是 EVAVisionTransformer:
CustomCLIP( visual: EVAVisionTransformer( patch_embed: PatchEmbed(proj=Conv2d(3, 768, 16×16)) # 将 224×224 图片切成 14×14 个 16×16 的 patch,投影为 768 维 ))内部结构:
| 组件 | 说明 |
|---|---|
patch_embed | Conv2d(3→768, kernel=16×16, stride=16×16) 将图片切成非重叠 patch,每个 patch → 768 维向量 |
pos_drop | Dropout(p=0.0) 占位,实际未使用 |
rope | VisionRotaryEmbeddingFast() — 旋转位置编码(RoPE),比传统绝对位置编码效果更好 |
blocks | 12 层 Block(EVAViT),每层包含: |
├─ norm1 | LayerNorm(768) |
├─ attn | Attention — 自注意力 |
│ ├─ q/k/v_proj | Linear(768→768, bias=False) |
│ ├─ inner_attn_ln | LayerNorm(768) — 注意力内部归一化 |
│ ├─ proj | Linear(768→768) 输出投影 |
│ └─ rope | 注意力内的 RoPE |
├─ drop_path | Identity() — 随机深度,此处未生效 |
├─ norm2 | LayerNorm(768) |
└─ mlp | SwiGLU — 比传统 FFN 更先进的 MLP |
│ ├─ w1/w2 | Linear(768→2048) — 两个门控分支 |
│ ├─ act | SiLU() — 激活函数 |
│ ├─ ffn_ln | LayerNorm(2048) |
│ └─ w3 | Linear(2048→768) 输出 |
norm | LayerNorm(768) — 最后归一化 |
head | Linear(768→512) — 映射到 512 维视觉特征 |
patch_dropout | PatchDropout() — 训练时随机丢弃 patch 做数据增强 |
💡 SwiGLU =
SiLU(x·W1) ⊗ (x·W2),是 Meta 在 LLaMA 中使用的先进 MLP 结构,效果优于传统 ReLU+GELU。
5️⃣ visual_proj — 视觉投影层
Linear(in_features=768, out_features=768, bias=True)作用:将图像分支输出的 512 维向量通过 head 映射到 768 维后,再经过此投影层,与文本分支的 768 维向量对齐到同一空间。
6️⃣ cross_entropy — 损失函数
CrossEntropyLoss()作用:训练时用于计算图文对比损失(Contrastive Loss),让匹配的图文对距离更近,不匹配的拉远。
1.7 整体数据流总结
文本: token → [bge_embeddings → 12层 BERT → bge_pooler] → 768维文本向量图片: pixels → [PatchEmbed → 12层 EVAViT → head → visual_proj] → 768维图像向量两个分支最终都输出 768 维向量,处在同一语义空间,所以可以直接计算相似度。训练时通过 CrossEntropyLoss 做对比学习,让图文对对齐。
with torch.no_grad(): text_emb = model.encode(text="datawhale开源组织的logo") img_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png") multi_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png", text="datawhale开源组织的logo") img_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png") multi_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png", text="datawhale开源组织的logo")# 计算相似度sim_1 = img_emb_1 @ img_emb_2.Tsim_2 = img_emb_1 @ multi_emb_1.Tsim_3 = text_emb @ multi_emb_1.Tsim_4 = multi_emb_1 @ multi_emb_2.T2. 相似度的数学计算
代码中的 @ 运算符是 矩阵乘法(即点积 / 内积)。在 PyTorch 中,每个 encode() 的输出形状都是 (1, 768)——即 1 个 768 维的向量。.T 是转置操作,把行向量变成列向量。
img_emb_1 形状: (1, 768)img_emb_2.T 形状: (768, 1)
img_emb_1 @ img_emb_2.T → 形状: (1, 1) ← 一个标量相似度值2.1 数学公式
对于两个 768 维的向量 和 :
它们的点积(内积)定义为:
用矩阵乘法的方式写就是:
2.2 向量点积的几何意义
点积在几何上的含义是:
其中:
- 是向量 的长度(模)
- 是向量 的长度
- 是两向量之间的夹角
如果两个向量方向一致(夹角接近 ),,点积最大 → 相似度高。
如果两个向量方向相反(夹角接近 ),,点积最小 → 相似度低。
如果两个向量垂直(夹角 ),,点积为 0 → 毫不相关。
2.3 结合代码具体分析
上面的四组计算可以这样理解:
| 代码 | 含义 | 几何理解 |
|---|---|---|
sim_1 = img_emb_1 @ img_emb_2.T | 图1 vs 图2 的相似度 | 两张图片的向量夹角 |
sim_2 = img_emb_1 @ multi_emb_1.T | 图1 vs 图文结合1 | 纯图片 vs 融合了文本信息的图片 |
sim_3 = text_emb @ multi_emb_1.T | 纯文本 vs 图文结合1 | 纯文本 vs 融合向量 |
sim_4 = multi_emb_1 @ multi_emb_2.T | 结合1 vs 结合2 | 两个不同图片的融合向量 |
所有向量都处于 768 维的统一语义空间,所以任意两者都能直接计算点积。
2.4 一个直观的小例子
假设二维空间中有两个向量:
a = [3, 4]b = [1, 2]
a · b = 3×1 + 4×2 = 3 + 8 = 11维度越高(这里是 768 维),计算方式完全一样——只是求和项更多而已。
2.5 补充:点积 vs 余弦相似度
你可能会想:为什么不用更常见的余弦相似度?
因为 Visualized_BGE 输出的向量已经做过归一化(L2 normalize),即 ,所以:
点积 = 余弦相似度,二者等价。所以代码中直接算点积就够了,不需要再除以模长。
print("=== 相似度计算结果 ===")print(f"纯图像 vs 纯图像: {sim_1}")print(f"图文结合1 vs 纯图像: {sim_2}")print(f"图文结合1 vs 纯文本: {sim_3}")print(f"图文结合1 vs 图文结合2: {sim_4}")输出为:
=== 相似度计算结果 ===纯图像 vs 纯图像: tensor([[0.8318]])图文结合1 vs 纯图像: tensor([[0.8291]])图文结合1 vs 纯文本: tensor([[0.7627]])图文结合1 vs 图文结合2: tensor([[0.9058]])# 向量信息分析print("\n=== 嵌入向量信息 ===")print(f"多模态向量维度: {multi_emb_1.shape}")print(f"图像向量维度: {img_emb_1.shape}")print(f"多模态向量示例 (前10个元素): {multi_emb_1[0][:10]}")print(f"图像向量示例 (前10个元素): {img_emb_1[0][:10]}")输出为:
=== 嵌入向量信息 ===多模态向量维度: torch.Size([1, 768])图像向量维度: torch.Size([1, 768])多模态向量示例 (前10个元素): tensor([ 0.0360, -0.0032, -0.0377, 0.0240, 0.0140, 0.0340, 0.0148, 0.0292, 0.0060, -0.0145])图像向量示例 (前10个元素): tensor([ 0.0407, -0.0606, -0.0037, 0.0073, 0.0305, 0.0318, 0.0132, 0.0442, -0.0380, -0.0270])3. 对比实验
我们可以尝试把 datawhale开源组织的logo 替换为 blue whale,看看结果有什么不同。
with torch.no_grad(): text_emb = model.encode(text="blue whale") img_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png") multi_emb_1 = model.encode(image="../../data/C3/imgs/datawhale01.png", text="blue whale") img_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png") multi_emb_2 = model.encode(image="../../data/C3/imgs/datawhale02.png", text="blue whale")# 计算相似度sim_1 = img_emb_1 @ img_emb_2.Tsim_2 = img_emb_1 @ multi_emb_1.Tsim_3 = text_emb @ multi_emb_1.Tsim_4 = multi_emb_1 @ multi_emb_2.Tprint("=== 相似度计算结果 ===")print(f"纯图像 vs 纯图像: {sim_1}")print(f"图文结合1 vs 纯图像: {sim_2}")print(f"图文结合1 vs 纯文本: {sim_3}")print(f"图文结合1 vs 图文结合2: {sim_4}")输出为:
=== 相似度计算结果 ===纯图像 vs 纯图像: tensor([[0.8318]])图文结合1 vs 纯图像: tensor([[0.9218]])图文结合1 vs 纯文本: tensor([[0.7572]])图文结合1 vs 图文结合2: tensor([[0.8719]])# 向量信息分析print("\n=== 嵌入向量信息 ===")print(f"多模态向量维度: {multi_emb_1.shape}")print(f"图像向量维度: {img_emb_1.shape}")print(f"多模态向量示例 (前10个元素): {multi_emb_1[0][:10]}")print(f"图像向量示例 (前10个元素): {img_emb_1[0][:10]}")输出为:
=== 嵌入向量信息 ===多模态向量维度: torch.Size([1, 768])图像向量维度: torch.Size([1, 768])多模态向量示例 (前10个元素): tensor([ 0.0277, -0.0514, -0.0233, 0.0135, 0.0278, 0.0198, 0.0089, 0.0285, -0.0357, -0.0242])图像向量示例 (前10个元素): tensor([ 0.0407, -0.0606, -0.0037, 0.0073, 0.0305, 0.0318, 0.0132, 0.0442, -0.0380, -0.0270])我们来对比一下两组输出:
3.1 结果对比
| 相似度 | 文本=“datawhale开源组织的logo” | 文本=“blue whale” | 变化 |
|---|---|---|---|
| sim₁ 纯图像 vs 纯图像 | 0.8318 | 0.8318 | → 不变 |
| sim₂ 图文结合1 vs 纯图像 | 0.8291 | 0.9218 | ↑ 上升 |
| sim₃ 图文结合1 vs 纯文本 | 0.7627 | 0.7572 | ↓ 略降 |
| sim₄ 图文结合1 vs 图文结合2 | 0.9058 | 0.8719 | ↓ 下降 |
3.2 逐一分析
3.2.1 sim₁ — 不变(0.8318)
两张图片没有换过,纯图像向量只取决于图片本身,与文本无关,所以结果完全一致 ✅
3.2.2 sim₂ — 显著上升(0.8291 → 0.9218)
图文结合1 = 图片 datawhale01 + 文本描述纯图像 = 图片 datawhale01 的纯图像向量两个都是基于同一张图片 datawhale01 的向量,只是多模态版本额外融合了文本信息。
- 「datawhale开源组织的logo」:这个描述比较抽象——它说的是”一个组织的 logo”,而不是图片里的视觉内容。模型需要去理解”logo”是一种抽象标识,与图片里具体画的是什么(一头鲸鱼)存在语义差距。
- 「blue whale」:这个描述直接对应了图片的视觉内容——Datawhale 的 logo 正是一头蓝色的鲸鱼!文本和图片在语义上高度一致。
当文本描述精确匹配图片内容时,融合向量 multi_emb_1 会更靠近纯图像向量,所以 sim₂ 大幅上升。
3.2.3 sim₃ — 略微下降(0.7627 → 0.7572)
图文结合1 = 图片 datawhale01 + 文本描述纯文本 = 文本描述的纯文本向量(不含图片信息)- 两种文本下,融合向量都包含了图片信息,而纯文本只是文本,两者天然有差异。
- 当文本改成「blue whale」后,融合向量被图片内容**“拉”得更偏向图像侧**,反而与纯文本向量的距离稍微拉远了,所以 sim₃ 小幅下降。
3.2.4 sim₄ — 下降(0.9058 → 0.8719)
图文结合1 = datawhale01 + 文本图文结合2 = datawhale02 + 文本两张图都是 Datawhale 的 logo,但 datawhale01 和 datawhale02 是不同版本的图片(可能颜色、构图、文字不同)。
- 「datawhale开源组织的logo」:文本强调了”同一个组织”的共同身份,相当于给两个融合向量加上了共同的主题锚点,让它们在高维空间里靠得更近。
- 「blue whale」:这是一个通用的视觉描述词,不再强调”同属一个组织”,反而突出了两个融合向量之间图片本身的差异(不同形象的鲸鱼 logo),所以相似度降低了。
3.3 一句话总结
文本描述越贴合图片的实际视觉内容,融合向量的图像成分就越突出,使得图像与融合向量的相似度(sim₂)更高;反之,文本越强调图片之间的共同语义标签,不同图片的融合向量之间就越相似(sim₄)。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!