文章目录

  • 算法思想
  • 算法原理
  • TF-IDF与信息论
  • 平滑处理
  • 正则化处理
  • 算法实现



算法思想

TF-IDF(term frequency–inverse document frequency,词频-逆向文件频率) 是用于信息检索与文本挖掘的重要算法,其中TF用于度量关键词在文档中的重要性,IDF用于度量关键词在全文档中的重要性, 即文档中某关键词的重要性,与它在当前文档中的频率成正比,而与包含它的文档数成反比。

TF-IDF的主要思想是,若一个关键词在一篇文档中出现的频率高,而在其他文档中很少出现,则该关键词可较好的反应当前文档的特征。


算法原理

度量某文档和查询的相关性,最简单的方法是利用各查询关键词在该文档中出现的总词频(Term Frequency,TF)

具体地,对于包含M个关键词的w1, w2,…wn查询,各关键词在某文档中出现的频率分别为:TF(w1), TF(w2),…,TF(wM),则该文档与查询的相关性为:
权重量化3bit 权重度量_权重量化3bit

某些关键词可能同时出现在多篇文档中,该类关键词的主题预测能力较弱,可见,仅使用TF不能很好的反应文档与查询的相关性。

关键词的主题预测能力越强,在度量与文档的相关性时,其权重应该越大。 也就是说,若某关键词在较少文档中出现,则该关键词的权重应该较高,如关键词原子能的权重大于应用的权重。因此,利用包含某关键词的文档数,修正仅用词频TF度量该关键词的权重。

在信息检索领域,使用逆文本频率(Inverse Document Frequency, IDF) 表示关键词的主题预测能力(权重),表示为
权重量化3bit 权重度量_权重量化3bit_02

其中D为全部文档数,DF(w)为包含关键词w的文档数。

利用IDF的思想,文档与查询的相关性计算由简单的词频求和,变为以IDF为权重的加权求和,即
权重量化3bit 权重度量_Python_03


TF-IDF与信息论

一个查询中,每个关键词的权重应该反应其为查询提供的信息量,简单的方法就是,用关键词的信息量,作为它在查询中的权重,即
权重量化3bit 权重度量_Python_04

其中N为整个语料库中的总词数,是可忽略的常数,此时
权重量化3bit 权重度量_机器学习_05

若两个关键词在全文档中出现的频率相同,但第一个关键词集中分布在少数文章中,而第二个关键词分布在多篇文章中,显然,第一个关键词具有更好的主题预测能力,应赋予更高的查询权重。

为此,提出以下假设(总文档数D,总词数N,包含关键词w的文档数DF(w)):

  • 每个文档含词数基本相同,即
    权重量化3bit 权重度量_机器学习_06
  • 每个关键词一旦在文档中出现,不论其出现多少次,权重都相同,即关键词w在文档中未出现,则权重为0;否则,则为
    权重量化3bit 权重度量_TF-IDF_07

因此,关键词w的信息量
权重量化3bit 权重度量_关键词权重_08

=>
权重量化3bit 权重度量_Python_09

易知,关键词w的TF-IDF值,与其信息量成正比;又由于M>c(w),知关键词w的TF-IDF值,与其在文档中出现的平均次数成反比,这些结论完全符合信息论。


平滑处理

经过平滑处理后, IDF的最终计算公式如下:
权重量化3bit 权重度量_Python_10

  • log项中分子项和分母项均加1,表示虚拟增加一篇包含任意词的文档,避免分母项为0;
  • IDF的最终值加1,避免某单词在所有文档中出现时,IDF的值为0,即不忽略出现在所有文档中的词;

正则化处理

sklearn中类TfidfTransformer默认对文档的TF-IDF特征向量做l2正则化,即某文档的TF-IDF特征向量为v,则
权重量化3bit 权重度量_Python_11

若单词表为{w1, w2, w3},文档A=(w1, w2, w2),B=(w1, w2, w3),且w1, w2, w3IDF值相同,则未正则化时
权重量化3bit 权重度量_关键词权重_12

此时,文档A、B中单词w1TF-IDF值相同。

若进行l2正则化,则
权重量化3bit 权重度量_机器学习_13

可见文档B中w1TF-IDF值(权重)更大,正则化后的意义为:考虑文档的TF-IDF特征分布,增加不同权重之间的差异。

不失一般性,文档A、B中正则化后w1的TF-IDF分别为
权重量化3bit 权重度量_关键词权重_14

如TF(A_w1) = TF(B_w1),且TF之和为1,知
权重量化3bit 权重度量_关键词权重_15

推导出
权重量化3bit 权重度量_Python_16

进而,推导出
权重量化3bit 权重度量_TF-IDF_17

当前仅当TF(B_w2) = 0或TF(B_w3) = 0,即B中w2或w3的频率为0时,等式成立。

算法实现

算法的实现参考了sklearn.feature_extraction.text中的CountVectorizerTfidfVectorizer类,如下:

import re
from collections import defaultdict

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
import numpy as np
from scipy.sparse import csr_matrix, spdiags
from scipy.sparse.linalg import norm

PTN_SYMBOL = re.compile(r'[.!?\'",]')


def tokenize(doc):
    """
    英文分词,小写输出
    """
    for word in PTN_SYMBOL.sub(' ', doc).split(' '):
        if word and word != ' ':
            yield word.lower()


def count_vocab(raw_documents):
    """
    返回文档词频的稀疏矩阵
    参考sklearn.feature_extraction.text.CountVectorizer._count_vocab

    矩阵大小:M*N, M个文档, 共计N个单词

    :param raw_documents: ['Hello world.', 'Hello word', ...]
    :return: csc_matrix, vocabulary
    """
    vocab = {}
    data, indices, indptr = [], [], [0]

    for doc in raw_documents:
        doc_feature = defaultdict(int)
        for term in tokenize(doc):
            # 词在词表中的位置
            index = vocab.setdefault(term, len(vocab))
            # 统计当前文档的词频
            doc_feature[index] += 1
        # 存储当前文档的词及词频
        indices.extend(doc_feature.keys())
        data.extend(doc_feature.values())
        # 累加词数
        indptr.append(len(indices))

    # 构造稀疏矩阵
    X = csr_matrix((data, indices, indptr), shape=(len(indptr) - 1, len(vocab)), dtype=np.int64)

    # 将单词表排序,同时更新压缩矩阵数据的位置
    map_index = np.empty(len(vocab), dtype=np.int32)
    for new_num, (term, old_num) in enumerate(sorted(vocab.items())):
        vocab[term] = new_num
        map_index[old_num] = new_num
    X.indices = map_index.take(X.indices, mode='clip')

    X.sort_indices()

    return X, vocab


def tfidf_transform(X, smooth_idf=True, normalize=True):
    """
    将词袋矩阵转换为TF-IDF矩阵

    :param X: 压缩的词袋矩阵 M*N, 文本数M, 词袋容量N
    :param smooth_idf: 是否对DF平滑处理
    :param normalize: 是否对TF-IDF执行l2标准化
    :return: TF-IDF压缩矩阵(csc_matrix)
    """
    n_samples, n_features = X.shape

    df = np.bincount(X.indices, minlength=X.shape[1])
    df += int(smooth_idf)
    new_n_samples = n_samples + int(smooth_idf)
    idf = np.log(float(new_n_samples) / df) + 1.0

    # 对角稀疏矩阵N*N,元素值对应单词的IDF
    idf_diag = spdiags(idf, diags=0, m=n_features, n=n_features, format='csr')

    # 等价于 DF * IDF
    X = X * idf_diag

    # 执行l2正则化
    if normalize:
        norm_l2 = 1. / norm(X, axis=1)
        tmp = spdiags(norm_l2, diags=0, m=n_samples, n=n_samples, format='csr')
        X = tmp * X

    return X


if __name__ == '__main__':
    # 源文档
    raw_documents = [
        'This is the first document.',
        'This is the second second document.',
        'And the third one.',
        'Is this the first document?',
    ]
    # 转换为词袋模型
    X, vocab = count_vocab(raw_documents)
    # X = CountVectorizer().fit_transform(raw_documents)
    """
    >> vocab
    {'this': 8, 'is': 3, 'the': 6, 'first': 2, 'document': 1, 'second': 5, 'and': 0, 'third': 7, 
    'one': 4}
    
    >> X.toarray()
    [[0 1 1 1 0 0 1 0 1]
     [0 1 0 1 0 2 1 0 1]
     [1 0 0 0 1 0 1 1 0]
     [0 1 1 1 0 0 1 0 1]]
    """

    # 计算TF-IDF
    tfidf_x = tfidf_transform(X)
    # tfidf_x = TfidfVectorizer().fit_transform(raw_documents)
    """
    >> tfidf_x.toarray()
    [   [0.       0.439       0.542       0.439       0.          0.      0.359   0.         0.439]
        [0.       0.272       0.          0.272       0.          0.853   0.223   0.         0.272]
        [0.553    0.          0.          0.          0.553       0.      0.288   0.553      0.   ]
        [0.       0.439       0.542       0.439       0.          0.      0.359   0.         0.439]	]
    """