☰
Python词嵌入语义距离计算与语义投影降维可视化源码实战
2026/10/3 10:25:15 网站建设 项目流程

简介:本资源面向深度学习与自然语言处理方向的初学者及毕业设计开发者,提供一套基于词嵌入模型的语义距离与语义投影分析工具包,同时兼顾传统文本分析方法。项目以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, axis=1, keepdims=True) + 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, axis=1, keepdims=True) + 1e-10) # 利用 (a-b)^2 = a^2 + b^2 - 2ab,归一化后 a^2=b^2=1 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_components=2): """PCA 线性投影,适合快速预览""" pca = PCA(n_components=n_components, random_state=42) return pca.fit_transform(vectors) def project_tsne(vectors, n_components=2, perplexity=30): """t-SNE 非线性投影,perplexity 通常取 5~50""" tsne = TSNE( n_components=n_components, perplexity=perplexity, random_state=42, init='pca', # 用 PCA 初始化,比随机初始化稳定 learning_rate='auto' ) return tsne.fit_transform(vectors) def project_umap(vectors, n_components=2, n_neighbors=15, min_dist=0.1): """UMAP 投影,n_neighbors 控制局部/全局平衡""" reducer = umap.UMAP( n_components=n_components, n_neighbors=n_neighbors, min_dist=min_dist, random_state=42 ) return reducer.fit_transform(vectors)

init='pca'这个参数是 t-SNE 的后悔药。默认随机初始化时,同一份数据跑两次结果可能完全不同,加上 PCA 初始化后结果可复现。learning_rate='auto'是较新版本 sklearn 的推荐值,老版本需要手动设成max(200, n_samples/12)。

3.2 投影前的向量预处理步骤

直接拿原始词向量跑投影,大概率得到一团糊。正确流程是:筛选词表 → 归一化 → 去重 → 降维。筛选词表时,别把所有词都塞进去,选你关心的领域词,控制在 500~2000 个。词太多,投影图上的点会重叠到无法辨认。

def prepare_vectors_for_projection(word_vectors, target_words, dim=300): """ 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, dtype=np.float32) # L2 归一化 norms = np.linalg.norm(vectors, axis=1, keepdims=True) vectors = vectors / (norms + 1e-10) return valid_words, vectors

dim参数必须和你的词向量模型一致。Word2Vec 常见 100/200/300 维,GloVe 有 50/100/200/300 维。维度对不上会直接报错,或者更隐蔽地产生错误结果。dtype=np.float32能省一半内存,投影算法对精度要求没那么高。

3.3 投影结果的定量验证方法

投影图好看不代表投影正确。我习惯用两个指标验证:邻域保持率和距离相关性。邻域保持率看的是:在高维空间里互为最近邻的词对,投影后是否仍然靠近。距离相关性看的是:高维距离和低维距离的秩相关系数。

from scipy.stats import spearmanr from sklearn.neighbors import NearestNeighbors def neighborhood_preservation(high_dim, low_dim, k=10): """计算 k 近邻保持率""" nbrs_high = NearestNeighbors(n_neighbors=k+1).fit(high_dim) nbrs_low = NearestNeighbors(n_neighbors=k+1).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_size=500): """计算高维与低维距离的 Spearman 秩相关""" idx = np.random.choice(len(high_dim), min(sample_size, len(high_dim)), replace=False) 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_state=42固定随机种子,设init='pca'用 PCA 初始化,设learning_rate='auto'。三件套加上后,同一份数据的结果基本可复现。如果还是不稳定,把perplexity调到sqrt(n_samples)附近。

4.3 现象:投影图上所有点挤成一团

原因:向量维度太高且没有做归一化,或者min_dist设得太小。

解决:先做 L2 归一化,再把 UMAP 的min_dist从默认 0.1 调到 0.3~0.5,n_neighbors从 15 调到 30~50。如果还挤,说明词表里语义太集中,需要扩大词表范围或换更分散的词。

4.4 现象:距离矩阵出现 NaN 或 Inf

原因:词向量里有零向量或全零行,归一化时除零。

解决:在归一化前检查np.linalg.norm(vectors, axis=1)是否有 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,保证不同年份的投影在同一坐标系下可比。这个做法在分析新词演化或领域术语变迁时,比看相似度数值直观得多。

最后说个我自己的习惯:每次做完投影,我都会把邻域保持率和距离相关性记下来,和上一次的数值对比。如果这两个指标突然下降,多半是词表或向量预处理出了问题,而不是投影算法本身。这个习惯帮我省了很多排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询