别再死记硬背公式了!用Python和sklearn手撕TF-IDF,5分钟搞懂核心原理

每次看到TF-IDF的公式就头疼?面试被问到"为什么要取对数"时哑口无言?别担心,今天我们就用几行Python代码和几个新闻标题,带你从厨房小白秒变算法大厨——不背公式,只讲"为什么"。

1. 为什么我们需要TF-IDF?

想象你在整理一周的新闻标题:

  1. "股市今日大幅上涨"
  2. "今日气温创历史新高"
  3. "新能源汽车今日发布"

如果让你找出每篇的核心词,"股市"、"气温"、"新能源"这些词显然比"今日"更重要。这就是TF-IDF要解决的问题—— 量化词语的重要性

传统词频统计的致命缺陷在于:

  • "今日"出现3次,但它毫无信息量
  • "新能源"只出现1次,却是关键信息

好的特征提取应该像优秀编辑:既能发现高频词,又能过滤"水词"

2. 拆解TF-IDF的物理意义

2.1 TF:词频的陷阱与出路

计算"股市"在第一句的TF值:

def compute_tf(word, document):
    words = document.split()
    return words.count(word) / len(words)

title1 = "股市 今日 大幅 上涨"
print(compute_tf("股市", title1))  # 输出0.25

但单纯TF会导致:

  • 长文档中正常词频被稀释
  • "的"、"是"等停用词干扰判断

2.2 IDF:给词语发"稀有度勋章"

关键理解log背后的设计哲学:

import math

def compute_idf(word, documents):
    doc_count = sum(1 for doc in documents if word in doc)
    return math.log(len(documents) / (doc_count + 1))

titles = ["股市 今日 大幅 上涨", 
         "今日 气温 创 历史 新高",
         "新能源 汽车 今日 发布"]
         
print(compute_idf("今日", titles))  # 0.0(太常见)
print(compute_idf("新能源", titles)) # 1.098(稀有词)

为什么是log?因为:

  1. 防止IDF值爆炸增长(文档数可能百万级)
  2. 更符合人类对"稀有度"的感知(对数尺度)

3. 手把手实现TF-IDF

3.1 从零开始造轮子

class SimpleTFIDF:
    def __init__(self, docs):
        self.docs = docs
        self.N = len(docs)
        self.doc_freq = {}
        
        # 统计每个词出现在多少文档中
        for doc in docs:
            words = set(doc.split())
            for word in words:
                self.doc_freq[word] = self.doc_freq.get(word, 0) + 1
    
    def score(self, word, doc):
        tf = doc.split().count(word) / len(doc.split())
        idf = math.log(self.N / (self.doc_freq.get(word, 0) + 1))
        return tf * idf

# 测试我们的实现
processor = SimpleTFIDF(titles)
print("'股市'的TF-IDF:", processor.score("股市", titles[0]))  # 0.366
print("'今日'的TF-IDF:", processor.score("今日", titles[0]))  # 0.0

3.2 用sklearn验证结果

from sklearn.feature_extraction.text import TfidfVectorizer

vec = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b")
tfidf = vec.fit_transform(titles)

print(vec.vocabulary_)  # 查看词表索引
print(tfidf[0].toarray())  # 第一句的TF-IDF向量

对比发现:

  • "股市"在手工计算和sklearn中都是最高值
  • "今日"得分均为0
  • 细微差异来自sklearn的归一化处理

4. 实战中的常见误区

4.1 TF-IDF高就一定是关键词?

看这个反例:

special_case = ["比特币 暴涨", "比特币 暴跌"]
processor = SimpleTFIDF(special_case)
print(processor.score("比特币", special_case[0]))  # 0.0

"比特币"TF-IDF为0,但它显然是关键词!这说明:

  • TF-IDF只适合区分文档集内的特征词
  • 当某个词在所有文档都出现时,需要其他方法辅助

4.2 中文处理的特殊技巧

import jieba

text = "自然语言处理很有趣"
words = " ".join(jieba.cut(text))  # 输出:"自然 语言 处理 很 有趣"

# 需要自定义tokenizer
def chinese_tokenizer(text):
    return jieba.cut(text)

vec = TfidfVectorizer(tokenizer=chinese_tokenizer)

5. 进阶:TF-IDF的现代变种

5.1 平滑技巧对比

方法 公式 适用场景
原始IDF log(N/df) 小型语料库
平滑IDF log(N/(df+1))+1 避免除零错误
概率IDF log((N-df)/df) 长尾分布数据

5.2 与深度学习的结合

from gensim.models import TfidfModel
from gensim.corpora import Dictionary

# 构建词袋模型
docs = [["股市", "今日", "上涨"], ["气温", "今日", "新高"]]
dct = Dictionary(docs)
corpus = [dct.doc2bow(doc) for doc in docs]

# 训练TF-IDF模型
model = TfidfModel(corpus)
print(model[corpus[0]])  # 输出各词的(TermID, TF-IDF)对

这种实现方式:

  • 更适合大规模语料
  • 方便与Word2Vec等嵌入模型结合

更多推荐