4. 传统机器学习方法:特征工程与意图分类实战
好,咱们进入第四章。说实话,这一章是我个人觉得最「实在」的一章。为什么?因为不管深度学习多火,传统方法在资源受限的场景下——比如嵌入式设备、低延迟服务——依然是首选。而且,你想想看,理解传统方法能帮你更深刻地理解深度学习为什么有效。
今天咱们聊三个核心话题:特征工程怎么做、朴素贝叶斯和SVM怎么选、以及一个完整的sklearn实战。
4.1 特征工程:把文本变成数字
计算机不认识「我要订机票」这句话。它只认识数字。所以,特征工程说白了就是——把人类语言翻译成机器能算的向量。
4.1.1 TF-IDF:不只是词频
很多人以为TF-IDF就是数数。其实不是。TF-IDF的核心思想是:一个词在文档中出现的次数越多,同时在所有文档中出现的次数越少,它就越重要。
公式很简单:
TF = 词在文档中的出现次数 / 文档总词数
IDF = log(总文档数 / 包含该词的文档数)
TF-IDF = TF × IDF
举个例子。在「我要订机票」和「今天天气不错」这两句话里,「订」这个词只出现在第一句。它的IDF值就会很高。而「的」这种词几乎每句都有,IDF值趋近于0。
重要提醒:TF-IDF会过滤掉高频但无意义的词。这在意图识别中非常关键——因为很多意图的关键词恰恰是低频词。
我在项目中遇到过一个问题:用户说「帮我查一下航班」,和「查航班」其实是同一个意图。但TF-IDF会把「帮我」「一下」这种词降权,只保留「查」「航班」。嗯,这就是它聪明的地方。
4.1.2 N-gram:捕捉上下文
TF-IDF有个硬伤——它不考虑词序。「我打你」和「你打我」在TF-IDF眼里是一样的。这显然不行。
N-gram就是来解决这个问题的。它把连续的N个词作为一个特征。
| N值 | 示例(「我要订机票」) | 特点 |
|---|---|---|
| Unigram (N=1) | 我、要、订、机票 | 丢失词序,但特征维度低 |
| Bigram (N=2) | 我要、要订、订机票 | 保留局部词序,维度适中 |
| Trigram (N=3) | 我要订、要订机票 | 保留更多上下文,但维度爆炸 |
我的建议:意图识别场景下,Bigram通常是最优解。Trigram会导致特征维度太高,而且很多组合根本不会出现。我曾经在一个客服数据集上试过Trigram,结果特征数量从5000直接飙到12万——模型训练时间长了10倍,准确率只提升了0.3%。不值当。
4.1.3 词性标注:给词打标签
词性标注(POS Tagging)就是给每个词标上它是名词、动词还是形容词。你可能会问:这有什么用?
用处大了。在意图识别中,动词往往是动作的核心。比如「订」「查」「退」这些动词,直接决定了用户的意图类型。而名词则是动作的对象——「机票」「酒店」「火车票」。
我习惯的做法是:把词性作为额外的特征拼接到TF-IDF向量后面。比如:
原始特征: [0.12, 0.45, 0.0, 0.78, ...] (TF-IDF向量)
词性特征: [1, 0, 0, 1, 0, ...] (one-hot编码的词性)
最终特征: [0.12, 0.45, 0.0, 0.78, ..., 1, 0, 0, 1, 0, ...]
注意:词性标注本身也有误差。中文词性标注的准确率一般在90%-95%之间。如果你的数据量很小,词性标注带来的噪声可能会抵消它带来的收益。我曾经在一个只有200条样本的小数据集上试过,加了词性特征后准确率反而下降了2%。
4.2 朴素贝叶斯分类器:简单但有效
朴素贝叶斯(Naive Bayes)是我个人非常喜欢的一个算法。它基于一个「朴素」的假设:所有特征之间相互独立。
你可能会笑:这假设也太假了吧?「订」和「机票」明明就是相关的。没错,但神奇的是——即使假设不成立,朴素贝叶斯在文本分类上依然表现很好。
为什么?因为文本特征通常是稀疏的。每个文档只包含少量词,这些词之间的相关性对最终决策的影响有限。说白了,它「傻人有傻福」。
朴素贝叶斯的计算公式:
P(意图|文本) ∝ P(意图) × Π P(词|意图)
在sklearn里实现起来极其简单:
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
# 假设我们有训练数据
X_train = ["我要订机票", "查一下航班", "今天天气怎么样"]
y_train = ["booking", "booking", "weather"]
# 特征提取
vectorizer = TfidfVectorizer(ngram_range=(1, 2))
X_train_vec = vectorizer.fit_transform(X_train)
# 训练模型
clf = MultinomialNB()
clf.fit(X_train_vec, y_train)
# 预测
X_test = ["帮我订一张机票"]
X_test_vec = vectorizer.transform(X_test)
pred = clf.predict(X_test_vec)
print(pred) # 输出: ['booking']
适用场景:朴素贝叶斯特别适合小数据集、多分类、且特征维度高的场景。如果你的意图类别超过20个,或者训练数据少于1000条,我建议你先试试朴素贝叶斯。
4.3 支持向量机(SVM):追求最大间隔
SVM和朴素贝叶斯的思路完全不同。朴素贝叶斯是「概率派」,SVM是「几何派」——它不关心概率,只关心怎么画一条线把不同类别的数据分开,而且这条线要离两边都尽可能远。
这个「尽可能远」就是最大间隔(Maximum Margin)的思想。你可以想象成在两类数据之间修一条最宽的马路。
在sklearn里,SVM的使用也很直接:
from sklearn.svm import SVC
# 使用线性核函数
svm_clf = SVC(kernel='linear', C=1.0)
svm_clf.fit(X_train_vec, y_train)
# 或者使用RBF核函数(处理非线性问题)
svm_clf_rbf = SVC(kernel='rbf', C=1.0, gamma='auto')
svm_clf_rbf.fit(X_train_vec, y_train)
这里有个关键参数——C。它控制着对错误分类的惩罚力度。C越大,模型越倾向于把所有训练样本都分对,但可能过拟合。C越小,模型容忍度越高,但可能欠拟合。
避坑指南:我曾经在一个电商意图识别项目上,把C设成了100,结果模型在训练集上准确率99%,在测试集上只有72%。这就是典型的过拟合。后来我把C调到了0.1,测试集准确率反而升到了85%。记住:SVM的C值,从0.1开始调,别一上来就设大数。
4.4 实践:基于sklearn的意图分类
好,理论说完了。咱们来一个完整的实战。假设我们要做一个智能客服的意图识别系统,识别用户的意图是「订票」「查天气」「咨询」还是「投诉」。
完整代码:
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# 1. 准备数据
X = [
"我要订一张去北京的机票",
"帮我查一下明天的航班",
"今天北京天气怎么样",
"我想咨询一下退票政策",
"我要投诉你们的服务",
"订两张去上海的火车票",
"明天上海会下雨吗",
"请问改签怎么收费",
"你们客服电话是多少",
"我要退票"
]
y = ["booking", "booking", "weather", "consult", "complaint",
"booking", "weather", "consult", "consult", "booking"]
# 2. 特征工程
vectorizer = TfidfVectorizer(
ngram_range=(1, 2), # 使用Unigram+Bigram
max_features=1000, # 限制特征数量
stop_words=None # 不自动过滤停用词
)
X_vec = vectorizer.fit_transform(X)
# 3. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_vec, y, test_size=0.3, random_state=42
)
# 4. 训练朴素贝叶斯
nb_clf = MultinomialNB(alpha=0.1) # alpha是平滑参数
nb_clf.fit(X_train, y_train)
nb_pred = nb_clf.predict(X_test)
# 5. 训练SVM
svm_clf = SVC(kernel='linear', C=1.0, probability=True)
svm_clf.fit(X_train, y_train)
svm_pred = svm_clf.predict(X_test)
# 6. 评估
print("=== 朴素贝叶斯 ===")
print(classification_report(y_test, nb_pred))
print("\n=== SVM ===")
print(classification_report(y_test, svm_pred))
# 7. 预测新样本
new_queries = ["帮我订机票", "今天热不热", "怎么投诉"]
new_vec = vectorizer.transform(new_queries)
print("\n=== 新样本预测 ===")
for query, pred_nb, pred_svm in zip(new_queries,
nb_clf.predict(new_vec),
svm_clf.predict(new_vec)):
print(f"输入: {query} -> NB: {pred_nb}, SVM: {pred_svm}")
关键点总结:
- TF-IDF + Bigram 是意图识别的黄金组合
- 数据量小(<1000条)时,朴素贝叶斯往往比SVM好
- 数据量大时,SVM(线性核)通常更优
- 词性标注可以作为额外特征,但要小心噪声
嗯,这一章的内容就到这儿。下一章咱们会聊深度学习的方法——CNN和RNN在意图识别中的应用。到时候你会发现,很多思路其实都是从传统方法演变过来的。
记住:别看不起传统方法。在工业界,朴素贝叶斯和SVM依然是很多线上系统的首选。原因很简单——它们快、稳定、可解释性强。