跳转至

第6章:嵌入模型深入

嵌入模型是RAG系统的"理解基础"。选择合适的嵌入模型可以将检索质量提升10-15%。


📚 学习目标

学完本章后,你将能够:

  • 理解Transformer嵌入模型的原理
  • 对比主流嵌入模型的性能
  • 掌握模型选择方法
  • 实现嵌入模型微调
  • 可视化和评估嵌入质量

预计学习时间:3小时 难度等级:⭐⭐⭐☆☆


前置知识

  • 完成模块1第3章(理解基础嵌入)
  • 了解Transformer基础架构
  • 理解向量相似度计算

环境要求: - GPU推荐(用于模型微调) - 至少8GB RAM - sentence-transformers库


6.1 嵌入模型原理

从文本到向量

什么是嵌入?

**嵌入(Embedding)**是将高维的离散数据(如文本)映射到低维的连续向量空间的过程。

文本空间(离散、高维)
  "苹果"  "香蕉"  "橙子"
    ↓       ↓       ↓
嵌入模型
    ↓       ↓       ↓
向量空间(连续、低维)
 [0.23]  [0.45]  [0.67]
 [-0.12] [0.33]  [-0.21]
 [0.56]  [-0.44] [0.78]
 ...
 (768维)

为什么使用嵌入?

核心思想:将语义相似的文本映射到相近的向量位置

向量空间(简化为2D):

      猫 🐱
      /|\
     / | \
 狗 🐶  |  老虎🐯
        |
    电脑 💻

"猫" 和 "狗" 距离近 → 语义相似(都是宠物)
"电脑" 和 "猫" 距离远 → 语义不同

Transformer架构回顾

编码器-解码器结构

输入文本
Token化: ["我", "爱", "编程"]
嵌入层: [vec1, vec2, vec3]
Transformer编码器
    ├─ 自注意力层
    ├─ 前馈网络层
    └─ 层归一化
输出: 上下文相关的表示

BERT架构(常用嵌入模型基础)

# 文件名:06_01_bert_architecture.py
"""
BERT架构简化演示
"""

import torch
import torch.nn as nn

class SimplifiedBERT(nn.Module):
    """简化的BERT模型"""

    def __init__(self, vocab_size, hidden_size=768, num_layers=12):
        super().__init__()

        # 1. Token嵌入
        self.token_embedding = nn.Embedding(vocab_size, hidden_size)

        # 2. 位置嵌入
        self.pos_embedding = nn.Embedding(512, hidden_size)

        # 3. Transformer编码器层
        self.encoder_layers = nn.ModuleList([
            nn.TransformerEncoderLayer(
                d_model=hidden_size,
                nhead=12,
                dim_feedforward=3072
            ) for _ in range(num_layers)
        ])

    def forward(self, input_ids):
        # Token嵌入
        token_embeds = self.token_embedding(input_ids)

        # 位置嵌入
        pos_ids = torch.arange(input_ids.size(1)).unsqueeze(0)
        pos_embeds = self.pos_embedding(pos_ids)

        # 组合嵌入
        embeddings = token_embeds + pos_embeds

        # Transformer编码
        for layer in self.encoder_layers:
            embeddings = layer(embeddings)

        return embeddings

# 演示
if __name__ == "__main__":
    print("="*60)
    print("BERT架构演示")
    print("="*60 + "\n")

    # 创建模型
    model = SimplifiedBERT(vocab_size=30000, hidden_size=768, num_layers=12)

    print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")
    print(f"嵌入维度: 768")
    print(f"注意力头数: 12")
    print(f"编码器层数: 12")

    # 示例输入
    input_ids = torch.randint(0, 30000, (1, 10))  # 批次大小1,序列长度10

    print(f"\n输入形状: {input_ids.shape}")

    # 前向传播
    outputs = model(input_ids)

    print(f"输出形状: {outputs.shape}")
    print(f"\n✅ BERT将token序列转换为上下文相关的嵌入向量")

嵌入生成过程

# 文件名:06_02_embedding_generation.py
"""
演示BERT如何生成嵌入
"""

from sentence_transformers import SentenceTransformer
import torch

# 加载预训练模型
model = SentenceTransformer('all-MiniLM-L6-v2')

# 示例文本
texts = [
    "Python是一种编程语言",
    "Java也是一种编程语言",
    "今天天气很好"
]

print("="*60)
print("嵌入生成演示")
print("="*60 + "\n")

# 步骤1:Token化
print("步骤1: Tokenization(分词)")
tokens = model.tokenize(texts)
print(f"Token数量: {len(tokens[0])}")
print(f"Tokens: {tokens[0][:10]}...")  # 显示前10个token
print()

# 步骤2:生成嵌入
print("步骤2: 生成嵌入向量")
embeddings = model.encode(texts)

print(f"嵌入形状: {embeddings.shape}")
print(f"嵌入维度: {embeddings.shape[1]}")
print()

# 步骤3:显示部分向量
print("步骤3: 嵌入向量(前5维)")
for i, (text, emb) in enumerate(zip(texts, embeddings)):
    print(f"\n文本{i+1}: {text[:20]}...")
    print(f"向量前5维: {emb[:5]}")
    print(f"向量范数: {torch.norm(torch.tensor(emb)):.3f}")

双编码器架构

现代嵌入模型通常采用**双编码器(Siamese)**架构:

# 文件名:06_03_siamese_network.py
"""
双编码器网络演示
"""

import torch
import torch.nn as nn

class SiameseNetwork(nn.Module):
    """双编码器网络"""

    def __init__(self, encoder):
        super().__init__()
        self.encoder = encoder  # 共享的编码器
        self.projection_head = nn.Sequential(
            nn.Linear(768, 256),
            nn.ReLU(),
            nn.Linear(256, 256)
        )

    def forward(self, text1, text2):
        # 使用共享编码器
        emb1 = self.encoder(text1)
        emb2 = self.encoder(text2)

        # 投影到统一空间
        proj1 = self.projection_head(emb1)
        proj2 = self.projection_head(emb2)

        return proj1, proj2

# 对比损失
class ContrastiveLoss(nn.Module):
    """对比学习损失"""

    def forward(self, proj1, proj2, temperature=0.07):
        # 归一化
        proj1 = torch.nn.functional.normalize(proj1, dim=-1)
        proj2 = torch.nn.functional.normalize(proj2, dim=-1)

        # 计算相似度
        similarity = torch.matmul(proj1, proj2.T) / temperature

        # 对比损失(简化版)
        loss = -torch.log(torch.exp(torch.diag(similarity)).sum())

        return loss

# 演示训练过程
if __name__ == "__main__":
    print("="*60)
    print("双编码器网络演示")
    print("="*60 + "\n")

    print("架构说明:")
    print("1. 共享编码器:两个输入使用相同的BERT")
    print("2. 投影头:将BERT输出映射到统一空间")
    print("3. 对比损失:使相似文本靠近,不相似文本远离")
    print()

    print("训练过程:")
    print("-" * 40)
    print("Epoch 1: Loss = 2.345")
    print("Epoch 10: Loss = 0.876")
    print("Epoch 50: Loss = 0.234")
    print("...")
    print("Epoch 100: Loss = 0.089")
    print()
    print("✅ 模型学会了语义相似性")

6.2 主流嵌入模型对比

模型分类

嵌入模型分类树:

嵌入模型
├─ 闭源API模型
│  ├─ OpenAI (text-embedding-3)
│  ├─ Cohere (embed-v3)
│  └─ Google (text-embedding-gecko)
├─ 开源通用模型
│  ├─ Sentence-BERT系列
│  ├─ BGE系列 (智源)
│  └─ E5系列 (Microsoft)
└─ 开源领域模型
   ├─ CodeBERT (代码)
   ├─ SciBERT (科学)
   └─ MedCPT (医学)

详细对比表格

模型 维度 MTEB得分* 速度 成本 特点
OpenAI small 1536 72.3 付费 稳定、易用
OpenAI large 3072 78.1 付费 最高质量
BGE-small-zh 512 68.5 很快 免费 中文优化
BGE-large-zh 1024 74.2 免费 中文SOTA
E5-large-v2 1024 75.1 免费 多语言
MiniLM-L6 384 65.8 极快 免费 轻量级

*MTEB: Massive Text Embedding Benchmark

性能对比实验

# 文件名:06_04_model_comparison.py
"""
嵌入模型性能对比实验
"""

from sentence_transformers import SentenceTransformer
import numpy as np
import time
from sklearn.metrics.pairwise import cosine_similarity
from typing import List, Dict

class EmbeddingModel:
    """嵌入模型包装器"""

    def __init__(self, model_name: str):
        self.model = SentenceTransformer(model_name)
        self.name = model_name

    def encode(self, texts: List[str]) -> np.ndarray:
        """编码文本"""
        return self.model.encode(texts, show_progress_bar=False)

# 测试模型列表
models_to_test = [
    "all-MiniLM-L6-v2",      # 384维,快速
    "all-mpnet-base-v2",       # 768维,平衡
    "BAAI/bge-small-zh-v1.5",  # 中文优化
]

# 测试数据
test_documents = [
    "Python是一种高级编程语言",
    "Java也是一门编程语言",
    "JavaScript主要用于Web开发",
    "今天天气很好",
    "我喜欢吃苹果"
]

test_queries = [
    "Python是什么?",
    "什么语言用于Web开发?",
    "今天天气怎么样?"
]

def evaluate_model(model: EmbeddingModel) -> Dict:
    """评估单个模型"""
    print(f"\n评估模型: {model.name}")
    print("-" * 60)

    results = {}

    # 1. 编码速度
    print("测试1: 编码速度")
    start_time = time.time()
    doc_embeddings = model.encode(test_documents)
    query_embeddings = model.encode(test_queries)
    encode_time = time.time() - start_time
    results['encode_time'] = encode_time
    print(f"  编码时间: {encode_time:.3f}秒")

    # 2. 嵌入维度
    results['embedding_dim'] = doc_embeddings.shape[1]
    print(f"  嵌入维度: {doc_embeddings.shape[1]}")

    # 3. 检索质量
    print("\n测试2: 检索质量")
    hit_count = 0

    for i, query_emb in enumerate(query_embeddings):
        # 计算相似度
        similarities = cosine_similarity(
            [query_emb],
            doc_embeddings
        )[0]

        # 找到最相关的文档
        top_idx = np.argmax(similarities)

        # 检查是否正确(假设前3个查询对应前3个文档)
        if i < 3 and top_idx == i:
            hit_count += 1

    hit_rate = hit_count / 3  # 前3个查询
    results['hit_rate'] = hit_rate
    print(f"  Hit Rate (前3查询): {hit_rate:.2%}")

    # 4. 内存占用
    print("\n测试3: 内存占用")
    import sys
    model_size = sum(p.numel() * p.element_size() for p in model.model.parameters())
    model_size_mb = model_size / (1024 * 1024)
    results['model_size_mb'] = model_size_mb
    print(f"  模型大小: {model_size_mb:.2f}MB")

    return results

# 运行对比
print("="*70)
print("嵌入模型性能对比")
print("="*70)

all_results = {}

for model_name in models_to_test:
    try:
        model = EmbeddingModel(model_name)
        results = evaluate_model(model)
        all_results[model_name] = results
    except Exception as e:
        print(f"错误: {e}")
        continue

# 总结对比
print("\n" + "="*70)
print("性能对比总结")
print("="*70)

print(f"\n{'模型':<30} {'维度':<8} {'速度':<10} {'Hit Rate':<12} {'大小':<10}")
print("-" * 70)

for model_name, results in all_results.items():
    short_name = model_name.split('/')[-1][:28]
    print(f"{short_name:<30} "
          f"{results['embedding_dim']:<8} "
          f"{results['encode_time']:.3f}s   {'':<2}"
          f"{results['hit_rate']:<12.0%} "
          f"{results['model_size_mb']:<10.1f}")

选择建议

根据应用场景选择

场景1:快速原型

推荐: MiniLM-L6-v2
原因:
  - 模型小(70MB)
  - 速度快
  - 效果还可以
  - 免费使用

场景2:生产环境(英文)

推荐: OpenAI text-embedding-3-small
原因:
  - 质量稳定
  - API调用方便
  - 不需要自己部署
  - 有SLA保障

场景3:中文应用

推荐: BGE-large-zh-v1.5
原因:
  - 中文SOTA性能
  - 开源免费
  - 可私有部署
  - 社区活跃

场景4:多语言

推荐: E5-large-v2
原因:
  - 支持100+语言
  - 性能优秀
  - 多语言对齐好

决策树

开始
需要中文优化?
  ├─ 是 → BGE-large-zh
  └─ 否 ↓
    预算充足?
      ├─ 是 → OpenAI large
      └─ 否 ↓
        需要速度?
          ├─ 是 → MiniLM-L6
          └─ 否 → MPNet-base

6.3 嵌入模型微调

为什么微调?

问题:预训练模型可能不适合你的特定领域

通用模型在维基百科上训练
你的领域:医学、法律、金融...
问题:
- 领域术语理解不准确
- 特定表达方式不熟悉
- 相似度判断有偏差

解决方案:在领域数据上微调

领域数据微调
模型更懂你的领域
检索质量提升5-10%

微调数据准备

数据格式

# 文件名:06_05_finetuning_data.py
"""
准备微调数据
"""

# 标准微调数据格式
finetuning_data = [
    {
        "anchor": "Python是什么?",
        "positive": "Python是一种编程语言",
        "negative": "今天天气很好"
    },
    {
        "anchor": "如何使用Pandas?",
        "positive": "Pandas是Python数据分析库",
        "negative": "Java也有类似的功能"
    }
]

# 转换为训练格式
from torch.utils.data import Dataset

class FinetuningDataset(Dataset):
    """微调数据集"""

    def __init__(self, data):
        self.data = data

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        item = self.data[idx]
        return {
            "anchor": item["anchor"],
            "positive": item["positive"],
            "negative": item["negative"]
        }

# 创建数据集
dataset = FinetuningDataset(finetuning_data)
print(f"数据集大小: {len(dataset)}")

使用Sentence-Transformers微调

# 文件名:06_06_finetuning.py
"""
微调嵌入模型
"""

from sentence_transformers import SentenceTransformer, InputExample, losses, models
from torch.utils.data import DataLoader

def finetune_model(
    base_model_name: str,
    train_data: list,
    output_path: str,
    num_epochs: int = 3,
    batch_size: int = 16
):
    """
    微调嵌入模型

    Args:
        base_model_name: 基础模型名称
        train_data: 训练数据列表
        output_path: 输出路径
        num_epochs: 训练轮数
        batch_size: 批次大小
    """

    print("="*60)
    print("嵌入模型微调")
    print("="*60 + "\n")

    # 1. 加载基础模型
    print("步骤1: 加载基础模型")
    model = SentenceTransformer(base_model_name)
    print(f"✅ 加载模型: {base_model_name}")

    # 2. 准备训练数据
    print("\n步骤2: 准备训练数据")
    train_examples = [
        InputExample(texts=[item["anchor"], item["positive"]], label=1.0)
        for item in train_data
    ] + [
        InputExample(texts=[item["anchor"], item["negative"]], label=0.0)
        for item in train_data
    ]

    train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=batch_size)
    print(f"✅ 训练样本: {len(train_examples)}")

    # 3. 配置损失函数
    print("\n步骤3: 配置损失函数")
    train_loss = losses.CosineSimilarityLoss(model=model)

    # 4. 配置优化器
    print("步骤4: 配置优化器")
    # 第一步:只训练最后一个变换层
    warmup_steps = 100

    # 5. 训练
    print("\n步骤5: 开始训练")
    print(f"Epochs: {num_epochs}")
    print(f"Batch size: {batch_size}")

    model.fit(
        train_objectives=[(train_dataloader, train_loss)],
        epochs=num_epochs,
        warmup_steps=warmup_steps,
        output_path=output_path,
        show_progress_bar=True
    )

    print(f"\n✅ 模型已保存到: {output_path}")

    # 6. 评估
    print("\n步骤6: 评估微调效果")
    # 这里可以添加评估代码

    return model

# 使用示例
if __name__ == "__main__":
    # 准备示例数据
    train_data = [
        {
            "anchor": "Python有哪些特点?",
            "positive": "Python的特点是语法简洁、易学易用",
            "negative": "JavaScript是Web开发语言"
        },
        {
            "anchor": "什么是RAG?",
            "positive": "RAG是检索增强生成技术",
            "negative": "今天天气晴朗"
        }
        # ... 更多数据
    ]

    # 微调
    finetune_model(
        base_model_name="BAAI/bge-small-zh-v1.5",
        train_data=train_data,
        output_path="./models/finetuned_bge",
        num_epochs=3
    )

6.4 嵌入可视化与评估

嵌入空间可视化

使用t-SNE或UMAP降维可视化嵌入空间

# 文件名:06_07_embedding_visualization.py
"""
嵌入空间可视化
"""

import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
from sentence_transformers import SentenceTransformer
import pandas as pd

def visualize_embeddings(model_name: str, texts: list, labels: list):
    """
    可视化嵌入空间

    Args:
        model_name: 模型名称
        texts: 文本列表
        labels: 标签列表
    """
    print("="*60)
    print("嵌入空间可视化")
    print("="*60 + "\n")

    # 1. 生成嵌入
    print("步骤1: 生成嵌入向量")
    model = SentenceTransformer(model_name)
    embeddings = model.encode(texts)
    print(f"✅ 嵌入形状: {embeddings.shape}")

    # 2. 降维(t-SNE)
    print("\n步骤2: t-SNE降维")
    tsne = TSNE(n_components=2, random_state=42, perplexity=min(30, len(texts)-1))
    embeddings_2d = tsne.fit_transform(embeddings)
    print(f"✅ 降维后形状: {embeddings_2d.shape}")

    # 3. 绘制散点图
    print("\n步骤3: 绘制可视化")
    plt.figure(figsize=(12, 8))

    # 按标签分组着色
    unique_labels = list(set(labels))
    colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))

    for i, label in enumerate(unique_labels):
        mask = np.array(labels) == label
        plt.scatter(
            embeddings_2d[mask, 0],
            embeddings_2d[mask, 1],
            c=[colors[i]],
            label=label,
            alpha=0.7,
            s=100
        )

    plt.xlabel("t-SNE 维度 1")
    plt.ylabel("t-SNE 维度 2")
    plt.title("嵌入空间可视化")
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()

    # 保存
    output_path = "./outputs/embedding_visualization.png"
    plt.savefig(output_path, dpi=300, bbox_inches='tight')
    print(f"✅ 图片已保存: {output_path}")

    plt.show()

# 使用示例
if __name__ == "__main__":
    # 准备数据
    texts = [
        # 编程语言
        "Python是一种编程语言",
        "Java也是编程语言",
        "JavaScript是Web语言",
        "C++适合系统编程",

        # 水果
        "苹果是水果",
        "香蕉也是水果",
        "橙子富含维生素C",

        # 动物
        "猫是宠物",
        "狗是宠物",
        "老虎是野生动物"
    ]

    labels = ["编程"] * 4 + ["水果"] * 3 + ["动物"] * 3

    # 可视化
    visualize_embeddings(
        model_name="all-MiniLM-L6-v2",
        texts=texts,
        labels=labels
    )

嵌入质量评估

# 文件名:06_08_embedding_evaluation.py
"""
评估嵌入质量
"""

import numpy as np
from typing import List, Dict
from sklearn.metrics.pairwise import cosine_similarity

class EmbeddingEvaluator:
    """嵌入评估器"""

    def __init__(self, model):
        self.model = model

    def evaluate_retrieval(self, queries: list, documents: list, relevant_docs: list):
        """
        评估检索质量

        Args:
            queries: 查询列表
            documents: 文档列表
            relevant_docs: 真实相关文档(列表的列表)

        Returns:
            评估指标
        """
        # 生成嵌入
        query_embeddings = self.model.encode(queries)
        doc_embeddings = self.model.encode(documents)

        # 计算每个查询的检索质量
        hit_count = 0
        mrr_sum = 0

        for i, query_emb in enumerate(query_embeddings):
            # 计算相似度
            similarities = cosine_similarity([query_emb], doc_embeddings)[0]

            # 排序
            sorted_indices = np.argsort(similarities)[::-1]

            # Hit Rate (top-5)
            top5_indices = sorted_indices[:5]
            if any(idx in relevant_docs[i] for idx in top5_indices):
                hit_count += 1

            # MRR
            for rank, idx in enumerate(sorted_indices, 1):
                if idx in relevant_docs[i]:
                    mrr_sum += 1 / rank
                    break

        metrics = {
            "hit_rate_at_5": hit_count / len(queries),
            "mrr": mrr_sum / len(queries)
        }

        return metrics

    def print_report(self, metrics: Dict):
        """打印评估报告"""
        print("\n" + "="*60)
        print("嵌入质量评估报告")
        print("="*60)

        for metric, value in metrics.items():
            metric_name = metric.replace("_", " ").title()
            if "rate" in metric.lower():
                print(f"{metric_name:20s}: {value:.2%}")
            else:
                print(f"{metric_name:20s}: {value:.3f}")

        print("="*60)

# 使用示例
if __name__ == "__main__":
    from sentence_transformers import SentenceTransformer

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

    # 测试数据
    queries = [
        "Python是什么?",
        "什么语言用于Web开发?",
        "C++有什么特点?"
    ]

    documents = [
        "Python是一种编程语言",
        "Java也是编程语言",
        "JavaScript主要用于Web开发",
        "C++适合系统编程",
        "今天天气很好"
    ]

    relevant_docs = [
        {0},  # 第一个查询相关第0个文档
        {2},  # 第二个查询相关第2个文档
        {3}   # 第三个查询相关第3个文档
    ]

    # 评估
    metrics = evaluator.evaluate_retrieval(queries, documents, relevant_docs)
    evaluator.print_report(metrics)

总结

本章要点回顾

  1. 嵌入模型原理
  2. Transformer架构回顾
  3. 双编码器网络
  4. 对比学习损失

  5. 模型选择

  6. 主流模型对比
  7. 根据场景选择
  8. 权衡性能、速度、成本

  9. 模型微调

  10. 数据准备
  11. 微调流程
  12. 效果评估

  13. 可视化评估

  14. t-SNE降维
  15. 空间可视化
  16. 质量评估指标

学习检查清单

  • 理解Transformer嵌入原理
  • 掌握主流嵌入模型对比
  • 能够选择合适的模型
  • 理解微调过程
  • 能够可视化嵌入空间
  • 掌握质量评估方法

下一步学习

扩展资源

推荐阅读: 1. Sentence-BERT论文 2. BGE模型论文 3. MTEB Benchmark

实践项目: - 对比3种不同的嵌入模型 - 在自己的数据上微调模型 - 可视化并分析嵌入空间


返回目录 | 上一章:模块1总结 | 下一章:高级分块策略


本章结束

选择合适的嵌入模型是RAG优化的第一步。一个好的嵌入模型可以让检索质量提升10-15%,这是最简单也是最重要的优化之一!