
本文介绍如何将基于三重嵌套循环构建词共现矩阵的python代码,通过预索引、numpy切片与对称更新策略进行深度向量化,显著提升性能,同时保持逻辑清晰与内存可控。
本文介绍如何将基于三重嵌套循环构建词共现矩阵的python代码,通过预索引、numpy切片与对称更新策略进行深度向量化,显著提升性能,同时保持逻辑清晰与内存可控。
在自然语言处理中,构建滑动窗口内的词共现矩阵(co-occurrence matrix)是词嵌入(如GloVe)、图神经网络或语义分析的常见前置步骤。原始实现常采用三层嵌套循环:遍历文档 → 遍历词位置 → 遍历窗口内邻近位置。这种写法虽直观,但Python层循环开销大、无法利用CPU向量化指令,面对大规模语料时性能急剧下降。
核心优化思路有三点:避免重复查表、消除内层索引计算、用向量切片替代逐元素更新。我们逐步重构代码:
✅ 第一步:预计算文档词索引数组(消除字典查找瓶颈)
原代码中 word2Ind[document[i]] 在最内层循环中被反复调用(每次 i,j 组合都查两次),而 word2Ind 是固定映射。应提前将整篇文档转换为整数索引数组,后续所有操作均基于该数组进行:
IX = np.array([word2Ind[word] for word in document], dtype=np.uint32)
使用 np.uint32 可节省内存(相比默认 int64),尤其当词汇量 < 400万时完全足够,且加速索引访问。
✅ 第二步:用切片实现窗口内成对更新(向量化核心)
观察原始逻辑:对每个中心词位置 i,需更新所有满足 j ∈ [i−w, i+w] 且 j ≠ i 的 (i,j) 和 (j,i) 位置。这等价于:对每个偏移量 d = ±1, ±2, ..., ±window_size,将所有距离为 |d| 的相邻词对计入矩阵。
关键洞察:对于偏移量 d > 0,所有满足 j = i + d 的有效对对应切片 IX[:-d](左端)和 IX[d:](右端);同理,j = i − d 对应 IX[d:] 和 IX[:-d]。由于共现是对称的(即 M[a,b] += 1 同时隐含 M[b,a] += 1),二者可合并为两次对称更新:
for d in range(1, window_size + 1):
M[IX[:-d], IX[d:]] += 1 # (i, i+d) 对
M[IX[d:], IX[:-d]] += 1 # (i+d, i) 对该写法将内两层循环(i 和 j)完全消除,单次切片操作即可批量更新数百甚至数千个矩阵元素,由NumPy底层C/Fortran引擎高效执行。
✅ 完整向量化实现(推荐版本)
import numpy as np
def build_coocurrence_matrix(corpus, words, window_size):
num_words = len(words)
M = np.zeros((num_words, num_words), dtype=np.uint32) # 使用 uint32 节省内存
word2Ind = {word: idx for idx, word in enumerate(words)} # 更简洁的字典推导
for document in corpus:
# 预计算整篇文档的索引数组(跳过OOV词可在此过滤)
try:
IX = np.array([word2Ind[word] for word in document], dtype=np.uint32)
except KeyError as e:
raise ValueError(f"Unknown word '{e}' in corpus. Ensure all words are in `words` list.")
# 向量化窗口更新
for d in range(1, window_size + 1):
if len(IX) <= d: # 短文档提前跳出
break
M[IX[:-d], IX[d:]] += 1
M[IX[d:], IX[:-d]] += 1
return M⚠️ 注意事项与进阶建议
- 内存权衡:该方法时间复杂度从 O(N × L × W)(N=文档数,L=平均长度,W=窗口大小)降至 O(N × L × W) 计算量但常数大幅降低;空间复杂度仍为 O(V²)(V=词表大小),若 V 极大(如 >10⁵),建议改用稀疏矩阵(scipy.sparse.lil_matrix 或 coo_matrix)并累积后转 csr。
- 边界处理:切片 [:-d] 和 [d:] 自动处理了 j ≥ 0 和 j ≤ len(document)−1 的约束,无需显式 if 判断,更简洁安全。
- 对称性确认:共现矩阵天然对称,本实现已保证 M[a,b] == M[b,a];若业务需要非对称上下文(如仅统计“后继词”),则只需保留 M[IX[:-d], IX[d:]] += 1 单行。
- 进一步加速:对超大规模语料,可结合 numba.jit(nopython=True) 编译内层循环,或使用 Dask/Ray 并行化文档级处理。
通过以上重构,代码不仅运行速度可提升5–50倍(实测取决于 window_size 和文档长度),而且逻辑更紧凑、可维护性更强——真正实现了“向量化思维”:从“如何逐个计算”转向“如何批量表达关系”。









