尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从词袋到Embedding:语义向量原理、相似度计算与本地搜索实战

从词袋到Embedding:语义向量原理、相似度计算与本地搜索实战 1. 从“词袋”到“词义”为什么我们需要Embedding如果你在十年前问我怎么让计算机理解“苹果”和“iPhone”这两个词的关系我可能会让你去统计它们同时出现在多少篇文章里。这就是经典的“词袋”模型它把文本看成一个个孤立的符号计算的是符号的共现频率。但这种方法有个致命伤它无法理解“苹果”既可以是一种水果也可以是一家科技公司。直到Embedding技术的出现我们才真正开始让机器“读懂”词语背后的含义。Embedding中文常译作“嵌入”或“向量化”它的核心思想简单而深刻将文本词、句、段落映射到一个高维的连续向量空间中使得语义相似的文本其对应的向量在空间中的距离也更近。你可以把它想象成一个“语义地图”在这个地图上“苹果”和“香蕉”因为都是水果所以位置靠得很近“苹果”和“微软”因为都是科技公司所以也在另一个区域相邻而“苹果水果”和“苹果公司”则可能位于两个不同的语义簇中但通过上下文可以区分。我最初接触Embedding是在做推荐系统的时候。当时我们用用户的历史点击行为来构建物品的向量效果比传统的协同过滤好了一大截。这让我意识到Embedding不仅仅是NLP领域的玩具它是一种通用的、将离散对象用户、商品、文章、词语转化为可计算、可比较的连续表示的方法。今天从搜索引擎的语义匹配到智能客服的意图识别再到AIGC应用中的知识库检索Embedding都是底层不可或缺的基石。网络上热议的LangChain、Spring AI、Ollama等框架或工具其核心能力之一就是便捷地生成和使用Embedding。2. Embedding模型的演进从Word2Vec到BERT及其后Embedding模型的发展是一部从静态到动态、从浅层到深层的进化史。理解这段历史你就能明白为什么今天会有这么多选择以及它们各自适合什么场景。2.1 静态词向量Word2Vec与GloVe的奠基2013年谷歌提出的Word2Vec是第一个将Embedding概念大规模普及的模型。它基于一个朴素的假设一个词的语义由其上下文决定。Word2Vec通过两种简单的神经网络结构CBOW和Skip-gram来学习词向量。CBOW连续词袋用上下文词预测中心词。例如给定“今天”、“天气”、“不错”预测中心词“很”。这适合数据量较小的场景。Skip-gram用中心词预测上下文词。例如给定“很”预测它周围的“今天”、“天气”、“不错”。这在数据充足时效果通常更好。Word2Vec生成的词向量是“静态”的。无论“苹果”出现在什么句子中它都对应同一个向量。这解决了“词袋”模型的部分问题但无法解决一词多义。随后出现的GloVe模型通过全局词-词共现矩阵的分解来生成向量在某些任务上表现更稳定。实操心得对于入门或处理相对固定的词典如商品名称、专业术语Word2Vec/GloVe依然是轻量且有效的选择。你可以用gensim库快速训练自己的词向量。关键参数是vector_size向量维度通常50-300、window上下文窗口大小通常5-10和min_count词频阈值过滤低频词。2.2 动态上下文向量ELMo、BERT与Transformer的革新静态向量的瓶颈在2018年被打破。ELMo模型首次提出了“动态”词向量的概念它使用双向LSTM根据词的完整上下文来生成该词的向量。这意味着“苹果”在“我吃了一个苹果”和“苹果发布了新手机”中会得到两个不同的向量。真正的革命来自谷歌的BERT。它基于Transformer架构采用“掩码语言模型”和“下一句预测”进行预训练。BERT的强大之处在于深度双向编码传统模型从左到右或从右到左编码BERT同时考虑了一个词左右两侧的全部上下文对语义的理解更加完整。强大的预训练在海量无标注文本上预训练让模型学到了丰富的语言知识。生成句子/段落向量虽然BERT本身为每个词生成向量但通过池化操作如取[CLS]标记的输出、或对词向量求平均可以方便地得到整个句子或段落的Embedding。BERT之后RoBERTa、ALBERT、DeBERTa等模型在预训练任务、模型结构上做了诸多优化。同时像text-embedding-ada-002这样的专用Embedding模型通常基于类似BERT的架构进行对比学习微调被设计出来它们在生成句子级向量用于相似度计算、检索等任务上效果通常比直接用BERT的原始输出更好。2.3 专用与轻量化模型Sentence-BERT、BGE与Ollama在实际应用中我们常常需要快速计算大量句子之间的相似度。直接用BERT两两配对计算时间复杂度是O(n²)效率极低。Sentence-BERTSBERT应运而生。它采用孪生/三元组网络结构对BERT进行微调使得生成的句子向量本身就富含语义相似度信息可以直接用余弦相似度等度量快速比较将复杂度降至O(n)。近年来中文社区也涌现了优秀的模型如智源的BGEBAAI General Embedding系列。它在多项中文语义相似度评测中表现优异并且提供了不同尺寸的模型兼顾效果与效率。而对于希望本地化、轻量化部署的开发者Ollama这样的工具提供了极大便利。它可以将很多开源模型包括Embedding模型封装成易于管理的本地服务。例如你可以通过Ollama一键拉取并运行nomic-embed-text这样的轻量级Embedding模型无需复杂的环境配置。踩坑实录早期我们直接使用BERT的[CLS]向量做句子相似度效果并不理想。因为BERT的预训练任务并非直接优化句子表示。后来切换到SBERT或BGE这类经过相似度任务微调的模型效果提升立竿见影。核心教训任务匹配是关键。预训练模型是“通才”但在特定任务上需要“专家”模型。3. 相似度计算度量、选择与陷阱得到了Embedding向量如何衡量它们的“相似度”这不仅仅是选一个公式那么简单它直接关系到下游任务的效果。3.1 主流相似度度量方法假设我们有两个向量 A 和 B维度均为 d。余弦相似度Cosine Similarity最常用、最直观的度量。公式cos_sim(A, B) (A·B) / (||A|| * ||B||)本质计算两个向量在方向上的差异忽略其长度模。值域为[-1, 1]1表示方向完全相同0表示正交无关-1表示方向完全相反。适用场景绝大多数文本语义相似度计算。因为Embedding向量的“语义信息”更多体现在方向上而非长度。例如一个长段落和一个短句若语义相同其向量方向应接近但长度可能相差很大。欧氏距离Euclidean Distance公式euclidean_dist(A, B) sqrt(∑(Ai - Bi)²)本质计算空间中两点间的直线距离。距离越小越相似。适用场景当向量的长度也包含重要信息时。但在高维空间中欧氏距离容易受到“维度灾难”影响且对向量尺度敏感。通常我们会先将向量归一化转为单位向量此时欧氏距离与余弦相似度存在单调关系dist sqrt(2 - 2*cos_sim)。内积Dot Product公式dot_product(A, B) ∑(Ai * Bi)本质未归一化的相似度度量。其结果受向量长度影响极大。适用场景在某些大规模向量检索系统如Facebook的Faiss中为了加速计算可能会直接使用内积但前提是向量已经过归一化处理此时内积等于余弦相似度。3.2 如何选择一个实战决策流程在实际项目中我的选择流程通常是这样的第一步看模型要求。许多现代Embedding模型如OpenAI的Embedding API、BGE模型在文档中会明确推荐使用余弦相似度。这是因为它们在训练时损失函数就是基于余弦相似度优化的。遵循模型建议是首要原则。第二步处理向量。无论最终使用哪种度量一个良好的实践是先将向量进行L2归一化。这能消除长度影响使相似度计算更稳定。在Python中一行代码即可完成vector_normalized vector / np.linalg.norm(vector)。第三步根据任务定度量。语义检索/问答99%的情况使用余弦相似度。这是业界的黄金标准。聚类分析K-Means等算法通常基于欧氏距离。但你可以输入归一化后的向量此时等同于在优化余弦相似度。向量数据库检索查看数据库引擎的索引类型。例如Milvus支持IP内积和L2欧氏距离索引。如果向量已归一化选择IP索引计算内积效率最高其结果排序与余弦相似度一致。3.3 相似度计算的常见陷阱“维度诅咒”下的虚假相似在高维空间中Embedding维度常为384、768、1024随机向量之间的余弦相似度可能并不接近0而是有一个较小的期望值。这可能导致一些本不相关的文本被误判为弱相关。解决方案是设置合理的相似度阈值不要认为所有正分数都代表相关。需要通过在验证集上测试来确定阈值。领域不匹配导致的失效用一个在通用语料如维基百科上训练的Embedding模型去计算特定领域如法律、医疗文本的相似度效果可能会打折扣。因为专业术语的语义在通用向量空间中可能无法准确表征。解决方案如果领域数据充足可以进行领域自适应微调继续预训练或微调如果数据不多可以尝试使用在相关领域预训练过的模型如生物医学领域的BioBERT。长文本处理的误区直接将长文本如一篇文档输入BERT等有长度限制如512 token的模型然后取平均向量会损失大量信息。更好的做法将长文本切分成语义完整的短段落或句子分别获取Embedding。进行检索或比较时针对这些片段进行。或者使用专门处理长文本的模型如Longformer、LED或支持更长上下文的最新模型如GPT-4的上下文窗口。4. 实战构建一个本地语义搜索系统理论说了这么多我们来点实际的。我将演示如何用Python、Sentence-Transformers库和Faiss向量数据库快速搭建一个本地的语义搜索系统。这个流程可以无缝迁移到LangChain或Spring AI的集成中。4.1 环境准备与数据加载首先安装核心库。我们选用sentence-transformers因为它封装了SBERT等优秀模型接口简单。pip install sentence-transformers pandas faiss-cpu假设我们有一个articles.csv文件包含很多文章的标题和内容。import pandas as pd from sentence_transformers import SentenceTransformer import faiss import numpy as np # 1. 加载数据 df pd.read_csv(articles.csv) # 假设有‘id‘, ‘title‘, ‘content‘三列 corpus df[content].tolist() corpus_ids df[id].tolist() # 2. 选择Embedding模型 # 这里选用轻量且效果不错的 paraphrase-multilingual-MiniLM-L12-v2 # 对于中文BGE模型是更优选择例如BAAI/bge-small-zh-v1.5 model_name paraphrase-multilingual-MiniLM-L12-v2 model SentenceTransformer(model_name)模型选型思考为什么选这个模型multilingual意味着它支持多种语言包括中文MiniLM是蒸馏模型在保持性能的同时大幅减小了尺寸和提升了速度L12指12层Transformer是速度和效果的平衡点。对于生产级中文应用强烈建议切换到BAAI/bge-*系列模型。4.2 生成向量并构建索引接下来我们将所有文本转化为向量并用Faiss建立索引以便快速检索。# 3. 生成文档向量 print(正在生成文档向量...) corpus_embeddings model.encode(corpus, convert_to_numpyTrue, show_progress_barTrue, normalize_embeddingsTrue) # 关键归一化向量 # 检查向量维度 embedding_dim corpus_embeddings.shape[1] print(f向量维度{embedding_dim}) print(f生成的向量数量{len(corpus_embeddings)}) # 4. 构建Faiss索引 # 因为我们使用了归一化后的向量使用内积IP索引等价于余弦相似度 index faiss.IndexFlatIP(embedding_dim) # IndexFlatIP 用于内积 faiss.normalize_L2(corpus_embeddings) # Faiss端也做一次归一化以确保无误 index.add(corpus_embeddings) print(f索引中的向量数量{index.ntotal}) # 可选保存索引和映射关系 faiss.write_index(index, my_index.faiss) id_map pd.Series(corpus_ids) id_map.to_pickle(id_map.pkl)关键点解析normalize_embeddingsTrue在生成向量时直接进行L2归一化。这是保证余弦相似度计算正确的关键一步。IndexFlatIPFaiss的“扁平”索引使用内积进行精确搜索。由于向量已归一化内积结果等于余弦相似度。这种索引搜索精度100%但搜索速度与数据量线性相关适合百万级以下数据。对于更大规模数据需要使用IndexIVFFlat倒排文件索引等近似搜索索引在可接受的小幅精度损失下换取百倍千倍的搜索速度。4.3 执行语义搜索现在我们可以用一段查询文本来搜索最相关的文档了。# 5. 定义搜索函数 def semantic_search(query, model, index, id_map, top_k5): # 生成查询向量 query_embedding model.encode([query], convert_to_numpyTrue, normalize_embeddingsTrue) # 搜索 distances, indices index.search(query_embedding, top_k) # distances 返回的是相似度分数内积因为我们用了归一化所以就是余弦相似度 # indices 返回的是最相似向量的索引 results [] for i, (dist, idx) in enumerate(zip(distances[0], indices[0])): if idx ! -1: # -1 表示无效索引 doc_id id_map.iloc[idx] # 这里可以根据id从原数据框df中取出原文 original_content df.loc[df[id] doc_id, content].values[0] results.append({ rank: i1, doc_id: doc_id, score: dist, # 余弦相似度分数 content_preview: original_content[:200] ... # 预览 }) return results # 6. 进行查询 query_text 如何选择合适的机器学习算法 print(f查询{query_text}) search_results semantic_search(query_text, model, index, id_map, top_k3) print(\n搜索结果) for res in search_results: print(f排名 {res[rank]} (相似度: {res[score]:.4f}) - ID: {res[doc_id]}) print(f内容预览: {res[content_preview]}\n)4.4 集成到现有框架以Spring AI为例你可能在热词中看到“Spring AI embedding可以不是用向量模型吗”和“Spring Cloud项目将文本转为高维向量只能调用外部的embedding服务API吗”。答案是Spring AI提供了灵活的Embedding抽象。并非只能用向量模型Spring AI的EmbeddingClient接口是一个抽象层。其默认实现如OpenAiEmbeddingClient确实调用外部API如OpenAI。但你可以实现自己的EmbeddingClient。上面的Python代码如果封装成一个HTTP服务你就可以写一个RestTemplateEmbeddingClient来调用自己的本地服务。或者社区已有一些集成本地模型如通过Ollama的客户端实现。不一定非要调用外部API正如我们上面演示的你可以将Embedding模型如SBERT、BGE部署为独立的微服务。你的Spring Cloud项目通过HTTP或gRPC调用这个内部服务从而避免依赖外部API保障数据隐私和服务的稳定性。Spring AI的抽象很好地支持了这种模式。一个简化的Spring AI自定义EmbeddingClient思路Component public class LocalEmbeddingClient implements EmbeddingClient { private final RestTemplate restTemplate; private final String embeddingServiceUrl; public LocalEmbeddingClient(Value(${embedding.service.url}) String url) { this.restTemplate new RestTemplate(); this.embeddingServiceUrl url; } Override public ListDouble embed(String text) { // 调用本地部署的向量化微服务 EmbeddingRequest request new EmbeddingRequest(text); EmbeddingResponse response restTemplate.postForObject(embeddingServiceUrl /embed, request, EmbeddingResponse.class); return response.getVector(); } Override public ListListDouble embed(ListString texts) { // 批量处理 // ... } }这样你在Spring AI中使用的VectorStore如Pinecone、Redis、PgVector的集成或任何需要Embedding的地方都会自动使用你这个本地的客户端。5. 性能优化与生产实践要点将Embedding用于生产环境除了效果我们更要关心性能和稳定性。5.1 批量处理与异步化生成Embedding是计算密集型或I/O密集型调用API操作。务必使用批量处理。本地模型如上面的model.encode()传入一个文本列表而不是循环调用。GPU下批量处理能极大提升吞吐量。API调用绝大多数Embedding API如OpenAI、Cohere都支持批量请求通常一次可处理上百条文本能显著减少网络延迟开销。在Spring Boot等Web服务中对于非实时性要求极高的检索请求可以考虑异步生成向量并缓存。例如新文档入库时触发异步任务生成其Embedding并存入向量数据库而不是在用户查询时同步处理。5.2 向量索引的选择与调优Faiss提供了丰富的索引类型选择不当会导致搜索慢或内存爆炸。索引类型原理优点缺点适用场景IndexFlatL2/IP暴力计算精确搜索精度100%速度慢O(n)数据量小10万要求绝对精度IndexIVFFlat聚类倒排列表速度快内存中等需要训练精度有损百万级数据平衡速度与精度IndexHNSW基于图的多层导航速度快精度高内存占用大构建慢千万级数据对精度要求高IndexIVFPQ聚类乘积量化内存占用极小精度损失较大十亿级数据内存受限调参经验使用IndexIVFFlat时nlist参数聚类中心数是关键。通常设置为sqrt(N)N为向量总数的倍数。nprobe参数搜索时探查的聚类数越大精度越高速度越慢。需要在验证集上权衡。HNSW的efConstruction构建时邻接数和efSearch搜索时动态列表大小是核心参数增大它们能提升精度和速度但会增加内存和构建时间。5.3 混合搜索与重排序单纯的向量搜索语义搜索有时会忽略关键词的重要性。混合搜索结合了传统的关键词搜索如BM25和语义搜索能取得更鲁棒的效果。常见架构召回阶段分别用关键词搜索和语义搜索从全库中召回Top K个候选文档例如各召回100个。融合阶段将两份结果列表进行融合。简单的方法可以是加权分数final_score α * bm25_score (1-α) * cosine_score。更复杂的方法可以使用学习排序模型。重排序阶段对融合后的Top N个结果如50个使用更强大但更耗资源的交叉编码器模型如Cross-Encoder进行两两精排得到最终顺序。交叉编码器将查询和文档同时输入模型计算相关性分数比双塔式的Embedding模型更准但无法预先计算只适合小规模重排。这个“召回-粗排-精排”的流水线是工业级搜索系统的标准做法。从静态的词向量到动态的上下文感知向量从简单的余弦相似度到复杂的混合搜索系统Embedding技术已经深入AI应用的骨髓。理解其原理掌握其工具链并能根据实际场景做出合理的选择和优化是当今开发者构建智能应用的一项核心能力。无论是选择调用云端API还是部署本地模型无论是使用Faiss还是Milvus、Weaviate这类新兴向量数据库背后的逻辑都是相通的将人类语言转化为机器可理解的数学空间并在这个空间中进行高效、准确的运算。
返回列表