ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python词嵌入语义距离计算与语义投影降维可视化源码实战

Python词嵌入语义距离计算与语义投影降维可视化源码实战 简介本资源面向深度学习与自然语言处理方向的初学者及毕业设计开发者提供一套基于词嵌入模型的语义距离与语义投影分析工具包同时兼顾传统文本分析方法。项目以Python实现涵盖文本统计指标、词典构建、文本相似度与认知分析四大模块具体包括词频统计、可读性分析、情感分析、互信息与词向量词典扩充、余弦与Jaccard及编辑距离相似度计算以及基于词向量的语义距离和语义投影分析可用于态度与偏见等认知方向研究。压缩包共95个文件约42MB包含13个ipynb示例笔记、7个py源码、18个pkl内置词典、20个txt语料及14张png图表并附说明文档与论文引用文件目录结构清晰便于按模块查阅与复现。目前已有75人学习下载适合需要完整项目源码、实验笔记与词典资源的读者参考使用。1. 词嵌入语义距离与投影一份 Python 源码能帮你解决什么搜索“python 词嵌入模型 语义距离 语义投影 源码”的人多半已经过了调包的阶段。你可能已经用gensim或sentence-transformers把词向量跑出来了但真正卡住的地方在于两个词的语义距离到底怎么算才靠谱把高维向量投影到二维平面上为什么相似词反而离得远这份源码加说明文档的组合核心就是回答这两个问题——它把语义距离的度量方式和语义投影的降维可视化串成了一条可复现的链路。适合谁做文本聚类、语义搜索、推荐召回或者单纯想搞明白“词向量空间里到底发生了什么”的 Python 开发者。下面我按自己复现这类项目的顺序把选型、代码、参数和踩过的坑讲清楚。2. 语义距离怎么算从余弦相似度到欧氏距离的选型逻辑2.1 为什么余弦相似度是默认首选词嵌入模型Word2Vec、GloVe、FastText训练出来的向量其绝对长度往往受词频影响高频词的模长普遍偏大。如果直接用欧氏距离两个语义高度相关但词频差异大的词距离会被模长差异拉偏。余弦相似度只看向量夹角天然屏蔽了模长干扰所以在语义距离任务里它是默认起点。但余弦相似度不是万能的。当你需要做“语义投影”时投影后的坐标是真实空间位置这时候距离的绝对尺度就有意义了。我的做法是相似度排序用余弦投影前的距离矩阵用归一化后的欧氏距离。这样既保留了方向信息又让投影算法如 t-SNE、PCA拿到尺度一致的输入。import numpy as np from numpy.linalg import norm def cosine_similarity(v1, v2): 余弦相似度只看向量方向屏蔽模长 return np.dot(v1, v2) / (norm(v1) * norm(v2) 1e-10) def euclidean_distance(v1, v2): 欧氏距离对模长敏感适合归一化后使用 return norm(v1 - v2) def normalize_vector(v): L2 归一化把向量投影到单位球面 return v / (norm(v) 1e-10) # 示例假设 vec_a, vec_b 是 300 维词向量 # cos_sim cosine_similarity(vec_a, vec_b) # euc_dist euclidean_distance(normalize_vector(vec_a), normalize_vector(vec_b))上面代码里1e-10是防止零向量导致除零。归一化函数是投影前的必备步骤很多人跳过这步直接跑 t-SNE结果就是相似词散落在不同簇里——这是血泪经验。2.2 距离矩阵的构建与批量计算单个词对的距离算起来简单但语义投影需要的是全量距离矩阵。假设你选了 N 个词就要算 N×N 的距离矩阵。这里有个性能坑用 Python 循环算 5000 个词的矩阵可能要跑好几分钟。正确做法是用 NumPy 的广播机制一次性算完。def pairwise_cosine_matrix(vectors): 批量计算余弦距离矩阵 vectors: shape (N, D) 的归一化词向量矩阵 返回: shape (N, N) 的余弦距离矩阵1 - 余弦相似度 # 归一化后余弦相似度 点积 normed vectors / (np.linalg.norm(vectors, axis1, keepdimsTrue) 1e-10) sim_matrix np.dot(normed, normed.T) # 转成距离相似度越高距离越小 dist_matrix 1.0 - sim_matrix # 对角线置零避免浮点误差导致负值 np.fill_diagonal(dist_matrix, 0.0) return np.clip(dist_matrix, 0.0, 2.0) def pairwise_euclidean_matrix(vectors): 批量欧氏距离矩阵适用于已归一化的向量 normed vectors / (np.linalg.norm(vectors, axis1, keepdimsTrue) 1e-10) # 利用 (a-b)^2 a^2 b^2 - 2ab归一化后 a^2b^21 sq_dist 2.0 - 2.0 * np.dot(normed, normed.T) return np.sqrt(np.clip(sq_dist, 0.0, None))np.clip那一步很关键。浮点运算会让本该为 0 的对角线出现-1e-16这种值后续投影算法遇到负距离可能直接报错或产生玄学结果。参数上1e-10是经验值如果你的向量模长普遍小于 1e-5要适当调大。2.3 距离度量的选择对照表度量方式适用场景对模长敏感计算复杂度投影前是否需归一化余弦相似度语义相似排序、检索召回否O(D)否欧氏距离聚类、投影坐标计算是O(D)是曼哈顿距离高维稀疏向量是O(D)建议是点积模型内部打分是O(D)否选型建议做语义距离展示用余弦做语义投影输入用归一化欧氏。别混用混用就是翻车现场。3. 语义投影怎么做PCA、t-SNE 与 UMAP 的落地对比3.1 投影的本质与三种算法的取舍语义投影就是把 D 维词向量通常 100~300 维压到 2 维或 3 维让人眼能看见。常见做法有三种PCA、t-SNE、UMAP。PCA 是线性投影速度快但会丢失非线性结构t-SNE 擅长保留局部邻域适合看聚类但全局结构容易失真UMAP 在局部和全局之间平衡得更好速度也比 t-SNE 快。我一般会先用 PCA 快速看整体分布再用 UMAP 出最终图。t-SNE 只在需要严格复现论文结果时用因为它对超参数太敏感perplexity调错一次图就完全变样。from sklearn.decomposition import PCA import umap from sklearn.manifold import TSNE def project_pca(vectors, n_components2): PCA 线性投影适合快速预览 pca PCA(n_componentsn_components, random_state42) return pca.fit_transform(vectors) def project_tsne(vectors, n_components2, perplexity30): t-SNE 非线性投影perplexity 通常取 5~50 tsne TSNE( n_componentsn_components, perplexityperplexity, random_state42, initpca, # 用 PCA 初始化比随机初始化稳定 learning_rateauto ) return tsne.fit_transform(vectors) def project_umap(vectors, n_components2, n_neighbors15, min_dist0.1): UMAP 投影n_neighbors 控制局部/全局平衡 reducer umap.UMAP( n_componentsn_components, n_neighborsn_neighbors, min_distmin_dist, random_state42 ) return reducer.fit_transform(vectors)initpca这个参数是 t-SNE 的后悔药。默认随机初始化时同一份数据跑两次结果可能完全不同加上 PCA 初始化后结果可复现。learning_rateauto是较新版本 sklearn 的推荐值老版本需要手动设成max(200, n_samples/12)。3.2 投影前的向量预处理步骤直接拿原始词向量跑投影大概率得到一团糊。正确流程是筛选词表 → 归一化 → 去重 → 降维。筛选词表时别把所有词都塞进去选你关心的领域词控制在 500~2000 个。词太多投影图上的点会重叠到无法辨认。def prepare_vectors_for_projection(word_vectors, target_words, dim300): word_vectors: dict, {词: 向量} target_words: list, 你要投影的词列表 返回: (词列表, 向量矩阵) valid_words [] vectors [] for w in target_words: if w in word_vectors: vec word_vectors[w] if len(vec) dim: valid_words.append(w) vectors.append(vec) vectors np.array(vectors, dtypenp.float32) # L2 归一化 norms np.linalg.norm(vectors, axis1, keepdimsTrue) vectors vectors / (norms 1e-10) return valid_words, vectorsdim参数必须和你的词向量模型一致。Word2Vec 常见 100/200/300 维GloVe 有 50/100/200/300 维。维度对不上会直接报错或者更隐蔽地产生错误结果。dtypenp.float32能省一半内存投影算法对精度要求没那么高。3.3 投影结果的定量验证方法投影图好看不代表投影正确。我习惯用两个指标验证邻域保持率和距离相关性。邻域保持率看的是在高维空间里互为最近邻的词对投影后是否仍然靠近。距离相关性看的是高维距离和低维距离的秩相关系数。from scipy.stats import spearmanr from sklearn.neighbors import NearestNeighbors def neighborhood_preservation(high_dim, low_dim, k10): 计算 k 近邻保持率 nbrs_high NearestNeighbors(n_neighborsk1).fit(high_dim) nbrs_low NearestNeighbors(n_neighborsk1).fit(low_dim) _, idx_high nbrs_high.kneighbors(high_dim) _, idx_low nbrs_low.kneighbors(low_dim) preserve 0 total 0 for i in range(len(high_dim)): set_high set(idx_high[i][1:]) set_low set(idx_low[i][1:]) preserve len(set_high set_low) total k return preserve / total def distance_correlation(high_dim, low_dim, sample_size500): 计算高维与低维距离的 Spearman 秩相关 idx np.random.choice(len(high_dim), min(sample_size, len(high_dim)), replaceFalse) high_sample high_dim[idx] low_sample low_dim[idx] from scipy.spatial.distance import pdist high_dist pdist(high_sample) low_dist pdist(low_sample) corr, _ spearmanr(high_dist, low_dist) return corr邻域保持率低于 0.7说明投影把原本相似的词拆散了需要调n_neighbors或换算法。距离相关性低于 0.5说明全局结构失真严重这时候投影图只能看局部聚类不能用来判断远距离词的关系。4. 避坑与排查语义距离和投影的 5 个常见翻车点4.1 现象相似词的余弦相似度算出来是负数原因词向量没有归一化或者用了未对齐的向量空间。不同模型训练出的向量空间是独立的直接跨模型算相似度没有意义。解决确认所有向量来自同一个模型、同一份词表。算相似度前先做 L2 归一化归一化后余弦相似度范围是 [-1, 1]出现负数说明两个词方向相反这在语义上通常意味着不相关或反义。4.2 现象t-SNE 投影图每次运行都不一样原因t-SNE 默认随机初始化且对perplexity和learning_rate敏感。解决设random_state42固定随机种子设initpca用 PCA 初始化设learning_rateauto。三件套加上后同一份数据的结果基本可复现。如果还是不稳定把perplexity调到sqrt(n_samples)附近。4.3 现象投影图上所有点挤成一团原因向量维度太高且没有做归一化或者min_dist设得太小。解决先做 L2 归一化再把 UMAP 的min_dist从默认 0.1 调到 0.3~0.5n_neighbors从 15 调到 30~50。如果还挤说明词表里语义太集中需要扩大词表范围或换更分散的词。4.4 现象距离矩阵出现 NaN 或 Inf原因词向量里有零向量或全零行归一化时除零。解决在归一化前检查np.linalg.norm(vectors, axis1)是否有 0有就剔除对应词。代码里统一加1e-10是兜底但最好从源头过滤。4.5 现象投影后语义相近的词反而离得远原因用了欧氏距离但没归一化或者投影算法把局部结构牺牲了。解决投影输入统一用归一化后的向量。如果用的是 t-SNE调大perplexity让算法关注更大范围的邻域。如果用的是 PCA换 UMAP 或 t-SNE因为 PCA 是线性的处理不了非线性语义关系。5. 进阶技巧用语义投影做词表诊断与模型对比投影不只是可视化它还能当诊断工具用。我常用的一个技巧是把两个不同模型比如 Word2Vec 和 FastText对同一批词的投影叠在一起看。如果两个模型的投影结构高度相似说明它们学到的语义关系一致如果差异很大说明其中一个模型可能训练不充分或语料领域不匹配。具体做法是对同一批词分别用两个模型提取向量各自做 UMAP 投影然后用 Procrustes 分析对齐两个投影结果计算对齐后的残差。残差越小模型一致性越高。from scipy.spatial import procrustes def compare_projections(vectors_a, vectors_b): 对比两个模型对同一批词的投影结构 返回: 残差越小越一致 proj_a project_umap(vectors_a) proj_b project_umap(vectors_b) # Procrustes 对齐标准化 旋转 缩放 _, _, disparity procrustes(proj_a, proj_b) return disparity # 使用示例 # disparity compare_projections(vecs_model_a, vecs_model_b) # disparity 0.1 说明两个模型语义结构高度一致 # disparity 0.3 说明模型差异大需要检查训练配置procrustes返回的disparity是残差平方和通常 0.1 以下算高度一致0.1~0.3 算中等超过 0.3 就要警惕了。这个技巧在选型阶段特别有用你不需要跑完整的下游任务就能快速判断哪个模型的语义空间更靠谱。另一个进阶用法是投影轨迹追踪。如果你有随时间变化的词向量比如不同年份的语料训练出的模型可以把同一个词在不同年份的投影点连成线观察它的语义漂移方向。我一般会固定 UMAP 的random_state和n_neighbors保证不同年份的投影在同一坐标系下可比。这个做法在分析新词演化或领域术语变迁时比看相似度数值直观得多。最后说个我自己的习惯每次做完投影我都会把邻域保持率和距离相关性记下来和上一次的数值对比。如果这两个指标突然下降多半是词表或向量预处理出了问题而不是投影算法本身。这个习惯帮我省了很多排查时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进