文本向量化的原理与实践

前言

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) 说明
unhappiness un + happi + ness 通过前缀、词根、后缀组合
refactoring re + factor + ing 即使没见过“重构”,也见过“重复”和“因素”
人工智能 人工 + 智能 中文通常切分为常用词或单字
yyds y + 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. 消除人为偏见(等距性):如果用数字表示(猫=1, 狗=2, 苹果=3),模型会错误地认为“狗”和“猫”的距离比“苹果”近,或者认为“苹果”比“猫”更“大”。One-hot 确保了任意两个词之间的距离都是相等的,在没有任何先验知识时,这是最公平的表示。
  2. 数学上的正交性:每个词都占据了一个独立的维度。在数学上,这意味着它们是相互垂直(正交)的。这为神经网络提供了一个“干净”的起点,让模型在后续的训练中自己去学习哪些词应该靠近,哪些应该远离。
  3. 简单直接:这是将离散的标签(文字)转换为计算机能理解的数字信号最简单、最不带主观色彩的方法。

致命缺陷:

  • 维度灾难:向量维度等于词表大小(动辄几万、几十万)。
  • 语义鸿沟(正交性):在数学上,任意两个 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

本文最后一次更新为 天前,文章中的某些内容可能已过时!