文本向量化的原理与实践
前言
Embedding(嵌入向量)是将文本、图像等数据转换为稠密向量表示的技术,是RAG、语义搜索、推荐系统等应用的基础。本文介绍Embedding的原理和实践应用。
什么是Embedding
核心概念
Embedding是一种将离散数据(如单词、句子、图像)映射到连续向量空间的技术。
| 特点 | 说明 |
|---|---|
| 稠密表示 | 相比One-hot,维度更低、信息更丰富 |
| 语义捕捉 | 相似含义的内容在向量空间中距离更近 |
| 可计算性 | 支持向量运算,如相似度计算 |
基础基石:词表 (Vocabulary)
在深入向量化之前,必须先理解词表。它是自然语言处理(NLP)系统的“字典”。
1. 什么是词表? 词表是一个简单的映射表,它将每一个离散的标记 (Token)(可以是单词、字符或子词)映射到一个唯一的整数 ID。
{"猫": 0, "狗": 1, "苹果": 2, ...}
2. 为什么需要词表?
- 计算机不识字:计算机底层只能处理数字。词表是将人类语言转化为计算机可处理信号的第一步。
-
建立索引:词表为模型提供了一个固定的“坐标系”。当模型看到 ID
0时,它知道这代表“猫”。
3. 词表是如何生成的? 通常通过扫描海量文本(语料库),统计出现频率最高的词,并为它们分配 ID。
🤔 词表里放的是世界上所有的词吗?
答案是否定的。 世界上每天都在产生新词(如“yyds”、“元宇宙”),如果词表要收录所有词,它将变得无限大,导致计算崩溃。现代 NLP 使用 子词切分 (Subword Tokenization) 技术(如 BPE, WordPiece)来解决这个问题。
核心思想:乐高积木原理 不以“完整单词”为单位,而是以“有意义的片段”为单位。只需 3-5 万个子词,就能拼凑出世界上几乎所有的单词,彻底解决了 OOV (Out of Vocabulary, 词汇溢出) 问题。
具体例子:
原始文本 子词切分结果 (Tokens) 说明 unhappinessun+happi+ness通过前缀、词根、后缀组合 refactoringre+factor+ing即使没见过“重构”,也见过“重复”和“因素” 人工智能人工+智能中文通常切分为常用词或单字 yydsy+y+d+s极端情况下退化为单字母,保证不报错 结果:模型不再因为遇到“没见过的词”而卡住,而是像读拼音一样,通过组合已知片段来推测含义。
为了处理特殊情况,词表通常还包含特殊标记:
-
[UNK](Unknown): 代表词表中没有出现的生僻词(在子词技术普及后已较少见)。 -
[PAD](Padding): 用于将不同长度的句子补齐到相同长度。
为什么需要Embedding:从 One-hot 到语义空间
为了理解 Embedding 的威力,我们需要对比传统的 One-hot 编码 与 Embedding 稠密向量 的本质区别。
1. One-hot 编码:孤立的维度
在 One-hot 编码中,每个词都被表示为一个极长的向量,其中只有一个位置是 1,其余全是 0。
-
“猫” =
[1, 0, 0, 0, 0, ...] -
“狗” =
[0, 1, 0, 0, 0, ...] -
“苹果” =
[0, 0, 1, 0, 0, ...]
为什么要设计成这种“极长”且“只有一个1”的形式?
- 消除人为偏见(等距性):如果用数字表示(猫=1, 狗=2, 苹果=3),模型会错误地认为“狗”和“猫”的距离比“苹果”近,或者认为“苹果”比“猫”更“大”。One-hot 确保了任意两个词之间的距离都是相等的,在没有任何先验知识时,这是最公平的表示。
- 数学上的正交性:每个词都占据了一个独立的维度。在数学上,这意味着它们是相互垂直(正交)的。这为神经网络提供了一个“干净”的起点,让模型在后续的训练中自己去学习哪些词应该靠近,哪些应该远离。
- 简单直接:这是将离散的标签(文字)转换为计算机能理解的数字信号最简单、最不带主观色彩的方法。
致命缺陷:
- 维度灾难:向量维度等于词表大小(动辄几万、几十万)。
- 语义鸿沟(正交性):在数学上,任意两个 One-hot 向量的点积均为 0。这意味着在计算机看来,“猫”和“狗”的距离,与“猫”和“苹果”的距离完全一样。它无法表达“猫”和“狗”都是动物这一语义联系。
💡 知识点拨:One-hot vs One-shot
很多初学者会混淆这两个术语,虽然听起来很像,但它们属于完全不同的领域:
- One-hot (独热编码):一种数据表示方式。将类别转换为 0/1 向量,没有语义,维度极高。
- One-shot (单样本提示):一种提示工程 (Prompt Engineering) 技巧。在给 LLM 下指令时,提供一个例子来引导模型生成。
简单来说:One-hot 是给计算机看的“数字标签”,One-shot 是给 AI 看的“参考范例”。
2. Embedding:分布式的语义特征
Embedding 将词映射到一个低维、稠密的连续空间(如 768 维)。
# 假设的 3 维简化 Embedding 空间
"猫" = [0.9, 0.1, 0.2] # 维度分别代表:[生物性, 宠物属性, 体型大小]
"狗" = [0.8, 0.2, 0.4]
"桌子" = [0.0, 0.0, 0.7]
它是如何表示“语义相近”的?
- 特征重叠:Embedding 的每一维都可以看作是一个潜在的“特征”。虽然我们无法准确说出第 123 维代表什么,但模型在训练过程中发现,“猫”和“狗”经常出现在类似的上下文里(如“我喂了___”、“___在睡觉”),因此它们在这些特征维度上的数值会非常接近。
-
空间距离:在多维空间中,语义相近的词会“聚”在一起。
- 余弦相似度 (Cosine Similarity):衡量两个向量的方向是否一致。如果两个向量夹角很小,说明它们语义高度相关。
- 欧氏距离 (Euclidean Distance):衡量空间中的绝对距离。
核心原理:分布假说 (Distributional Hypothesis)
“You shall know a word by the company it keeps.” —— J.R. Firth (1957)
Embedding 的本质是通过上下文定义含义。如果两个词经常被同样的词包围,模型就会认为它们是相似的。例如,“橙子”和“橘子”在语料库中都经常出现在“剥皮”、“酸甜”、“水果”等词附近,模型在优化过程中会自动将它们的向量拉近。
文本Embedding模型
主流模型对比
| 模型 | 维度 | 特点 | 适用场景 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 高质量,需API | 通用场景 |
| OpenAI text-embedding-3-large | 3072 | 最高质量 | 高精度需求 |
| BGE-large-zh | 1024 | 中文优化,开源 | 中文场景 |
| m3e-base | 768 | 中文,轻量 | 资源受限 |
| sentence-transformers | 384-768 | 多语言,开源 | 多语言场景 |
| Jina Embeddings | 768 | 长文本支持 | 长文档处理 |
OpenAI Embedding
from openai import OpenAI
client = OpenAI()
def get_embedding(text: str, model: str = "text-embedding-3-small") -> list:
"""获取文本的Embedding向量"""
response = client.embeddings.create(
input=text,
model=model
)
return response.data[0].embedding
# 示例
text = "机器学习是人工智能的一个分支"
embedding = get_embedding(text)
print(f"文本: {text}")
print(f"向量维度: {len(embedding)}")
print(f"前5个值: {embedding[:5]}")
开源模型(Sentence Transformers)
from sentence_transformers import SentenceTransformer
import numpy as np
# 加载模型
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
# 编码文本
texts = [
"机器学习是人工智能的一个分支",
"深度学习是机器学习的子集",
"今天天气很好"
]
embeddings = model.encode(texts)
print(f"向量形状: {embeddings.shape}") # (3, 1024)
# 计算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity_matrix = cosine_similarity(embeddings)
print("\n相似度矩阵:")
print(similarity_matrix)
本地部署Embedding服务
from fastapi import FastAPI
from pydantic import BaseModel
from sentence_transformers import SentenceTransformer
import uvicorn
app = FastAPI()
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
class EmbeddingRequest(BaseModel):
texts: list[str]
class EmbeddingResponse(BaseModel):
embeddings: list[list[float]]
dimensions: int
@app.post("/embeddings", response_model=EmbeddingResponse)
async def create_embeddings(request: EmbeddingRequest):
embeddings = model.encode(request.texts).tolist()
return EmbeddingResponse(
embeddings=embeddings,
dimensions=len(embeddings[0])
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
相似度计算
常用距离度量
import numpy as np
def cosine_similarity(a, b):
"""余弦相似度(最常用)"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def euclidean_distance(a, b):
"""欧氏距离"""
return np.linalg.norm(a - b)
def dot_product(a, b):
"""点积(需要归一化向量)"""
return np.dot(a, b)
# 示例
vec1 = np.array([0.1, 0.2, 0.3, 0.4])
vec2 = np.array([0.15, 0.25, 0.28, 0.38])
vec3 = np.array([-0.3, -0.2, 0.1, -0.4])
print(f"vec1 vs vec2 余弦相似度: {cosine_similarity(vec1, vec2):.4f}")
print(f"vec1 vs vec3 余弦相似度: {cosine_similarity(vec1, vec3):.4f}")
批量相似度计算
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# 假设有1000个文档向量
doc_embeddings = np.random.randn(1000, 768)
# 查询向量
query_embedding = np.random.randn(1, 768)
# 计算查询与所有文档的相似度
similarities = cosine_similarity(query_embedding, doc_embeddings)[0]
# 获取Top-K最相似的文档
k = 5
top_k_indices = np.argsort(similarities)[-k:][::-1]
top_k_scores = similarities[top_k_indices]
print(f"Top {k} 最相似文档:")
for idx, score in zip(top_k_indices, top_k_scores):
print(f" 文档{idx}: 相似度 {score:.4f}")
实战:语义搜索系统
完整实现
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List, Tuple
class SemanticSearch:
def __init__(self, model_name: str = "BAAI/bge-large-zh-v1.5"):
self.model = SentenceTransformer(model_name)
self.documents = []
self.embeddings = None
def add_documents(self, documents: List[str]):
"""添加文档到索引"""
self.documents.extend(documents)
new_embeddings = self.model.encode(documents)
if self.embeddings is None:
self.embeddings = new_embeddings
else:
self.embeddings = np.vstack([self.embeddings, new_embeddings])
print(f"已索引 {len(self.documents)} 个文档")
def search(self, query: str, top_k: int = 5) -> List[Tuple[str, float]]:
"""语义搜索"""
query_embedding = self.model.encode([query])
# 计算相似度
similarities = np.dot(self.embeddings, query_embedding.T).flatten()
# 归一化(如果向量未归一化)
query_norm = np.linalg.norm(query_embedding)
doc_norms = np.linalg.norm(self.embeddings, axis=1)
similarities = similarities / (query_norm * doc_norms)
# 获取Top-K
top_indices = np.argsort(similarities)[-top_k:][::-1]
results = []
for idx in top_indices:
results.append((self.documents[idx], float(similarities[idx])))
return results
# 使用示例
search_engine = SemanticSearch()
# 添加文档
documents = [
"Python是一种流行的编程语言,适合数据科学和机器学习",
"TensorFlow和PyTorch是两个主流的深度学习框架",
"向量数据库用于存储和检索高维向量",
"RAG通过检索增强来提升大模型的回答质量",
"今天的天气非常晴朗,适合户外活动",
"机器学习模型需要大量数据进行训练",
]
search_engine.add_documents(documents)
# 搜索
query = "如何选择深度学习工具"
results = search_engine.search(query, top_k=3)
print(f"\n查询: {query}")
print("搜索结果:")
for doc, score in results:
print(f" [{score:.4f}] {doc}")
Embedding优化技巧
1. 文本预处理
import re
def preprocess_text(text: str) -> str:
"""文本预处理"""
# 去除多余空白
text = re.sub(r'\s+', ' ', text).strip()
# 去除特殊字符(可选)
# text = re.sub(r'[^\w\s]', '', text)
# 截断过长文本
max_length = 512
if len(text) > max_length:
text = text[:max_length]
return text
2. 分块策略
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list:
"""将长文本分割成重叠的块"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start = end - overlap
return chunks
# 示例
long_text = "这是一段很长的文本..." * 100
chunks = chunk_text(long_text, chunk_size=200, overlap=20)
print(f"分割成 {len(chunks)} 个块")
3. 查询优化
def enhance_query(query: str, instruction: str = None) -> str:
"""增强查询(某些模型需要指令前缀)"""
# BGE模型推荐的查询前缀
if instruction:
return f"{instruction}: {query}"
return f"为这个句子生成表示以用于检索相关文章: {query}"
常见问题
Q1: 如何选择Embedding模型?
| 场景 | 推荐模型 |
|---|---|
| 中文通用 | BGE-large-zh, m3e |
| 英文通用 | OpenAI text-embedding-3 |
| 多语言 | multilingual-e5 |
| 长文本 | Jina Embeddings v2 |
| 资源受限 | bge-small-zh, m3e-small |
Q2: 向量维度越高越好吗?
不一定。高维度信息更丰富但计算成本更高,需要根据实际需求平衡。
Q3: 如何评估Embedding质量?
- 语义相似度任务(STS)
- 检索评估(Recall@K, MRR)
- 下游任务表现
Q4: Embedding会过期吗?
模型更新后Embedding会变化,需要重新计算。建议记录模型版本。
总结
| 概念 | 说明 |
|---|---|
| Embedding | 将文本映射到向量空间 |
| 相似度 | 余弦相似度最常用 |
| 分块 | 长文本需要分割处理 |
| 模型选择 | 根据语言和场景选择 |
参考资料
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:《 LLM应用开发——Embedding嵌入向量 》
本文链接:http://localhost:3015/ai/Embedding%E5%B5%8C%E5%85%A5%E5%90%91%E9%87%8F.html
本文最后一次更新为 天前,文章中的某些内容可能已过时!
目录
- 前言
- 什么是Embedding
- 核心概念
- 基础基石:词表 (Vocabulary)
- 为什么需要Embedding:从 One-hot 到语义空间
- 1. One-hot 编码:孤立的维度
- 2. Embedding:分布式的语义特征
- 文本Embedding模型
- 主流模型对比
- OpenAI Embedding
- 开源模型(Sentence Transformers)
- 本地部署Embedding服务
- 相似度计算
- 常用距离度量
- 批量相似度计算
- 实战:语义搜索系统
- 完整实现
- Embedding优化技巧
- 1. 文本预处理
- 2. 分块策略
- 3. 查询优化
- 常见问题
- Q1: 如何选择Embedding模型?
- Q2: 向量维度越高越好吗?
- Q3: 如何评估Embedding质量?
- Q4: Embedding会过期吗?
- 总结
- 参考资料