3、特征工程入门:TF-IDF原理、词袋模型、N-gram特征提取
各位同学好,我是老李。今天咱们聊聊特征工程里最基础、也最绕不开的三个概念:词袋模型、TF-IDF 和 N-gram。说实话,我当年刚入行时,觉得这玩意儿不就是数数词频嘛,有啥好学的?结果第一次做法律文书分类,就被现实狠狠教育了一顿。
你想想看,法律案例检索里,一份判决书动辄几千字,里面充斥着「本院认为」、「依照规定」、「驳回上诉」这些高频词。如果直接拿原始文本去训练模型,那模型学到的全是噪音,根本抓不住关键信息。所以,特征工程就是帮模型「划重点」的过程。
核心观点:特征工程不是简单的数学变换,而是把人类对文本的理解,翻译成机器能懂的数学语言。做得好,模型事半功倍;做得糙,再牛的算法也白搭。
3.1 词袋模型:最朴素的文本向量化
词袋模型(Bag of Words,简称 BoW)是文本特征提取的起点。它的思路极其简单:把文档看成一堆词的集合,忽略语法和词序,只关心每个词出现了多少次。
举个例子,假设有两份法律文书:
- 文书A:「原告主张被告违约,被告辩称合同无效」
- 文书B:「合同无效导致违约,原告主张赔偿损失」
用词袋模型处理后,我们会先构建一个词典,然后统计每份文书中每个词的出现次数。结果大概长这样:
| 词 | 文书A | 文书B |
|---|---|---|
| 原告 | 1 | 1 |
| 主张 | 1 | 1 |
| 被告 | 1 | 0 |
| 违约 | 1 | 1 |
| 合同 | 1 | 1 |
| 无效 | 1 | 1 |
| 辩称 | 1 | 0 |
| 赔偿 | 0 | 1 |
| 损失 | 0 | 1 |
你看,每份文书都变成了一个固定长度的向量,向量的维度就是词典的大小。这就是词袋模型的核心——把文本映射到高维稀疏空间。
我的经验:词袋模型虽然简单,但在法律场景下有个致命问题——它完全忽略了词的语义关系。比如「违约」和「违反合同」在词袋模型里是两个完全独立的特征,但实际意思很接近。所以,我一般只在做基线模型时用词袋,或者配合后面的 TF-IDF 一起用。
3.2 TF-IDF:给词加上权重
词袋模型有个明显的缺陷:它认为所有词同等重要。但在法律文书中,「的」、「了」、「是」这些停用词出现频率极高,却几乎不携带任何信息。而像「违约金」、「管辖权」、「再审」这些词,虽然出现次数不多,但恰恰是区分案件类型的关键。
TF-IDF 就是来解决这个问题的。它的全称是 Term Frequency-Inverse Document Frequency,翻译过来就是「词频-逆文档频率」。说白了,它给每个词算两个数:
- TF(词频):这个词在当前文档中出现了多少次。出现越多,说明它对当前文档越重要。
- IDF(逆文档频率):这个词在所有文档中出现了多少次。出现越少,说明它越稀有,越能区分不同文档。
最终的 TF-IDF 值就是 TF 乘以 IDF。一个词如果在当前文档中出现很多,但在其他文档中出现很少,那它的 TF-IDF 值就很高。反之,像「的」这种到处都有的词,IDF 值会非常低,最终权重几乎为零。
公式长这样:
TF(t, d) = 词 t 在文档 d 中出现的次数 / 文档 d 的总词数
IDF(t) = log(总文档数 / 包含词 t 的文档数 + 1)
TF-IDF(t, d) = TF(t, d) × IDF(t)
避坑指南:我曾经在一个法律案例检索项目里,直接用原始 TF-IDF 跑模型,结果发现「本院认为」这个词的权重特别高。为什么?因为几乎每份判决书都有这个词,IDF 值很低,但 TF 值也不高啊?后来一查,原来是分词出了问题——「本院认为」被当成了一个整体词。所以,做 TF-IDF 之前,一定要先做好分词和停用词过滤,否则结果会非常离谱。
在实际应用中,我习惯用 scikit-learn 的 TfidfVectorizer,几行代码就能搞定:
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
"原告主张被告违约",
"合同无效导致违约",
"被告辩称合同无效"
]
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)
# 查看特征词
print(vectorizer.get_feature_names_out())
# 输出:['合同' '原告' '被告' '辩称' '违约' '无效' '导致' '主张']
# 查看 TF-IDF 矩阵
print(X.toarray())
输出结果是一个稀疏矩阵,每一行代表一份文档,每一列代表一个词,值就是 TF-IDF 权重。你会发现,「导致」这个词只在第三份文档中出现,所以它的权重会相对较高。
3.3 N-gram:捕捉词序信息
词袋模型和 TF-IDF 都有一个共同的问题:它们把词当成独立的个体,完全忽略了词与词之间的顺序。但在法律文本中,词序往往决定了语义。比如「不构成侵权」和「构成不侵权」,词袋模型会认为它们很相似,但实际意思完全相反。
N-gram 就是来解决这个问题的。它的思路很简单:不再只考虑单个词,而是考虑连续的 N 个词组成的词组。比如:
- Unigram(1-gram):单个词,如「原告」、「被告」、「违约」
- Bigram(2-gram):连续两个词,如「原告主张」、「被告违约」、「合同无效」
- Trigram(3-gram):连续三个词,如「原告主张被告」、「被告辩称合同」
用 N-gram 处理后,上面的例子会变成:
| 特征 | 文书A | 文书B |
|---|---|---|
| 原告 | 1 | 1 |
| 原告主张 | 1 | 0 |
| 被告违约 | 1 | 0 |
| 合同无效 | 0 | 1 |
| 无效导致 | 0 | 1 |
你看,通过 Bigram,我们捕捉到了「原告主张」和「被告违约」这些短语信息,模型就能更好地区分不同文书的语义。
我的建议:N 的取值很关键。N 太小,捕捉不到长距离依赖;N 太大,特征空间会爆炸,而且很多 N-gram 只出现一次,变成噪音。我个人的经验是,在法律文本中,Bigram 和 Trigram 的效果最好。比如「不构成侵权」这个 Trigram,比单独的「不」、「构成」、「侵权」要有用得多。
3.4 三者的对比与选择
说了这么多,你可能想问:到底该用哪个?我整理了一个对比表,方便你参考:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 词袋模型 | 简单、快速、易于理解 | 忽略词序、忽略语义、高维稀疏 | 基线模型、短文本分类 |
| TF-IDF | 降低高频词影响、突出关键信息 | 仍忽略词序、对长文本效果一般 | 信息检索、关键词提取、文本分类 |
| N-gram | 捕捉局部词序、保留短语信息 | 特征维度爆炸、数据稀疏 | 情感分析、法律条款匹配、命名实体识别 |
在实际项目中,我通常的做法是:先用词袋模型快速搭建一个基线,然后换成 TF-IDF 看看效果有没有提升,最后再尝试加入 N-gram 特征。如果效果不明显,就果断放弃,不要为了用而用。
3.5 知识体系总览
为了让你更直观地理解这三者的关系,我画了一张图:
嗯,这张图基本把三者的关系说清楚了。你想想看,从词袋模型到 TF-IDF,再到 N-gram,其实是一个逐步「精细化」的过程。词袋模型是基础,TF-IDF 加了权重,N-gram 加了词序。每一步都在解决前一步的某个缺陷。
最后说一句,特征工程没有银弹。我在不同项目里试过各种组合,有时候简单的词袋模型加上好的分类器,效果反而比花里胡哨的特征工程要好。关键是要理解你的数据和业务场景,然后有针对性地选择方法。
公众号:蓝海资料掘金营,微信deep3321