信息检索及文本挖掘之TF-IDF从原理到实战(上)

320 阅读18分钟

全文内容概要:

  • TF-IDF都能做什么

  • TF-IDF的数学原理

  • TF-IDF在《民法典》中的python实战

 

一、TF-IDF都能做什么

TF-IDF(Term Frequency-Inverse Document Frequency)是一种用于信息检索与文本挖掘的常用加权技术,用于评估一个词对于一个文档集或语料库中的一份文档的重要程度。

  1. 信息检索与搜索引擎优化

它的核心功能是通过计算词语的TF-IDF值,搜索引擎能更精准地排序搜索结果,将相关性高的文档优先展示。广泛地应用于网页排名;

并且能够匹配用户查询关键词与文档内容。它的优势在于降低高频但无意义词的权重,突出关键主题词。

  1. 文本分类与聚类

所谓文本分类是指将文档归类到预定义的类别(如新闻分类、垃圾邮件过滤),例如可以通过TF-IDF向量化文本,训练分类模型(如SVM支持向量机、朴素贝叶斯,关于此部分涉及算法AI的内容后续文章讲解);

文本聚类主要用于从海量无标签文本中自动识别潜在主题或话题、对文档库自动分类,提升检索效率、个性化推荐和舆情监控等,而TF-IDF作为主要工具之一发挥着巨大的作用。

  1. 关键词提取与摘要生成

关键词提取:自动识别文档中的核心词汇。采用方法:选取TF-IDF值最高的词作为关键词(适用于单文档或语料库)。摘要生成:结合句子中词的TF-IDF值权重,抽取重要句子形成摘要。

  1. 推荐系统

内容推荐:分析用户浏览或搜索的文本内容(如商品描述、文章),计算TF-IDF相似度以推荐相关内容。如:新闻推荐、电商商品推荐。还可以用于协同过滤补充,TF-IDF可提供基于内容的初始推荐依据。

  1. 自然语言处理(NLP)预处理

将文本转化为数值特征(TF-IDF矩阵),供机器学习模型使用。用于降维与优化,可结合PCA主成分分析或LDA线性判别分析(这部分内容将在后续算法文章中进一步讲解)进一步压缩特征维度。作为词嵌入(如Word2Vec)的补充或替代方案。

虽然TF-IDF仅考虑词频和文档频率,忽略语义和上下文关系(需结合BERT等现代模型弥补)。但是它依然在日志分析、专利检索、学术论文查重甚至是NLP、AI领域发挥着举足轻重的作用。

二、TF-IDF的数学原理

以下内容可能很枯燥,我尽量用通俗的方式来讲解,实在提不起兴趣的也可以看个大概,不必细究

1. 基本公式

TF-IDF由两部分组成:

词频(TF - Term Frequency) :衡量一个词在某一个文档中出现的频率

tf(t,d)=ft,dtdft,d\operatorname{tf}(t, d)=\frac{f_{t, d}}{\sum_{t^{\prime} \in d} f_{t^{\prime}, d}}

其中ft,d{f_{t, d}} 是词 t在文档 d中的出现次数,分母为文档总词数,其计算结果是体现词在某一个文档中的重要性

逆文档频率(IDF - Inverse Document Frequency) :衡量一个词在整个文档集合中的普遍重要性

idf(t,D)=logN{dD:td}\operatorname{idf}(t, D)=\log \frac{N}{|\{d \in D: t \in d\}|}

其中 N是语料库中文档总数,分母为包含词 t的文档数,其计算结果是降低常见词的重要性,提高稀有词的权重。

最终计算公式TF-IDF:在单个文档中出现频率高的词更重要,在文档集合中出现频率低的词更有区分度

tf-idf(t,d,D)=tf(t,d)×idf(t,D)\operatorname{tf-idf}(t, d, D)=\operatorname{tf}(t, d) \times \operatorname{idf}(t, D)

其中t:词项(term)、d:单个文档、D:文档集合(语料库)

2. 变体

TF变体

布尔频率(Boolean Frequency) :目的仅关注是否出现,忽略重复次数

{1td,0td. \begin{cases} 1 & \text{若}t\in d, \\ 0 & \text{若}t\notin d. & \end{cases}

对数缩放(Logarithmic Scaling) :目的抑制高频词绝对数值的影响

log(1+ft, d)\log \left(1+f_{t, d}\right)

增强归一化(Augmented Normalization) :目的平衡长文档与短文档的词频差异

0.5+0.5×ft, dmax(ft, d)0.5+\frac{0.5 \times f_{t, d}}{\max \left(f_{t^{\prime}, d}\right)}

IDF变体

平滑IDF(Add-1 Smoothing) :避免未登录词({d}=0)的除零错误,适合动态更新语料库,对小规模的语料库更加稳定。

idfsimooth (t, D)=log(1+N1+{d:t(d})\begin{aligned} & \operatorname{idf}_{\text {simooth }}(t, D)= \log \left(1+\frac{N}{1+\|\{d: t(d\} \|}\right)\end{aligned}

概率IDF(Probabilistic IDF) :强化稀有词权重(如医学术语“细胞瘤”在病例中的区分度),需要强惩罚高频词的领域(如垃圾邮件过滤)

idfprob (t, D)=logN{d:t<d}{d:t<d}\operatorname{idf}_{\text {prob }}(t, D)=\log \frac{N-\|\{d: t<d\}\|}{\|\{d: t<d\}\|}

最大IDF(Max IDF) :消除语料库规模影响,以最高文档频率为基准,适用于跨数据集对比(如合并多来源新闻)

idfmax (t, D)=logmaxt{d:td}+1{d:t(d}+1\operatorname{idf}_{\text {max }}(t, D)=\log \frac{\max _{t^{\prime}}\left\|\left\{d: t^{\prime} \subset d\right\}\right\|+1}{\|\{d: t(d\} \|+1}

双对数IDF(Double Log IDF) :进一步压缩高频词权重,降低极端值影响,应用于高维稀疏特征,如N-gram(后续文章详述),适合社交媒体短文本(如话题挖掘)

idfloglog(t, D)=log(1+logN{dtd})\begin{aligned} & \operatorname{idf}_{\log \log }(t, D)= \\ & \log \left(1+\log \frac{N}{\|\{d t \in d\}\|}\right)\end{aligned}

基于信息熵的IDF(Information Entropy-based) :基于信息熵的IDF通过分布熵与文档频率的双重约束,更精准量化词的重要性,尤其适合需要细粒度权重调整的场景(如学术文献挖掘、垂直领域搜索)

专业术语提取:低熵高IDF词(如“量子纠缠”)比均匀分布的高频词(如“研究”)权重更高。

垃圾内容检测:广告词(集中出现在少数文档)会被强化识别。

长尾关键词挖掘:小众但分布集中的词(如方言词汇)获得合理权重。

idfentropy(t)=1H(t)logN+αlog(Nnt)\mathrm{idf}_{\mathrm{entropy}}(t)=1-\frac{H(t)}{\log N}+\alpha\cdot\log\left(\frac{N}{n_t}\right)

H(t){H(t)}词t的文档分布熵

N 语料库总文档数

nt{n_t}包含词t的文档数

α\alpha平衡系数(0.5-1.0)

其中熵的计算公式是

H(t)=dDtf(t,d)Ftlog(f(t,d)Ft)H(t)=-\sum_{d\in D_t}\frac{f(t,d)}{F_t}\log\left(\frac{f(t,d)}{F_t}\right)

Dt{D_t}:包含t的文档数

f(t,d){f(t,d)}:词t在文档d中的频次TF值

Ft{F_t}:词t在语料库的总频次

熵值H(t) 越高,表明词t的分布越均匀,反之则越集中。同标准IDF相比,关键词区分度除了依赖于稀有性之外,还依赖于分布集中性。并且抗停用词干扰能力远高于标准IDF。

对应几种IDF变体,对比如下

变体核心调整推荐场景
标准IDF原始对数比通用文本分类、搜索引擎
平滑IDF避免除零错误小规模/动态语料库
概率IDF强惩罚高频词垃圾内容检测、异常词挖掘
最大IDF动态基准调整增量学习、流数据处理
双对数IDF压缩权重范围高维特征(如生物医学文本)
信息熵IDF考虑词分布均匀性关键词提取、主题建模

 

三、 《民法典》的python实战

中华人民共和国民法典共分七编,我们每一编形成一个word文件,统一放在一个文件夹内,我们的需求是计算出词对应的TF-IDF值,为后续的法条智能检索系统做准备。相应文件可以到下面网盘下载。

pan.baidu.com/s/1Xo0uVC1n… 提取码: rock)

1. 实现步骤

需要做的步骤如下:

(1)读入每一个word文件

《中华人民共和国民法典》共七编,我们每一编形成一个word文件,构成了我们要使用的语料库:

  • 第一编总则.docx

  • 第二编物权.docx

  • 第三编合同.docx                                                           

  • 第四编人格权.docx

  • 第五编婚姻家庭.docx

  • 第六编继承.docx

  • 第七编侵权责任.docx

(2)文本清洗预处理

所谓文本清洗就是对语料库中进行必要的清理工作,对于中文文档,清理过程包括:保留中文汉字,去除标点、空格、换行、特殊符号、非汉字字符等等

(3)分词

由于信息检索及文本挖掘等基本对象是词,需要把语料库里的句子拆分成一个个的词汇。对于英文文章的分词简单,词之间是以空格隔开的,但是中文就麻烦了,需要合理的分词技术和手段

(4)去重

对于TF-IDF的应用场景来讲,分析的事一个个的词汇个体,因此每一篇文章的词要去除重复

(5)过滤停用词

有些词汇对应用目的来讲是无意义的,例如(序数词、的、了、从、给、他等等),这些词是不需要计算的,要排除掉

(6)计算TF、IDF、TF-IDF

按前述计算公式及变体计算每一个词汇的TF、IDF、TF-IDF值

2. 文档预处理

读入每一个word文件

import os
from docx import Document
def read_docx(file_path):
    """
    读取 Word 文档,并返回一个字符串,该字符串包含文档中的所有内容
    args:
        file_path: Word 文档的路径
    return:
        str: 文档中的段落内容用空格连接起来,返回一个字符串
    """
    try:
        doc = Document(file_path)
        return " ".join([para.text for para in doc.paragraphs])
    except KeyError:
        print(f"警告:无法读取文件 {file_path},可能是损坏的 Word 文件。")
        return ""
def load_documents_from_folder(folder_path):
    """
    从文件夹加载所有docx文档
    args:
        folder_path: 文件夹路径
    return:
        dict: 键为文件名,值为文档内容
    """
    dict_documents = {}
    for filename in os.listdir(folder_path):
        if filename.endswith(".docx"):
            file_path = os.path.join(folder_path, filename)
            dict_documents[filename] = read_docx(file_path)
    return dict_documents

调用

def main():
    # 设置docx文件所在文件夹路径
    folder_path = "E:\\training\\projects\\jupyter\\tfidf\\docx"
    dict_docs = load_documents_from_folder(folder_path)
    print(f"已加载 {len(dict_docs)} 个文档")
    for filename, doc in dict_docs.items():
        print(f"文档: {filename}-有字符数: {len(doc)}")

文本清洗预处理

import re
def preprocess_text(text):
    """
    预处理文本:移除标点符号和特殊字符,保留字母、数字、和中文字符
    args:
        text: 输入的文本
    return:
        list: 处理后的单词列表
    """
    return re.sub(r"[^\w\u4e00-\u9fff]""", text)

这里使用了正则表达式来把匹配上的文本替换成空字符

调用

def main():
.............................
    for filename, doc in dict_docs.items():
        print(f"文档: {filename}-有字符数: {len(doc)}")
    processed_docs = {
        filename: preprocess_text(doc) for filename, doc in dict_docs.items()
    }
    for filename, doc in processed_docs.items():
        print(f"预处理后的文档: {filename}-有字符数: {len(doc)}")

分词

import jieba
def cut_word(text):
    """
    分词:使用 jieba 精确模式分词
    args:
        text: 输入的文本
    return:
        list: 分词后的单词列表
    """
    return list(jieba.cut(text, cut_all=False))

这里使用了“结巴”分词库,可选的中文分词还有PKUSEG北大分词、FudanNLP复旦分词和多种专业领域的分词工具;

另外要注意的是jieba.cut方法返回的是个生成器Generator,如果对结果要多次访问的话,要转成List

调用

def main():
.............................
    for filename, doc in processed_docs.items():
        print(f"预处理后的文档: {filename}-有字符数: {len(doc)}")
    cuted_docs = {filename: cut_word(doc) for filename, doc in processed_docs.items()}
    for filename, doc in cuted_docs.items():
        print(f"{filename}-分出来{len(doc)}个词")

去重

def deduplication(words):
    """
    去重:使用 set 集合去重
    args:
        words: 输入的单词列表
    return:
        list: 去重后的单词列表
    """
    return list(set(word for word in words))

去重的原理是把元素词汇放在set内,天然去重了

过滤停用词

def filter_stop_words(words):
    """
    过滤停用词:使用 合并的 chinese_stop_words.txt 文件进行过滤
    args:
        words: 输入的单词列表
    return:
        list: 过滤停用词后的单词列表
    """
    try:
        with open("chinese_stop_words.txt""r", encoding="utf-8") as f:
            chinese_stop_words = set(line.strip() for line in f if line.strip())
        return [word for word in words if word not in chinese_stop_words]
    except FileNotFoundError:
        print("未找到中文停用词文件 chinese_stop_words.txt,将不使用中文停用词过滤")
        return list()

这里的中文停用词文件是结合了百度停用词、武汉大学停用词、英文停用词合并而来的,你也可以根据需要自行添加停用词

以上python代码中充分使用了列表推导式、字典推导式和生成器表达式这些非常pythonic的语法功能,简化了代码。

3. 计算TF

原始公式TF

def tf_original(words, doc):
    """
    计算 TF(词频),使用原始计算公式TF(t,d) = (词t在文档d中出现的次数) / (文档d中所有词的总数)
    :param words: 去重、去停的词语列表
    :param doc: 要计算的目标文档
    :return: TF(词频)列表,列表中每一个元素是一个元组,元组中第一个元素是词语,第二个元素是 TF(词频)
    """
    word_tf_list = []
    counter = Counter(doc)
    word_count = len(doc)
    for word in words:
        word_tf_list.append((word, counter[word] / word_count))
    return word_tf_list

调用

def main():
.............................
    for filename, words in filtered_words.items():
        print(f"{filename}-去停用词后有{len(words)}个词")
    for filename, words in filtered_words.items():
        word_tf_list = tf_original(words, cuted_docs[filename])
        word_tf_list.sort(key=lambda x: x[1], reverse=True)
        print(f"{filename}-tf值最大的10个词:")
        print(
            "\n".join(
                f"tf前10个词:{word},tf值:{tf:.6f}" for word, tf in word_tf_list[:10]
            )
        )

运行结果如下

第一编总则.docx-tf值最大的10个词:tf前10个词:权利,tf值:0.001193tf前10个词:职责,tf值:0.001193tf前10个词:合法权益,tf值:0.001193tf前10个词:核心,tf值:0.001193tf前10个词:申请,tf值:0.001193tf前10个词:显失,tf值:0.001193tf前10个词:损害,tf值:0.001193tf前10个词:收养,tf值:0.001193tf前10个词:还,tf值:0.001193tf前10个词:健康,tf值:0.001193第七编侵权责任.docx-tf值最大的10个词:tf前10个词:职责,tf值:0.001931tf前10个词:多人,tf值:0.001931tf前10个词:合法权益,tf值:0.001931tf前10个词:权利,tf值:0.001931tf前10个词:水平,tf值:0.001931tf前10个词:损害,tf值:0.001931tf前10个词:还,tf值:0.001931tf前10个词:健康,tf值:0.001931tf前10个词:消毒,tf值:0.001931tf前10个词:社会,tf值:0.001931第三编合同.docx-tf值最大的10个词:tf前10个词:权利,tf值:0.000659tf前10个词:合法权益,tf值:0.000659tf前10个词:封存,tf值:0.000659tf前10个词:收件人,tf值:0.000659tf前10个词:悬赏,tf值:0.000659tf前10个词:之日起,tf值:0.000659tf前10个词:转让权,tf值:0.000659tf前10个词:社会,tf值:0.000659tf前10个词:疫情,tf值:0.000659第五编婚姻家庭.docx-tf值最大的10个词:tf前10个词:权利,tf值:0.001953tf前10个词:所负,tf值:0.001953tf前10个词:医治,tf值:0.001953tf前10个词:合法权益,tf值:0.001953tf前10个词:职责,tf值:0.001953tf前10个词:申请,tf值:0.001953tf前10个词:消失,tf值:0.001953tf前10个词:相差,tf值:0.001953tf前10个词:吸毒,tf值:0.001953tf前10个词:日常生活,tf值:0.001953第六编继承.docx-tf值最大的10个词:tf前10个词:权利,tf值:0.003067tf前10个词:职责,tf值:0.003067tf前10个词:申请,tf值:0.003067tf前10个词:损害,tf值:0.003067tf前10个词:书写,tf值:0.003067tf前10个词:正当理由,tf值:0.003067tf前10个词:指定,tf值:0.003067tf前10个词:二为,tf值:0.003067tf前10个词:民政部门,tf值:0.003067tf前10个词:养,tf值:0.003067第四编人格权.docx-tf值最大的10个词:tf前10个词:行踪,tf值:0.002119tf前10个词:权利,tf值:0.002119tf前10个词:科学研究,tf值:0.002119tf前10个词:合法权益,tf值:0.002119tf前10个词:申请,tf值:0.002119tf前10个词:损害,tf值:0.002119tf前10个词:健康,tf值:0.002119tf前10个词:社会,tf值:0.002119tf前10个词:肖像权,tf值:0.002119tf前10个词:身心健康,tf值:0.002119
 

标准的TF计算公式优点:简单直观,保留原始统计信息。缺点:对长文档偏向性大(长文档词频天然更高),需配合其他变体来优化。

对数缩放TF

def tf_logarithmic_scaling(words, doc):
    """
    计算 TF(词频),使用对数计算公式tf(t,d) = log(1 + f(t,d))
    :param words: 去重、去停的词语列表
    :param doc: 要计算的目标文档
    :return: TF(词频)列表,列表中每一个元素是一个元组,元组中第一个元素是词语,第二个元素是 TF(词频)
    """
    word_tf_list = []
    counter = Counter(doc)
    word_count = len(doc)
    for word in words:
        word_tf_list.append((word, math.log(1 + counter[word] / word_count)))
    return word_tf_list

原始计数保留原始频次,可能放大长文档偏差,而对数缩放压缩增长趋势,削弱绝对优势。

应用场景与效果差异

(1) 长文档 vs 短文档

原始计数:

长文档问题:若某词在长文档中出现100次,短文档出现5次,直接计数会导致长文档权重过高(如 100100 vs 55)。

适用场景:需精确量化词频的任务(如词云生成、热点词统计)。

对数缩放:

平滑效果:将100次和5次分别映射为 log(101)≈4.62 和 log(6)≈1.79,差异从95缩小到2.83。

适用场景:文本分类、信息检索等需平衡长短文档权重的任务。

(2) 高频词敏感性

原始计数:

对停用词(如“的”“是”)敏感,可能因高频但无意义的词干扰模型。

对数缩放:

通过对数压缩,减少高频停用词的绝对影响(如100次→4.62,10次→2.40),保留相对重要性。

(3) 计算效率

原始计数:计算更快(无需对数运算),适合实时系统。

对数缩放:需额外计算,但现代硬件下开销可忽略。

实际案例对比

案例1:新闻分类

文档A(体育新闻):"比赛"出现50次,"进球"出现20次。

文档B(科技新闻):"算法"出现10次,"数据"出现15次。

TF值计算:

原始计数(A/B)对数缩放(A/B)
比赛50 / 03.93 / 0
进球20 / 03.04 / 0
算法0 / 100 / 2.40
数据0 / 150 / 2.77

分析:

原始计数下,体育新闻因高频词主导,可能掩盖科技新闻特征;对数缩放后,权重差异更合理。

案例2:搜索引擎排序

文档1:"Python"出现30次,"教程"出现5次。

文档2:"Python"出现10次,"教程"出现10次。

TF值对比:

原始计数(文档1/2)对数缩放(文档1/2)
Python3/13.43 / 2.40
教程1/21.79 / 2.40

原始计数:文档1因"Python"高频排名更高,但可能内容冗余。

对数缩放:文档2因"教程"权重提升,更符合用户意图(均衡需求)。

选择建议
选择对数缩放选择原始计数
需平衡长短文档权重需保留绝对词频信息(如词频统计)
高频词可能干扰模型(如停用词)任务依赖精确频次(如热点分析)
文本分类/聚类等需标准化场景实时系统要求极低计算延迟

 

增强归一化 TF

增强归一化通过文档内相对词频压缩和最低权重保护,解决了原始计数的长度偏向性和低频词忽略问题,更适合公平性要求高的场景

def tf_augmented_normalization(words, doc):
    """
    计算 TF(词频),使用增强的归一化公式tf(t,d) = 0.5 + 0.5 × (f(t,d)/max{f(w,d):w∈d})
    :param words: 去重、去停的词语列表
    :param doc: 要计算的目标文档
    :return: TF(词频)列表,列表中每一个元素是一个元组,元组中第一个元素是词语,第二个元素是 TF(词频)
    """
    word_tf_list = tf_original(words, doc)
    max_tf = max([tf for _, tf in word_tf_list])
    return [(word, 0.5 + 0.5 * (tf / max_tf)) for word, tf in word_tf_list]
应用场景与效果差异

(1) 长文档与短文档的公平性

原始计数:

长文档中高频词的绝对优势明显(如某词出现100次 vs 短文档的5次),导致长文档主导排序。

适用场景:需要绝对频次统计的任务(如词频热点分析)。

增强归一化:

将词频除以文档内最大词频,消除文档长度差异(如100次→1.0,5次→0.5),再通过线性变换保留最低权重。

适用场景:跨文档比较(如搜索引擎结果排序)、长短文本混合的数据集。

(2) 高频词与低频词的平衡

原始计数:

停用词(如“的”)或超高频主题词可能垄断权重。

增强归一化:

最高频词权重为1,其他词按比例分配,且最低0.5,避免低频词被完全忽略。

(3) 抗噪声能力

增强归一化对以下场景更鲁棒:

重复刷关键词的垃圾文本(如“优惠 优惠 优惠...”会被压缩到1.0)。

短文本中偶然出现的低频词(至少保留0.5权重)。

实际案例对比

案例1:电商评论分析

文本:

评论A(长):"好 好 好 好 好 好 好 好 好 好"(“好”出现10次,其他词各1次)。

评论B(短):"好 差"(“好”“差”各1次)。

方法评论A的“好”评论A的其他词评论B的“好”评论B的“差”
原始计数10111
增强归一化10.5511

原始计数下,评论A因刷词垄断权重;增强归一化后,评论B的“差”未被淹没,更适合情感分析。

案例2:学术论文检索

查询词:"深度学习 模型"

论文1:"深度学习"出现20次(最大词频),"模型"出现10次。

论文2:"深度学习"出现5次(最大词频),"模型"出现4次。

原始计数(论文1/2)增强归一化(论文1/2)
深度学习4/11.0 / 1.0
模型5/20.75 / 0.9

增强归一化更突出论文2中“模型”的相对重要性,避免论文1仅因篇幅长排名靠前。

选择建议
选择增强归一化选择原始计数
需消除文档长度差异的跨文档比较依赖绝对频次(如词频统计报告)
保护低频词信号(如短文本、稀疏特征)实时系统要求极简计算
抗刷词干扰的场景(如评论、社交媒体)长文档内部词频分析(如主题建模)