TF-IDF 到 BM25

387 阅读5分钟

IF-IDF

IF_IDF (Term Frequency-Inverse Document Frequency): 词频-逆文档词频

1. IF:

给定的单词word在文档集合中出现的次数除以文档中所有单词总数

tf(t,d) = count of t in d / number of words in d # d值得是所有文档,可以是一个也可以是多个

2. IDF

总文件数目除以包含该词语的文件的数目再将得到的商取对数得到

  • 最常见的逆文档频率(IDF)计算公式为:IDF=logNDFIDF=\log\frac{N}{DF},其中:

    • NN是文档的总数。例如,你有一个包含 100 篇文章的文档集合,那么 N=100N = 100
    • DFDF(Document Frequency)是包含某个特定词的文档的数量。比如,“人工智能” 这个词在 100 篇文章中有 10 篇文章出现,那么对于 “人工智能” 这个词,DF=10DF = 10
  • 以这个例子来计算 “人工智能” 的 IDF=log10010IDF = \log\frac{100}{10}

通常为了避免分母为0,会对公式进行平滑处理,对分子分母分别加一, IDF=logN+1DF+1IDF=\log\frac{N + 1}{DF + 1}

如果包含词条t的文档越少, IDF越大,则说明词条具有很好的类别区分能力,词条出现越频繁,说明该词越不重要。

4. 应用

(1)搜索引擎;(2)关键词提取;(3)文本相似性;(4)文本摘要

5. 优缺点

  • 优点:
    • 简单,易懂
  • 缺点:
    • 忽视语义:将文档当词袋模型,忽略词序与语义关系,在需理解语义任务中效果欠佳。

    • 低频敏感:因逆文档频率计算,易使低频词权重过高,可能受拼写错误、罕见词等干扰。

    • 缺上下文感知:不能考虑词的上下文含义差异,在需精细语义理解任务应用受限。

BM25

公式

BM25 公式:

BM25(q,D)=i=1nIDF(qi)f(qi,D)(k1+1)f(qi,D)+k1(1b+bDavgDL)\text{BM25}(q, D) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left( 1 - b + b \cdot \frac{|D|}{\text{avgDL}} \right)}

其中:

  • qq 是查询,DD 是文档,qiq_i是查询中的单个词项。

  • f(qi,D)f(q_i, D) 是词项 qiq_i 在文档 DD 中的出现频率(Term Frequency,TF)。

  • D|D| 是文档 DD 的长度(即文档中的总词数)。

  • avgDLavgDL 是文档集中文档的平均长度。

  • k1k_1bb 是两个调节参数,通常取值:

    • k1k_1 in [1.2,2.0][1.2, 2.0],控制词频对评分的影响。
    • bb in [0,1][0, 1],控制文档长度的归一化。
  • IDFIDF 是逆文档频率(Inverse Document Frequency),计算公式为:

    IDF(qi)=log(Ndf(qi)+0.5df(qi)+0.5+1.0)\text{IDF}(q_i) = \log \left( \frac{N - \text{df}(q_i) + 0.5}{\text{df}(q_i) + 0.5} + 1.0 \right)

    其中:

    • NN 是文档集中的总文档数。
    • df(qi)df(q_i)是包含词 q_的文档数(即文档频率,Document Frequency)。

改进之处

非线性词频(TF)函数: 在TF-IDF中,词频(TF)是一个简单的线性函数,即词在文档中出现的次数。BM25引入了一个非线性函数来限制词频的影响。具体来说,当某个词出现的频率很高时,它对相关性的贡献会逐渐减少。这是通过一个称为“k1”的参数来控制的。

文档长度归一化:通过参数b调整文档长度对得分的影响,避免长文档得分偏高。

改进的IDF:对于常见词的惩罚更强,减少其对相关性的贡献。

灵活的参数调节:通过调整k1b,可以优化模型以适应不同应用场景

代码实现

IF-IDF:


from collections import defaultdict
import math


# 英文停用词列表,可根据实际需求扩展或替换为中文停用词(如果处理中文文本)
"""
terms = ["a", "an", "the", "and", "or", "of", "in", "to", "is", "are", "was", "were", "it", "this", "that", "for",
              "on", "at", "by", "with", "from", "as", "but", "not", "if", "you", "he", "she", "they", "we", "my", "your",
              "his", "her", "its", "our", "their"]
"""



def preprocess_text(text):
    """
    对文本进行预处理,包括统一小写、去除停用词
    """
    # text = [word.lower() for word in text if word.lower() not in terms]
    return text


def compute_tf_for_term(documents, term):
    """
    计算指定词汇term在每个文档中的词频(TF)
    """
    tf_dict = {}
    for doc_index, doc in enumerate(documents):
        doc = preprocess_text(doc)
        term_count = doc.count(term)
        total_word_count = len(doc)
        if total_word_count > 0:
            tf_dict[doc_index] = term_count / total_word_count
        else:
            tf_dict[doc_index] = 0
    return tf_dict


def compute_idf_for_term(documents, term):
    """
    计算指定词汇term的逆文档频率(IDF)
    """
    doc_num = len(documents)
    doc_count_with_term = 0
    for doc in documents:
        doc = preprocess_text(doc)
        if term in doc:
            doc_count_with_term += 1
    if doc_count_with_term > 0:
        return math.log(doc_num / doc_count_with_term)
    return 0


def compute_tf_idf_for_term(documents, term):
    """
    计算指定词汇term与每个文档的TF-IDF值
    """
    tf_dict = compute_tf_for_term(documents, term)
    idf = compute_idf_for_term(documents, term)
    tf_idf_dict = {}
    for doc_index in tf_dict:
        tf_idf_dict[doc_index] = tf_dict[doc_index] * idf
    return tf_idf_dict


if __name__ == '__main__':
    documents = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                 ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                 ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                 ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                 ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                 ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    term = "dog"  # 这里指定要计算TF-IDF值的词汇,可根据需要替换
    tf_idf_result = compute_tf_idf_for_term(documents, term)
    for doc_index, value in tf_idf_result.items():
        print(f"词汇 {term} 在文档 {doc_index + 1} 中的TF-IDF值为: {value}")

BM25:

import math
import re
from collections import defaultdict


class BM25:
    def __init__(self, documents, k1=1.2, b=0.75):
        """
        BM25初始化函数
        :param documents: 文档集合,每个元素为一个文档(可以是字符串形式,内部会进行预处理)
        :param k1: BM25算法中的调节参数,默认值为1.2
        :param b: BM25算法中的调节参数,默认值为0.75
        """
        self.documents = documents
        self.k1 = k1
        self.b = b
        self.avg_doc_length = self._compute_avg_doc_length()
        self.freq_matrix = self._build_freq_matrix()
        self.idf_dict = self._compute_idf()

    def _preprocess_text(self, text):
        """
        对文本进行预处理,包括转换为小写、去除标点符号、分词(简单按空格分词,可替换为专业分词库如jieba)
        :param text: 输入的文本字符串
        :return: 处理后的词列表
        """
        text = text.lower()
        text = re.sub(r'[^\w\s]', '', text)
        words = text.split()
        return words

    def _compute_avg_doc_length(self):
        """
        计算文档集合中平均文档长度
        :return: 平均文档长度
        """
        total_length = sum(len(self._preprocess_text(doc)) for doc in self.documents)
        return total_length / len(self.documents) if len(self.documents) > 0 else 0

    def _build_freq_matrix(self):
        """
        构建词频矩阵,记录每个词在每个文档中的出现频率
        :return: 词频矩阵(以字典形式存储,键为词,值为字典,内层字典键为文档索引,值为词频)
        """
        freq_matrix = defaultdict(lambda: defaultdict(int))
        for doc_index, doc in enumerate(self.documents):
            words = self._preprocess_text(doc)
            for word in words:
                freq_matrix[word][doc_index] += 1
        return freq_matrix

    def _compute_idf(self):
        """
        计算每个词的逆文档频率(IDF)
        :return: IDF字典(键为词,值为对应的IDF值)
        """
        doc_num = len(self.documents)
        idf_dict = defaultdict(int)
        word_doc_count = defaultdict(int)
        for word in self.freq_matrix:
            for doc_index in self.freq_matrix[word]:
                word_doc_count[word] += 1
            idf_dict[word] = math.log((doc_num - word_doc_count[word] + 0.5) / (word_doc_count[word] + 0.5))
        return idf_dict

    def get_score(self, query):
        """
        计算查询词与文档集合中各文档的BM25得分
        :param query: 查询词列表(可以是经过预处理的词列表形式)
        :return: BM25得分字典(键为文档索引,值为对应的BM25得分)
        """
        score_dict = defaultdict(float)
        query = [self._preprocess_text(word)[0] if isinstance(word, str) else word for word in query]
        for word in query:
            if word in self.idf_dict:
                idf = self.idf_dict[word]
                for doc_index in range(len(self.documents)):
                    freq = self.freq_matrix[word][doc_index]
                    doc_length = len(self._preprocess_text(self.documents[doc_index]))
                    score = idf * (freq * (self.k1 + 1) / (
                            freq + self.k1 * (1 - self.b + self.b * doc_length / self.avg_doc_length)))
                    score_dict[doc_index] += score
        return score_dict


if __name__ == "__main__":
    # 示例文档集合
    documents = [
        "This is an article about natural language processing.",
        "Natural language processing techniques are very important in today's society.",
        "The article mainly introduces some applications of natural language processing."
    ]

    # 创建BM25实例并传入文档集合
    bm25 = BM25(documents)

    # 示例查询词
    query = ["natural", "language", "processing"]

    # 获取查询词与文档的BM25得分
    scores = bm25.get_score(query)

    # 将文档和对应的得分组成元组,方便排序
    doc_score_tuples = [(doc_index, score) for doc_index, score in scores.items()]

    # 按照得分从高到低对文档进行排序
    doc_score_tuples = sorted(doc_score_tuples, key=lambda x: x[1], reverse=True)

    for doc_index, score in doc_score_tuples:
        print(f"文档 {doc_index} 的BM25得分: {score}")

参考

TF-IDF算法介绍及实现-CSDN博客

【搜索核心技术】经典搜索核心算法:BM25及其变种-CSDN博客