词袋模型和TF-IDF(数学公式推导、手动实现、调库使用、示例:使用词袋模型处理多个文档)详解

发布于:2025-04-16 ⋅ 阅读:(31) ⋅ 点赞:(0)

1. 基本概念介绍

1.1 词袋模型(Bag of Words, BoW)

词袋模型是一种将文本表示成向量的方法,其基本思想是:

  • 把一个文本视作“装满单词的袋子”,忽略单词出现的顺序和语法信息
  • 构造一个词汇表(词典),然后统计文档中每个词汇出现的次数
  • 得到的向量维度等于词典中不同词的数量,向量的每个元素通常代表相应单词的词频(TF,Term Frequency)

词袋模型简单高效,广泛应用于文本分类、聚类、信息检索等任务,但其主要缺点在于:

  • 忽略了单词之间的顺序和上下文信息
  • 对常见无意义词(例如停用词)的区分能力较差

1.2 TF-IDF

TF-IDF(Term Frequency–Inverse Document Frequency)是一种加权方案,其目标是衡量一个词对某个文档的重要性,同时降低在整个语料库中频繁出现的词的权重。
其直观思路是:

  • 如果一个词在当前文档中出现频繁(高 TF),那么它可能是当前文档的关键词
  • 同时,如果这个词在其它文档中出现得较少(低 DF,即高逆文档频率 IDF),则它更能代表文档特征

TF-IDF 常被用来加强词袋模型的特征表现,得到的向量不仅包含词频信息,更能突出关键信息。


2. 数学公式推导与解释

2.1 词频(TF)

对给定文档 d j d_j dj 中的单词 t i t_i ti,词频可以计算为:
T F ( t i , d j ) = n i , j ∑ k n k , j \mathrm{TF}(t_i, d_j) = \frac{n_{i,j}}{\sum\limits_{k} n_{k,j}} TF(ti,dj)=knk,jni,j

  • n i , j n_{i,j} ni,j:词 t i t_i ti 在文档 d j d_j dj中出现的次数
  • 分母是文档 d j d_j dj 中所有单词的总个数,用于归一化,使得每个文档中的 TF 值在 [0,1] 之间

2.2 逆文档频率(IDF)

逆文档频率反映了词语在整个语料库中出现的分布情况,其公式为:
I D F ( t i ) = log ⁡ ∣ D ∣ ∣ { d j ∈ D : t i ∈ d j } ∣ \mathrm{IDF}(t_i) = \log \frac{|D|}{|\{ d_j \in D : t_i \in d_j \}|} IDF(ti)=log{djD:tidj}D

  • ∣ D ∣ |D| D:语料库中文档的总数

  • 分母表示包含词 t i t_i ti 的文档数量

  • 常见做法是在分母加 1,以避免除零情况,例如:
    I D F ( t i ) = log ⁡ ∣ D ∣ 1 + ∣ { d j : t i ∈ d j } ∣ \mathrm{IDF}(t_i) = \log \frac{|D|}{1 + |\{ d_j : t_i \in d_j \}|} IDF(ti)=log1+{dj:tidj}D
    不同实现中也有加常数或平滑项(例如在 scikit-learn 中默认实现为 log ⁡ 1 + ∣ D ∣ 1 + ∣ { d j : t i ∈ d j } ∣ + 1 \log \frac{1+|D|}{1+|\{ d_j : t_i \in d_j \}|}+1 log1+{dj:tidj}1+D+1)。

2.3 TF-IDF 综合公式

将上面两个部分相乘,就得到 TF-IDF 加权:
T F - I D F ( t i , d j ) = T F ( t i , d j ) × I D F ( t i ) \mathrm{TF\text{-}IDF}(t_i, d_j) = \mathrm{TF}(t_i, d_j) \times \mathrm{IDF}(t_i) TF-IDF(ti,dj)=TF(ti,dj)×IDF(ti)
权重较高的词通常具有以下特点:

  • 在当前文档中出现频率较高
  • 在其他文档中出现频率较低

这样可以有效地过滤掉常见的(但信息量低的)词汇,如“的”、“是”等停用词。


3. 用例说明

假设有一个简单的语料库包含两篇文档:

  • 文档1: “我 爱 北京 天安门”
  • 文档2: “我 爱 吃 苹果”

构建词典后可能得到词汇表:[“我”,“爱”,“北京”,“天安门”,“吃”,“苹果”]

对于文档1,词频向量(未经 TF-IDF 加权)可能为: [ 1 , 1 , 1 , 1 , 0 , 0 ] [1,1,1,1,0,0] [1,1,1,1,0,0]

对于文档2,向量为: [ 1 , 1 , 0 , 0 , 1 , 1 ] [1, 1, 0, 0, 1, 1] [1,1,0,0,1,1]

如果使用 TF-IDF 进行加权,假设在整个语料库中每个词的分布如下:

  • “我”、“爱”在两篇文档中均出现,故其 IDF 较低
  • “北京”、“天安门”只出现在文档1,IDF 较高
  • “吃”、“苹果”只出现在文档2,IDF 较高

这样计算后:

  • 对于文档1,“北京”和“天安门”的 TF-IDF 权重就会比“我”和“爱”高,突出文档内容的特征
  • 同理文档2中,“吃”和“苹果”的权重较高

这种做法可以提高文本分类或信息检索时特征表示的区分能力。


4. Python 代码简单使用

下面给出两段示例代码,一段使用 scikit-learn 的高层 API(TfidfVectorizer),另一段展示如何先用 CountVectorizer 得到词频再计算 TF-IDF。

  • 注意

    • scikit-learn 中的 TfidfVectorizer 默认使用了一个正则表达式参数来进行分词,该正则表达式要求每个词至少包含两个连续的“单词字符”。默认参数为:

      token_pattern=r"(?u)\b\w\w+\b"
      

      这里 “\w\w+” 的意思是匹配至少两个字母、数字或下划线的连续字符。当文档内容是:

      • “我 爱 北京 天安门”
      • “我 爱 吃 苹果”

      时,“我”、“爱”、“吃” 都只有一个汉字,因此不符合默认正则表达式的要求,会被被过滤掉。只有“北京”、“天安门”、“苹果”这三个词因为长度大于或等于2,所以被包括进词汇表中。

      因此,输出结果为:

      • 词汇表: [‘北京’ ‘天安门’ ‘苹果’]
    • 如果希望包含单个汉字,可以自定义 token_pattern 参数,例如设为 token_pattern=r"(?u)\b\w+\b",这样就可以识别长度为 1 的词汇。

4.1 使用 TfidfVectorizer

# 示例:利用 scikit-learn 直接计算 TF-IDF
from sklearn.feature_extraction.text import TfidfVectorizer

# 构造样例语料库(每个元素为一篇文档)
corpus = [
    "我 爱 北京 天安门",
    "我 爱 吃 苹果"
]

# 创建 TfidfVectorizer 对象,识别长度为 1 的词汇(token_pattern=r"(?u)\b\w+\b")
vectorizer = TfidfVectorizer()

# 计算 TF-IDF 矩阵,得到一个稀疏矩阵
tfidf_matrix = vectorizer.fit_transform(corpus)

# 获取词袋模型中的所有词
words = vectorizer.get_feature_names_out()

# 将 TF-IDF 矩阵转换为数组(每行对应一篇文档,每列对应词汇)
tfidf_array = tfidf_matrix.toarray()

print("词汇表:", words)
print("TF-IDF 矩阵:")
print(tfidf_array)

运行结果会显示每个文档中每个词的 TF-IDF 权重,常见输出类似于:

词汇表: ['北京' '吃' '天安门' '我' '爱' '苹果']
TF-IDF 矩阵:
[[0.57615236 0.         0.57615236 0.40993715 0.40993715 0.        ]
 [0.         0.57615236 0.         0.40993715 0.40993715 0.57615236]]

可以看出,“北京”与“天安门”在文档1上有较高权重,而“吃”、“苹果”在文档2上较高。

4.2 使用 CountVectorizer + TfidfTransformer

from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer

# 同样的样例语料库
corpus = [
    "我 爱 北京 天安门",
    "我 爱 吃 苹果"
]

# 第一步:统计词频,得到词袋模型表示,识别长度为 1 的词汇(token_pattern=r"(?u)\b\w+\b")
count_vectorizer = CountVectorizer(token_pattern=r"(?u)\b\w+\b")
X_counts = count_vectorizer.fit_transform(corpus)

# 第二步:使用 TfidfTransformer 将词频转换为 TF-IDF 权重
tfidf_transformer = TfidfTransformer()
X_tfidf = tfidf_transformer.fit_transform(X_counts)

# 获取所有词汇
words = count_vectorizer.get_feature_names_out()

print("词汇表:", words)
print("TF-IDF 矩阵:")
print(X_tfidf.toarray())

运行结果也会显示每个文档中每个词的 TF-IDF 权重,常见输出类似于:

词汇表: ['北京' '吃' '天安门' '我' '爱' '苹果']
TF-IDF 矩阵:
[[0.57615236 0.         0.57615236 0.40993715 0.40993715 0.        ]
 [0.         0.57615236 0.         0.40993715 0.40993715 0.57615236]]

这两段代码分别展示了使用 scikit-learn 内建的两种方式计算 TF-IDF 的方法。通常更推荐直接使用 TfidfVectorizer,它将词频统计和 TF-IDF 权重计算集成在一起。

4.3 手动实现中文分词、词袋模型和 TF-IDF 的计算

1. 中文分词

将采用基于最大匹配的分词方法。这种方法通过构建一个词典,然后从左到右匹配文本中的最长词语。

def load_dictionary():
    # 简化的词典示例
    return {"我", "爱", "北京", "天安门", "吃", "苹果", "你", "喜欢", "中国"}

def tokenize(text, dictionary):
    max_len = max(len(word) for word in dictionary)
    tokens = []
    i = 0
    while i < len(text):
        matched = False
        for l in range(max_len, 0, -1):
            if i + l <= len(text):
                word = text[i:i+l]
                if word in dictionary:
                    tokens.append(word)
                    i += l
                    matched = True
                    break
        if not matched:
            tokens.append(text[i])
            i += 1
    return tokens

2. 构建词袋模型(Bag of Words)

词袋模型将每个文档表示为一个词频向量。我们首先构建一个词汇表,然后统计每个文档中词汇的出现次数。

def build_vocabulary(tokenized_documents):
    vocab = {}
    for tokens in tokenized_documents:
        for token in tokens:
            if token not in vocab:
                vocab[token] = len(vocab)
    return vocab

def vectorize(tokens, vocab):
    vector = [0] * len(vocab)
    for token in tokens:
        if token in vocab:
            index = vocab[token]
            vector[index] += 1
    return vector

3. 计算 TF-IDF 权重

TF-IDF(Term Frequency-Inverse Document Frequency)是一种衡量词语重要性的统计方法。我们先计算词频(TF),然后计算逆文档频率(IDF),最后将两者相乘得到 TF-IDF 值。

import math

def compute_tf(vector):
    total = sum(vector)
    if total == 0:
        return [0] * len(vector)
    return [count / total for count in vector]

def compute_idf(vectors):
    import math
    N = len(vectors)
    df = [0] * len(vectors[0])
    for vector in vectors:
        for i, count in enumerate(vector):
            if count > 0:
                df[i] += 1
    idf = [math.log((N + 1) / (df_i + 1)) + 1 for df_i in df]
    return idf

def compute_tfidf(tf_vector, idf_vector):
    return [tf * idf for tf, idf in zip(tf_vector, idf_vector)]

4. 完整流程演示
# 示例文档
documents = ["我爱北京天安门", "我爱吃苹果", "你喜欢中国吗"]

# 加载词典
dictionary = load_dictionary()

# 分词
tokenized_documents = [tokenize(doc, dictionary) for doc in documents]

# 构建词汇表
vocab = build_vocabulary(tokenized_documents)

# 构建词频向量
vectors = [vectorize(tokens, vocab) for tokens in tokenized_documents]

# 计算 TF 向量
tf_vectors = [compute_tf(vector) for vector in vectors]

# 计算 IDF 向量
idf_vector = compute_idf(vectors)

# 计算 TF-IDF 向量
tfidf_vectors = [compute_tfidf(tf_vector, idf_vector) for tf_vector in tf_vectors]

# 输出结果
for i, tfidf in enumerate(tfidf_vectors):
    print(f"文档 {i+1} 的 TF-IDF 向量:")
    for word, index in vocab.items():
        print(f"{word}: {tfidf[index]:.4f}")
    print()

运行结果

文档 1 的 TF-IDF 向量:: 0.3219: 0.3219
北京: 0.4233
天安门: 0.4233: 0.0000
苹果: 0.0000: 0.0000
喜欢: 0.0000
中国: 0.0000: 0.0000

文档 2 的 TF-IDF 向量:: 0.3219: 0.3219
北京: 0.0000
天安门: 0.0000: 0.4233
苹果: 0.4233: 0.0000
喜欢: 0.0000
中国: 0.0000: 0.0000

文档 3 的 TF-IDF 向量:
...
喜欢: 0.4233
中国: 0.4233: 0.4233

5. 示例:使用词袋模型处理多个文档

使用词袋模型(Bag of Words, BoW)对多个中文文档进行处理,并结合哈工大停用词表进行预处理,可以按照以下步骤操作:

步骤 1:准备文档和停用词表

假设您有两个需要处理的文档,以及一个包含哈工大停用词的文件 hit_stopwords.txt

# 示例文档
doc1 = "我爱自然语言处理。"
doc2 = "自然语言处理是人工智能的一个重要领域。"

# 读取停用词表
def load_stopwords(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        stopwords = set(line.strip() for line in f)
    return stopwords

stopwords = load_stopwords('hit_stopwords.txt')

步骤 2:文本预处理(分词和去除停用词)

使用 jieba 进行中文分词,并去除停用词。

import jieba

def preprocess(text, stopwords):
    words = jieba.lcut(text)
    return [word for word in words if word.strip() and word not in stopwords]

# 对文档进行预处理
processed_doc1 = preprocess(doc1, stopwords)
processed_doc2 = preprocess(doc2, stopwords)

# 构建语料库
corpus = [' '.join(processed_doc1), ' '.join(processed_doc2)]

步骤 3:构建词袋模型

使用 CountVectorizer 将文本转换为词频向量。

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)

# 输出词汇表和词频矩阵
print("词汇表:", vectorizer.get_feature_names_out())
print("词频矩阵:\n", X.toarray())

运行结果

词汇表: ['人工智能' '处理' '自然语言' '重要' '领域']
词频矩阵:
 [[0 1 1 0 0]
 [1 1 1 1 1]]

扩展:处理多个文档

如果您有多个文档,可以将它们添加到 corpus 列表中,并重复上述预处理步骤。

# 示例:添加更多文档
doc3 = "机器学习是人工智能的核心。"
processed_doc3 = preprocess(doc3, stopwords)
corpus.append(' '.join(processed_doc3))

# 重新构建词袋模型
X = vectorizer.fit_transform(corpus)
print("更新后的词汇表:", vectorizer.get_feature_names_out())
print("更新后的词频矩阵:\n", X.toarray())

运行结果

更新后的词汇表: ['人工智能' '处理' '学习' '机器' '核心' '自然语言' '重要' '领域']
更新后的词频矩阵:
 [[0 1 0 0 0 1 0 0]
 [1 1 0 0 0 1 1 1]
 [1 0 1 1 1 0 0 0]]

通过上述步骤,可以对任意数量的中文文档进行词袋模型处理,并结合哈工大停用词表进行有效的文本预处理。