SWE / RAG Team · Technical Coding

Apple RAG 组手写 TF-IDF:词频、文档频率与口径

从文档集合计算稀疏 TF-IDF 向量,说明 tokenization、平滑 IDF 和空文档边界。

题目重点:从文档集合计算稀疏 TF-IDF 向量,说明 tokenization、平滑 IDF 和空文档边界。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版以 Unicode `\w+` 抽词并统一小写;TF=某词在文档内次数/该文档 token 总数。
  • IDF 取 `log((N+1)/(df+1))+1`,以文档集合中出现该词的文档数 df 计算;这只是一种常用约定,原面试可能使用 `log(N/df)` 或其他方案。
  • 空文档返回空向量;空语料返回空列表。输入只需一次批处理,不含在线增量更新。

解题路线

  1. 先把每个文档规范化并切词;对每篇文档的唯一 token 更新 document frequency,重复词在同一文档只算一次 df。
  2. 再计算每篇的 term count 和长度,按 TF×IDF 输出稀疏 map。
  3. 文本很大时可流式完成第一遍 df,第二遍重读文档生成向量;若无法重读,就须暂存 token/count 或做在线近似。
  4. 本文训练代码以 400 组小语料和独立显式循环参考实现对照,覆盖空语料、重复词、标点和大小写。

手动走一遍

文档 D1='red red blue'、D2='red green'。N=2;red 出现在两文档,idf=1,D1 的 red tf=2/3,分数 2/3。blue 只在 D1,idf=1+ln(3/2),D1 分数约 0.468。若误把 D1 的两个 red 算作 df=2,会改变 IDF。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from collections import Counter
from math import log
from re import findall


def tfidf(documents: list[str]) -> list[dict[str, float]]:
    """Count/L term frequency and smoothed IDF log((N+1)/(df+1))+1."""
    token_lists = [findall(r"\w+", document.lower()) for document in documents]
    document_frequency = Counter(
        token for tokens in token_lists for token in set(tokens)
    )
    n = len(documents)
    vectors = []
    for tokens in token_lists:
        counts = Counter(tokens)
        length = len(tokens)
        vectors.append({
            token: count / length * (log((n + 1) / (document_frequency[token] + 1)) + 1)
            for token, count in counts.items()
        })
    return vectors

复杂度

总 token 数 T、不同 token 总数 V;平均 O(T) 哈希计数与 O(T) 输出,空间 O(T+V) 存 token 列表、词频和向量。若要检索 Top K 文档,还需要倒排索引与余弦归一化策略,不属于此题目可见题干。

易错点与追问

容易失分的边界

  • 把文档频率 df 当作整个语料的词出现总次数。
  • 不给 tokenization 口径就声称分数唯一。
  • 空文档按长度 0 去除,产生除零。
  • 高频常见词与稀有词的 IDF 平滑方案在训练、查询两端不一致。
  • 直接称 TF-IDF 为语义 embedding,混淆检索阶段能力。

继续深挖

  • 如果语料每天更新,IDF 变化会使既有索引怎样失效?
  • 如何结合 BM25 的长度归一化?
  • RAG 中向量检索与 TF-IDF 倒排检索怎样混合与重排?
  • 中文无空格文本要怎样切词?
✓已复制微信号!