在自然语言处理项目中,我经常遇到这样的场景:手头有一堆文本数据,需要让机器学习算法理解这些文字的含义。这就像教一个外国人学中文——直接给算法看原始文本是行不通的,必须先把文字转换成它能理解的"数字语言"。这就是文本特征工程的核心价值。
scikit-learn提供的CountVectorizer和TfidfVectorizer是我最常用的两把瑞士军刀。前者实现经典的词袋模型,简单粗暴但有效;后者进阶版的TF-IDF模型,能更好地捕捉词语的重要性差异。下面我将结合多年实战经验,带你深入掌握这两种方法的原理、实现和避坑技巧。
工欲善其事,必先利其器。我推荐使用以下工具组合:
bash复制pip install scikit-learn jieba pandas
对于中文文本处理,jieba分词是必备工具。虽然示例中使用了预先分好的语料,但真实场景中90%的情况需要自己处理原始文本。这里分享一个我常用的预处理函数:
python复制import jieba
import re
def chinese_text_preprocess(text):
# 去除特殊字符和标点
text = re.sub(r'[^\w\s]', '', text)
# 结巴分词
words = jieba.cut(text)
# 过滤停用词(需自行准备停用词表)
stopwords = set(['的', '了', '是', '我'])
return ' '.join([w for w in words if w not in stopwords and len(w) > 1])
示例中的微型语料仅用于演示原理,实际项目中语料质量决定模型上限。我总结了几点经验:
一个真实的电商评论语料可能长这样:
python复制corpus = [
'手机 质量 很好 运行 流畅 拍照 清晰',
'电脑 速度 慢 散热 差 后悔 购买',
'耳机 音质 不错 但 续航 一般',
'快递 速度 快 包装 完好 服务 态度 好'
]
大多数教程只会介绍默认参数,但实践中这些参数调节至关重要:
python复制vectorizer = CountVectorizer(
min_df=0.02, # 忽略文档频率<2%的词
max_df=0.8, # 忽略文档频率>80%的词
ngram_range=(1,3), # 同时提取1-3个词的组合
max_features=5000, # 限制特征数量
token_pattern=r'(?u)\b\w+\b' # 自定义token匹配规则
)
特别说明ngram_range的选择:
CountVectorizer输出的稀疏矩阵有几种高效处理方法:
python复制# 转换为DataFrame方便查看
import pandas as pd
df_bow = pd.DataFrame(
X_bow.toarray(),
columns=vectorizer.get_feature_names_out()
)
# 保存稀疏矩阵到磁盘
from scipy.sparse import save_npz
save_npz('bow_matrix.npz', X_bow)
# 与其他特征合并
from scipy.sparse import hstack
X_combined = hstack([X_bow, other_features])
注意:当特征维度>10万时,一定要保持稀疏格式,转为稠密矩阵可能导致内存溢出
TF-IDF的计算远比表面看起来复杂,其核心公式:
code复制TF-IDF(t,d) = TF(t,d) × IDF(t)
其中:
scikit-learn的默认实现还包含L2归一化:
code复制final_weight = TF-IDF(t,d) / sqrt(sum(TF-IDF(t,d)^2))
通过网格搜索找到最优参数组合:
python复制from sklearn.model_selection import GridSearchCV
params = {
'ngram_range': [(1,1), (1,2)],
'max_df': [0.7, 0.9],
'min_df': [0.001, 0.01],
'norm': ['l1', 'l2']
}
grid = GridSearchCV(TfidfVectorizer(), params, cv=5)
grid.fit(corpus)
print(grid.best_params_)
英文文本处理相对直接,但中文需要特别注意:
我的解决方案是构建领域词典:
python复制jieba.load_userdict('custom_words.txt')
当语料规模极大时(如千万级文档),可以使用特征哈希:
python复制from sklearn.feature_extraction.text import HashingVectorizer
hv = HashingVectorizer(
n_features=2**18,
alternate_sign=False
)
X_hash = hv.transform(corpus)
优势:
缺点:
对于无法一次性加载的大数据:
python复制from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
for chunk in pd.read_csv('big_data.csv', chunksize=10000):
vectorizer.partial_fit(chunk['text'])
使用Dask或Spark处理超大规模数据:
python复制# Dask示例
import dask.dataframe as dd
from dask_ml.feature_extraction.text import HashingVectorizer
ddf = dd.read_csv('s3://bucket/*.csv')
vectorizer = HashingVectorizer()
X = vectorizer.fit_transform(ddf['text'])
现象:处理大文本时程序崩溃
解决方案:
现象:模型训练极慢,效果差
解决方法:
现象:分词结果出现乱码
处理步骤:
将文本特征与其他特征结合:
python复制from sklearn.pipeline import FeatureUnion
feature_union = FeatureUnion([
('text', TfidfVectorizer()),
('meta', StandardScaler())
])
X = feature_union.fit_transform(data)
结合预训练语言模型:
python复制from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
inputs = tokenizer(corpus, return_tensors='pt', padding=True)
实现BM25等高级权重算法:
python复制from sklearn.feature_extraction.text import TfidfVectorizer
class BM25Vectorizer(TfidfVectorizer):
def _calculate_idf(self, df):
# 重写IDF计算逻辑
return np.log((self.n_samples_ - df + 0.5) / (df + 0.5))
在真实项目中,我的标准工作流程是:
对于不同场景的推荐方案:
最后分享一个血泪教训:曾有一个项目因为忽略min_df设置,导致模型被低频噪声词干扰,准确率下降15%。后来通过分析特征重要性才发现这个问题。所以强烈建议:
python复制# 一定要检查特征分布
df_freq = pd.DataFrame({
'word': vectorizer.get_feature_names_out(),
'freq': np.asarray(X.sum(axis=0)).ravel()
}).sort_values('freq', ascending=False)