1. 从“调参噩梦”到“自动寻优”:为什么我们需要用遗传算法优化回归模型
做数据分析或者机器学习的朋友,对多元线性回归肯定不陌生。公式摆在那里,y = β0 + β1*x1 + β2*x2 + ... + βn*xn + ε,看起来清晰明了。我们用最小二乘法(OLS)去拟合,软件一跑,系数、R方、p值全都出来了,似乎一切尽在掌握。但不知道你有没有遇到过这种情况:模型的理论解释很完美,可一到预测新数据,表现就惨不忍睹;或者,当你尝试引入多项式项、交互项来捕捉非线性关系时,模型瞬间变得异常复杂,系数多如牛毛,而且结果对初始值和算法设置极其敏感,稍微动一下,整个模型就面目全非。
这背后往往是一个经典的优化问题:我们如何为这个复杂的回归函数找到一组“最优”的系数?这里的“最优”不仅仅是让训练集上的误差最小(那会导致严重的过拟合),还要兼顾模型的简洁性、稳健性和泛化能力。传统的最小二乘法在面对多重共线性、异常值或者复杂的非线性结构时,可能会力不从心,甚至求出的解不稳定。而当我们使用梯度下降这类迭代优化方法时,又很容易陷入局部最优的陷阱,特别是参数空间维度高、地形复杂的时候。
这时候,遗传算法(Genetic Algorithm, GA)这种启发式全局优化算法就派上用场了。它不依赖于目标函数的梯度信息,而是模拟生物进化中的“物竞天择,适者生存”原理,通过选择、交叉、变异等操作,在庞大的参数空间中并行地搜索更优解。它特别擅长处理传统优化方法搞不定的、非凸的、多峰的、甚至不可导的复杂优化问题。用GA来优化多元回归函数,本质上就是让算法自动为我们寻找那组能使某个定制化评价指标(如调整R方、AIC、BIC,或测试集上的误差)最佳的回归系数。这相当于把一个枯燥且容易出错的“调参”过程,交给了不知疲倦的“进化”过程去完成。
接下来,我将结合一个具体的实战案例,手把手带你走完用遗传算法优化多元回归模型的完整流程。我们会从问题定义、算法设计、编码实现,一直讲到结果分析和避坑指南。你会发现,这不仅仅是套用一个算法库那么简单,其中关于“如何定义优秀”、“如何引导进化”的思考,才是整个过程的精髓。
2. 问题定义与建模:我们要优化什么?
在把遗传算法这个“引擎”装上车之前,我们必须先明确这辆车要开往何方,也就是精确地定义我们的优化目标。对于多元回归模型的优化,这个目标绝不是单一的。
2.1 核心优化目标的权衡:拟合优度 vs. 模型复杂度
最直接的冲动是让预测误差最小化,比如最小化均方误差(MSE)或均方根误差(RMSE)。如果我们只优化这个,遗传算法很可能会给我们找到一个在训练集上表现完美但极度复杂的模型(例如,一个包含所有可能的高阶项和交互项的模型)。这就是过拟合。
因此,一个稳健的优化目标必须在拟合优度和模型复杂度之间取得平衡。在实际应用中,我们通常采用以下一种或多种综合指标作为遗传算法中“个体适应度”的计算依据:
调整R方(Adjusted R²):这是最常用的指标之一。它在普通R方的基础上,引入了惩罚项来抵消变量增加带来的虚假提升。我们的目标是最大化调整R方。
Adjusted R² = 1 - [(1-R²)(n-1)/(n-k-1)],其中n是样本数,k是变量数。遗传算法可以搜索能使该值最大的变量组合及系数。信息准则(AIC/BIC):AIC(赤池信息准则)和BIC(贝叶斯信息准则)是更严格的准则,不仅惩罚变量数量,还惩罚样本量。我们的目标是最小化AIC或BIC。
AIC = 2k - 2ln(L),BIC = k*ln(n) - 2ln(L),其中L是模型似然函数的最大值。在实际编程中,我们通常用基于残差平方和的公式来近似计算。交叉验证误差:这是评估泛化能力的金标准。我们可以将适应度函数定义为模型在K折交叉验证中平均测试误差的倒数(因为适应度通常越大越好)。例如,使用5折交叉验证的均方误差的负数作为适应度。
定制化复合指标:在某些业务场景下,我们可能对误差的分布有特殊要求。例如,在预测房价时,高估比低估带来更严重的后果。此时,可以设计一个非对称的损失函数,并让遗传算法去优化它。
在我们的实战案例中,假设我们有一个包含10个潜在预测变量(x1到x10)的数据集,想要预测目标变量y。我们不确定哪些变量是真正重要的,也不确定是否需要引入二次项(如x1²)。那么,我们的优化问题可以定义为:寻找一个最优的变量子集以及它们的系数(包括可能的二次项系数),使得模型在预留的验证集上的调整R方最高。
这就意味着,遗传算法中的每个“个体”(即一组候选解),不仅要编码一组连续的系数值,还可能要以某种方式编码“某个变量是否被选中”这个离散信息。这是设计染色体编码的关键。
2.2 决策变量与搜索空间
明确了优化目标,接下来要定义遗传算法操作的对象——决策变量。对于我们的多元回归问题,决策变量有两类:
- 连续变量:每个入选模型的预测变量所对应的回归系数 β。它们的值域通常是实数域,我们可以设定一个合理的搜索范围,如 [-10, 10]。
- 离散变量(二进制):每个潜在预测变量(包括其二次项)是否被包含在模型中的开关。1表示包含,0表示不包含。
假设我们考虑10个原始变量和它们的10个二次项,那么就有20个“特征开关”。加上每个开关对应的1个系数(如果开关为0,其系数实际上不发挥作用,但为了编码统一,我们仍为其保留位置),那么一个完整的解就需要20个二进制位 + 20个连续系数值。
这个搜索空间有多大?仅二进制部分就是2^20 ≈ 104万种特征组合。再乘以每个连续系数的无穷可能性(在离散化后也是巨大的网格),暴力穷举是完全不可能的。这正是遗传算法这类启发式算法大显身手的地方。
3. 遗传算法核心组件设计与Python实现
理论清晰后,我们开始动手搭建。这里使用Python的deap库,它是一个非常灵活强大的进化计算框架。当然你也可以用PyGAD或自己从头实现,但deap提供了足够的底层控制,适合学习原理。
3.1 环境准备与数据加载
首先,安装必要的库并准备一份模拟数据。我们使用sklearn生成一个具有非线性关系和冗余特征的数据集,这样更贴近真实场景。
!pip install deap scikit-learn numpy pandas matplotlib import numpy as np import pandas as pd from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import random from deap import base, creator, tools, algorithms import matplotlib.pyplot as plt # 1. 生成模拟数据 # 生成1000个样本,20个特征,但其中只有5个特征与目标有真实关系,并加入一些非线性 X, y = make_regression(n_samples=1000, n_features=10, n_informative=5, noise=20, random_state=42) # 手动添加一些非线性:例如,对前两个重要特征添加平方项效应 y = y + 0.5 * X[:, 0]**2 - 0.3 * X[:, 1]**2 # 添加10个纯噪声特征,模拟现实中大量无关变量的情况 X_noise = np.random.randn(1000, 10) X = np.hstack([X, X_noise]) # 现在总共有20个特征 # 2. 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 3. 标准化特征(对于回归和遗传算法中的系数范围设定很重要) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) print(f"训练集形状: {X_train_scaled.shape}, 测试集形状: {X_test_scaled.shape}")3.2 染色体编码设计:混合编码策略
这是最关键的一步。我们需要设计一条染色体来同时表示特征选择和系数值。这里采用一种混合编码策略:
- 染色体前半部分:20个二进制基因,代表20个原始特征是否被选中。
- 染色体后半部分:20个浮点数基因,代表对应特征被选中时的回归系数。
如果某个二进制位是0,即使其对应的系数不为零,在计算模型时我们也忽略该特征。这样设计简化了编码,但意味着搜索空间中存在冗余(一个被关闭的特征,其系数值是无意义的)。遗传算法可以处理这种冗余。
# 定义问题类型:我们要最大化适应度(如调整R方) creator.create("FitnessMax", base.Fitness, weights=(1.0,)) # 权重为1.0表示最大化 creator.create("Individual", list, fitness=creator.FitnessMax) # 初始化工具箱 toolbox = base.Toolbox() # 定义基因属性 # 二进制基因(特征选择) toolbox.register("attr_bool", random.randint, 0, 1) # 浮点基因(系数),范围设为[-5, 5],根据数据尺度调整 toolbox.register("attr_coef", random.uniform, -5, 5) # 定义个体创建函数 def create_individual(): # 前20个是二进制特征选择器 part1 = [toolbox.attr_bool() for _ in range(20)] # 后20个是对应的浮点系数 part2 = [toolbox.attr_coef() for _ in range(20)] return creator.Individual(part1 + part2) toolbox.register("individual", create_individual) # 定义种群创建函数 toolbox.register("population", tools.initRepeat, list, toolbox.individual)3.3 适应度函数设计:模型评估的核心
适应度函数是连接遗传算法和回归问题的桥梁。它接收一个染色体(个体),解码出特征掩码和系数,构建回归模型,计算在训练集上的调整R方,并将其作为适应度返回。
from sklearn.metrics import r2_score def evaluate_individual(individual): """ 计算个体的适应度(调整R方)。 个体染色体 = [b1, b2, ..., b20, c1, c2, ..., c20] 前20位是二进制特征选择掩码,后20位是系数。 """ # 解码染色体 mask = np.array(individual[:20], dtype=bool) # 特征选择掩码 coefficients = np.array(individual[20:]) # 原始系数 # 如果掩码全为0,没有选中任何特征,返回一个极差的适应度(避免除零错误) if not mask.any(): return (-1e10,) # 返回一个极小的值 # 获取被选中的特征和对应的系数 selected_features = X_train_scaled[:, mask] selected_coeffs = coefficients[mask] # 计算预测值: y_pred = X_selected * coeffs_selected # 注意:我们这里没有截距项。如果需要,可以在染色体中额外增加一个截距基因。 y_pred = np.dot(selected_features, selected_coeffs) # 计算R方 r2 = r2_score(y_train, y_pred) # 计算调整R方 n = len(y_train) # 样本数 k = np.sum(mask) # 选中的特征数 if n - k - 1 <= 0: # 防止自由度非正 adjusted_r2 = -1e10 else: adjusted_r2 = 1 - (1 - r2) * (n - 1) / (n - k - 1) # 可以添加惩罚项:强烈倾向于更简单的模型(L0正则化思想) # 例如,对特征数量进行惩罚: penalty = -0.01 * k # adjusted_r2_penalized = adjusted_r2 + penalty return (adjusted_r2,) # 注册适应度函数 toolbox.register("evaluate", evaluate_individual)注意:这里做了一个重要简化,即模型不包含截距项。在实际问题中,截距项通常很重要。你可以在染色体中额外增加一个浮点基因作为截距,或者在数据预处理阶段将目标变量y中心化(减去均值),这样最优的截距就是0,可以省略。
3.4 遗传算子配置:选择、交叉与变异
选择合适的遗传算子并调整其参数,是算法能否成功收敛的关键。
# 选择算子:这里使用锦标赛选择,压力较大,容易保留优秀个体 toolbox.register("select", tools.selTournament, tournsize=3) # 交叉算子:对于混合编码,我们需要自定义交叉方式 # 对二进制部分使用均匀交叉,对浮点部分使用模拟二进制交叉(SBX)或混合交叉 def cxMixed(ind1, ind2): # 对前20位二进制部分进行均匀交叉 for i in range(20): if random.random() < 0.5: # 交叉概率 ind1[i], ind2[i] = ind2[i], ind1[i] # 对后20位浮点部分进行模拟二进制交叉(SBX) # DEAP内置了cxSimulatedBinary,但要求个体是纯浮点列表。 # 我们手动实现一个简单的混合交叉:以一定概率交换系数 for i in range(20, 40): if random.random() < 0.7: # 浮点部分的交叉概率可以设高一些 # 简单交换 ind1[i], ind2[i] = ind2[i], ind1[i] # 更高级的做法:SBX交叉,能产生子代在父代之间的随机插值 # 这里为了简化,先使用交换 return ind1, ind2 toolbox.register("mate", cxMixed) # 变异算子:同样需要自定义 def mutMixed(individual, indpb): """ 变异操作。 indpb: 每个基因的独立变异概率 """ size = len(individual) for i in range(size): if random.random() < indpb: if i < 20: # 二进制部分:位翻转 individual[i] = 0 if individual[i] == 1 else 1 else: # 浮点部分:高斯扰动 individual[i] += random.gauss(0, 0.5) # 均值为0,标准差为0.5的扰动 # 确保系数仍在合理范围内 if individual[i] > 5: individual[i] = 5 elif individual[i] < -5: individual[i] = -5 return individual, toolbox.register("mutate", mutMixed, indpb=0.05) # 每个基因有5%的概率发生变异3.5 算法主循环与进化过程
现在,让我们把种群初始化,并运行进化过程。
# 创建初始种群 population_size = 100 pop = toolbox.population(n=population_size) # 统计器,用于记录进化过程中的数据 stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("avg", np.mean) stats.register("std", np.std) stats.register("min", np.min) stats.register("max", np.max) # 运行进化算法 hof = tools.HallOfFame(5) # 保留历代最优的5个个体 logbook = tools.Logbook() # 简单进化流程 ngen = 50 # 进化代数 cxpb = 0.7 # 交叉概率 mutpb = 0.2 # 变异概率 print("开始进化...") for gen in range(ngen): # 评估所有个体的适应度 fitnesses = toolbox.map(toolbox.evaluate, pop) for ind, fit in zip(pop, fitnesses): ind.fitness.values = fit # 记录本代统计信息 record = stats.compile(pop) logbook.record(gen=gen, **record) if gen % 10 == 0: print(f"第 {gen} 代: {record}") # 选择下一代 offspring = toolbox.select(pop, len(pop)) # 克隆选中的个体(DEAP要求先克隆再操作) offspring = list(map(toolbox.clone, offspring)) # 对选出的后代进行交叉和变异 # 交叉 for child1, child2 in zip(offspring[::2], offspring[1::2]): if random.random() < cxpb: toolbox.mate(child1, child2) del child1.fitness.values del child2.fitness.values # 变异 for mutant in offspring: if random.random() < mutpb: toolbox.mutate(mutant) del mutant.fitness.values # 用后代完全替换父代(简单遗传算法) pop[:] = offspring # 最后再评估一次最终种群 fitnesses = toolbox.map(toolbox.evaluate, pop) for ind, fit in zip(pop, fitnesses): ind.fitness.values = fit hof.update(pop) print("进化完成!")4. 结果解码、验证与深度分析
进化结束后,我们保存在HallOfFame(hof)中的就是历代最优的个体。让我们解码出最优模型,并在测试集上验证其性能。
4.1 解码最优个体
# 获取历史最优个体 best_ind = hof[0] best_mask = np.array(best_ind[:20], dtype=bool) best_coefficients = np.array(best_ind[20:]) selected_coeffs = best_coefficients[best_mask] print("="*50) print("最优个体分析报告") print("="*50) print(f"选中的特征索引: {np.where(best_mask)[0]}") print(f"选中特征的数量: {np.sum(best_mask)}") print(f"对应的系数值: {selected_coeffs}") print(f"在训练集上的适应度(调整R方): {best_ind.fitness.values[0]:.4f}") # 在训练集上计算详细指标 selected_features_train = X_train_scaled[:, best_mask] y_pred_train = np.dot(selected_features_train, selected_coeffs) train_r2 = r2_score(y_train, y_pred_train) train_mse = np.mean((y_train - y_pred_train)**2) print(f"训练集 R²: {train_r2:.4f}") print(f"训练集 MSE: {train_mse:.4f}")4.2 在独立测试集上验证
这是检验模型是否过拟合的关键一步。
# 在测试集上评估 selected_features_test = X_test_scaled[:, best_mask] y_pred_test = np.dot(selected_features_test, selected_coeffs) test_r2 = r2_score(y_test, y_pred_test) test_mse = np.mean((y_test - y_pred_test)**2) n_test = len(y_test) k_test = np.sum(best_mask) if n_test - k_test - 1 > 0: test_adjusted_r2 = 1 - (1 - test_r2) * (n_test - 1) / (n_test - k_test - 1) else: test_adjusted_r2 = None print("\n" + "="*50) print("独立测试集验证结果") print("="*50) print(f"测试集 R²: {test_r2:.4f}") if test_adjusted_r2: print(f"测试集调整R²: {test_adjusted_r2:.4f}") print(f"测试集 MSE: {test_mse:.4f}") # 绘制预测值与真实值对比图 plt.figure(figsize=(10, 6)) plt.scatter(y_test, y_pred_test, alpha=0.6, edgecolors='k') plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2, label='理想线') plt.xlabel('真实值 (y_test)') plt.ylabel('预测值 (y_pred_test)') plt.title('测试集:预测值 vs 真实值') plt.legend() plt.grid(True, linestyle='--', alpha=0.7) plt.show()4.3 进化过程可视化
观察适应度随代数的变化,可以帮助我们判断算法是否收敛,以及参数设置是否合理。
# 从logbook中提取数据 gen = logbook.select("gen") avg_fitness = logbook.select("avg") max_fitness = logbook.select("max") min_fitness = logbook.select("min") plt.figure(figsize=(12, 6)) plt.plot(gen, avg_fitness, 'b-', label="平均适应度") plt.plot(gen, max_fitness, 'r-', label="最大适应度") plt.plot(gen, min_fitness, 'g-', label="最小适应度") plt.fill_between(gen, min_fitness, max_fitness, color='gray', alpha=0.1) plt.xlabel("进化代数") plt.ylabel("适应度 (调整R方)") plt.title("遗传算法进化过程") plt.legend(loc="best") plt.grid(True, linestyle='--', alpha=0.7) plt.show()4.4 与经典方法对比
为了体现遗传算法的价值,我们将其结果与两种经典方法进行对比:
- 全特征普通最小二乘回归(OLS):使用所有20个特征。
- 基于统计检验的特征选择(如向后消除):使用
statsmodels库进行。
import statsmodels.api as sm from sklearn.feature_selection import SequentialFeatureSelector from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score # 方法1:全特征OLS X_train_with_const = sm.add_constant(X_train_scaled) # 添加截距项 ols_model = sm.OLS(y_train, X_train_with_const).fit() y_pred_ols_test = ols_model.predict(sm.add_constant(X_test_scaled)) test_r2_ols = r2_score(y_test, y_pred_ols_test) test_mse_ols = mean_squared_error(y_test, y_pred_ols_test) print(f"全特征OLS - 测试集 R²: {test_r2_ols:.4f}, MSE: {test_mse_ols:.4f}") print(f"全特征OLS使用了 {X_train_with_const.shape[1]} 个参数(含截距)。") # 方法2:基于AIC的向后特征消除(使用statsmodels) # 这是一个简化演示,实际应用中可能需要更严谨的循环 def backward_elimination(X, y, significance_level=0.05): X_with_const = sm.add_constant(X) num_features = X.shape[1] for i in range(num_features): model = sm.OLS(y, X_with_const).fit() pvalues = model.pvalues[1:] # 排除截距项 max_pvalue = pvalues.max() if max_pvalue > significance_level: removed_feature_idx = pvalues.argmax() print(f"移除特征 {removed_feature_idx} (p值={max_pvalue:.4f})") # 从数据中移除该特征 X_with_const = np.delete(X_with_const, removed_feature_idx+1, axis=1) # +1是因为有const列 else: break final_model = sm.OLS(y, X_with_const).fit() return final_model, X_with_const # 注意:由于我们的数据是模拟的,且特征间可能相关,向后消除可能不稳定,此处仅作流程演示。 # 在实际分析中,你可能需要结合VIF、业务知识等。 print("\n向后消除特征选择(演示):") try: final_be_model, X_be_selected = backward_elimination(X_train_scaled, y_train) # 预测测试集需要对齐特征 # 这里需要记录被选中的特征索引,然后从测试集中选取对应列,并添加常数项。 # 由于演示简化,我们跳过完整的测试集预测代码。 print("向后消除完成。") except Exception as e: print(f"向后消除过程中出现错误(可能是共线性导致): {e}") print("\n" + "="*50) print("方法对比总结") print("="*50) print(f"遗传算法优化模型: 选中 {np.sum(best_mask)} 个特征,测试集R² = {test_r2:.4f}") print(f"全特征OLS模型: 使用 20 个特征,测试集R² = {test_r2_ols:.4f}") # 如果GA模型的测试集R²更高或相当,但使用了更少的特征,则说明GA成功找到了一个更简洁、泛化能力可能更好的模型。5. 实战中的关键技巧与避坑指南
通过上面的完整流程,你已经可以跑通一个基本的遗传算法优化回归模型了。但在实际项目中,直接套用可能会遇到各种问题。下面是我在多次实践中总结出的核心技巧和常见陷阱。
5.1 适应度函数设计:避免过拟合的“指挥棒”
适应度函数是遗传算法的“指挥棒”,它决定了进化的方向。设计不当,算法会迅速过拟合。
- 陷阱:只使用训练集误差(如MSE)作为适应度。这会导致算法疯狂地增加特征和复杂度来拟合训练集噪声。
- 技巧1:使用交叉验证。最稳健的方法是使用K折交叉验证的平均误差作为适应度。虽然这会使每次评估的计算量增加K倍,但能极大提升找到泛化能力好模型的概率。在
deap中,你需要修改evaluate_individual函数,内部进行交叉验证。 - 技巧2:使用带惩罚项的信息准则。如我们之前所用,调整R方、AIC、BIC都内置了对于模型复杂度的惩罚。BIC的惩罚比AIC更重,倾向于选择更简单的模型。
- 技巧3:引入正则化先验。你可以在适应度函数中显式地加入L1或L2正则化项,例如
fitness = -MSE - lambda * sum(|coefficients|)。这相当于在进化过程中融入了LASSO或岭回归的思想。
5.2 编码与搜索空间:平衡表达力与效率
- 陷阱:像我们例子中一样,为每个特征设置一个独立的“开关”和“系数”基因。当特征数量巨大(如成百上千)时,染色体长度爆炸,搜索空间呈指数增长,算法效率极低,且难以收敛。
- 技巧1:分组编码。对于高度相关的特征,可以将它们编码为一个“超级特征”组,用同一个开关控制。这需要先验知识或进行聚类分析。
- 技巧2:两阶段优化。第一阶段,用遗传算法优化特征选择(二进制部分),固定系数为简单线性回归的结果。第二阶段,在选定的特征子集上,用遗传算法或其他优化方法精细调整系数。这可以分解问题,降低复杂度。
- 技巧3:实数编码与嵌入式选择。可以不使用显式的二进制开关,而是让系数本身趋近于零来实现特征选择。这时可以使用能够产生稀疏解的变异算子(如将某些系数直接置零的“冲击变异”),或者在适应度函数中对非零系数数量进行惩罚(L0正则)。
5.3 遗传算子与参数调优:控制进化节奏
- 陷阱:使用默认的交叉变异概率和选择压力。过高的交叉/变异率会导致优秀基因结构被破坏,算法像随机搜索;过低则导致种群多样性丧失,早熟收敛。
- 技巧1:自适应参数。让交叉概率
cxpb和变异概率mutpb随着进化代数或种群适应度的收敛情况而动态变化。例如,前期可以设置较高的变异率来探索空间,后期降低变异率进行精细搜索。 - 技巧2:精英保留策略。务必使用
HallOfFame或tools.selBest来确保每一代的最优个体不被破坏地传递到下一代。这是保证算法收敛性的重要手段。 - 技巧3:多样性的维持。如果发现种群过早收敛(所有个体适应度接近),可以引入“小生境”技术或“共享”机制,惩罚过于相似的个体,鼓励探索新的区域。
deap中的tools.selTournament可以通过调整tournsize来控制选择压力,tournsize越大,选择压力越大,收敛越快但多样性可能丢失也快。
5.4 处理现实数据中的“脏”问题
- 缺失值:遗传算法本身不处理缺失值。你必须在数据预处理阶段解决。一种方法是在编码时,为每个特征增加一个“是否缺失”的二进制基因,并对应一个“缺失值填充值”的浮点基因,让算法同时学习如何处理缺失。
- 量纲差异:正如我们在代码中所做,标准化或归一化特征至关重要。这能确保系数的大小具有可比性,防止适应度函数被量级大的特征主导,也让变异算子施加的高斯扰动对每个系数的影响尺度一致。
- 共线性:高度共线性的特征会使得系数估计不稳定。遗传算法可能会反复选择共线性特征组中的不同成员,导致结果波动。可以在适应度函数中加入对特征间相关性的惩罚,或者在预处理时使用PCA等降维方法创建不相关的新特征,再让GA选择。
5.5 算法停止与结果评估
- 停止准则:不要仅仅固定运行50代。可以设置组合停止条件:1) 达到最大代数;2) 连续N代最优适应度没有显著提升(如提升小于1e-5);3) 种群的平均适应度与最优适应度差距小于某个阈值。
- 多次运行:由于遗传算法的随机性,单次运行的结果可能有偶然性。务必独立运行算法多次(如30次),记录每次找到的最优解。最后分析这些解的特征选择频率和系数分布,选择那些被多次、独立找到的稳定解,这比单次运行的结果可靠得多。
- 最终验证:无论如何,最终模型的性能必须在一个全新的、从未参与过任何优化过程(包括交叉验证)的测试集上进行报告。这是我们评估模型泛化能力的最终标准。
用遗传算法优化回归模型,与其说是一个简单的工具应用,不如说是一次对问题建模和搜索策略的深度思考。它迫使你明确“什么是一个好模型”的标准,并设计一套自动化的机制去逼近这个标准。这个过程可能比单纯跑一个回归要复杂,但当你面对高维、非线性、充满噪声的真实数据,而标准方法束手无策时,这种“进化”出来的解决方案,往往会给你带来意想不到的惊喜。