CH3_01_bge_visualized

3264 字
16 分钟
CH3_01_bge_visualized
import torch
from visual_bge.visual_bge.modeling import Visualized_BGE
  • visual_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 模型类似的架构。

flowchart TB subgraph Text["📝 文本分支"] T1[输入文本] --> T2[bge_embeddings] T2 --> T3[bge_encoder] T3 --> T4[bge_pooler<br>768维] end subgraph Image["🖼️ 图像分支"] I1[输入图片] --> I2[PatchEmbed] I2 --> I3[12层 EVAViT] I3 --> I4[LayerNorm] I4 --> I5[image_head<br>输出 512维] end %% 分别投影 T4 --> text_proj[text_proj<br>Linear 768→768] I5 --> visual_proj[visual_proj<br>Linear 512→768] text_proj --> Out[输出向量<br>768维统一空间] visual_proj --> Out

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 包含:

子模块组件说明
attentionBertSdpaSelfAttention自注意力 (Scaled Dot-Product Attention)
query/Key/Value: Linear(768→768)Q、K、V 线性投影
BertSelfOutput残差连接 + LayerNorm + Dropout
intermediateLinear(768→3072) + GELU前馈网络放大 4 倍
outputLinear(3072→768)缩回 768 维

作用:这 12 层 Transformer 对文本进行上下文建模,每个 token 都能”看到”句子中的其他 token。

这里可以看到,BGE 模型的隐藏层架构与 BERT 是一致的,都是第 0 层 embedding 层 + 12 层 Transformer 层。


3️⃣ bge_pooler — 池化层#

BertPooler(
dense: Linear(768768)
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_embedConv2d(3→768, kernel=16×16, stride=16×16) 将图片切成非重叠 patch,每个 patch → 768 维向量
pos_dropDropout(p=0.0) 占位,实际未使用
ropeVisionRotaryEmbeddingFast()旋转位置编码(RoPE),比传统绝对位置编码效果更好
blocks12 层 Block(EVAViT),每层包含:
├─ norm1LayerNorm(768)
├─ attnAttention — 自注意力
│ ├─ q/k/v_projLinear(768→768, bias=False)
│ ├─ inner_attn_lnLayerNorm(768) — 注意力内部归一化
│ ├─ projLinear(768→768) 输出投影
│ └─ rope注意力内的 RoPE
├─ drop_pathIdentity() — 随机深度,此处未生效
├─ norm2LayerNorm(768)
└─ mlpSwiGLU — 比传统 FFN 更先进的 MLP
│ ├─ w1/w2Linear(768→2048) — 两个门控分支
│ ├─ actSiLU() — 激活函数
│ ├─ ffn_lnLayerNorm(2048)
│ └─ w3Linear(2048→768) 输出
normLayerNorm(768) — 最后归一化
headLinear(768→512) — 映射到 512 维视觉特征
patch_dropoutPatchDropout() — 训练时随机丢弃 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.T
sim_2 = img_emb_1 @ multi_emb_1.T
sim_3 = text_emb @ multi_emb_1.T
sim_4 = multi_emb_1 @ multi_emb_2.T

2. 相似度的数学计算#

代码中的 @ 运算符是 矩阵乘法(即点积 / 内积)。在 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 维的向量 a\mathbf{a}b\mathbf{b}

a=[a1,a2,...,a768],b=[b1,b2,...,b768]\mathbf{a} = [a_1, a_2, ..., a_{768}], \quad \mathbf{b} = [b_1, b_2, ..., b_{768}]

它们的点积(内积)定义为:

sim(a,b)=ab=i=1768ai×bi\text{sim}(\mathbf{a}, \mathbf{b}) = \mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{768} a_i \times b_i

用矩阵乘法的方式写就是:

a@bT=[a1,a2,...,a768][b1b2b768]=a1b1+a2b2++a768b768\mathbf{a} @ \mathbf{b}^T = [a_1, a_2, ..., a_{768}] \cdot \begin{bmatrix} b_1 \\ b_2 \\ \vdots \\ b_{768} \end{bmatrix} = a_1b_1 + a_2b_2 + \cdots + a_{768}b_{768}

2.2 向量点积的几何意义#

点积在几何上的含义是:

ab=abcosθ\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\| \|\mathbf{b}\| \cos\theta

其中:

  • a\|\mathbf{a}\| 是向量 a\mathbf{a}长度(模)
  • b\|\mathbf{b}\| 是向量 b\mathbf{b}长度
  • θ\theta 是两向量之间的夹角

如果两个向量方向一致(夹角接近 00^\circ),cosθ1\cos\theta \approx 1,点积最大 → 相似度高。

如果两个向量方向相反(夹角接近 180180^\circ),cosθ1\cos\theta \approx -1,点积最小 → 相似度低。

如果两个向量垂直(夹角 9090^\circ),cosθ=0\cos\theta = 0,点积为 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 余弦相似度#

你可能会想:为什么不用更常见的余弦相似度

cosθ=abab\cos\theta = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|}

因为 Visualized_BGE 输出的向量已经做过归一化(L2 normalize),即 a=b=1\|\mathbf{a}\| = \|\mathbf{b}\| = 1,所以:

ab=1×1×cosθ=cosθ\mathbf{a} \cdot \mathbf{b} = 1 \times 1 \times \cos\theta = \cos\theta

点积 = 余弦相似度,二者等价。所以代码中直接算点积就够了,不需要再除以模长。

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.T
sim_2 = img_emb_1 @ multi_emb_1.T
sim_3 = text_emb @ multi_emb_1.T
sim_4 = multi_emb_1 @ multi_emb_2.T
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.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.83180.8318→ 不变
sim₂ 图文结合1 vs 纯图像0.82910.9218↑ 上升
sim₃ 图文结合1 vs 纯文本0.76270.7572↓ 略降
sim₄ 图文结合1 vs 图文结合20.90580.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,但 datawhale01datawhale02不同版本的图片(可能颜色、构图、文字不同)。

  • 「datawhale开源组织的logo」:文本强调了”同一个组织”的共同身份,相当于给两个融合向量加上了共同的主题锚点,让它们在高维空间里靠得更近。
  • 「blue whale」:这是一个通用的视觉描述词,不再强调”同属一个组织”,反而突出了两个融合向量之间图片本身的差异(不同形象的鲸鱼 logo),所以相似度降低了。

3.3 一句话总结#

文本描述越贴合图片的实际视觉内容,融合向量的图像成分就越突出,使得图像与融合向量的相似度(sim₂)更高;反之,文本越强调图片之间的共同语义标签,不同图片的融合向量之间就越相似(sim₄)。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CH3_01_bge_visualized
https://datawhalechina.github.io/all-in-rag/#/chapter1/03_get_start_rag
作者
HAC
发布于
2026-06-19
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
HAC
观之非易,行且克难
Greetings
欢迎来到我的博客!这里主要分享我的学习笔记与兴趣爱好。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
5
标签
13
总字数
79,889
运行时长
0
最后活动
0 天前

文章目录