Qwen3-VL-Embedding 的向量取值有何不同

作者:吴佳浩

撰稿时间:2026-2-1

最后更新:2026-2-3

测试模型: Qwen3-VL-Embedding-8B

前言

为什么这个话题重要?

理解不同 Embedding 模型的向量提取机制,对于以下场景至关重要:

  • 构建高性能的语义搜索系统
  • 设计多模态 RAG(检索增强生成)应用
  • 选择合适的 Embedding 模型以优化特定任务
  • 正确处理模型输出以获得最佳检索效果

通过本文的分析,你将清楚地了解到:Qwen3-VL-Embedding 的向量提取方式是其 Decoder-only 架构的必然选择,而非一个可以随意替换的设计决策。这种理解将帮助你在实际应用中做出更明智的技术选型和实现决策。

常规 Embedding 模型的向量提取有两种主流策略:

  • CLS token (第一个向量): 简单快速,适合大多数场景
  • Mean Pooling (平均池化): 对长文本/多模态内容更稳定

但是,当我最近深入研究 Qwen3-VL-Embedding 的官方实现代码时,发现了一个令人意外的事实:它既不使用 CLS Token,也不使用 Mean Pooling

这个发现促使我重新审视了不同 Embedding 模型在向量提取策略上的根本差异。事实证明,Qwen3-VL-Embedding 采用的是一种完全不同的方法——Last Token Pooling(最后有效 token 池化),这种选择并非偶然,而是由其底层架构特性所决定的。

简介

本文将通过对比分析,深入探讨常规 Embedding 模型(如 BERT、Sentence-BERT)与 Qwen3-VL-Embedding 在向量提取机制上的本质区别。我们将从以下几个维度展开讨论:

核心议题:

  1. 架构差异:Encoder-only vs Decoder-only 架构如何影响向量提取策略
  2. 注意力机制:双向注意力与因果注意力的根本区别
  3. Pooling 策略:为什么 Qwen3-VL 选择 Last Token 而非传统的 CLS Token
  4. 代码实现:通过官方源码解析具体的向量提取过程
  5. 应用场景:不同策略在实际任务中的适用性分析

架构对比流程图

图表 1:常规 Embedding 模型 (BERT-like 架构)

flowchart TB
    subgraph title1["BERT-like Embedding 模型 (双向编码器)"]
        A1["输入文本"] --> B1["Tokenization"]
        B1 --> C1["添加特殊标记<br/>(CLS) text (SEP)"]
        C1 --> D1["Bi-directional Encoder<br/>双向注意力"]
        D1 --> E1{"Pooling 策略"}
        E1 -->|最常用| F1["CLS Token<br/>取 position 0"]
        E1 -->|备选| F2["Mean Pooling<br/>平均所有有效 token"]
        E1 -->|少用| F3["Max Pooling"]
        F1 --> G1["L2 归一化"]
        F2 --> G1
        F3 --> G1
        G1 --> H1["最终向量"]
    end

    style title1 fill:#f0f9ff,stroke:#1890ff,stroke-width:2px,stroke-dasharray: 5 5
    style F1 fill:#e6f7ff,stroke:#1890ff,stroke-width:2px
    style E1 fill:#fafafa,stroke:#d9d9d9
    style D1 fill:#f9f0ff,stroke:#722ed1

图表 2:Qwen3-VL-Embedding 模型 (Decoder-only 架构)

flowchart TB
    subgraph title2["Qwen3-VL-Embedding (因果解码器)"]
        A2["多模态输入<br/>文本/图像/视频"] --> B2["Vision Encoder<br/>处理图像/视频"]
        A2 --> B3["Tokenization<br/>处理文本"]
        B2 --> C2["模态融合"]
        B3 --> C2
        C2 --> D2["Causal Decoder<br/>单向注意力"]
        D2 --> E2["Last Token Pooling<br/>取最后有效 token"]
        E2 --> F4["提取 hidden_state<br/>at last position"]
        F4 --> G2["L2 归一化"]
        G2 --> H2["最终向量"]
    end

    style title2 fill:#fff7e6,stroke:#fa8c16,stroke-width:2px,stroke-dasharray: 5 5
    style E2 fill:#fff7e6,stroke:#fa8c16,stroke-width:2px
    style D2 fill:#fff0f6,stroke:#eb2f96
    style C2 fill:#f6ffed,stroke:#52c41a

核心区别对比表

维度常规 Embedding 模型
(BERT/Sentence-BERT)
Qwen3-VL-Embedding
架构类型Encoder-only (双向)Decoder-only (单向)
注意力机制Bi-directional Attention
每个token看到全局
Causal Attention
只看左侧context
特殊token[CLS] 在开头无专用CLS token
默认PoolingCLS Token (position 0)Last Token (最后有效位置)
支持模态纯文本 (部分支持图像)文本 + 图像 + 视频
向量维度通常 384/768/1024根据模型配置

详细机制解析

1. 常规 Embedding 模型:CLS Token Pooling

sequenceDiagram
    participant Input as 输入文本
    participant Tokenizer
    participant Encoder as Bi-directional<br/>Encoder
    participant CLS as [CLS] Token
    participant Output as 输出向量

    Input->>Tokenizer: "How are you?"
    Tokenizer->>Encoder: [CLS] How are you [SEP]
    Note over Encoder: 每个token都能看到<br/>全部其他tokens
    Encoder->>CLS: 汇聚全局语义到[CLS]
    CLS->>Output: 提取 position 0<br/>的 hidden state
    Output->>Output: 归一化

代码示例:

# BERT-style embedding
def get_bert_embedding(text, model, tokenizer):
    # 添加 [CLS] 和 [SEP]
    inputs = tokenizer(text, return_tensors='pt')
    # input_ids: [CLS, token1, token2, ..., SEP]
    
    outputs = model(**inputs)
    # 提取 [CLS] token (position 0)
    cls_embedding = outputs.last_hidden_state[:, 0, :]  # 第一个位置
    
    # 归一化
    embedding = F.normalize(cls_embedding, p=2, dim=-1)
    return embedding

为什么用 CLS Token?

  • BERT 预训练时专门优化 [CLS] 用于句子级任务
  • 双向注意力让 [CLS] 能"看到"整个句子
  • 简单高效,一个位置就能代表全局语义

2. Qwen3-VL-Embedding:Last Token Pooling

sequenceDiagram
    participant Input as 多模态输入
    participant Processor as Vision+Text<br/>Processor
    participant Decoder as Causal<br/>Decoder
    participant Last as Last Valid<br/>Token
    participant Output as 输出向量

    Input->>Processor: 文本 + 图像 + 视频
    Processor->>Decoder: token1, token2, ..., tokenN
    Note over Decoder: 单向注意力<br/>每个token只看左侧
    Decoder->>Last: tokenN 看到了<br/>所有前面的信息
    Last->>Output: 提取最后有效token<br/>的 hidden state
    Output->>Output: L2 归一化

官方代码实现:

@staticmethod
def _pooling_last(hidden_state: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
    """
    从 hidden_state 中提取每个样本最后一个有效 token 的向量
    
    Args:
        hidden_state: [batch_size, seq_len, hidden_dim]
        attention_mask: [batch_size, seq_len]  # 1=有效token, 0=padding
    
    Returns:
        [batch_size, hidden_dim]  # 每个样本的 embedding
    """
    # 步骤1: 翻转 attention_mask (从右往左看)
    flipped_tensor = attention_mask.flip(dims=[1])
    # 例: [1,1,1,1,0,0] → [0,0,1,1,1,1]
    
    # 步骤2: 找到第一个1的位置 (翻转后)
    last_one_positions = flipped_tensor.argmax(dim=1)
    # 例: [0,0,1,1,1,1] → argmax=2 (从右数第3个是最后一个1)
    
    # 步骤3: 转换回原始索引
    col = attention_mask.shape[1] - last_one_positions - 1
    # 例: 6 - 2 - 1 = 3 (实际最后一个有效token在位置3)
    
    # 步骤4: 提取对应位置的 hidden state
    row = torch.arange(hidden_state.shape[0], device=hidden_state.device)
    return hidden_state[row, col]  # 关键:最后一个有效token

为什么用 Last Token?

  • 因果语言模型的特性:最后一个token "看到" 了所有历史信息
  • 与 GPT-style 预训练对齐(预测下一个token)
  • 自然处理变长输入(通过 attention_mask 自动定位)

可视化:注意力流向对比

graph LR
    subgraph "BERT: Bi-directional Attention"
        CLS1[CLS] -.双向.-> T1[Token1]
        CLS1 -.双向.-> T2[Token2]
        CLS1 -.双向.-> T3[Token3]
        T1 -.双向.-> T2
        T1 -.双向.-> T3
        T2 -.双向.-> T3
        CLS1 ==> E1[Embedding]
        style CLS1 fill:#90EE90
    end
    
    subgraph "Qwen3-VL: Causal Attention"
        T4[Token1] --> T5[Token2]
        T5 --> T6[Token3]
        T6 --> T7[TokenN]
        T4 -.看不到.-> T5
        T4 -.看不到.-> T6
        T5 -.看不到.-> T6
        T7 ==> E2[Embedding]
        style T7 fill:#FFB6C1
    end

实际使用示例对比

场景1:纯文本检索

常规模型(Sentence-BERT)
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')

# 编码文本
text = "What is machine learning?"
embedding = model.encode(text)  # 自动使用 CLS token
print(embedding.shape)  # (384,)
Qwen3-VL-Embedding
from qwen3_vl_embedder import Qwen3VLEmbedder

embedder = Qwen3VLEmbedder('Qwen/Qwen3-VL-Embedding')

# 编码文本
inputs = [{'text': "What is machine learning?"}]
embedding = embedder.process(inputs)  # 自动使用 Last Token
print(embedding.shape)  # (1, hidden_dim)

场景2:多模态检索

常规模型
# 大多数 BERT-style 模型不支持多模态
# 需要使用 CLIP 等专门的多模态模型

from transformers import CLIPModel, CLIPProcessor

model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')

# 分别编码文本和图像
text_inputs = processor(text=["a photo of a cat"], return_tensors="pt")
text_embedding = model.get_text_features(**text_inputs)

image_inputs = processor(images=image, return_tensors="pt")
image_embedding = model.get_image_features(**image_inputs)
Qwen3-VL-Embedding
# 原生支持文本+图像+视频混合输入
embedder = Qwen3VLEmbedder('Qwen/Qwen3-VL-Embedding')

# 单次调用处理多模态
inputs = [{
    'text': "Describe this image",
    'image': "/path/to/cat.jpg",
    'instruction': "Generate a comprehensive embedding"
}]

embedding = embedder.process(inputs)  # 融合所有模态信息

内部处理流程对比

flowchart TD
    subgraph "常规 Embedding 处理流程"
        A1[输入文本] --> B1[Tokenization]
        B1 --> C1["添加 [CLS] + text + [SEP]"]
        C1 --> D1[Padding to max_length]
        D1 --> E1[Encoder Forward]
        E1 --> F1["提取 hidden_state[:, 0, :]"]
        F1 --> G1[Normalize]
        G1 --> H1[返回向量]
    end
    
    subgraph "Qwen3-VL-Embedding 处理流程"
        A2[多模态输入] --> B2{输入类型}
        B2 -->|图像| C2[Vision Encoder]
        B2 -->|视频| C3[Video Processor<br/>采样帧]
        B2 -->|文本| C4[Tokenization]
        
        C2 --> D2[融合模态tokens]
        C3 --> D2
        C4 --> D2
        
        D2 --> E2[Apply chat template<br/>添加系统prompt]
        E2 --> F2[Causal Decoder Forward]
        F2 --> G2["找到最后有效token位置<br/>(通过 attention_mask)"]
        G2 --> H2["提取 hidden_state[row, col]"]
        H2 --> I2[L2 Normalize]
        I2 --> J2[返回向量]
    end
    
    style F1 fill:#90EE90
    style H2 fill:#FFB6C1

性能特点对比

特性常规 EmbeddingQwen3-VL-Embedding
处理速度快速中等(多模态处理更复杂)
内存占用中高(需处理视觉数据)
适用场景纯文本检索/分类多模态检索/RAG/跨模态搜索
上下文长度通常 512 tokens默认 8192 tokens
灵活性单一模态文本+图像+视频混合

总结:关键差异

1. Pooling 位置不同

# 常规模型
embedding = hidden_state[:, 0, :]  # 第一个位置 (CLS)

# Qwen3-VL
embedding = hidden_state[row, last_valid_col]  # 最后有效位置

2. 注意力机制不同

graph LR
    A[CLS Token<br/>双向注意力] -->|同时看到| B[所有tokens]
    C[Last Token<br/>因果注意力] -->|只看到| D[左侧所有tokens]
    
    style A fill:#90EE90
    style C fill:#FFB6C1

3. 设计理念不同

方面常规模型Qwen3-VL
目标句子级表示学习多模态理解 + 生成
预训练Masked LMCausal LM
架构选择专门为编码设计适配生成式模型

实际应用建议

何时使用常规 Embedding?

  • 纯文本检索/相似度匹配
  • 需要极致速度
  • 简单的语义搜索任务
  • 资源受限环境

何时使用 Qwen3-VL-Embedding?

  • 多模态检索(文本+图片+视频)
  • 需要理解图像内容
  • 跨模态搜索(用文本搜图片)
  • 复杂的 RAG 系统
  • 长文本场景(8K+ tokens)

代码实现:完整对比

# ============ 常规 Embedding 模型 ============
from sentence_transformers import SentenceTransformer
import numpy as np

class RegularEmbedder:
    def __init__(self, model_name='all-MiniLM-L6-v2'):
        self.model = SentenceTransformer(model_name)
    
    def encode(self, texts):
        # 内部使用 CLS token pooling
        return self.model.encode(texts, normalize_embeddings=True)

# ============ Qwen3-VL-Embedding ============
from qwen3_vl_embedder import Qwen3VLEmbedder

class MultimodalEmbedder:
    def __init__(self, model_path):
        self.embedder = Qwen3VLEmbedder(model_path)
    
    def encode(self, inputs):
        # 内部使用 Last Token pooling
        return self.embedder.process(inputs, normalize=True)

# ============ 使用对比 ============
# 纯文本
text_embedder = RegularEmbedder()
text_emb = text_embedder.encode(["Hello world"])

# 多模态
mm_embedder = MultimodalEmbedder('Qwen/Qwen3-VL-Embedding')
mm_emb = mm_embedder.encode([{
    'text': "Hello world",
    'image': "cat.jpg"  # 常规模型做不到
}])

最终结论

问题答案
核心区别是什么?常规用 CLS Token (首位),Qwen3-VL 用 Last Token (末位)
为什么不同?架构不同:Encoder(双向) vs Decoder(单向)
哪个更好?取决于任务:纯文本用常规,多模态用 Qwen3-VL
可以互换吗?不建议,各自有优化场景

关键记忆点

  • BERT-style: 头部取值(CLS token 在开头汇总全局)
  • GPT-style: 尾部取值(Last token 累积所有历史)

这是我最经在使用的模型分析,如果你也感兴趣的话可以看一下官方提供的脚本希望我的分享对你有帮助

参考官方的脚本地址:

Qwen/Qwen3-VL-Embedding-8B

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com