ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

共现矩阵完全指南:从原理到Python实现与应用场景

共现矩阵完全指南:从原理到Python实现与应用场景 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论文用的是5GloVe论文实验发现窗口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]停用词表我这里就不全列了网上随便下个中文停用词表或者自己维护一个几十个词的小表把的、了、和、是这些高频虚词加进去就好。重点强调一个容易踩的坑分词和清洗时不要把英文字母全部干掉比如AI、Python这种词在科技类语料里是有效信号我在清洗时用正则保留英文就是为了这个。4.3 滑动窗口统计共现次数核心逻辑来了。用窗口大小为2来统计共现。实现思路是先对每个句子分词得到词序列然后遍历序列中的每个位置作为中心词再遍历中心词左右一定距离内的邻居词将中心词邻居词这个词对的出现次数加1。def build_cooccurrence_matrix(tokens_list, window_size2, with_distance_weightFalse): 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(i1, min(len(tokens), iwindow_size1)): 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), dtypenp.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_size2, with_distance_weightFalse) matrix, word2idx, idx2word dict_to_matrix(vocab, cooccur_dict)运行上面的代码输出矩阵的形状比如(N, N)。直接用pandas展示会更直观df pd.DataFrame(matrix, indexidx2word.values(), columnsidx2word.values()) print(df.head(10))你可以看到篮球和跑步在矩阵里和运动相关词有共现记录而它们和音乐、代码这类词的共现数大概率是0。这本身就是语义关系的体现——共现矩阵不用训练任何模型纯粹统计就能反映出词之间的关联。4.5 内存优化稀疏矩阵是必经之路上面的代码能跑通但真实语料一旦稍微大一点问题就来了。假设词表有5万个词稠密矩阵需要存储50000 × 50000个float32也就是50000²×4字节10GB内存这还只是词表5万的情形。真实大规模语料词表上几十万很常见内存直接吃不消。生产环境我用的是scipy.sparse的coo_matrix或csr_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等算法时直接对接sklearn的TruncatedSVD或lightning这类库都很方便不用转换格式。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.sparse的csr_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把几万维压缩到几百维得到的低维向量就能当作词向量来用。这个思路其实就是传统LSALatent Semantic Analysis在信息检索时代是非常主流的方案。和Word2Vec相比LSA训练快、结果可复现但精度稍逊。GloVe则是把共现矩阵和神经网络结合的一个经典模型核心思想是直接以共现矩阵的统计信息作为学习目标学习词向量的内积能拟合共现概率的对数。在实际项目中如果数据量不大直接用GloVe预训练权重是个很省事的方案。我在实际使用中的体会是共现矩阵最大的价值在于它的可解释性。深度学习模型给你的是一个黑盒网络共现矩阵给的是这个词和那个词有多少次出现在一起的明明白白的证据。在需要对结果做解释、做审计、做调试的场景里共现矩阵是任何复杂模型都无法替代的底稿。就算是现在大模型满天飞的阶段我在做语料分析时还是会先跑一版共现矩阵花5分钟摸清数据的底细再决定下一步怎么走。最后再分享一个小技巧给你的共现矩阵做一版Top-5共现词输出检查一下每个中心词的前5个关联词是否符合直觉这是判断预处理和参数是否合理的最快方式。
返回列表