Embedding 优化技巧
问题
如何优化 Embedding 的效果和效率?有哪些实用的优化策略?
答案
一、优化方向总览
二、效果优化
1. Embedding 微调
用业务数据微调通用 Embedding 模型,可显著提升领域效果:
from sentence_transformers import SentenceTransformer, losses
from torch.utils.data import DataLoader
# 加载基础模型
model = SentenceTransformer("BAAI/bge-base-zh-v1.5")
# 准备训练数据:(query, positive_doc, negative_doc)
train_examples = [
InputExample(texts=["如何退款", "退款流程说明", "Python 入门"]),
InputExample(texts=["账号被封", "封号申诉指南", "美食推荐"]),
]
# 使用三元组损失微调
train_dataloader = DataLoader(train_examples, batch_size=16)
train_loss = losses.TripletLoss(model)
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=3,
warmup_steps=100,
)
微调经验
- 数据量:几百到几千对训练数据即可见效
- 负采样:Hard Negative(语义相近但不相关的文档)比随机负样本效果好 3-5 倍
- 评测先行:微调前先建立评测集,量化效果提升
2. 查询指令前缀
BGE、E5 等模型支持在查询前加指令前缀以区分查询和文档:
# BGE 模型:查询加前缀,文档不加
query = "为这个句子生成表示以用于检索相关文章:如何退款"
doc = "退款流程说明..." # 文档不加前缀
# E5 模型
query = "query: 如何退款"
doc = "passage: 退款流程说明..."
3. Late Chunking
先对完整文档编码,再分块提取向量(保留跨块上下文):
三、效率优化
1. Matryoshka Embedding(维度裁剪)
Matryoshka Representation Learning 让向量的前 N 维就包含足够信息:
| 维度 | MTEB 平均分 | 存储 | 搜索速度 |
|---|---|---|---|
| 1536 | 62.3 | 基准 | 基准 |
| 768 | 61.5 | 50% | ~2x |
| 256 | 59.8 | 17% | ~6x |
适用模型
OpenAI text-embedding-3 系列、nomic-embed-text、Jina-v3 等支持此特性。
2. 向量量化
将 float32 向量量化为低精度格式,大幅减少存储和计算:
| 量化 | 精度 | 空间节省 | Recall 损失 |
|---|---|---|---|
| float32 | 32bit | 基准 | 0% |
| float16 | 16bit | 50% | < 0.1% |
| int8 | 8bit | 75% | 1-3% |
| binary | 1bit | 97% | 5-10% |
import numpy as np
def quantize_int8(vectors: np.ndarray) -> np.ndarray:
"""将 float32 向量量化为 int8"""
vmin, vmax = vectors.min(), vectors.max()
# 线性映射到 [-128, 127]
scale = (vmax - vmin) / 255
return ((vectors - vmin) / scale - 128).astype(np.int8)
3. 二值化(Binary Embedding)
将向量转为 0/1 布尔型,用汉明距离做粗筛:
def binarize(vector: np.ndarray) -> np.ndarray:
"""正值为1,负值为0"""
return (vector > 0).astype(np.uint8)
# 粗筛:二值汉明距离
# 精排:原始向量余弦相似度
四、工程优化
批量处理
# ❌ 逐条编码:N 次 API 调用
for text in texts:
embedding = model.encode(text)
# ✅ 批量编码:1 次调用
embeddings = model.encode(texts, batch_size=64)
缓存策略
import hashlib
from functools import lru_cache
# 内存缓存(适合重复查询)
@lru_cache(maxsize=10000)
def get_embedding(text: str) -> tuple:
return tuple(model.encode(text))
# Redis 缓存(适合持久化)
def get_embedding_cached(text: str, redis_client):
key = f"emb:{hashlib.md5(text.encode()).hexdigest()}"
cached = redis_client.get(key)
if cached:
return np.frombuffer(cached, dtype=np.float32)
emb = model.encode(text)
redis_client.set(key, emb.tobytes(), ex=86400) # 缓存 24h
return emb
常见面试问题
Q1: 如何在不换模型的情况下提升检索效果?
答案(按优先级排序):
- 查询改写:将用户查询展开为更明确的搜索语句
- 添加指令前缀:BGE/E5 模型使用 query/passage 前缀
- 调整分块策略:更大/更小的分块、增加重叠
- Embedding 微调:用业务数据微调,提升领域效果
- Reranking:检索后用交叉编码器重排序(参考 Reranking)
Q2: Matryoshka Embedding 的原理是什么?
答案:
- 训练时同时在多个维度截断点计算损失(如 256/512/768/1536 维)
- 强制让前 N 维包含最重要的语义信息
- 推理时可以按需截取前 N 维,在效果和效率间灵活权衡
- 类似信息论中的"渐进式编码"