共现矩阵完全指南:从原理到Python实现与应用场景
2026/9/19 1:59:09 网站建设 项目流程

1. 从"共现"说起:这个矩阵到底在算什么

很多朋友第一次听说"共现矩阵"这四个字,是在做词向量、推荐系统或者文本特征工程的时候。我当时也是,对着教科书上的定义看了半天,什么"统计语料中词对共同出现的次数",字都认识,但完全不知道这东西到底有什么用、算出来长什么样、拿到手能干什么。这篇文章就用最直白的方式把共现矩阵讲透——从它解决什么问题、怎么构建、参数怎么选,到实际怎么用代码写出来,再到踩过的坑,一篇全给你捋明白。

先说结论:共现矩阵是一张记录"哪些东西经常一起出现"的表。如果处理的是文本,"东西"就是词;如果是推荐系统,"东西"就是商品或用户;如果是知识图谱,"东西"就是实体。表格的行和列放着同一批"东西",每一个格子里的数字,就是左边那个"东西"和上面那个"东西"在指定范围内一起出现的次数。就这么简单。

但简单归简单,为什么这个概念几十年了还是NLP、推荐系统、搜索这些领域的基本功?因为"一起出现"这个信号,是我们在没有标注数据的情况下,最容易拿到的、也是最可靠的语义线索之一。一句话里"苹果"和"手机"频繁出现在一起,你就知道这俩在语义上有关联;用户购物车里"薯片"经常和"可乐"一起出现,你就知道这俩适合捆绑推荐。诸如此类。

这篇文章适合谁看?刚入门NLP和数据挖掘的读者、要做推荐系统但搞不清user-item矩阵和共现矩阵区别的人、以及想用共现矩阵做词向量或特征工程但不知道怎么入手的工程师。读完你可以自己从零写出一个共现矩阵的构建脚本,也能说清楚窗口大小、对称化、稀疏处理这些细节到底在解决什么问题。

2. 共现矩阵的核心设计与构建逻辑

2.1 从一个例子理解矩阵结构

先拿一个最经典的场景——文本——来拆解。假设我们有这么一小段语料,就三句话:

我 喜欢 喝 咖啡 我 也 喜欢 茶 咖啡 和 茶 都 不错

如果以"词"为行和列,构建一个"词与词"的共现矩阵,第一步是去重拿到所有不重复的词:我、喜欢、喝、咖啡、也、茶、和、都、不错,一共9个词。那么矩阵就是9×9的方阵。接下来要决定什么叫"共现"。

这里就涉及第一个关键设计:共现的认定范围。最简单的认定方式有几种:

  • 同句共现:只要出现在同一句话里,就算共现一次。
  • 滑动窗口共现:在一个大小固定的上下文窗口(比如中心词左右各2个词)内出现,就算共现。
  • 文档共现:只要出现在同一篇文档里就算共现,常用于文本分类或主题建模。

哪种好?看任务需求。如果拿来做词向量,滑动窗口更好,因为它更贴近"局部上下文"的语义,一句话里隔了三个分句的词是不可能有什么强语义关系的;如果是做推荐系统里的商品共现,"同一订单/同一用户"就是天然窗口;如果是做文本主题分析,同文档共现就有意义。

用滑动窗口来算上面这个例子。设窗口大小为2(左右各2个词),对"咖啡"这个词来说,第一句里它的左右邻居是喜欢(左边2个)和句尾,所以"咖啡"和"喜欢"、"喝"各共现1次。第二句里"咖啡"没出现。第三句里"咖啡"的左边是,右边是不错,所以"咖啡"分别和这四个词再各共现1次。最终统计下来,矩阵里咖啡这一行,喜欢=1,=1,=1,=1,=1,不错=1,其余为0。

看到没有,整个过程就是"遍历语料,数次数"。概念上零门槛,困难在工程处理和参数选择上。

2.2 共现矩阵和词频、TF-IDF有什么不同

这一步不搞清楚,后面很容易用错。不少人把词频向量、TF-IDF向量和共现矩阵混为一谈,实际上它们解决问题的层次完全不同。

  • 词频(Bag of Words):记录每个词在文档中出现了几次,向量只有词一个维度,不关心词和词的顺序或位置关系。
  • TF-IDF:在词频基础上加了逆文档频率权重,降低常用词的干扰,本质还是"文档-词"的矩阵。
  • 共现矩阵:记录的是词对的关系,不再问"某词在某文档里出现几次",而是问"某词和另一个词在上下文中一起出现的频率"。

一句话概括:词频和TF-IDF告诉你"这个词重不重要",共现矩阵告诉你"这个词和哪些词关系近"。

用生活类比的话,词频像是统计每个人手机通讯录里联系人的数量,TF-IDF是给不常联系但很重要的人加了权重,而共现矩阵则是记录"这个人和那个人互相打过多少次电话"——它关注的是关系本身。

2.3 对称化:行列要不要一样

一个容易忽略但很重要的操作:共现矩阵往往需要对称化

什么叫对称化?就是在统计完共现次数后,把矩阵变成对称矩阵,即第i行第j列的值等于第j行第i列的值。原因很简单:如果窗口是对称的,词A出现在词B的窗口内,词B也一定出现在词A的窗口内,所以理论上原始统计结果天然对称。但工程实现里,由于窗口设置不对称(比如只统计左侧或右侧共现)、或者用了有向共现(例如A出现在B左边才计数),矩阵就是非对称的。

具体处理方式一般取两个方向统计值之和:M[i][j] = M[j][i] = Count(i,j) + Count(j,i)。这样构建出的矩阵,后续做相似度计算、矩阵分解或聚类时都会方便很多,因为很多算法默认输入是对称矩阵。

3. 搞清楚窗口、权重和过滤这几个参数

3.1 窗口大小怎么定,为什么5是常用值

滑动窗口的窗口大小,直接决定了"多远算邻居"。窗口越大,共现矩阵越稠密,但语义关联越模糊;窗口越小,矩阵越稀疏,但保留的关系更紧致。

假设一句话是我 昨天 在 星巴克 喝 了 一杯 拿铁,目标词是"星巴克"。窗口=1时,只有和"星巴克"共现;窗口=5时,昨天一杯拿铁全部进入共现统计。

实际项目里窗口取2到10都比较常见,Word2Vec论文用的是5,GloVe论文实验发现窗口6到10在某些任务上效果更好。窗口太小,长距离依赖完全丢失,比如"北京"和"首都"隔着修饰语就统计不到;窗口太大,大量噪声进来,比如"昨天"和"拿铁"这种没有实质语义关系的词对也会被记上一笔。我自己的经验是,先按5跑一版,对比一下按2和10跑出来的词向量效果,再根据具体任务微调。没有绝对最优,只有针对任务调出来的合适。

3.2 距离衰减权重:不是所有邻居都该一视同仁

在统计窗口内的共现时,一个很容易想到的改进是:距离目标词越近的词,和它的语义关联越强;隔得越远的词,关联越弱。响应用这个直觉,我们可以给共现次数加权重——距离1的词权重1,距离2的词权重1/2,距离3的1/3……这种加权方式在GloVe等经典模型中有体现,叫距离衰减权重。

这个操作带来的好处是显著的:拿铁咖啡这种经常贴在一起出现的词对,权重能完整保留;而拿铁这种隔着四五个词才共现的弱关联,权重被压下去,矩阵的语义纯度更高。代价是计算稍微复杂了一些——每统计一个词对,都要先算距离再乘权重。

在工程上,我通常不会对所有场景都用距离衰减。如果只是做推荐系统的商品共现统计,订单里的商品没有天然顺序,就不存在"距离"这个概念,直接用频次计数就行。是否需要距离衰减,取决于你的数据是否有序列关系。有,用;没有,别硬套。

3.3 停用词和低频词怎么处理

任何玩NLP的人对停用词都不陌生。但在共现矩阵这里,停用词问题比词频里更严重:这些词几乎和所有词都共现,如果不处理,它们会把矩阵的统计淹没。比如上面例子里的这种功能性词汇,它们在各种句子结构里反复出现,导致共现矩阵中最高的词对往往是"的-在"、"是-了"这类毫无语义价值的组合。

处理方案分三种层次:

  • 直接过滤:构建一个停用词表,统计时直接跳过。这是最常用的方式,但风险是误删有意义的词,比如"和"在"医生和患者"这种语境里其实是有语义的。
  • 低频过滤:把语料中出现次数少于设定阈值的词直接删掉。比如语料总共1万词,某个词只出现过3次,统计它意义不大,还让矩阵多一行一列的维度。一般min_count取5到10是常见配置。
  • 高频截断:这个相对少见。对出现频率高到离谱的词(比如超过语料句子总量50%),单独降低权重或者特殊处理。

我的建议是:先做低频过滤,再做停用词过滤,顺序不要反。因为有些在语料里只出现几次的"生僻停用词",统计出来后对结果影响很小,过滤不过滤都无所谓,但先把维度降下来可以让后续的停用词表匹配更快。

4. 从零构建共现矩阵:Python实操全记录

4.1 环境准备和数据集选择

先定个技术栈。纯Python实现适合理解原理,生产环境我用的是更工程化的方案,但为了演示逻辑,这里用Python + NumPy + pandas就够了。环境版本也不挑,Python 3.8以上都能跑。

数据集用一个小文本语料就行,我这里构造了20条短文本,涉及运动、音乐、科技三个主题,用来演示效果已经足够。如果你手头有真实语料,格式上只需要保证是一个列表,里面每个元素是一句话/一段文本。

import jieba import re from collections import defaultdict import numpy as np import pandas as pd corpus = [ "我喜欢打篮球和跑步", "篮球比赛需要团队配合", "跑步能增强心肺功能", "听音乐可以放松心情", "古典音乐和流行音乐各有魅力", "程序员喜欢边写代码边听音乐", "人工智能正在改变编程方式", "机器学习是人工智能的核心方向", "写代码需要逻辑思维", "智能手机让生活更方便", "科技公司都在布局人工智能", "运动让人保持健康状态", "音乐节吸引了大量年轻人", "篮球运动员需要体能训练", "最近在学吉他弹唱", "数据科学和机器学习密不可分", "跑步锻炼注意膝盖保护", "编程语言排行榜不断变化", "新技术改变了音乐创作方式", "团队运动讲究战术配合" ]

4.2 数据预处理:分词和清洗

中文做共现矩阵,分词是绕不开的一步。用jieba做基础分词,同时清洗掉标点、数字和不必要的空格。这里有个小细节——分词结果直接影响共现质量。比如"篮球比赛"如果被切成一个整体词,它和其他词的共现统计的是"篮球比赛"这个完整词的关系;如果切成"篮球"和"比赛"两个词,共现统计的就是更细粒度的关系。没有对错,取决于下游任务需要什么粒度。

def preprocess(text): text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', text) words = jieba.lcut(text) return [w for w in words if w.strip() and w not in stopwords]

停用词表我这里就不全列了,网上随便下个中文停用词表,或者自己维护一个几十个词的小表,把这些高频虚词加进去就好。重点强调一个容易踩的坑:分词和清洗时不要把英文字母全部干掉,比如AIPython这种词在科技类语料里是有效信号,我在清洗时用正则保留英文就是为了这个。

4.3 滑动窗口统计共现次数

核心逻辑来了。用窗口大小为2来统计共现。实现思路是:先对每个句子分词得到词序列,然后遍历序列中的每个位置作为中心词,再遍历中心词左右一定距离内的邻居词,将(中心词,邻居词)这个词对的出现次数加1。

def build_cooccurrence_matrix(tokens_list, window_size=2, with_distance_weight=False): vocab = set() cooccur_dict = defaultdict(int) for tokens in tokens_list: for i, center in enumerate(tokens): vocab.add(center) # left context for j in range(max(0, i-window_size), i): dist = i - j if with_distance_weight: weight = 1.0 / dist cooccur_dict[(center, tokens[j])] += weight cooccur_dict[(tokens[j], center)] += weight else: cooccur_dict[(center, tokens[j])] += 1 cooccur_dict[(tokens[j], center)] += 1 # right context for j in range(i+1, min(len(tokens), i+window_size+1)): dist = j - i if with_distance_weight: weight = 1.0 / dist cooccur_dict[(center, tokens[j])] += weight cooccur_dict[(tokens[j], center)] += weight else: cooccur_dict[(center, tokens[j])] += 1 cooccur_dict[(tokens[j], center)] += 1 return vocab, cooccur_dict

注意几个细节。上面代码的写法是每遍历一个邻居,就同时给(center, neighbor)(neighbor, center)计数,这就实现了对称化,后面拿到矩阵直接对称矩阵,不用再额外处理。当初我写第一版的时候没做对称化,结果用余弦相似度算出来的矩阵结果全是错的,后来debug半天才发现方向统计不对称导致词向量偏差。

4.4 从词对字典到稠密矩阵

得到cooccur_dict后,下一步就是把稀疏的词典形式转换成矩阵。按词典中所有词的顺序,初始化一个N×N的零矩阵,然后遍历cooccur_dict填入对应的行列即可。

def dict_to_matrix(vocab, cooccur_dict): word_list = sorted(vocab) idx2word = {i: w for i, w in enumerate(word_list)} word2idx = {w: i for i, w in enumerate(word_list)} N = len(word_list) matrix = np.zeros((N, N), dtype=np.float32) for (w1, w2), count in cooccur_dict.items(): i, j = word2idx[w1], word2idx[w2] matrix[i][j] = count return matrix, word2idx, idx2word tokens_list = [preprocess(sent) for sent in corpus] vocab, cooccur_dict = build_cooccurrence_matrix(tokens_list, window_size=2, with_distance_weight=False) matrix, word2idx, idx2word = dict_to_matrix(vocab, cooccur_dict)

运行上面的代码,输出矩阵的形状,比如(N, N)。直接用pandas展示会更直观:

df = pd.DataFrame(matrix, index=idx2word.values(), columns=idx2word.values()) print(df.head(10))

你可以看到"篮球"和"跑步"在矩阵里和运动相关词有共现记录,而它们和"音乐"、"代码"这类词的共现数大概率是0。这本身就是语义关系的体现——共现矩阵不用训练任何模型,纯粹统计就能反映出词之间的关联。

4.5 内存优化:稀疏矩阵是必经之路

上面的代码能跑通,但真实语料一旦稍微大一点,问题就来了。假设词表有5万个词,稠密矩阵需要存储50000 × 50000个float32,也就是50000²×4字节=10GB内存,这还只是词表5万的情形。真实大规模语料词表上几十万很常见,内存直接吃不消。

生产环境我用的是scipy.sparsecoo_matrixcsr_matrix来存储。因为共现矩阵极其稀疏,绝大多数格子都是0,稀疏存储可以把空间占用降低几个数量级。

from scipy.sparse import coo_matrix def dict_to_sparse_matrix(vocab, cooccur_dict): word_list = sorted(vocab) word2idx = {w: i for i, w in enumerate(word_list)} N = len(word_list) rows, cols, data = [], [], [] for (w1, w2), count in cooccur_dict.items(): rows.append(word2idx[w1]) cols.append(word2idx[w2]) data.append(count) matrix = coo_matrix((data, (rows, cols)), shape=(N, N)) return matrix.tocsr(), word2idx

稀疏矩阵的另一个好处是后续做矩阵分解、LSA、GloVe等算法时,直接对接sklearnTruncatedSVDlightning这类库都很方便,不用转换格式。

5. 共现矩阵在哪些场景里真正扛事

5.1 词向量与语义相似度:从统计到语义的桥梁

共现矩阵最经典的用法,是作为词向量的前置步骤。Word2Vec、GloVe这些模型本质上是把共现矩阵里的统计信息压缩成稠密向量。GloVe的核心思想就是直接对共现矩阵建模,用共现概率的比值来学习词向量,论文里明确说了它是基于整个语料的词共现矩阵来训练的。

如果不想引入神经网络,直接拿共现矩阵算词和词的余弦相似度也能做简单的语义相似度任务。比如在上面构造的语料里,"篮球"的行向量和"跑步"的行向量,余弦相似度应该显著高于"篮球"和"吉他"的相似度。原理就是共现模式相近的词,语义也相近。

这种"共现统计 → 相似度"的朴素做法,虽然效果比不上深度模型,但在数据量小、没有GPU的情况下,是一种极其轻量的baseline方案。我在做一些冷启动项目时经常用这个当兜底策略。

5.2 推荐系统:用户行为序列的共现矩阵

共现矩阵在推荐系统里的应用,本质是物与物的关系挖掘。把用户在会话/订单里依次交互的商品序列当成"句子",把商品当成"词",用完全相同的窗口共现逻辑统计"哪些商品经常同时出现"或者"哪些商品经常连续出现"。统计结束后,矩阵里每个商品就有一行向量,可以用余弦相似度找出和当前商品最相近的其他商品——这就是常说的Item-based Collaborative Filtering的一种落地形态。

和NLP里的区别有两个:第一,推荐场景中通常不存在"左右窗口"的概念,用户在购物车里的商品顺序没有强语义,所以一般直接用同会话/同订单共现,窗口大小等于订单长度。第二,推荐场景可以用时间衰减权重——用户上周买的和昨天买的"共现"强度应该不同。这些都是在NLP基础上的变体,核心思想完全一致。

5.3 特征工程:给模型加一维共现信号

在文本分类、情感分析这些任务里,共现矩阵不一定要做主体,也能当特征工程的一部分。一种常见做法是,从共现矩阵中选出和目标词共现最强的Top-K个词,作为该目标的"上下文特征"拼进模型。比如做"苹果"这个实体的情感判别时,如果历史语料显示"苹果"与"好吃"、"新鲜"共现强,而"苹果"与"发布会"、"系统"共现弱,这一信号就能帮模型区分讨论的是水果还是手机。

这个做法的好处是不依赖BERT这类大模型,在传统机器学习模型上就能产生可解释的特征,适合对可解释性要求高的场景。

6. 实操中踩过的坑和排查清单

6.1 共现矩阵常见问题速查

  • 矩阵太稀疏,所有相似度都是0:通常窗口太小或语料太少。解决方案:增大窗口、合并同义词、用降维算法先压缩再算相似度。
  • 高频词噪声太大:可能没做停用词过滤。回看2.3一节,按"低频过滤→停用词过滤"的顺序调整。
  • 对称化没做导致下游结果异常:检查M[i][j]M[j][i]是否相等。统计时直接双向加1是最简单的规避方式。
  • 矩阵内存爆炸:换scipy.sparsecsr_matrix,同时做低频词过滤,把词表控制在合理范围。
  • 分词粒度不一致:比如"机器学习"有时候切成一个词,有时候切成"机器"和"学习"两个词,会导致共现统计碎片化。统一分词策略,最好固定一个分词工具和词表。

6.2 一个真实调试案例:词对统计值莫名不对

有次用共现矩阵做新闻语料的主题词挖掘,发现"北京"和"首都"明明经常一起出现,共现次数却很低,反而"北京"和"的"的共现次数奇高。查了半天,问题出在窗口大小上——窗口设得太大,"的"这种功能词频繁进入各种词的窗口,淹没了真正的语义词对。后来用距离衰减权重,又对停用词做了严格过滤,高频虚词的噪声立刻被压下去,"北京"和"首都"这类的共现排名才恢复正常。

这次经历给我的教训是:共现矩阵的效果,参数的敏感度排序大约是 停用词处理 > 窗口大小 > 距离权重。优先把停用词处理干净,再去调其他参数。

6.3 我的参考配置

给一个起点配置,后续按自己场景微调:

参数参考值说明
窗口大小2~5NLP语义任务取5,推荐场景等同会话长度
min_count5词频低于5的词直接过滤
距离衰减建议开启对文本序列任务有效
对称化必须开启生产环境默认强制
存储格式scipy.sparse.csr词表上万后必须用稀疏格式

7. 进一步扩展的思路:PMI、降维与神经网络

共现矩阵本身只是"统计的起点",搭配几个经典的后续处理,能把这个工具的价值放大很多倍。

**PMI(点互信息)**是最常见的扩展之一。原始共现次数有一个问题:两个高频词天然共现次数高,比如"我"和"你",但这不代表它们语义关联强。PMI的公式是PMI(w1,w2)=log[P(w1,w2)/(P(w1)P(w2))],相当于用"实际共现概率"除以"如果独立会有的期望共现概率",比值越大,说明这两个词超越偶然性、具有真实关联的强度越高。在NLP任务里,PMI加权的共现矩阵往往比纯次数矩阵效果更好。

矩阵降维是另一个方向。直接拿共现矩阵算词相似度,维度高、噪声多。用TruncatedSVD把几万维压缩到几百维,得到的低维向量就能当作词向量来用。这个思路其实就是传统LSA(Latent Semantic Analysis),在信息检索时代是非常主流的方案。和Word2Vec相比,LSA训练快、结果可复现,但精度稍逊。

GloVe则是把共现矩阵和神经网络结合的一个经典模型,核心思想是直接以共现矩阵的统计信息作为学习目标,学习词向量的内积能拟合共现概率的对数。在实际项目中,如果数据量不大,直接用GloVe预训练权重是个很省事的方案。

我在实际使用中的体会是,共现矩阵最大的价值在于它的可解释性。深度学习模型给你的是一个黑盒网络,共现矩阵给的是"这个词和那个词有多少次出现在一起"的明明白白的证据。在需要对结果做解释、做审计、做调试的场景里,共现矩阵是任何复杂模型都无法替代的底稿。就算是现在大模型满天飞的阶段,我在做语料分析时还是会先跑一版共现矩阵,花5分钟摸清数据的底细,再决定下一步怎么走。最后再分享一个小技巧:给你的共现矩阵做一版"Top-5共现词输出",检查一下每个中心词的前5个关联词是否符合直觉,这是判断预处理和参数是否合理的最快方式。

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

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

立即咨询