ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

共现分析实战:用NLTK与Seaborn构建词共现热力图

共现分析实战:用NLTK与Seaborn构建词共现热力图 做文本分析熬过几个大夜之后我越来越觉得共现分析才是真正能让人快速读懂一堆文本的利器。词频统计只能告诉你哪些词出现得多但完全看不出词和词之间的关系共现矩阵则完全不同它能回答“哪些词总是一起出现”这个问题。比如你统计一百篇人工智能新闻词频只会告诉你“AI”出现了两千次但共现分析能告诉你“AI”和“医疗”“金融”“伦理”分别有多紧密这种关系结构一出来整个语料的脉络基本就摆在眼前了。在Python生态里用NLTK做文本预处理、再用seaborn把共现矩阵画成热力图是性价比很高的一套组合。NLTK负责分词、去停用词、词形还原这些脏活累活seaborn负责把冷冰冰的矩阵变得一眼就能看懂。这套流程不算复杂但细节很多网上的教程要么只讲其中一步要么代码根本跑不通。这篇博文我会把所有环节拆开讲清楚从环境准备、数据下载到预处理、矩阵构建、可视化再到常见坑的排查全部揉碎了写出来。这篇内容适合刚接触文本分析的Python用户也适合已经会做词频统计、但不知道下一步该怎么进阶的读者。文中的代码都是可直接复制运行的完整版本你只需要有一台装着Python的电脑再准备一小段英文文本就能跑起来。1. 共现分析到底在分析什么1.1 一个词没有意义一群词才有意义人类理解语言靠的从来不是单个词而是词与词之间的关系。比如你在学术文献里看到几十次“climate”光看这个词你只知道这篇文献在聊气候但“climate”旁边总跟着“model”“prediction”“uncertainty”还是“policy”“justice”意味着研究的完全是两个方向。共现分析的核心思路其实特别朴素如果两个词在同一个窗口内频繁出现就认为它们之间存在某种关联。这个“窗口”可以是同一句话也可以是某个固定长度范围内的词序列具体取决于你研究的问题。关联强度用共现次数表示多次累计之后生成共现矩阵再用热力图可视化词与词之间的“社交关系网”就一目了然了。这种技术最早在文献计量学领域大量使用用来分析某一学科的知识结构。后来文本挖掘、NLP、舆情分析都把它当成最基础的一步。很多看似高深的“关键词网络图”底层就是共现矩阵换了一种画法而已。1.2 谁能用上共现分析我自己的经验里下面几类需求是最常见也最实用的竞品分析把几十篇行业报告拉下来看哪些技术词和竞品名共现频率高判断对方现阶段在押注什么。舆情与用户反馈抓取社交平台上关于某款产品的讨论看“价格”“续航”“售后”各自和哪些词经常一起出现快速定位用户的核心痛点和好评点。论文综述把同领域的几百篇摘要跑一遍找出高频共现的关键词组合五分钟摸清这个领域的研究热点分布。推荐系统特征工程把用户评论里的词对共现次数当作一种特征辅助做相似度计算或内容聚类。这一点我想先说明白共现分析不是一个“高深”的算法它更像是一把通用扳手不管你是做报告、写论文还是做产品调研只要手上有文本它就能帮你从“单个词”的视角升级到“词之间关系”的视角。这也是我推荐每个人都掌握它的原因投入产出比极高。2. 环境准备先把锅架好2.1 安装Python、NLTK和可视化库这部分是基础中的基础但最容易被卡住所以我花点篇幅说清楚。如果你已经装过Python并且能正常执行pip命令可以直接跳过去。Python建议装3.9或更高版本我平时用的是3.11下面所有代码在这个版本上都能正常跑。编辑器方面VSCode或者PyCharm都行。如果你跟我一样经常写一些临时脚本做数据分析VSCode会更轻量启动快而且Python插件装好后调试和代码补全都够用。PyCharm更适合做完整项目开发但启动慢跑小实验反而有点笨重。依赖库用一条命令装pip install nltk pandas numpy matplotlib seaborn如果你只有一个干净的Python环境上面的命令会把NLTK、pandas、numpy、matplotlib、seaborn一起装上。如果网络比较慢可以加上清华镜像源pip install nltk pandas numpy matplotlib seaborn -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后建议在命令行里验证一下python -c import nltk, pandas, seaborn; print(ok)能输出ok就说明环境没问题。2.2 NLTK数据下载慢到怀疑人生三种解法NLTK和普通库不太一样它把很多语料和模型数据拆出来了装完NLTK主库之后还得单独下载数据资源。常用的就这几个punkt分句分词模型、stopwords停用词表、wordnet词形还原用的词典、averaged_perceptron_tagger词性标注模型旧版本是averaged_perceptron_tagger新版本可能下载averaged_perceptron_tagger_eng。第一次运行下载时很多人都会卡在这一步网速慢或者连接超时半天下载不下来。这里有三个实测有效的方案等它慢慢下在代码里加nltk.download(all)不要用这个会下几百MB的全量数据只下需要的资源即可import nltk nltk.download(punkt) nltk.download(stopwords) nltk.download(wordnet) nltk.download(averaged_perceptron_tagger)手动下载再放进指定目录。NLTK数据资源在GitHub上有一个官方仓库你可以直接用浏览器访问https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/tokenizers/punkt.zip把zip文件下载下来解压后放到对应的nltk_data目录里。目录结构是tokenizers/punkt/每一个压缩包对应一个子目录不要改结构。直接从别人已经配好的机器上拷贝nltk_data目录整个拷贝到新机器然后设置环境变量NLTK_DATA指向这个目录。这个方法在离线环境或者内网环境下特别有用。关于数据目录的位置macOS和Linux上一般是~/nltk_dataWindows上通常是C:\Users\你的用户名\AppData\Roaming\nltk_data。不在这些地方的话运行下面这行代码它会把所有可能的路径打出来import nltk for path in nltk.data.path: print(path)2.3 NLTK数据文件都装到哪了搞清楚数据装在哪不只是为了“知道”更重要的是方便你排查问题和复用配置。比如当你换了新电脑、配服务器跑脚本很多时候重新下载NLTK数据会浪费大量时间。我的做法是第一次下载好之后直接把整个nltk_data目录做成一个压缩包归档。下次新环境只需要解压再设置环境变量NLTK_DATA指向归档目录即可五分钟就能搞定比在线下载稳定太多。还有一个容易踩的坑NLTK的版本差异会导致数据资源名称变化。举例来说NLTK在3.8.2之后分句用的默认资源从punkt改为punkt_tab不少人在新版本上执行nltk.download(punkt)后依然报错找不到数据这时候你还需要再下载punkt_tab。这不是你代码的问题就是版本升级带来的资源名变化把两个资源都下载就能解决。3. 用NLTK做预处理质量决定了共现矩阵的上限3.1 分句是分词的爸爸很多人一上来就对整段文本直接分词这是不对的。共现分析里我们通常默认“同一句话中的两个词才算共现”因为句子是语义的基本单位跨句子的词对它们之间存在关系但这种关系很弱混在矩阵里还会制造噪音。NLTK的sent_tokenize专门干这个把一段英文拆成一个个句子。它对标点、缩写、引号的处理比我们自己写正则靠谱得多。比如Dr. Smith went home.这句sent_tokenize能识别出Dr.是称呼缩写不会错误断句。这个细节看似不起眼实际处理长篇文本时能避免大量无效拆句。预处理的第一步是拿到句子列表然后逐句分词而不是对整段文本直接分词。按照这个思路后面构建共现矩阵时天然就是“句内共现”逻辑上更严谨。3.2 分词、停用词和词形还原分词的英文叫tokenization就是把句子拆成一个个词语单元。NLTK的word_tokenize能利用punkt模型识别英文的标点、缩写、数字等边界比直接split()按空格切要准确得多。光分词还不够原始文本里到处都是the、is、of这类冠词、介词和助动词它们在我跟你交流时承担语法功能但在共现分析里只会拉低信噪比。所以需要去停用词把NLTK自带的英文停用词表过滤掉。接着是词形还原。英文里analysis、analyze、analyses是同一个词根的不同形态但在计算机眼里它们是完全不同的字符串。词形还原的目的就是把它们归并成基础形式。NLTK的WordNetLemmatizer在生产环境中很常用核心逻辑是查WordNet词典把词还原为词典中的词条。三个基础操作串起来的代码如下import nltk import string from nltk.tokenize import word_tokenize, sent_tokenize from nltk.corpus import stopwords from nltk.stem import WordNetLemmatizer lemmatizer WordNetLemmatizer() stop_words set(stopwords.words(english)) def preprocess_text(text): sentences sent_tokenize(text) clean_tokens [] for sentence in sentences: raw_tokens word_tokenize(sentence.lower()) for token in raw_tokens: if token in string.punctuation: continue if token.isdigit(): continue if token in stop_words: continue clean_tokens.append(lemmatizer.lemmatize(token)) return clean_tokens注意这个实现里lemmatize默认把所有词都当成名词处理即posn。这对名词和动名词基本有效但对动词形态的还原效果有限。如果想要动词还原准确就需要先用词性标注拿到每个词的词性再传给lemmatize。后面讲词性筛选时会一起解决这个问题。3.3 词性筛选留名词和形容词就够了实际做共现分析时我强烈建议加一步词性筛选。原因很简单动词的共现语义往往太泛比如get、make、use这类通用动词几乎能跟任何名词搭配放进矩阵只会贡献大量无意义的高频值。而连词、副词、代词更是噪音。真正能代表文本主题的绝大多数是名词和形容词顶多再加上专有名词和动词原形。NLTK的词性标注器pos_tag能给出每个词的词性标签比如NN表示普通名词、NNS表示复数名词、JJ表示形容词、NNP表示专有名词。我们可以做一个白名单过滤只保留这些有意义的内容词from nltk import pos_tag KEEP_POS {NN, NNS, NNP, NNPS, JJ, JJR, JJS} def lemmatize_with_pos(word, pos): if pos.startswith(J): return lemmatizer.lemmatize(word, posa) if pos.startswith(V): return lemmatizer.lemmatize(word, posv) if pos.startswith(R): return lemmatizer.lemmatize(word, posr) return lemmatizer.lemmatize(word, posn) def preprocess_text_pos(text): sentences sent_tokenize(text) clean_tokens [] for sentence in sentences: raw_tokens word_tokenize(sentence.lower()) tagged pos_tag(raw_tokens) for word, tag in tagged: if word in string.punctuation or word.isdigit(): continue if word in stop_words: continue if tag not in KEEP_POS: continue clean_tokens.append(lemmatize_with_pos(word, tag)) return clean_tokens这段代码里pos_tag返回(单词, 词性标签)的元组组合过滤规则就是“只要名词和形容词”。加了这一步之后共现矩阵里的词几乎都是主题相关的核心词画出来的热力图也干净很多。这里有一个我自己的使用心得供你参考如果语料很短比如一篇文章只有几百个词过滤太狠会导致词表太少热力图非常空如果语料很大词性筛选就非常有必要能显著压缩词表规模。3.4 中文文本怎么办看标题你可能以为这套方案只支持英文我简单补充一点NLTK对英文的开箱即用支持是最好的但如果你手头是中文语料核心链路完全不用推翻。中文需要换成jieba分词停用词表换成中文停用词表词性标注也由jieba.posseg来完成。分句可以用re.split(r[。!?], text)简单实现。预处理做完后你会得到一个干净的token列表接下来所有矩阵构建和可视化的代码都是语言无关的。所以这篇文章里讲的核心方法你完全可以用中文语料复现。4. 构建共现矩阵从理论到一行行代码4.1 共现窗口怎么选在句子内部我们要定义一个“共现窗口”。假设一句话分词后得到[artificial, intelligence, drives, innovation]窗口大小是2那么对于中心词drives来说它左看2个词内的artificial和intelligence右看2个词内的innovation这4个词都算与drives共现一次。窗口大小直接决定了共现矩阵反映的关系粒度窗口1时只有相邻词才算共现关系最紧密但矩阵会非常稀疏很多词对压根不会出现在相邻位置。窗口2时能捕捉“修饰关系”和“邻近搭配”比如“artificial intelligence”这种固定短语以及“AI drives innovation”这种句法邻近是我最常用的默认值。窗口5时可以捕捉同一句话里相距较远的语义关联适合分析主题词之间的松散关系但噪音也明显增多。我自己的建议是先设2跑一遍看结果如果矩阵过密、看不出差异再调大窗口如果矩阵太稀疏考虑缩小窗口或者增加语料。没有绝对正确的大小只有适不适合你当前的数据。还有一个容易忽略的细节窗口越界处理。某个词位于句子开头时它左侧没有词位于结尾时右侧没有词。处理方式是取窗口范围与句子范围的交集。4.2 数据结构设计字典、DataFrame还是稀疏矩阵共现矩阵本质上是一个方阵行列都是词表单元格存共现次数。数据量小的时候直接用pandas的DataFrame最方便展示和导出都很直观。但语料一大词表动辄几千方阵就是几百万个格子用密集矩阵存会非常浪费内存。我的建议是分两种情况处理词表在几百的量级直接用二维numpy数组或者DataFrame清晰、直观、方便调试。词表破万用scipy.sparse的稀疏矩阵只存非零元素内存能省好几个数量级。本文的演示代码和案例用第一种方案因为重在讲清楚原理。等到你真正处理大规模语料时把矩阵构建部分替换成稀疏实现即可。4.3 核心代码共现矩阵算法下面这段是我常用的共现矩阵构建代码逐行解释一下逻辑from collections import Counter, defaultdict import pandas as pd import numpy as np def build_cooccurrence_matrix(tokens, window_size2, min_freq1): # 第一步统计词频筛选词表 freq Counter(tokens) vocab [word for word, count in freq.items() if count min_freq] vocab_index {word: idx for idx, word in enumerate(vocab)} vocab_set set(vocab) # 第二步只保留词表内的token其他词直接丢掉 filtered_tokens [w for w in tokens if w in vocab_set] # 第三步用字典记录共现对数键是词对值是次数 cooc defaultdict(int) n len(filtered_tokens) for i, word in enumerate(filtered_tokens): start max(0, i - window_size) end min(n, i window_size 1) for j in range(start, end): if i j: continue neighbor filtered_tokens[j] idx_word vocab_index[word] idx_neighbor vocab_index[neighbor] # 按大小排列键避免重复计数 if idx_word idx_neighbor: cooc[(idx_word, idx_neighbor)] 1 else: cooc[(idx_neighbor, idx_word)] 1 # 第四步填充对称矩阵 matrix np.zeros((len(vocab), len(vocab)), dtypeint) for (a, b), count in cooc.items(): matrix[a][b] count matrix[b][a] count return vocab, pd.DataFrame(matrix, indexvocab, columnsvocab)这段代码有几个关键点值得说清楚。第一min_freq参数控制了词表的最小出现次数。语料越大高频词越能代表主题低频词往往是专有名词、拼写错误或者罕见实体把它们过滤掉能让矩阵更聚焦。对小语料这个值设为1对大规模文本建议至少3到5。第二矩阵对称性的处理。word出现在neighbor的窗口里和neighbor出现在word的窗口里本质是同一个词对。如果不做排序同一词对会被累计两次矩阵数值翻倍且不对称。我这里的做法是始终让索引小的排在前面最后填充矩阵时对称赋值这样数值正确且逻辑清晰。第三窗口的边界处理。max(0, i - window_size)和min(n, i window_size 1)保证了不会越界。窗口是闭区间注意python的range右侧不包含end所以end要加一才能把窗口右边界包含进来。4.4 速度优化别让O(n^2)的写法拖垮你看到上面这段双重循环有人会问不就是一个嵌套循环吗复杂度岂不是很糟糕其实不然。对于每个中心词内层循环只遍历窗口范围内的词而不是遍历全部词表。每个窗口最多2 * window_size个词所以总复杂度是O(n * window_size)其中n是过滤后的token总数。对一万个token、窗口为2来说也就是几万次操作秒出结果完全不用担心。真正的性能陷阱是另一种写法对每对词都做一次全局判断比如“i遍历全句j遍历全句”那个复杂度就是O(n^2)了语料一上来就扛不住。搞明白这个区别你就能理解为什么窗口滑动是一个优秀的设计。另外上面代码用defaultdict(int)累计次数避免了先判断键是否存在再赋值的过程。这是Python里处理“计数型字典”最顺手的方式性能也最好。如果语料极大还可以考虑用collections.Counter直接累计zip生成的窗口词对但逻辑会更加绕不如上面的可读性好。5. 热力图可视化把矩阵变成人话5.1 用seaborn把矩阵画出来共现矩阵本质是数值表格但直接看数字非常费劲尤其当词表有几十个词的时候密密麻麻的数字谁也读不动。这时候seaborn.heatmap就是最好的助手它把数值映射成颜色深浅一眼就能看出哪些词对关系密切。绘图代码非常简洁import matplotlib.pyplot as plt import seaborn as sns def plot_heatmap(df, figsize(12, 10), cmapYlOrRd): plt.figure(figsizefigsize) sns.heatmap(df, annotTrue, fmtd, cmapcmap, linewidths0.5, cbar_kws{label: Co-occurrence Count}) plt.title(Word Co-occurrence Heatmap) plt.xticks(rotation45, haright) plt.yticks(rotation0) plt.tight_layout() plt.show()这里annotTrue表示在格子里显示原始数字fmtd告诉seaborn这些数字是整数不要显示成小数。linewidths0.5给每个格子加一点分割线大矩阵情况下会显得更清爽。颜色映射YlOrRd是我个人最推荐的从浅黄到深红数值越大颜色越重视觉直觉很好。5.2 热力图的高级修饰等词表变大之后全矩阵热力图上三角和下三角是完全对称的信息重复。很多人喜欢用mask参数把上三角遮掉只保留下三角视觉上更聚焦效果也更专业import numpy as np # 生成上三角掩码 mask_upper np.triu(np.ones_like(df, dtypebool)) plt.figure(figsize(12, 10)) sns.heatmap(df, maskmask_upper, annotTrue, fmtd, cmapYlOrRd, linewidths0.5, cbar_kws{label: Co-occurrence Count}) plt.title(Word Co-occurrence Heatmap (Lower Triangle)) plt.xticks(rotation45, haright) plt.yticks(rotation0) plt.tight_layout() plt.show()np.triu生成的是对角线上方的真值矩阵用maskmask_upper把它遮住图表就只剩下左下角的下三角部分。这种画法尤其适合词表比较大的时候一半的信息量但图面更清晰。还有个实际细节词表一旦超过20个横轴标签就会互相重叠旋转45度还不够可能完全叠在一起。不管你的图多漂亮标签看不清就全白搭。我踩过几次坑之后总结了两条经验一是限制最终展示的词表数量只保留top N高频词二是调大figsize比如列表20个词时用(12, 10)50个词时用(16, 14)。5.3 把完整代码串起来一键运行版我直接给一个可以完整跑的脚本用一小段英文语料做演示。你复制到本地按顺序执行就能看到热力图输出import nltk import string import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from collections import Counter, defaultdict from nltk.tokenize import word_tokenize, sent_tokenize from nltk.corpus import stopwords from nltk.stem import WordNetLemmatizer from nltk import pos_tag # 第一次使用请取消下面的注释执行下载 # nltk.download(punkt) # nltk.download(stopwords) # nltk.download(wordnet) # nltk.download(averaged_perceptron_tagger) lemmatizer WordNetLemmatizer() stop_words set(stopwords.words(english)) KEEP_POS {NN, NNS, NNP, NNPS, JJ, JJR, JJS} def preprocess_text(text): sentences sent_tokenize(text) clean_tokens [] for sentence in sentences: raw_tokens word_tokenize(sentence.lower()) tagged pos_tag(raw_tokens) for word, tag in tagged: if word in string.punctuation or word.isdigit(): continue if word in stop_words: continue if tag not in KEEP_POS: continue if tag.startswith(J): clean_tokens.append(lemmatizer.lemmatize(word, posa)) elif tag.startswith(V): clean_tokens.append(lemmatizer.lemmatize(word, posv)) elif tag.startswith(R): clean_tokens.append(lemmatizer.lemmatize(word, posr)) else: clean_tokens.append(lemmatizer.lemmatize(word, posn)) return clean_tokens def build_cooccurrence_matrix(tokens, window_size2, min_freq1): freq Counter(tokens) vocab [word for word, count in freq.items() if count min_freq] vocab_index {word: idx for idx, word in enumerate(vocab)} vocab_set set(vocab) filtered_tokens [w for w in tokens if w in vocab_set] cooc defaultdict(int) n len(filtered_tokens) for i, word in enumerate(filtered_tokens): start max(0, i - window_size) end min(n, i window_size 1) for j in range(start, end): if i j: continue neighbor filtered_tokens[j] idx_word vocab_index[word] idx_neighbor vocab_index[neighbor] if idx_word idx_neighbor: cooc[(idx_word, idx_neighbor)] 1 else: cooc[(idx_neighbor, idx_word)] 1 matrix np.zeros((len(vocab), len(vocab)), dtypeint) for (a, b), count in cooc.items(): matrix[a][b] count matrix[b][a] count return vocab, pd.DataFrame(matrix, indexvocab, columnsvocab) sample_text The rapid development of artificial intelligence has transformed many industries. Companies are investing heavily in machine learning and data science. The new AI model shows remarkable performance in natural language processing. Many researchers believe that deep learning will continue to drive innovation. However, some experts worry about the ethical issues of artificial intelligence. The future of machine learning depends on more transparent algorithms and better data management. tokens preprocess_text(sample_text) vocab, co_df build_cooccurrence_matrix(tokens, window_size2, min_freq1) print(词汇表:, vocab) print(co_df) plt.figure(figsize(12, 10)) sns.heatmap(co_df, masknp.triu(np.ones_like(co_df, dtypebool)), annotTrue, fmtd, cmapYlOrRd, linewidths0.5, cbar_kws{label: Co-occurrence Count}) plt.title(Word Co-occurrence Heatmap (Lower Triangle)) plt.xticks(rotation45, haright) plt.yticks(rotation0) plt.tight_layout() plt.show()这一段代码把预处理、词性筛选、矩阵构建和热力图绘制串在一起。我特意保留了词性筛选和词形还原词性参数的部分因为这是保证矩阵质量的关键。5.4 结果解读怎样向别人解释这张图矩阵出来之后图本身不是终点能不能讲清楚它背后的含义才是重点。拿上面这段示例文本来说你会发现artificial和intelligence这对词共现次数非常高因为原文里多次直接出现“artificial intelligence”。machine和learning同理它们是一组固定搭配。而data会和science、management、algorithms共现说明这个语料里“data”是一个枢纽词连接了多个主题。解读热力图时我习惯用两个角度第一个角度是找“亮块”。热力图里颜色最深的小块往往对应固定短语、强搭配比如“artificial intelligence”。这些亮块告诉你语料里最核心的实体或概念是什么。第二个角度是找“桥梁词”。矩阵中某一行或某一列整体颜色偏深的词通常说明它跟很多词都有关系。这种词一般是主题聚合点值得重点关注。你可以想象它在词网里就是一个中心节点。在实际业务场景中你不需要把每一个格子都读一遍重点盯住“少数几个词的强关联”和“某一个词的广关联”就够了。6. 从共现矩阵还能往哪走6.1 网络图展示词与词之间的关系热力图适合看整体分布和密集型关系但要说“谁和谁是一伙的”网络图往往更直观。把词当作节点共现次数当作边的权重用NetworkX画出来的图能展现出明显的聚类结构某些词聚成一团另一些词负着连接多个团。简单实现思路如下import networkx as nx # 从共现矩阵构建图 G nx.Graph() for word in vocab: G.add_node(word) for i, word_i in enumerate(vocab): for j, word_j in enumerate(vocab): if i j and co_df.iloc[i, j] 0: G.add_edge(word_i, word_j, weightco_df.iloc[i, j]) # 只保留权重较大的边突出核心结构 edges [(u, v, d[weight]) for u, v, d in G.edges(dataTrue)] edges [e for e in edges if e[2] 2] # 只保留共现次数2的边 pos nx.spring_layout(G, k0.8, seed42) plt.figure(figsize(12, 8)) nx.draw_networkx_nodes(G, pos, node_size800, node_colorskyblue) nx.draw_networkx_labels(G, pos, font_size10) nx.draw_networkx_edges(G, pos, edgelistedges, alpha0.5, width2) plt.axis(off) plt.show()我没法在这个脚本里画出特别复杂的网络但思路已经完整从共现矩阵取出边和权重过滤掉低频共现只画核心关系。这个技巧适合在汇报PPT里用比热力图更抓眼球也更能讲出“故事感”。6.2 把共现矩阵喂给下游模型共现矩阵不只是用来画图的它本身是可以参与计算的数值特征。最直接的一个应用是作为文档或词向量的基础对词表内每个词它的共现向量就是一个“上下文特征”能描述这个词在语料里的行为模式。在业界常用的一种做法是PPMI变换把原始共现次数转换成点互信息削弱高频词的影响、放大词对之间的专属关联度。简单说两个词如果总是一起出现且各自单独出现时并不泛滥那它们的关联强度就是被放大的反之如果两个词各自都高频出现共现次数也多但只是泛泛地常见PPMI会将其下调。PPMI的计算公式是PPMI(w1, w2) max(0, log(P(w1, w2) / (P(w1) * P(w2))))其中概率用频率近似。跑完后得到的矩阵可以用于词相似度计算、聚类甚至作为简单版的词嵌入输入。如果你想更进一步把共现矩阵的行向量拿去做k-means聚类就能自动把语料中的词分成几组语义簇这一点在很多调研分析场景里非常吃香。7. 常见问题与排查技巧实录7.1 问题速查表我做了一个实操向的问题排查表都是自己或身边同事踩过的坑按出现频率排的问题现象原因解决方案下载NLTK数据超时卡在Downloading半天不动网络连接不稳定或资源服务访问慢手动下载zip解压到nltk_data目录或直接拷贝别人的nltk_data目录报错punkt找不到调用sent_tokenize时报LookupErrorNLTK新版需要punkt_tab资源重新运行nltk.download(punkt)和nltk.download(punkt_tab)分词结果带nt例如doesnt被拆成does和ntNLTK正常行为使用英文缩写拆分在停用词表或过滤规则中手动删掉nt这类无意义token热力图中文乱码图表里中文变成方框matplotlib默认字体不支持中文设置plt.rcParams[font.sans-serif] [SimHei]或换用中文字体如Noto Sans CJK矩阵数字全是0或只有对角线有数热力图基本全蓝窗口太小或者分词后词表太大导致词对太稀疏增大窗口提高min_freq只留下核心词增加语料量内存占用过高脚本运行缓慢甚至卡死词表过大使用密集矩阵存储改用scipy.sparse.csr_matrix构建矩阵词性标注报错average_perceptron_taggerpos_tag运行时报缺少数据新版本NLTK下载资源名不一致尝试nltk.download(averaged_perceptron_tagger_eng)7.2 我踩过的三个坑第一个坑和punkt_tab有关。某次给新同事配环境明明在他的机器上执行了nltk.download(punkt)一切正常但我的脚本一跑sent_tokenize就报错说找不到资源。最后排查了半天才发现是NLTK版本的问题新版本的分句器默认使用punkt_tab资源需要额外下载。这个坑几乎是所有NLTK新版用户都会碰到的提醒一次能帮你省下半天时间。第二个坑是词形还原的词性默认值。如果你没做词性标注就调用lemmatizer.lemmatize(word)默认按名词处理。这会导致studied还原成studied而不是studysaw还原成saw而不是see。我在做新闻语料分析时因为动词还原不彻底跑出来的共现矩阵里全是过去式动词的独立形式把词表撑大了一倍多。后来统一改成lemmatize(word, pos)以后效果立竿见影。第三个坑是数据目录的权限问题。在服务器或共享电脑上默认的~/nltk_data可能因为用户权限无法自动创建或者写入。NLTK下载时不会明确报错只是静默跳过最后啥都没装上。我的方案是手动创建nltk_data目录并把环境变量NLTK_DATA指向一个有写权限的位置然后再执行下载。遇到NLTK下载“没反应”的情况优先检查这个目录的结构和权限。7.3 几个提高效率的小习惯预处理函数建议存成一个utils.py文件所有文本分析项目共用。我自己的版本里preprocess_text和build_cooccurrence_matrix已经稳定运行了好几个项目每次新建分析任务时直接from utils import preprocess_text, build_cooccurrence_matrix省去重复调试的时间。运行小语料验证流程时不要把整个流程一次性跑完而是分成“预处理 → 矩阵构建 → 可视化”三段来执行。这样一旦出问题你能立刻定位到是哪一步出了差错排查成本低非常多。如果你要做多次实验比如不同窗口大小、不同min_freq建议在矩阵构建后先把结果存成CSV文件再统一绘图。否则每次改参数都要重新跑一遍预处理纯属浪费时间。一条co_df.to_csv(cooccurrence.csv)就够用。最后再分享一个我个人的体会共现分析看起来是文本挖掘里最基础的技术但恰恰是这种基础技术在你真正理解它之后能够组合出很多有洞察力的分析框架。词频告诉你“什么热门”共现告诉你“热门背后由哪些主题支撑”热力图再进一步把这种支撑结构视觉化。掌握这条链路之后你手上相当于多了一台可以透视任意文本结构的显微镜任何领域的长篇报告、评论集合、论文摘要都可以拿来快速拆解一遍。
返回列表