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)=k∑nk,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∣{dj∈D:ti∈dj}∣∣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:ti∈dj}∣∣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:ti∈dj}∣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]]
通过上述步骤,可以对任意数量的中文文档进行词袋模型处理,并结合哈工大停用词表进行有效的文本预处理。