文本特征工程详解:TF-IDF算法、相似度计算与词袋模型演进及PyTorch实现

本文已精简冗余内容,去掉数学公式,新增PyTorch极简实现,控制在2500字内,重点突出实用代码和工程场景。

目录


什么是文本特征工程?

机器看不懂“自然语言处理”这种字符组合,只能处理数值数据——文本特征工程就是把原始文本转成有语义线索的数值向量

它的核心作用:

  1. 数值化文本
  2. 提取有用特征(比如关键词)
  3. 合理降维(避免稀疏爆炸)
  4. 尽量保留文本语义

词袋模型(Bag of Words)

词袋是文本向量化的“初代机”:完全忽略词序、语法,只统计每个词在文档里的出现次数,就像把所有词倒进一个袋子里数个数。

极简代码实现

sklearn 和中文分词库 jieba 做演示:

from sklearn.feature_extraction.text import CountVectorizer
import jieba

# 定义中文分词器
def zh_tokenizer(text):
    return jieba.lcut(text)

# 示例语料
docs = [
    "自然语言处理是人工智能的重要分支",
    "机器学习和深度学习是核心技术",
    "自然语言处理和机器学习密切相关"
]

# 初始化词袋向量化器
bow_vec = CountVectorizer(
    tokenizer=zh_tokenizer,
    lowercase=False,  # 中文不需要
    token_pattern=None  # 用自定义分词器
)

# 拟合语料,生成矩阵
bow_matrix = bow_vec.fit_transform(docs)

print("词汇表:", bow_vec.get_feature_names_out())
print("词袋矩阵:\n", bow_matrix.toarray())

优缺点总结

优点缺点
简单易懂,实现快忽略词序(“狗咬人”和“人咬狗”向量一样)
计算效率极高无法捕捉语义(“汽车”“车辆”完全无关)
适合快速原型无法区分“的”“是”这类停用词

TF-IDF算法详解

TF-IDF是词袋的升级版,给每个词加权重,让在当前文档频繁出现,但在整个语料罕见的词脱颖而出(比如“机器学习”在技术文档里重要,但“的”在所有文档里都没用)。

核心权重逻辑

  1. 词频(TF):某个词在当前文档里的“存在感”
  2. 逆文档频率(IDF):某个词在整个语料里的“稀缺度”——越稀缺越重要
  3. 最终权重:两者相乘

实用Sklearn实现

sklearnTfidfVectorizer 封装了所有细节,还支持ngram、归一化等高级功能:

from sklearn.feature_extraction.text import TfidfVectorizer
import jieba
import numpy as np

def zh_tokenizer(text):
    return jieba.lcut(text)

# 更丰富的语料
docs = [
    "自然语言处理是人工智能的重要分支",
    "机器学习和深度学习是人工智能的核心技术",
    "深度学习在计算机视觉领域应用广泛",
    "数据科学结合统计学和计算机科学"
]

# 高级向量化器配置
tfidf_vec = TfidfVectorizer(
    tokenizer=zh_tokenizer,
    max_features=1000,  # 限制词汇表大小,降维
    min_df=1,           # 至少在1个文档出现
    max_df=0.8,         # 过滤80%以上文档都有的词(停用词)
    ngram_range=(1,2),  # 同时考虑单字/词(1-gram)和双词(2-gram,比如“自然语言”)
    norm='l2',          # L2归一化,便于计算余弦相似度
    sublinear_tf=True   # 用对数缩放TF,避免词频过高的词权重过大
)

# 生成矩阵
tfidf_matrix = tfidf_vec.fit_transform(docs)

# 查看文档1的Top3关键词
feature_names = tfidf_vec.get_feature_names_out()
doc1_tfidf = tfidf_matrix[0].toarray().flatten()
top3_idx = np.argsort(doc1_tfidf)[::-1][:3]
print("文档1的Top3关键词:")
for idx in top3_idx:
    print(f"  {feature_names[idx]}: {doc1_tfidf[idx]:.4f}")

相似度度量方法

得到文本向量后,最常用的操作就是计算文本相似度——比如文档检索、问答匹配。

1. 余弦相似度(最常用)

余弦相似度看两个向量的夹角大小:夹角越小(越接近0),相似度越高(越接近1)。它不受向量长度影响,最适合TF-IDF这种归一化后的向量。

from sklearn.metrics.pairwise import cosine_similarity

# 计算整个语料的相似度矩阵
sim_matrix = cosine_similarity(tfidf_matrix)
print("语料相似度矩阵:\n", sim_matrix.round(4))

2. 其他方法对比

方法逻辑适用场景
欧氏距离向量在空间中的直线距离向量长度有意义的场景(比如词频未归一化)
曼哈顿距离向量在各维度上的绝对差之和稀疏向量、对异常值不敏感的场景
杰卡德相似度两个词集的交集/并集短文本、关键词匹配的场景

极简PyTorch TF-IDF实现

原标题提到PyTorch,这里补一个极简可运行的版本,适合自定义底层逻辑或结合深度学习模型:

import jieba
import torch
from collections import defaultdict

def zh_tokenizer(text):
    return jieba.lcut(text)

class PyTorchTFIDF:
    def __init__(self, max_features=1000, min_df=1, max_df=0.8):
        self.max_features = max_features
        self.min_df = min_df
        self.max_df = max_df
        self.vocab = None
        self.idf = None
    
    def fit(self, texts):
        # 分词
        tokenized = [zh_tokenizer(t) for t in texts]
        total_docs = len(tokenized)
        
        # 统计词频和文档频率
        word_doc_count = defaultdict(int)
        word_total_count = defaultdict(int)
        for tokens in tokenized:
            unique_tokens = set(tokens)
            for token in unique_tokens:
                word_doc_count[token] += 1
            for token in tokens:
                word_total_count[token] += 1
        
        # 过滤词汇
        filtered_words = [
            w for w in word_total_count 
            if self.min_df <= word_doc_count[w] <= self.max_df * total_docs
        ]
        # 取TopN词频
        filtered_words.sort(key=lambda x: word_total_count[x], reverse=True)
        self.vocab = {w: i for i, w in enumerate(filtered_words[:self.max_features])}
        
        # 计算IDF
        self.idf = torch.zeros(len(self.vocab))
        for w, idx in self.vocab.items():
            # 平滑处理(避免除以0)
            self.idf[idx] = torch.log(torch.tensor(total_docs / (word_doc_count[w] + 1))) + 1
    
    def transform(self, texts):
        tokenized = [zh_tokenizer(t) for t in texts]
        tfidf_matrix = torch.zeros(len(texts), len(self.vocab))
        
        # 计算TF
        for i, tokens in enumerate(tokenized):
            token_count = defaultdict(int)
            for t in tokens:
                if t in self.vocab:
                    token_count[t] += 1
            # 归一化TF
            doc_len = len(tokens) if len(tokens) > 0 else 1
            for t, cnt in token_count.items():
                tf = cnt / doc_len
                tfidf_matrix[i, self.vocab[t]] = tf * self.idf[self.vocab[t]]
        
        # L2归一化
        norm = torch.norm(tfidf_matrix, p=2, dim=1, keepdim=True)
        norm[norm == 0] = 1  # 避免空文档除以0
        tfidf_matrix /= norm
        return tfidf_matrix

# 测试
pytorch_tfidf = PyTorchTFIDF()
pytorch_tfidf.fit(docs)
pytorch_matrix = pytorch_tfidf.transform(docs)
print("PyTorch TF-IDF矩阵形状:", pytorch_matrix.shape)

实际应用与案例

文档相似度搜索

一个最实用的场景:输入查询,返回最相似的文档。

class SimpleDocSearch:
    def __init__(self, docs):
        self.docs = docs
        # 用Sklearn的更稳定
        self.vec = TfidfVectorizer(tokenizer=zh_tokenizer, ngram_range=(1,2), norm='l2')
        self.matrix = self.vec.fit_transform(docs)
    
    def search(self, query, top_k=2):
        query_vec = self.vec.transform([query])
        sims = cosine_similarity(query_vec, self.matrix).flatten()
        top_idx = sims.argsort()[::-1][:top_k]
        return [(self.docs[i], sims[i].round(4)) for i in top_idx]

# 测试
searcher = SimpleDocSearch(docs)
print("搜索结果(查询:计算机):", searcher.search("计算机"))

局限性与现代替代方案

TF-IDF的局限性

  1. 完全忽略词序
  2. 无法捕捉语义(“汽车”“车辆”无关)
  3. 高维稀疏(词汇表大时效率低)

现代替代方案

方法维度语义理解计算复杂度适用场景
TF-IDF高(词汇表)快速检索、关键词提取
Sentence-BERT低(768)✅✅语义相似度、问答匹配
Doc2Vec低(100-300)文档聚类

总结

TF-IDF是NLP文本特征工程的入门必学+实用工具

  1. 核心逻辑简单清晰
  2. 计算效率极高,适合大规模语料
  3. 仍广泛用于文档检索、关键词提取等场景
  4. 建议先掌握TF-IDF,再学习现代预训练模型
词袋模型 → TF-IDF → 相似度计算 → 预训练词向量 → Sentence-BERT