4. 特征工程:从原始数据到模型能“吃”的养料
说实话,我见过太多团队把大量精力花在调模型参数上,结果效果死活上不去。最后我一查,问题出在特征上——数据没处理好,模型再牛也白搭。特征工程,说白了就是帮模型“看懂”数据的过程。今天咱们就聊聊这块硬骨头。
核心观点:特征工程的质量,往往决定了模型效果的上限。模型只是逼近这个上限的工具。
4.1 特征提取:把非结构化数据“翻译”成向量
特征提取,就是把文本、图像这类非结构化数据,转成模型能理解的数值向量。我早期做 NLP 项目时,最头疼的就是怎么把一篇文章变成数字。
4.1.1 文本特征:TF-IDF
TF-IDF 是文本特征提取的经典方法。它由两部分组成:
- TF(词频):某个词在文档中出现的次数。词出现越多,越重要。
- IDF(逆文档频率):包含该词的文档数占总文档数的比例取倒数。词越稀有,权重越高。
公式很简单:TF-IDF = TF × IDF。我习惯用 scikit-learn 的 TfidfVectorizer 来实现。
from sklearn.feature_extraction.text import TfidfVectorizer
docs = [
"我喜欢机器学习和深度学习",
"深度学习是机器学习的一个分支",
"自然语言处理很有趣"
]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(docs)
print(vectorizer.get_feature_names_out())
print(tfidf_matrix.toarray())
我的经验:TF-IDF 对短文本效果不错,但长文档容易受噪声词干扰。我一般会先做分词和去停用词,效果能提升 10%-15%。
4.1.2 图像特征:CNN 特征提取
图像特征提取,现在主流是用预训练的 CNN 模型。比如 ResNet、VGG,把图像输入网络,取倒数第二层的输出作为特征向量。说白了,就是让模型帮我们“看”出图像的关键信息。
import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image
model = models.resnet50(pretrained=True)
# 去掉最后的全连接层,只保留特征提取部分
model = torch.nn.Sequential(*list(model.children())[:-1])
model.eval()
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
img = Image.open("example.jpg")
img_tensor = transform(img).unsqueeze(0)
with torch.no_grad():
features = model(img_tensor)
print(features.shape) # 输出特征向量维度
注意:预训练模型是在 ImageNet 上训练的,如果你的图像领域差异很大(比如医学影像),建议做微调。我曾经直接拿通用模型提取医学 CT 特征,效果惨不忍睹。
4.2 特征选择:去芜存菁
特征不是越多越好。我见过一个项目,原始特征 2000 多维,模型训练要跑 3 天。后来做了特征选择,降到 200 维,效果反而提升了。特征选择有三种主流方法。
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 过滤法 | 基于统计指标(卡方、互信息、方差)筛选特征 | 计算快,独立于模型 | 忽略特征间交互 |
| 包裹法 | 用模型效果评估特征子集(如递归特征消除) | 考虑特征组合效果 | 计算开销大 |
| 嵌入法 | 在模型训练过程中自动选择特征(如 L1 正则化) | 效率与效果平衡 | 依赖模型选择 |
4.2.1 过滤法实战
过滤法我常用方差阈值和卡方检验。方差太小的特征,说白了就是“没变化”,对模型没贡献。
from sklearn.feature_selection import VarianceThreshold, SelectKBest, chi2
# 方差过滤:去掉方差小于0.1的特征
selector = VarianceThreshold(threshold=0.1)
X_high_var = selector.fit_transform(X)
# 卡方检验:选择与目标最相关的K个特征
chi2_selector = SelectKBest(chi2, k=10)
X_selected = chi2_selector.fit_transform(X, y)
4.2.2 包裹法实战
包裹法我习惯用递归特征消除(RFE)。它反复训练模型,每次去掉最不重要的特征。嗯,这里要注意:特征多的时候跑起来很慢。
from sklearn.feature_selection import RFE
from sklearn.svm import SVC
estimator = SVC(kernel="linear")
selector = RFE(estimator, n_features_to_select=10, step=1)
selector = selector.fit(X, y)
print(selector.support_) # 哪些特征被选中
4.2.3 嵌入法实战
嵌入法我推荐用 Lasso 回归(L1 正则化)。它会自动把不重要特征的系数压成 0,相当于帮我们做了特征选择。
from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.01)
lasso.fit(X, y)
# 系数不为0的特征就是被选中的
selected_features = np.where(lasso.coef_ != 0)[0]
我的建议:先用过滤法快速筛掉明显无用的特征,再用嵌入法或包裹法精细选择。这样效率最高。
4.3 特征构造:创造新特征
有时候,原始特征不够用,需要我们自己“造”一些特征出来。特征构造有两种常见思路。
4.3.1 交叉特征
交叉特征就是把两个或多个特征组合起来。比如“年龄”和“收入”交叉,可以产生“年龄-收入”组合特征。这在推荐系统里特别常用。
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
df = pd.DataFrame({
'age': [25, 30, 35],
'income': [50000, 60000, 70000]
})
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
cross_features = poly.fit_transform(df)
print(poly.get_feature_names_out())
4.3.2 聚合特征
聚合特征常用于时序数据或分组数据。比如按用户 ID 分组,计算每个用户的平均消费金额、消费次数等。
# 假设有用户交易数据
df_grouped = df.groupby('user_id').agg({
'amount': ['mean', 'sum', 'count', 'std'],
'timestamp': ['min', 'max']
}).reset_index()
# 展平列名
df_grouped.columns = ['_'.join(col).strip() for col in df_grouped.columns]
避坑指南:我曾经在构造交叉特征时,不小心引入了“未来信息”——用未来的数据构造了当前的特征,导致模型在线上效果暴跌。做聚合特征时,一定要确保只用历史数据。
4.4 特征降维:化繁为简
特征降维,说白了就是把高维数据压缩到低维,同时尽量保留原始信息。我常用两种方法:PCA 和 t-SNE。
| 方法 | 原理 | 适用场景 | 输出 |
|---|---|---|---|
| PCA | 线性变换,找到方差最大的方向 | 数据压缩、去噪、加速模型训练 | 低维稠密向量 |
| t-SNE | 非线性降维,保持局部结构 | 数据可视化(2D/3D) | 低维嵌入 |
4.4.1 PCA 实战
PCA 是线性降维的经典方法。我习惯先标准化数据,再做 PCA,否则量纲大的特征会主导结果。
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# PCA 降到 50 维
pca = PCA(n_components=50)
X_pca = pca.fit_transform(X_scaled)
# 查看解释方差比
print(pca.explained_variance_ratio_)
print(f"累计解释方差: {pca.explained_variance_ratio_.sum():.2f}")
我的习惯:选择 n_components 时,我会看累计解释方差比达到 0.95 或 0.99 的维度数。这样既降维又不损失太多信息。
4.4.2 t-SNE 实战
t-SNE 主要用于可视化。它能把高维数据映射到 2D 或 3D,让我们“看”到数据的聚类结构。
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X_scaled)
plt.figure(figsize=(10, 8))
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis', alpha=0.7)
plt.colorbar()
plt.title('t-SNE 可视化')
plt.show()
注意:t-SNE 的计算复杂度是 O(n²),数据量超过 1 万条时跑起来很慢。我一般先随机采样 5000 条再做 t-SNE。另外,t-SNE 每次运行结果可能不同,记得固定 random_state。
4.5 本章小结
特征工程不是一蹴而就的。我做了这么多年,最大的体会是:多尝试、多验证。没有万能的方法,只有最适合你数据的方法。记住这四步:提取、选择、构造、降维,每一步都可能成为模型效果的转折点。
一句话总结:特征工程的质量,决定了模型效果的天花板。花时间在特征上,永远值得。