
做文本挖掘的人迟早会遇到一个需求把一堆文本里反复出现的关键词理成一张清晰的关系图直观看出哪些词总是扎堆出现、谁和谁关系最铁。这个场景里共现矩阵和共现图就是绕不开的两个核心工具。共现矩阵负责把“谁和谁在一起出现”这件事老老实实统计成数字共现图则把这些数字变成一张能一眼看懂的网。很多人卡在中间矩阵建好了却不知道怎么转成图或者转完图发现乱成一团根本没法用。这篇文章我从头到尾讲一遍从语料清洗、窗口选择、矩阵构建到矩阵转图的三种路径再到可视化和排查把这条链路一次性走通。适合刚接触文本挖掘的初学者也适合已经在做词向量、主题聚类、舆情分析但没系统整理过共现流程的从业者。1. 先讲明白共现矩阵和共现图到底分别解决什么问题1.1 共现矩阵的本质是一张“关系统计表”共现矩阵的逻辑非常简单给定一个语料集合统计任意两个词在指定的上下文范围内共同出现的次数。比如“苹果”和“安卓”在一句话里同时出现那矩阵里这两个词对应的格子就加1。统计对象可以是词、短语、实体也可以是分类标签、商品类目只要是离散的符号都能放进去。为什么不直接统计词频就行了因为词频只告诉你“这个词本身出现了多少次”却不知道“这个词和别的词是什么关系”。很多场景需要的是关系信息搜索引擎要做查询推荐新闻客户端要做关联阅读电商平台要做搭配购分析靠的都是“词与词之间是否频繁同现”这一层信号。共现矩阵把这种信号量化成数值是后续一切分析的地基。一个标准的共现矩阵行和列都是同一组词。理论上矩阵规模是“词表大小的平方”。如果词表有1万个词矩阵元素就是1亿个所以实际工程里几乎没人用稠密矩阵存基本都是稀疏存储这也是后面转图时第一个要想到的点。1.2 共现图把数字变成可感知的网络结构矩阵的优势是精确、可计算但劣势也很明显1万乘1万的矩阵人眼根本看不出结构。把矩阵转为图之后词变成节点共现关系变成连边共现次数变成边权重。此时聚类结构、关键节点、桥梁词、孤立词等信息都一目了然。图结构带来的红利是“算法视角”的改变。矩阵适合算相似度、做主成分、做聚类图则适合做社区发现、中心性分析、路径分析。一个典型的场景舆情分析中把高频词转成共现图用Louvain算法做社区检测能自动分出“合同纠纷”“技术故障”“价格争议”等话题群比直接看词频表高效得多。所以我的观点是共现矩阵负责“算”共现图负责“看”两者缺一不可。矩阵是图的产生基础图是矩阵的表达升华一套完整流程里它们本来就是连贯的。对比维度共现矩阵共现图数据形态二维稀疏矩阵节点和边组成的网络信息视角数值统计视角拓扑结构视角典型算法降维、聚类、相似度计算社区发现、中心性、路径分析人眼可读性差好存储复杂度空间占用高依赖稀疏存储依赖节点数和边数通常更轻2. 从原始语料到共现矩阵构建细节全拆解2.1 语料清洗和分词是决定矩阵质量的上游环节共现矩阵是“垃圾进垃圾出”的典型代表。如果语料没洗干净分词结果乱七八糟后面统计出来的共现关系就是噪音。我处理评论语料的一般顺序是这样去除无用字符HTML标签、URL、邮箱、特殊符号用正则表达式一次清掉。统一英文大小写并保留中英文混合中文场景常混着英文品牌词要统一成小写避免“NLP”和“nlp”被当成两个词。去重和去短文本重复评论会放大某些共现频次导致矩阵失真过短文本缺乏有效上下文也会稀释统计质量。分词和词性过滤中文用 jieba 分词英文用 nltk 或 spaCy。分词后一般只保留名词、动词、形容词等有实义的词过滤停用词、单字虚词、数字和纯符号。分词环节最容易踩的坑是“词典不完整”。行业术语、品牌词、网络新词往往被错误切分比如“吃鸡”被切成“吃”和“鸡”“区块链”被切成“区块”和“链”。解决办法是维护一份自定义词典在 jieba 里用jieba.load_userdict()加载分词前先强制把这些词作为整体识别出来。清洗完成后要生成一个“文本列表”每个元素是分词后的词序列这是构建共现矩阵的标准输入格式。我习惯把所有过滤后的词用空格连接保存成临时文件既方便调试也方便后面换窗口参数重跑不用重新做一遍清洗。2.2 窗口大小怎么定算法参数背后的业务逻辑共现统计依赖一个关键参数——“共现窗口”。它的定义是在目标词左右两侧各多少个词的范围里出现的词算作与目标词共现。窗口大小直接影响矩阵的语义粒度。窗口为1时只统计直接相邻的词对捕捉的是搭配关系比如“机器”和“学习”几乎总是相邻出现。窗口为5或10时统计的是宽松同现捕捉的是主题关系比如一篇文章里“机器学习”和“算法”虽然没有紧挨着但在同一段落反复出现。怎么选我的经验是如果目的是做词汇搭配或输入法联想窗口选2到3如果目的是做主题发现、话题聚类窗口选5到10如果语料句子普遍较短窗口可以适当缩小否则窗口跨越了句子边界统计进一堆不相关的词对。滑动窗口还有两个细节值得注意。第一是否按句切分后统计。我建议先按句号、问号、叹号切句在单句内部滑动窗口避免跨句产生虚假同现。第二窗口是否对称。左右两侧窗口大小可以不同在某些场景下单向共现也有意义但绝大多数场景是左右对称更好解释。2.3 共现次数的两种表达原始频次和PPMI矩阵里的数字可以有两种形态理解了它们的区别后面转图和选阈值的逻辑会顺很多。第一种是原始共现频次。好处是直观“苹果”和“手机”共现了100次就是100次。坏处是受词频影响太大高频词之间哪怕没什么实际关联也会因为各自单独出现次数多而显得关系强。比如“的”如果没被完全过滤几乎和所有词都共现整张图会变成一坨以“的”为中心的蜘蛛网。第二种是PPMI也就是正点互信息。它能有效抑制高频词的干扰。PPMI的计算逻辑是比较“两个词实际上一起出现的概率”和“假设它们完全独立时应该一起出现的概率”之间的差距。PPMI的公式是PPMI(w1, w2) max(0, log( P(w1, w2) / (P(w1) * P(w2)) ))其中P(w1, w2)是两个词共现概率P(w1)和P(w2)分别是各自出现的边际概率。如果两个词共现次数高于独立假设PPMI为正如果低于或等于独立假设归一化到0。PPMI为0的词对通常可以直接丢弃因为它们在统计意义上没有超出随机期望的关系。用PPMI替代原始频次图网络中的边权会更贴近“真实语义关联强度”。比如“的”因为和所有词共现算出来的PPMI反而很低连边会自然被过滤掉这是很多图分析项目中最实用的一个技巧。3. 矩阵转图的三种实现路径各取所需3.1 纯手工实现用字典和列表搭出邻接表在没有任何图处理库的情况下从零构建共现图并不复杂。共现矩阵的稀疏本质决定了我们完全可以用字典来替代二维数组。以词为键内部再嵌套一个字典记录目标词与哪些词共现、权重是多少。借这种结构从矩阵到图的映射几乎一一对应外层字典的键就是图节点内层字典的键就是目标节点的邻居节点内层字典的值就是边权重。这种手工方案的优势是零依赖适合放在线上服务里也适合自定义复杂权重逻辑。写代码时的注意点节点集合要单独维护一份因为有些词虽然出现频次不低但没有任何共现对象最终不会出现在邻接表中可它们仍然需要作为孤立节点被保留到图里。转图时如果不单独处理这类词就会被静默丢弃导致最终图谱少一批真实存在的节点。3.2 调用现成图算法库NetworkX的快速路径如果项目允许引入第三方库最省心的方案是用 NetworkX。它是Python生态里最主流的复杂网络分析库矩阵转图只需要一行核心操作。操作分两步先把共现矩阵转为pandas DataFrame行索引和列索引都是词然后调用nx.from_pandas_adjacency()函数DataFrame直接变成无向加权图。如果共现矩阵是用numpy数组保存的对应函数是nx.from_numpy_array()但要提前把词表整列传入并重命名节点不然后续处理时节点只能看到数字编号。为什么优先用NetworkX而不是自己手写因为它内置了大量图算法Louvain社区发现、PageRank、度中心性、介数中心性都有现成接口。转图只是第一步后面做社区分析时这些算法能省大量时间。另外NetworkX可以轻松导出成GraphML或GEXF格式给可视化工具使用。3.3 可视化工具终局Gephi和Cytoscape的作用边界如果目标是发表级别或汇报级别的图单靠Python绘图往往不够好看。Gephi 是这类需求的经典选择导出GraphML后直接导入就能通过高斯分布布局、模块化统计、边权重映射等功能做出一张信息量饱满的可视化图。Cytoscape 更偏生物网络分析但功能上也能处理普通共现图且插件生态丰富。如果你的分析对象是基因、蛋白质这类生物实体Cytoscape可能更顺如果只是普通文本关键词Gephi上手更快。需要说明的是可视化工具不是算法的替代品而是表达层。社区检测、重要节点计算最好在NetworkX里做结论再放进Gephi配色布局。否则直接在Gephi里算也未尝不可但复现性差手工操作一多别人就很难按照你的步骤再跑一遍。4. 实操演示从商品评论到共现图的全流程4.1 数据准备和预处理代码我用一个商品评论小数据集做演示大约2000条中文评论原始文本长这样“这个耳机音质不错但是续航差了点”“降噪效果很好戴着也舒服”。目标是提炼出评论里的关键词关系进一步判断用户最在意的产品属性组合。先做清洗、分词和停用词过滤import jieba import re import pandas as pd from collections import defaultdict # 自定义停用词表示例实际使用中需要更完整的停用词列表 stopwords {这个, 那个, 但是, 还是, 就是, 东西, 感觉, 一个, 可以, 比较, 有点} # 自定义词典确保领域词不被切开 custom_words [降噪, 音质, 续航, 佩戴, 性价比, 蓝牙] for w in custom_words: jieba.add_word(w) def clean_segment(text): # 去除URL和HTML标签本项目简单处理 text re.sub(rhttp\S|www\.\S|.*?, , 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 and len(w) 1] df pd.read_csv(comments.csv, encodingutf-8) df[words] df[comment].apply(clean_segment)4.2 滑动窗口统计共现次数构建共现矩阵时我选择窗口大小为3按单个评论内部滑动窗口。这样统计的词对既有一定上下文跨度又不至于跨到无关内容。def build_cooccurrence(df, window3): cooccur defaultdict(lambda: defaultdict(int)) node_freq defaultdict(int) for words in df[words]: n len(words) for i, w in enumerate(words): node_freq[w] 1 start max(0, i - window) end min(n, i window 1) for j in range(start, end): if i j: continue cooccur[w][words[j]] 1 return cooccur, node_freq cooccur, node_freq build_cooccurrence(df, window3)两个细节特别说明一下一是窗口边界用max和min做了截断避免越界二是i j时跳过词和自己的共现没有统计意义。到这里共现矩阵已经以稀疏字典形式构建完成接下来直接转图。4.3 使用NetworkX完成矩阵到图的一步转换把稀疏字典转成pandas DataFrame再交给NetworkX生成图对象import networkx as nx import pandas as pd # 稀疏字典转DataFrame缺失值填0 matrix_df pd.DataFrame(cooccur).fillna(0).astype(int) # 按行索引和列索引的交集过滤保证节点一致 matrix_df matrix_df.loc[matrix_df.index.intersection(matrix_df.columns), matrix_df.columns.intersection(matrix_df.index)] G nx.from_pandas_adjacency(matrix_df)这步执行完成后G就是一个无向加权图。每个节点是一个词每条边权重是共现次数。产品属性组合和图谱结构在这一刻已经成形。我习惯马上看几个基础指标验证图质量print(节点数:, G.number_of_nodes()) print(边数:, G.number_of_edges()) print(密度:, nx.density(G))实操中节点数在50到200之间比较适合可视化。如果节点过多后续一定需要按权重阈值过滤边否则导出的图会密得看不清。4.4 边权过滤和图结构优化让图“能看”的关键一步原始图往往边太多噪音也大。我的过滤策略分两个层面词频门槛和共现次数门槛。先过滤词频节点出现次数小于5次的词直接移除。这个门槛可以根据语料大小调整语料越大门槛越高。再过滤边权重只保留权重排名前200的边或设定最小共现次数阈值比如5次。两种方式效果不同前者保证边数量恒定适合对比不同语料时统一图的规模后者更自然删掉弱关联的边适合探索性分析。过滤后重新构图可以再做一次连通分量分析。通常会有一个包含大部分节点的最大连通分量周边散布少量孤立小组团。孤立小组团往往是低频话题在分析主体话题时可以暂时不关注。5. 做共现分析一定会踩的哪些坑5.1 矩阵稀疏性导致的存储和计算问题词表稍微大一点比如两三万个词共现矩阵就可能达到数亿个潜在元素。直接新建二维数组会直接内存爆炸。我的方案是始终用稀疏结构构建阶段用嵌套字典分析阶段转为pandas sparse DataFrame或scipy sparse matrix转图阶段由NetworkX内部处理。还有一个小教训不要频繁把稀疏结构转成稠密结构。我之前有次为了做pandas操作直接.toarray()结果内存一下吃满程序崩溃。如果非要做密集计算务必确认矩阵规模在可控范围内比如节点数小于2000时再考虑。5.2 高频词压制低频词导致图被“大词”绑架前文提到过原始频次矩阵会被高频词绑架。一个处理舆情评论的案例里“价格”和“服务”出现频次极高导致它们几乎与所有话题都产生共现关系整个社区检测算法被判词带偏。用PPMI替代原始频次后这种问题明显缓解。PPMI需要重新计算矩阵计算逻辑要遍历所有词对。由于涉及对数运算词表大时效率会低建议在numpy数组上向量化计算比纯Python循环快很多。5.3 可视化布局和节点标签重叠问题就算共现图计算正确可视化效果不好时依然无法直接交付。标签重叠是最常遇到的情况尤其是有几百个节点时。我用的解决思路是边权重过滤到100条左右节点尺寸按度排序后对高频词截断显示标签只在鼠标悬停时显示减少画布上文字堆积。布局算法也值得试不同选项。Gephi里ForceAtlas2布局通常效果稳定圆形布局适合展示聚类结构径向布局适合展示中心节点。选布局要看目的强调核心关键词就用径向强调话题组团就用ForceAtlas2加色块分区。5.4 数据版本与参数复现的坑共现分析有一个隐蔽问题窗口大小、过滤阈值、停用词表稍微变了整个图结构就大不相同。项目复盘时如果没记录参数后患无穷。我的习惯是把关键参数写进配置每一次跑图都自动输出一份配置日志。比如窗口大小、PPMI阈值、最小词频、过滤后的节点数和边数全部留在结果目录下。几个月后回看还能完整复现当时的图是从什么参数来的。6. 一些扩展玩法从静态共现图到动态时序分析共现图分析并不是只能在一份整合语料上运行。如果语料带时间戳按天或按周切分分别构建同一词表的共现图就能追踪话题结构的演化过程。某个词的中心度突然上升可能意味着新话题的爆发两个社区之间的桥梁边消失可能意味着话题分化。这种方法常用于舆情监控、学术热点追踪和竞品评论分析非常实用。另外共现图也不一定只用于词。商品类目、用户标签、知识图谱实体等离散符号只要存在同现关系都能套同一套流程。我做过一个类目标签的共现网络最终用来优化推荐系统里的关联推荐规则思路和文本完全一致。我在实际项目中还发现把共现图得到的高权重边直接输出为“关联规则”候选集再结合置信度指标验证可以快速建成一个可解释的关联推荐模块。机器学习的黑盒模型固然厉害但业务方很多时候要的是“因为用户看了A所以推荐B”这种能讲清楚的理由共现图恰好能补上这个短板。最后再分享一个小技巧如果你用NetworkX导出GraphML给Gephi记得在节点属性里加上“词频”字段在边的属性里加上“权重”字段。Gephi里节点大小映射到词频边粗细映射到权重配色映射到社区一张信息量完整且层次分明的共现关系图就出来了。整个流程从原始语料到最终可视化核心就一句话矩阵负责把关系算清楚图负责把关系讲明白。