自然语言处理应用篇 · 第5章

词频、TF-IDF、关键词提取与词云

如何让计算机自动判断一篇文章"在讲什么"?从词频统计到 TF-IDF 加权、TextRank 图排序,最后用词云直观呈现。

🎯 学习目标

  • 理解齐夫定律与词频分布,熟练用 Counter 做词频统计
  • 手动推导 TF-IDF,真正理解 IDF 的直觉含义
  • 掌握 jieba.analyse 的 TF-IDF 与 TextRank 关键词抽取
  • 会用 wordcloud 生成中文词云,并解决字体/蒙版问题
  • 配置 matplotlib 中文显示,绘制词频柱状图
  • 完成"文章关键词 + 词频图 + 词云"完整分析案例

1词频与齐夫定律

词频(Term Frequency):词在文档中出现的次数。自然语言的词频有一个惊人规律——齐夫定律(Zipf's Law):少数词(的、了、是、在)出现极多次,绝大多数词只出现一两次。

排名 r 的词,其频次 f 大致满足:f × r ≈ 常数
第1高频词频次约为第2的2倍、第3的3倍……
长尾中大量"低频词"恰恰是区分文章主题的关键。
import jieba
from collections import Counter

text = ("数据分析 是 数据科学 的 基础,数据 分析 依赖 统计 方法,"
        "好 的 数据 分析 报告 能 指导 业务 决策")
words = [w for w in jieba.lcut(text) if len(w) > 1]
freq = Counter(words)

freq.most_common(5)         # 频次 Top5
freq["数据"]                # 4  指定词频次
list(freq.elements())       # 展开
sum(freq.values())          # 总词数

# 文档频率 DF:有多少篇文档包含该词(需要多篇文档统计,见下文)

2为什么词频不够:停用词与 TF-IDF

直接用频次选关键词会被"的、了、是、我们"霸榜。停用词表是一种硬过滤,但治标不治本——"数据"在一篇数据类文章里出现 50 次很关键,可它在所有数据类文章里都高频,区分力并不强;而"碳中和"只在少数文章出现,一旦出现就极具区分度。

TF-IDF(词频-逆文档频率,1972 年提出,至今仍是搜索引擎与文本特征的基石)的核心直觉:

权重 = 在本文出现多(TF高) × 在别处很少见(IDF高)

一个词对某文档的重要性 = 它在该文档中出现的频率 × 它在整个文档集合中的稀有程度。

公式与手算

TF(t,d)  = 词 t 在文档 d 中出现次数 / d 的总词数(或直接用次数)

IDF(t)   = ln( 文档总数 N / (包含词 t 的文档数 DF + 1) ) + 1
           (+1 平滑:防止词出现在所有文档中时分母问题、除零)

TF-IDF(t,d) = TF(t,d) × IDF(t)

手算例子:语料库共 1000 篇文章,其中 100 篇含"数据",只有 5 篇含"碳中和"。某篇共 200 词的文章中两词都出现 4 次:

import math
N = 1000
tf_data = tf_carbon = 4 / 200           # 0.02
idf_data = math.log(N / (100 + 1)) + 1  # 含"数据"的文档多 → IDF 小
idf_carbon = math.log(N / (5 + 1)) + 1  # 含"碳中和"的文档少 → IDF 大

print("数据 TF-IDF:", round(tf_data * idf_data, 4))    # 0.0463
print("碳中和 TF-IDF:", round(tf_carbon * idf_carbon, 4))  # 0.1082
# 同样出现 4 次,"碳中和"的权重是"数据"的两倍多!

IDF 的极端情形:所有文档都有的词 IDF≈0(类似停用词,权重自动被压低,无需手工词表);只在一篇出现的词 IDF 最大。注意:TF-IDF 不理解语义,"电脑"和"计算机"会被当成两个无关词——这正是第 7 章词向量要解决的问题。

3jieba 抽取关键词:TF-IDF

import jieba
import jieba.analyse

article = """人工智能正在深刻改变医疗行业。通过大语言模型,医生可以快速分析
电子病历,辅助诊断罕见疾病;计算机视觉技术能够识别CT影像中的早期肿瘤。
多家医院已经开始试点人工智能阅片系统,大幅提升了诊断效率和准确率。
未来,人工智能药物研发也将显著缩短新药上市周期。"""

# TF-IDF 关键词(jieba 内置约 35 万词的 IDF 语料库)
keywords = jieba.analyse.extract_tags(
    article,
    topK=8,                # 返回前 K 个
    withWeight=True,       # 同时返回权重
    allowPOS=()            # 允许的词性,默认所有;如 allowPOS=("n","nr","ns","nt","nz","v","vn")
)
for word, weight in keywords:
    print(f"{word}: {weight:.4f}")
# 人工智能 / 人工智能药物 / CT / 阅片 / 肿瘤 / 病历 / 罕见 / 影像
# 领域定制:用自己的语料生成 IDF 表,效果更贴合业务
# jieba.analyse.set_idf_path("my_idf.txt")
#   my_idf.txt 格式:词语 IDF值,每行一个
# jieba.analyse.set_stop_words("my_stopwords.txt")

# 只用名词类实体词
kw = jieba.analyse.extract_tags(article, topK=10,
                                allowPOS=("n", "nr", "ns", "nt", "nz", "vn"))

4TextRank:不依赖语料库的图排序

TextRank 源自网页排名算法 PageRank:把词看成节点,共现(窗口内一起出现)的词之间连边;被越多"重要词"连接的词越重要,经过多轮投票迭代收敛得到排序。

构造:① 分词 → ② 取候选词(过滤词性/停用词)
     ③ 滑动窗口(默认 span=5)内共现则连无向边
     ④ 类似 PageRank 迭代计算每个词的得分 → 排序取 TopK
优点:单文档即可运行,不依赖外部 IDF 语料
缺点:计算量更大,短文本效果不稳
for word, weight in jieba.analyse.textrank(
        article, topK=8, withWeight=True,
        allowPOS=("n", "nr", "ns", "nt", "nz", "vn", "v")):
    print(f"{word}: {weight:.4f}")
# 医生 / 诊断 / 医疗 / 系统 / 影像 / 识别 / 辅助 / 分析

# TF-IDF 与 TextRank 结果通常互补,工程上可以各取 TopK 做并集/加权融合
TF-IDFTextRank
依据词在文档集的稀有度词在本文的共现图结构
外部语料需要 IDF 表不需要
计算快迭代较慢
偏好罕见专名、长尾词语义中心词、连接枢纽词

5matplotlib 中文与词频图

import matplotlib
matplotlib.use("Agg")                  # 无界面环境保存图片用;交互式可省略
import matplotlib.pyplot as plt
from collections import Counter
import jieba

# 中文显示:指定系统中文字体(Windows 自带 SimHei / Microsoft YaHei)
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
plt.rcParams["axes.unicode_minus"] = False   # 正常显示负号

words = [w for w in jieba.lcut(article) if len(w) > 1]
top = Counter(words).most_common(10)
labels = [w for w, _ in top][::-1]          # 倒序使最高频在顶部
counts = [c for _, c in top][::-1]

plt.figure(figsize=(8, 5))
plt.barh(labels, counts, color="#3776AB")
plt.xlabel("出现次数")
plt.title("高频词 Top10")
plt.tight_layout()
plt.savefig("freq_bar.png", dpi=150)        # plt.show() 弹窗查看

6词云 wordcloud(中文必踩的坑)

from wordcloud import WordCloud
import jieba

text = "人工智能 人工智能 人工智能 大语言模型 机器学习 深度学习 " \
       "自然语言处理 计算机视觉 数据分析 神经网络 智能推荐"

# ① 中文必须先分词,再用空格拼成字符串(WordCloud 默认按空格切)
words = " ".join(jieba.lcut(text))

# ② 必须指定中文字体路径,否则显示成方块/直接报错
#    Windows:C:\Windows\Fonts\simhei.ttf 或 msyh.ttc
#    Mac:/System/Library/Fonts/PingFang.ttc
#    Linux:/usr/share/fonts/ 下放任意中文 ttf
wc = WordCloud(
    font_path=r"C:\Windows\Fonts\simhei.ttf",
    width=1000, height=600,
    background_color="white",
    max_words=100,
    colormap="viridis",       # 配色:viridis/plasma/Set2/RdYlBu...
    collocations=False        # 关闭二元词组搭配,避免重复词
)

# 方式A:直接吃"空格分隔的文本",内部统计
wc.generate(words)
wc.to_file("wordcloud.png")

# 方式B:直接传 {词: 权重} 字典(推荐!可直接用 TF-IDF 权重控制大小)
# 权重越大概率越高,词越大
import jieba.analyse
kw = dict(jieba.analyse.extract_tags(article, topK=80, withWeight=True))
wc2 = WordCloud(font_path=r"C:\Windows\Fonts\simhei.ttf",
                background_color="white", width=1200, height=700)
wc2.generate_from_frequencies(kw)
wc2.to_file("wordcloud_tfidf.png")

中文词云三大坑:① 不指定 font_path 必乱码成方框(WordCloud 只自带英文 DroidSansMono);② 必须先 jieba 分词再空格拼接,否则整句被当作一个"词";③ 形状蒙版 PNG 需要白底,代码 from imageio import imread; mask=imread("mask.png") 后传 mask=mask。

形状蒙版与停用词

from imageio import imread
from wordcloud import WordCloud, STOPWORDS

mask = imread("cloud_mask.png")     # 白色背景 + 实心形状
stop = set(STOPWORDS) | {"一个", "我们", "可以"}   # 中英文停用词合并

wc = WordCloud(font_path=r"C:\Windows\Fonts\msyh.ttc",
               mask=mask, background_color="white",
               stopwords=stop, contour_width=2, contour_color="#3776AB")
wc.generate_from_frequencies(freq_dict)
wc.to_file("masked_wc.png")

7综合案例:一键文章分析报告

import jieba, jieba.analyse
from collections import Counter
from wordcloud import WordCloud
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False

def analyze(article, out_prefix="report"):
    # 分词
    tokens = [w for w in jieba.lcut(article) if len(w.strip()) > 1]
    # 关键词:TF-IDF + TextRank 融合
    tfidf = dict(jieba.analyse.extract_tags(article, 20, withWeight=True))
    tr = dict(jieba.analyse.textrank(
        article, 20, withWeight=True,
        allowPOS=("n", "nr", "ns", "nt", "nz", "vn", "v")))

    print("【词频 Top5】", Counter(tokens).most_common(5))
    print("【TF-IDF 关键词】", list(tfidf)[:8])
    print("【TextRank 关键词】", list(tr)[:8])

    # 词频柱状图
    top = Counter(tokens).most_common(10)[::-1]
    plt.figure(figsize=(8, 5))
    plt.barh([w for w, _ in top], [c for _, c in top], color="#7c3aed")
    plt.title("高频词 Top10"); plt.tight_layout()
    plt.savefig(f"{out_prefix}_bar.png"); plt.close()

    # 词云(用 TF-IDF 权重)
    WordCloud(font_path=r"C:\Windows\Fonts\simhei.ttf",
              width=1000, height=600, background_color="white"
              ).generate_from_frequencies(tfidf
              ).to_file(f"{out_prefix}_wc.png")

analyze(article)
print("报告图片已生成:report_bar.png / report_wc.png")

8本章小结与练习

📌 关键结论

  • 权重思想:本文多 × 全局少 = TF-IDF;手算公式要能默写。
  • extract_tags 快且偏长尾专名;textrank 无需语料、偏语义中心。
  • 中文词云两个必须:先分词拼空格、指定中文字体路径。
  • TF-IDF 不懂同义语义,下一章的词向量解决这个问题。
练习1:停用词表过滤与 IDF 加权都能压低"的、了、是"的影响,二者机制有何不同?
停用词表是硬性规则:词在名单里就直接删除,一刀切且需人工维护;IDF 是软加权:不出现在任何文档的稀有度统计中——常见词因出现在几乎所有文档中而 IDF≈0,权重被自动压到很低,无需名单且能自适应语料(某领域高频通用词也会被压低)。实际系统通常两者结合。
练习2:手算:100 篇文档中,词 A 出现在 50 篇、词 B 出现在 2 篇;某篇中 A、B 各出现 3 次,文档总词数 150。求两者 TF-IDF 并比较。
TF 都是 3/150=0.02;IDF(A)=ln(100/51)+1≈1.673,IDF(B)=ln(100/3)+1≈4.507。TF-IDF(A)≈0.0335,TF-IDF(B)≈0.0901。B 的权重约为 A 的 2.7 倍,因为 B 更稀有、区分力更强。
练习3:用 jieba 生成的词云打开后全是方框,可能的原因和修复步骤?
原因是 WordCloud 默认字体不含中文字形。修复:① 准备中文 ttf/ttc(Windows 用 C:\Windows\Fonts\simhei.ttf);② WordCloud(font_path=...) 指定路径;③ 文本先 jieba.lcut 再用空格 join;④ 若仍异常确认字体文件路径存在、未被占用,以及使用 generate_from_frequencies 时传入的是 {str:number} 字典。