机器学习特征选择:穷举法与随机优化实践
1. 特征选择与优化算法概述
在机器学习项目中,特征选择是一个至关重要的预处理步骤。当数据集包含大量特征时,选择合适的特征子集不仅能提高模型性能,还能降低计算成本。决策树这类模型对特征选择尤为敏感,合理的特征子集可以显著提升其准确率。
传统特征选择方法主要分为两大类:
- 过滤式方法(Filter Methods):基于统计指标评估特征与目标变量的相关性
- 包裹式方法(Wrapper Methods):通过实际建模评估特征子集效果
对于特征数量较少的情况(通常指20个以下),我们可以穷举所有可能的特征组合。例如5个特征时,共有2^5=32种组合方式。但当特征数量增加到500个时,组合数达到2^500≈10^150种,这已经完全超出了计算机的处理能力。
实际经验表明,当特征超过30个时,穷举法就变得不切实际。我曾在一个包含42个特征的项目中尝试全组合搜索,即便使用并行计算也花费了3天时间。
2. 小规模特征集的穷举策略
2.1 实验数据集构建
我们首先生成一个包含5个特征的小型分类数据集,其中2个是有效特征,3个是冗余特征:
from sklearn.datasets import make_classification
# 生成1000个样本,5个特征(2个有效,3个冗余)
X, y = make_classification(n_samples=1000, n_features=5,
n_informative=2, n_redundant=3,
random_state=1)
print(X.shape) # (1000, 5)
2.2 基准模型建立
使用决策树和分层K折交叉验证建立基准:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from numpy import mean
model = DecisionTreeClassifier()
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
scores = cross_val_score(model, X, y, scoring='accuracy', cv=cv, n_jobs=-1)
print(f'基准准确率: {mean(scores):.3f} (±{std(scores):.3f})')
典型输出结果:
基准准确率: 0.805 (±0.030)
2.3 全组合搜索实现
使用itertools.product生成所有可能的特征组合:
from itertools import product
n_cols = X.shape[1]
best_subset, best_score = None, 0.0
for subset in product([True, False], repeat=n_cols):
ix = [i for i, x in enumerate(subset) if x]
if not ix: continue
X_new = X[:, ix]
scores = cross_val_score(model, X_new, y, scoring='accuracy', cv=cv, n_jobs=-1)
current_score = mean(scores)
if current_score > best_score:
best_subset, best_score = ix, current_score
print(f'发现更好组合: {ix} 准确率: {current_score:.3f}')
关键点说明:
- 每个组合用布尔序列表示特征是否被选中
- 跳过全False的空组合
- 使用相同的交叉验证配置保证结果可比性
2.4 结果分析与优化
最终我们可能发现类似如下的最优组合:
最优特征组合: [0, 3, 4] 准确率: 0.830
这表明原始数据集中存在冗余特征,通过特征选择将准确率从80.5%提升到了83.0%。在实际项目中,这种幅度的提升往往意味着模型性能的显著改善。
3. 大规模特征集的随机优化
3.1 大规模数据集构建
当特征数量增加到500个时(其中10个有效,490个冗余):
X, y = make_classification(n_samples=10000, n_features=500,
n_informative=10, n_redundant=490,
random_state=1)
3.2 随机优化算法设计
我们采用随机爬山算法进行特征选择优化:
from numpy.random import rand, choice
def objective(X, y, subset):
ix = [i for i, x in enumerate(subset) if x]
if not ix: return 0.0
X_new = X[:, ix]
scores = cross_val_score(model, X_new, y, scoring='accuracy', cv=3, n_jobs=-1)
return mean(scores), ix
def mutate(solution, p_mutate=0.02):
return [not x if rand() < p_mutate else x for x in solution]
def hill_climbing(X, y, n_iter=100):
current_solution = choice([True, False], size=X.shape[1])
current_score, _ = objective(X, y, current_solution)
for i in range(n_iter):
candidate = mutate(current_solution)
candidate_score, ix = objective(X, y, candidate)
if candidate_score >= current_score:
current_solution, current_score = candidate, candidate_score
print(f'迭代{i+1}: 特征数{len(ix)} 准确率{candidate_score:.3f}')
return current_solution, current_score
3.3 优化过程分析
典型优化过程输出:
迭代1: 特征数12 准确率0.872
迭代5: 特征数9 准确率0.885
迭代17: 特征数11 准确率0.891
...
迭代83: 特征数8 准确率0.902
算法参数选择经验:
- 变异概率:通常设置为1/n_features(本例为0.002)
- 迭代次数:根据特征数量,一般100-1000次
- 初始解:随机生成,包含约50%特征
3.4 性能对比
与全特征模型对比:
- 全特征模型:准确率0.913,使用500个特征
- 优化后模型:准确率0.902,使用8-12个特征
虽然准确率略有下降(1.1%),但特征数量减少了98%以上,大大提高了模型的可解释性和部署效率。
4. 工程实践建议
4.1 算法选择策略
根据特征数量选择合适方法:
| 特征数量 | 推荐方法 | 计算时间 | 准确率 |
|---|---|---|---|
| <20 | 穷举法 | 分钟级 | 最优 |
| 20-100 | 遗传算法 | 小时级 | 接近最优 |
| >100 | 随机优化 | 天级 | 次优 |
4.2 参数调优技巧
-
变异概率调整:
- 初期:较高概率(0.05)促进探索
- 后期:降低概率(0.001)精细调优
-
记忆机制: 保留历史最优解,防止优化过程退化
-
并行评估: 使用多进程同时评估多个候选解
4.3 实际项目经验
在电商用户行为分析项目中,我们面对387个原始特征:
- 第一轮:使用随机森林重要性筛选出前50个特征
- 第二轮:采用改进的爬山算法优化
- 最终得到23个特征组合,使AUC提升2.3%
关键教训:
- 不要完全依赖自动优化,要结合业务理解
- 记录每个候选解的评价指标,便于后期分析
- 特征选择后务必检查特征间的相关性
5. 高级优化技巧
5.1 混合优化策略
结合过滤法和优化算法:
- 先用方差阈值或互信息筛选掉明显无关特征
- 对剩余特征进行随机优化
- 最终人工复核特征合理性
5.2 多目标优化
同时优化多个指标:
def multi_objective(X, y, subset):
ix = [i for i, x in enumerate(subset) if x]
if not ix: return -np.inf, np.inf
X_new = X[:, ix]
model = DecisionTreeClassifier()
# 计算准确率和特征数量
acc = cross_val_score(model, X_new, y, scoring='accuracy').mean()
num_features = len(ix)
return acc, -num_features # 最大化准确率,最小化特征数量
5.3 早停机制
当连续N次迭代没有改进时提前终止:
no_improve = 0
best_score = -np.inf
while no_improve < patience:
# ...优化步骤...
if current_score > best_score:
best_score = current_score
no_improve = 0
else:
no_improve += 1
6. 不同算法的实现对比
6.1 遗传算法实现
def genetic_algorithm(X, y, pop_size=50, generations=100):
population = [choice([True, False], size=X.shape[1]) for _ in range(pop_size)]
for gen in range(generations):
scores = [objective(X, y, ind)[0] for ind in population]
# 选择、交叉、变异...
# 保留精英个体
6.2 模拟退火实现
def simulated_annealing(X, y, temp=1.0, cooling=0.99):
current = choice([True, False], size=X.shape[1])
current_score, _ = objective(X, y, current)
while temp > 0.01:
neighbor = mutate(current)
neighbor_score, _ = objective(X, y, neighbor)
if neighbor_score > current_score or rand() < np.exp((neighbor_score - current_score)/temp):
current, current_score = neighbor, neighbor_score
temp *= cooling
6.3 算法对比表
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 穷举法 | 找到全局最优 | 计算复杂度高 | 特征数<20 |
| 爬山算法 | 实现简单 | 易陷入局部最优 | 快速初步筛选 |
| 遗传算法 | 探索能力强 | 参数调优复杂 | 中等规模特征 |
| 模拟退火 | 能跳出局部最优 | 收敛速度慢 | 复杂优化问题 |
在实际项目中,我通常会先使用爬山算法快速获得基线结果,然后再尝试更复杂的优化算法。这种渐进式的方法往往能在有限的计算资源下获得最佳性价比。
更多推荐
所有评论(0)