>
传统搜索引擎(如早期百度、图书馆OPAC系统)使用倒排索引(Inverted Index)技术,本质上是"字符串匹配"。用户搜索"电脑维修",系统只能匹配包含"电脑"或"维修"这两个词的文档。
这种方法的根本问题在于:人类语言充满歧义和表达多样性。同一个意思可以用完全不同的词表达:
关键词检索无法理解语义,只能做表层字符串比对。Embedding的出现,就是为了解决这个问题:让机器理解语义。
Embedding的核心思想是:将每个词(或句子、文档)编码成一个稠密向量,让语义相似的对象在向量空间中距离相近。
举例说明(简化示例,实际向量维度远高于此):
# 这是一个3维词向量的简单示例(真实Embedding维度通常是768/1024/1536)
# 向量空间中的词向量示例(用3维近似)
词汇 向量表示
─────────────────────────────
"国王" → [0.8, 0.2, 0.1]
"皇帝" → [0.79, 0.21, 0.09] # 与"国王"距离很近
"男人" → [0.6, 0.7, 0.3]
"女人" → [0.61, 0.71, 0.29] # 与"男人"距离很近
"苹果" → [0.1, 0.9, 0.4]
"香蕉" → [0.12, 0.88, 0.38] # 水果类,距离近
"手机" → [0.1, 0.85, 0.5] # 科技类,与"苹果"有一定距离
# 使用向量运算验证语义关系:
# "国王" - "男人" + "女人" ≈ "女王"
# 这就是著名的 word2vec 语义算术(king - man + woman ≈ queen)
在Word2Vec出现之前,语言表示使用的是One-Hot编码:
# One-Hot编码示例:假设词表大小为10000
# 每个词表示为一个10000维的向量,只有一个位置是1,其余都是0
"猫" → [1, 0, 0, 0, 0, 0, ..., 0] # 第1个位置是1
"狗" → [0, 1, 0, 0, 0, 0, ..., 0] # 第2个位置是1
"苹果" → [0, 0, 1, 0, 0, 0, ..., 0] # 第3个位置是1
# 问题:
# 1. 向量极其稀疏(10000维中只有1个1),内存浪费严重
# 2. 所有词之间的距离都相同(正交),无法表达语义关系
# 3. 无法处理未知词汇(OOV问题)
# 即使"猫"和"狗"都是动物,在One-Hot表示中它们的相似度为0
2013年,Mikolov等人提出Word2Vec,通过神经网络从大规模语料中自动学习词向量。其核心有两种模型架构:
CBOW的目标是:根据上下文预测中心词。给定窗口内的上下文词,预测当前词。
# CBOW模型结构示例
# 句子:"我想买一台新的笔记本电脑"
# 窗口大小:2(左右各2个词)
# 目标:预测中心词"笔记本"
上下文窗口:
位置: [我] [想] [买] [一] [台] [新] [的] [笔记本] [电脑]
角色: -2 -1 +1 +2 目标词
# CBOW的输入是["我","想","买","一"]四个词的向量之和
# 经过隐藏层变换后,输出是"笔记本"的概率分布
# 数学表达:
# input = sum(embedding(context_words)) # 上下文词的向量求和
# hidden = W1 @ input + b1 # 线性变换
# output = softmax(W2 @ hidden + b2) # 输出概率分布
# 训练目标:最大化正确中心词的概率(最小化交叉熵损失)
Skip-gram与CBOW相反:根据中心词预测上下文。一个词可以预测它周围的词。
# Skip-gram模型结构示例
# 目标词:"笔记本"
# 需要预测:位置-2、-1、+1、+2的词
# Skip-gram的输入是中心词"笔记本"的向量
# 输出是各个位置的上下文词概率分布
# 训练数据构建(以句子"我想买一台新的笔记本电脑"为例):
# (笔记本, 我)
# (笔记本, 想)
# (笔记本, 买)
# (笔记本, 一)
# (笔记本, 台)
# (笔记本, 新)
# (笔记本, 的)
# (笔记本, 电脑)
# 这样每个中心词-上下文词对就是一个训练样本
# Skip-gram的损失函数:
# Loss = -sum(log P(context_i | center_word))
# 目标是让中心词与上下文词的向量点积尽可能大(内积最大化)
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 小规模语料 + 高频词 | CBOW | 训练更快,对稀有词有一定平滑效果 |
| 大规模语料 + 稀有词为主 | Skip-gram | 对稀有词训练更充分,语义关系更精确 |
| 短语/成语学习 | Skip-gram | 短语的语义更依赖中心词 |
# 使用Python + Gensim 实现Word2Vec训练
# 完整可运行的训练脚本
from gensim.models import Word2Vec
import jieba
# ============ Step 1: 准备中文语料 ============
# 语料:可以是小说、新闻、百科等中文文本
sentences = [
"深度学习是机器学习的一个分支,主要使用神经网络模型",
"卷积神经网络在图像识别领域取得了突破性进展",
"自然语言处理让计算机能够理解和生成人类语言",
"循环神经网络适合处理序列数据,如文本和时间序列",
"Transformer模型通过自注意力机制解决了RNN的长距离依赖问题",
"预训练语言模型如BERT和GPT大幅提升了NLP任务的效果",
"向量数据库用于存储和检索高维向量,实现语义搜索",
"RAG系统结合了检索和生成,提高了问答系统的准确性",
]
# ============ Step 2: 中文分词 ============
# 对每个句子进行分词
tokenized_sentences = [list(jieba.cut(s)) for s in sentences]
print("分词结果示例:", tokenized_sentences[0])
# 输出: ['深度学习', '是', '机器学习', '的', '一个', '分支', ',', '主要', '使用', '神经网络', '模型']
# ============ Step 3: 训练Word2Vec模型 ============
model = Word2Vec(
sentences=tokenized_sentences,
vector_size=100, # 向量维度(实际中文模型通常用256/512/768)
window=5, # 上下文窗口大小(左右各5个词)
min_count=1, # 词频阈值,低于此频率的词被丢弃
workers=4, # 并行训练线程数
sg=1, # 1=Skip-gram, 0=CBOW
epochs=100, # 训练轮数
hs=0, # 0=负采样, 1=层次Softmax
negative=5, # 负采样数量(推荐5-20)
)
# 保存模型
model.save("word2vec.model")
# ============ Step 4: 使用模型 ============
# 查看词向量
word = "神经网络"
vector = model.wv[word]
print(f"'{word}' 的向量维度: {vector.shape}")
print(f"向量前10维: {vector[:10]}")
# 输出: (100,) 向量前10维: [-0.023, 0.045, -0.012, 0.078, -0.034, ...]
# ============ Step 5: 语义相似度计算 ============
# 计算两个词的余弦相似度
similarity = model.wv.similarity("神经网络", "深度学习")
print(f"'神经网络' 与 '深度学习' 的相似度: {similarity:.4f}")
# 输出: 0.8532
# ============ Step 6: 找最相似的词 ============
similar_words = model.wv.most_similar("神经网络", topn=5)
print(f"与'神经网络'最相似的5个词:")
for word, score in similar_words:
print(f" {word}: {score:.4f}")
# ============ Step 7: 验证语义算术(类比推理) ============
# Word2Vec最神奇的能力:向量运算能反映语义关系
# king - man + woman ≈ queen
try:
result = model.wv.most_similar(positive=["神经网络", "深度学习"], negative=["机器学习"], topn=3)
print(f"神经网络 - 机器学习 + 深度学习 = {result}")
except KeyError as e:
print(f"词表中缺少: {e}")
Word2Vec学习的是静态词向量——每个词只有一个固定的向量表示。这导致了一个根本问题:无法处理一词多义。
# 一词多义的经典例子:"苹果"
# 句子1:"苹果公司发布了新款iPhone"
# 句子2:"每天吃一个苹果对健康有益"
# Word2Vec只能给"苹果"生成一个向量,无法区分两种语义:
"苹果" → [0.1, 0.9, 0.4, ...] # 无论在哪种语境下,词向量都相同
# BERT通过上下文感知的方式解决此问题:
# 句子1中"苹果" → [0.1, 0.95, 0.02, ...] # 科技/公司语义
# 句子2中"苹果" → [0.8, 0.15, 0.05, ...] # 水果语义
# BERT的核心是双向Transformer编码器
# 每个词的最终表示 = 其本身 embedding + 位置 embedding + 上下文加权
# BERT的Embedding由三部分相加组成(简化示意):
# final_embedding = token_embedding + position_embedding + segment_embedding
import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer
# 加载预训练BERT模型和分词器
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
bert_model = BertModel.from_pretrained(model_name)
# 示例句子(包含多义词"苹果")
sentence = "苹果公司发布了新款iPhone手机"
tokens = tokenizer.tokenize(sentence)
print(f"分词结果: {tokens}")
# 输出: ['苹果', '公司', '发布', '了', '新款', 'i', '##Phone', '手机']
# 转换为IDs并获取BERT输出
input_ids = tokenizer.convert_tokens_to_ids(tokens)
input_tensor = torch.tensor([input_ids])
with torch.no_grad():
outputs = bert_model(input_tensor)
# last_hidden_state: [batch_size, seq_len, hidden_size]
# 每一层的隐状态,包含上下文信息
last_hidden = outputs.last_hidden_state
print(f"隐状态维度: {last_hidden.shape}") # [1, 8, 768]
# 取每个token的最终表示(即最后一层Transformer的输出)
token_embeddings = last_hidden[0] # [seq_len, hidden_size]
print(f"每个token的向量维度: {token_embeddings.shape}") # [8, 768]
# 取"苹果"这个token的向量
apple_idx = tokens.index("苹果")
apple_embedding = token_embeddings[apple_idx]
print(f"'苹果'的向量维度: {apple_embedding.shape}") # [768]
| 维度 | Word2Vec | BERT |
|---|---|---|
| 向量类型 | 静态(每个词一个固定向量) | 动态上下文感知(依句子而变) |
| 训练目标 | 语言模型(预测词/上下文) | 掩码语言模型(MLM)+ 下句预测(NSP) |
| 模型架构 | 浅层神经网络(2-3层) | 深层Transformer(12-24层) |
| 向量维度 | 通常50-300维 | 768-1536维 |
| 训练数据 | 无需标注的大规模语料 | 无需标注的大规模语料 |
| 推理速度 | 快(简单矩阵运算) | 慢(需要完整Transformer前向传播) |
| 多义词处理 | ❌ 无法区分 | ✅ 根据上下文动态生成 |
当向量数据库中有1000万条文档时,每次检索需要计算1000万次余弦相似度或欧氏距离。即使每次计算只需要0.1毫秒,1000万次也需要1000秒——完全无法接受。
向量索引算法的核心目标是:用近似方法替代精确计算,将检索时间从O(N)降低到O(log N)或O(1)。
HNSW是目前最流行的向量索引算法,被Qdrant、Milvus、Weaviate等主流向量数据库采用。
HNSW构建一个多层图结构:
# HNSW的多层图结构(ASCII示意)
# Layer 2 (最稀疏): A ------------------ B
# \ /
# Layer 1 (中等): C ---- D ---- E ---- F
# \ | \ / \
# Layer 0 (最密集): G -- H -- I -- J -- K -- L -- M
# 检索示例:找K的最近邻
# Step 1: 从最高层开始(Layer 2),随机选择入口点A
# Step 2: 计算A的邻居(B),如果B比A更接近目标,则移动到B
# Step 3: 重复Step 2直到无法继续优化(局部最优),下沉到Layer 1
# Step 4: 在Layer 1继续上述过程,下沉到Layer 0
# Step 5: 在Layer 0做最精细的搜索,返回最近邻
# 计算复杂度:
# 暴力检索: O(N) — N=1000万时需要1000万次计算
# HNSW: O(log N) — log(10000000) ≈ 23次计算即可
# Qdrant中HNSW的配置参数详解
# 1. m(每层最大连接数)
# 默认值: 16
# 值越大: 检索精度越高,内存占用越大,构建时间越长
# 建议值: 8-64,推荐16-32
# 公式估算: m >= (ln(N) / ln(score_threshold)),N是向量数量
# 2. ef_construction(构建时的动态列表大小)
# 默认值: 100
# 值越大: 索引精度越高,构建时间越长
# 建议值: 64-512,推荐128-256
# 影响:在插入向量时搜索最近邻候选集的大小
# 3. ef(检索时的动态列表大小)
# 默认值: 100
# 值越大: 检索精度越高,检索时间越长
# 建议值: 64-512,推荐设置为与ef_construction相近
# 注意:ef可以动态调整,不需要重建索引
# 完整的HNSW配置示例(Qdrant API):
curl -X PUT "http://localhost:6333/collections/my_collection" \
-H "Content-Type: application/json" \
-d '{
"hnsw_config": {
"m": 16,
"ef_construct": 128,
"full_scan_threshold": 10000,
"on_disk": false
},
"params": {
"hnsw_web_cache": true
}
}'
另一种常用的索引策略是IVF(倒排索引)与PQ(乘积量化)的组合:
# IVF+PQ的工作原理:
# 1. IVF(倒排索引)
# 步骤:将向量空间预先聚类成K个簇(Cluster)
# 每个向量属于某个簇,用倒排列表存储
# 检索时:先确定查询向量属于哪个簇,只在相关簇内搜索
# 示例(Python伪代码):
from sklearn.cluster import KMeans
import numpy as np
# 假设有100万个128维向量
vectors = np.random.randn(1000000, 128).astype('float32')
# 用K-Means聚类成1024个簇
n_clusters = 1024
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
kmeans.fit(vectors)
# 获取每个向量所属的簇
labels = kmeans.predict(vectors)
# 构建倒排索引:簇ID → 向量ID列表
inverted_index = {}
for vec_id, cluster_id in enumerate(labels):
if cluster_id not in inverted_index:
inverted_index[cluster_id] = []
inverted_index[cluster_id].append(vec_id)
# 检索:只搜索查询向量最近的K个簇
query_vector = np.random.randn(128).astype('float32')
query_cluster = kmeans.predict([query_vector])[0]
# 在query_cluster及其相邻簇中搜索,而不是全量100万条
| 算法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| HNSW | 需要高精度(recall>0.95) | 检索速度快、精度高、支持GPU加速 | 内存占用大、构建慢 |
| IVF+PQ | 超大规模数据(>1000万) | 内存效率高、支持增量更新 | 精度相对较低、检索速度中等 |
| IVF+HNSW | 超大规模+高精度 | 兼具两者优点 | 实现复杂、参数调优困难 |
余弦相似度是向量检索中最常用的距离度量,它衡量两个向量方向的相似程度,取值范围[-1, 1]:
# 余弦相似度计算公式:
# Cosine(A, B) = (A · B) / (||A|| * ||B||)
# = sum(A[i] * B[i]) / (sqrt(sum(A[i]^2)) * sqrt(sum(B[i]^2)))
import numpy as np
def cosine_similarity(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
"""
计算两个向量的余弦相似度
Args:
vec_a: 第一个向量,shape=(d,) 或 (d, 1)
vec_b: 第二个向量,shape=(d,) 或 (d, 1)
Returns:
余弦相似度,范围[-1, 1]
Example:
>>> a = np.array([1.0, 0.0])
>>> b = np.array([1.0, 0.0])
>>> cosine_similarity(a, b)
1.0
"""
# 将向量展平为1D
vec_a = np.asarray(vec_a).flatten()
vec_b = np.asarray(vec_b).flatten()
# 计算点积
dot_product = np.dot(vec_a, vec_b)
# 计算模长(L2范数)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
# 防止除零错误
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
# 测试:验证"国王-男人+女人≈女王"的语义关系
# 这是word2vec最著名的语义算术例子
king = np.array([0.8, 0.2, 0.1])
man = np.array([0.6, 0.7, 0.3])
woman = np.array([0.61, 0.71, 0.29])
queen = np.array([0.62, 0.70, 0.28])
# 计算 "国王" - "男人" + "女人" 的结果
result = king - man + woman
print(f"语义算术结果: {result}") # [0.81, 0.21, 0.09]
# 计算与"女王"的相似度
similarity = cosine_similarity(result, queen)
print(f"与女王向量的相似度: {similarity:.4f}") # ≈ 0.9997
# 结果接近1,说明语义算术非常准确
# 在实际向量数据库中,点积(Dot Product)和余弦相似度都有使用
# 两者的区别在于是否对向量做了归一化
def dot_product(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
"""计算点积(内积)"""
return np.dot(np.asarray(vec_a).flatten(), np.asarray(vec_b).flatten())
# 两种度量方式的选择:
# 1. 如果向量已经归一化(||A|| = ||B|| = 1):点积 = 余弦相似度
# 2. 如果向量未归一化但长度重要:使用点积(长向量表示更强的信号)
# 3. 如果只关心方向相似度:使用余弦相似度
# 实际工程中的经验:
# - OpenAI text-embedding-3-small 输出的向量已归一化,用点积即可
# - BERT类的Embedding通常未归一化,用余弦相似度更准确
# 批量计算:快速找出最相似的K个向量
def find_top_k_similar(query_vec: np.ndarray,
candidate_vectors: np.ndarray,
k: int = 5,
metric: str = "cosine") -> list:
"""
在候选向量中找到与查询向量最相似的K个
Args:
query_vec: 查询向量,shape=(d,)
candidate_vectors: 候选向量矩阵,shape=(N, d)
k: 返回前K个最相似的结果
metric: "cosine" 或 "dotproduct"
Returns:
[(index, score), ...] 按相似度降序排列的前K个结果
"""
# 将查询向量扩展为(N, d)以便批量计算
query_vec = np.asarray(query_vec).flatten()
candidates = np.asarray(candidate_vectors)
N, d = candidates.shape
if metric == "cosine":
# 归一化
query_norm = query_vec / np.linalg.norm(query_vec)
cand_norms = candidates / np.linalg.norm(candidates, axis=1, keepdims=True)
scores = np.dot(cand_norms, query_norm)
else:
scores = np.dot(candidates, query_vec)
# 取最大的K个索引
top_k_idx = np.argsort(scores)[-k:][::-1]
return [(int(idx), float(scores[idx])) for idx in top_k_idx]
# 示例:模拟1000条文档的向量检索
np.random.seed(42)
doc_vectors = np.random.randn(1000, 768).astype('float32') # 1000条768维向量
query = np.random.randn(768).astype('float32')
top5 = find_top_k_similar(query, doc_vectors, k=5, metric="cosine")
print("最相似的5个文档索引和相似度:", top5)
# 一个简化但完整的向量数据库实现(用于理解原理)
# 不依赖任何外部向量数据库,用NumPy + 暴力检索实现
class SimpleVectorDB:
"""
简化的向量数据库,支持添加向量和检索最相似的结果
适合理解原理,生产环境请使用Qdrant/Milvus等
"""
def __init__(self, dimension: int, metric: str = "cosine"):
self.dimension = dimension
self.metric = metric
self.vectors = [] # 存储向量
self.metadata = [] # 存储元数据(如文档ID、文本内容)
def add(self, vector: np.ndarray, metadata: dict):
"""
添加向量到数据库
Args:
vector: 待添加的向量,shape=(dimension,)
metadata: 与向量关联的元数据(如文档内容)
"""
vec = np.asarray(vector).flatten()
assert len(vec) == self.dimension, f"向量维度必须为{self.dimension}"
self.vectors.append(vec)
self.metadata.append(metadata)
def search(self, query: np.ndarray, top_k: int = 5) -> list:
"""
检索最相似的top_k个结果
Args:
query: 查询向量
top_k: 返回前K个结果
Returns:
[(metadata, score), ...] 最相似的top_k个结果
"""
query_vec = np.asarray(query).flatten()
vectors = np.array(self.vectors) # shape: (N, dimension)
if len(vectors) == 0:
return []
# 批量计算相似度
if self.metric == "cosine":
# 归一化后计算点积 = 余弦相似度
query_norm = query_vec / np.linalg.norm(query_vec)
norms = vectors / np.linalg.norm(vectors, axis=1, keepdims=True)
scores = np.dot(norms, query_norm)
else:
scores = np.dot(vectors, query_vec)
# 取top_k
top_k = min(top_k, len(scores))
top_indices = np.argsort(scores)[-top_k:][::-1]
return [(self.metadata[idx], float(scores[idx])) for idx in top_indices]
def count(self) -> int:
"""返回数据库中的向量数量"""
return len(self.vectors)
# 使用示例
db = SimpleVectorDB(dimension=5, metric="cosine")
# 添加向量及其元数据
db.add(np.array([0.8, 0.2, 0.1, 0.3, 0.5]), {"text": "这是一只猫", "doc_id": "doc_001"})
db.add(np.array([0.79, 0.21, 0.09, 0.31, 0.49]), {"text": "这是一只家猫", "doc_id": "doc_002"})
db.add(np.array([0.1, 0.9, 0.4, 0.2, 0.1]), {"text": "苹果是一种水果", "doc_id": "doc_003"})
db.add(np.array([0.3, 0.7, 0.2, 0.8, 0.1]), {"text": "苹果手机很贵", "doc_id": "doc_004"})
# 检索
query = np.array([0.78, 0.22, 0.10, 0.30, 0.50])
results = db.search(query, top_k=2)
print(f"数据库中共有 {db.count()} 条向量")
print("检索结果:")
for metadata, score in results:
print(f" 相似度 {score:.4f}: {metadata['text']} ({metadata['doc_id']})")
# 使用Qdrant Python客户端进行向量检索
# 安装:pip install qdrant-client
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from qdrant_client.http.exceptions import UnexpectedResponse
import numpy as np
# ============ 连接Qdrant ============
client = QdrantClient(
host="localhost", # Qdrant服务地址
port=6333 # REST API端口
)
# ============ 创建Collection ============
collection_name = "tech_articles"
try:
# 如果已存在则先删除
client.delete_collection(collection_name=collection_name)
print(f"已删除旧Collection: {collection_name}")
except Exception:
pass
# 创建新Collection(向量维度=768,对应text-embedding-3-small)
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=768, # 向量维度(必须与Embedding模型输出维度一致)
distance=Distance.COSINE # 使用余弦相似度
)
)
print(f"创建Collection成功: {collection_name}")
# ============ 插入向量数据 ============
# 构造示例数据(实际使用时替换为真实文档的Embedding)
articles = [
{"id": "1", "text": "RAG系统结合了检索和生成技术,提高了问答准确性", "embedding": np.random.randn(768).astype('float32')},
{"id": "2", "text": "向量数据库使用HNSW算法实现高速近似检索", "embedding": np.random.randn(768).astype('float32')},
{"id": "3", "text": "Ollama是一个本地运行LLM的工具,支持多种模型", "embedding": np.random.randn(768).astype('float32')},
{"id": "4", "text": "Dify是一个开源的LLM应用开发平台", "embedding": np.random.randn(768).astype('float32')},
]
# 归一化Embedding(Qdrant用余弦相似度需要先归一化)
for article in articles:
vec = article["embedding"]
article["embedding"] = (vec / np.linalg.norm(vec)).tolist()
# 批量插入
points = [
PointStruct(
id=article["id"],
vector=article["embedding"],
payload={"text": article["text"]}
)
for article in articles
]
client.upsert(
collection_name=collection_name,
points=points
)
print(f"插入 {len(points)} 条向量成功")
# ============ 检索 ============
query_text = "如何用本地LLM构建问答系统"
# 实际使用时:用Embedding模型将query_text转换为向量
query_embedding = np.random.randn(768).astype('float32')
query_embedding = (query_embedding / np.linalg.norm(query_embedding)).tolist()
# 执行检索
search_results = client.search(
collection_name=collection_name,
query_vector=query_embedding,
limit=3 # 返回前3个最相似的结果
)
print("\n检索结果:")
for result in search_results:
print(f" ID: {result.id}")
print(f" 相似度: {result.score:.4f}")
print(f" 内容: {result.payload['text']}")
print()
# ============ 范围检索(相似度大于阈值) ============
# 只返回相似度 > 0.8 的结果
range_results = client.search(
collection_name=collection_name,
query_vector=query_embedding,
query_filter=None, # 可以添加过滤条件
score_threshold=0.8, # 最小相似度阈值
limit=10
)
print(f"相似度>0.8的结果数: {len(range_results)}")
从One-Hot到Word2Vec再到BERT,Embedding技术经历了从稀疏到稠密、从静态到动态的演进。理解这些技术背后的数学原理和工程权衡,是构建高质量RAG系统的基础。
核心要点回顾: