C++实现遗传算法求解Rastrigin函数优化问题

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:遗传算法是一种模拟自然选择与遗传机制的全局优化方法,广泛应用于复杂问题的近似最优解搜索。本文介绍了一个基于C++实现的遗传算法项目,旨在求解具有多个局部极小值的Rastrigin函数全局最小值问题。通过初始化种群、适应度评估、选择、交叉、变异等核心步骤,算法迭代优化直至收敛。项目包含可执行文件、源码及可视化资源,帮助理解算法运行过程与参数调优策略,是学习遗传算法原理与工程实现的典型实战案例。

遗传算法实战:从Raстрigin函数优化到C++框架设计

想象一下,你正在训练一个AI模型,但无论怎么调参,性能总是卡在某个瓶颈上;或者你在设计芯片布局时,面对成千上万个元件的排列组合,穷举法根本行不通。这时候,有没有一种“智能”的搜索方式,能像自然界的生物进化那样,自动探索最优解?这正是遗传算法(Genetic Algorithm, GA)的魅力所在。

它不依赖梯度、不怕非线性、还能跳出局部陷阱——听起来是不是有点玄乎?别急,今天我们就用最经典的测试函数之一: Rastrigin函数 ,带你亲手实现一套完整的遗传算法系统,并通过C++构建高性能计算框架。整个过程不仅有理论剖析,还有代码实战和动态可视化,保证让你看得懂、写得出、跑得通!🚀


为什么是Rastrigin函数?因为它专治“收敛困难症”!

我们先来认识这位“难缠”的对手——Rastrigin函数。它的数学表达式长这样:

$$
f(\mathbf{x}) = A n + \sum_{i=1}^{n} \left[ x_i^2 - A \cos(2\pi x_i) \right]
$$

其中 $A=10$ 是振幅参数,$n$ 是维度,每个变量 $x_i \in [-5.12, 5.12]$。

乍一看好像挺简单?但别被外表骗了。这个函数表面像个平滑的碗,实际上布满了密密麻麻的小坑洼——就像一张铺满鸡蛋托的桌面。全局最小值只有一个,在原点 $(0,0,\dots,0)$ 处取得 $f=0$,可周围却藏着 $10^n$ 个局部极小值!

比如当 $n=10$ 时,局部极小值数量高达 100亿个 !🤯 这意味着传统优化方法很容易一头扎进某个“假谷底”,再也爬不出来。而遗传算法恰恰擅长在这种复杂地形中“广撒网、精捕鱼”。

下面是它的等高线示意图(二维情况),你可以直观感受到那种“步步为营皆陷阱”的压迫感:

graph TD
    A[Rastrigin Function Contour Plot (n=2)] --> B[Global Minimum at (0,0)]
    A --> C[Periodic Local Minima on Grid]
    A --> D[Radial Symmetry Around Origin]
    B --> E[f(x)=0]
    C --> F[Peaks Every 1 Unit Along Axes]
    D --> G[Rotationally Invariant Structure]

所以,用 Rastrigin 来测试遗传算法,简直就是一场压力测试。只要能在这里稳定收敛,换到其他问题上基本就是降维打击。


编码决定命运:实数编码为何成为连续优化首选?

在GA的世界里,每一个候选解都被抽象成一个“个体”,而个体的表现形式就是 染色体编码 。常见的有二进制编码、整数编码、实数编码等。对于像 Rastrigin 这类连续优化问题,我们的选择非常明确: 实数编码

为什么?来看一组对比:

编码方式 数据类型 适用场景 精度 计算开销
二进制编码 bit array 组合优化、布尔问题 低(需量化) 高(编/解码耗时)
整数编码 int[] 排列调度 中等 中等
实数编码 double[] 连续函数优化 高(原生精度)

看到没?如果你要用8位二进制表示一个浮点数,那在10维空间就得处理80位字符串,还得反复做编码转换……想想都头大。而实数编码直接用 std::vector<double> 表示个体,干净利落。

更关键的是,很多高级遗传操作如 算术交叉 高斯变异 ,都是基于实数域设计的。它们能让子代“继承”父代的优点,又能适度扰动探索新区域,非常适合处理光滑或近似可微的目标函数。

我们可以这样定义一个个体结构:

struct Individual {
    std::vector<double> chromosome;  // 染色体,存储n个实数变量
    double fitness;                  // 对应适应度值
};

简洁明了,数据与属性聚合在一起,后续扩展也方便(比如加个年龄标签、约束违反程度啥的)。当然,如果维度固定且追求极致性能,也可以用 std::array<double, DIM> 替代 vector,享受栈分配带来的速度优势。


种群初始化:别让起点就注定了结局

很多人写GA程序时,随手来一句 rand() % range 初始化种群,结果发现每次都卡在同一个地方——其实问题出在初始多样性不足。

理想的初始种群应该满足两个条件:
1. 覆盖广 :尽量均匀分布在可行域内,避免一开始就被困在某个角落;
2. 合法化 :所有个体都在边界范围内,防止无效计算。

最常用的方法是 均匀随机采样 。假设我们要生成一个大小为 $N_p$、维度为 $n$ 的种群,每个变量在 $[-5.12, 5.12]$ 内取值:

#include <random>
#include <vector>

std::vector<std::vector<double>> initialize_population_uniform(
    int pop_size, 
    int dim, 
    double lb = -5.12, 
    double ub = 5.12
) {
    std::vector<std::vector<double>> population(pop_size, std::vector<double>(dim));
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(lb, ub);

    for (int i = 0; i < pop_size; ++i) {
        for (int j = 0; j < dim; ++j) {
            population[i][j] = dis(gen);
        }
    }
    return population;
}

这里用了 <random> 库里的 std::mt19937 (梅森旋转算法),周期长达 $2^{19937}-1$,统计特性远胜老式的 rand() ,适合大规模模拟。

不过要注意,纯随机初始化虽然简单高效,但在高维空间中容易出现“稀疏分布”——某些区域密集,某些区域空旷。这时候可以考虑更高级的策略,比如 拉丁超立方采样 (LHS),它能保证每一维都被均匀覆盖,提升初始质量。

另外,万一有些个体越界了怎么办?别慌,加上一段裁剪逻辑就行:

void clamp_to_bounds(std::vector<double>& individual, 
                     const std::vector<double>& lower_bounds,
                     const std::vector<double>& upper_bounds) {
    for (size_t i = 0; i < individual.size(); ++i) {
        if (individual[i] < lower_bounds[i]) 
            individual[i] = lower_bounds[i];
        else if (individual[i] > upper_bounds[i])
            individual[i] = upper_bounds[i];
    }
}

虽然牺牲了一点多样性,但确保了后续评估的有效性,稳得很 ✅


适应度评价:如何把“差”变成“好”的指标?

在自然界,“适者生存”;在GA里,“高适应度者优先繁殖”。但问题来了:Rastrigin 函数是一个 最小化目标 ,值越小越好,而适应度应该是越大越好。

所以我们需要一次“反转映射”。最简单的做法是取倒数:

$$
F(\mathbf{x}) = \frac{1}{1 + f(\mathbf{x})}
$$

为什么加个1?防止 $f=0$ 时除零错误。这个公式的好处是:
- 当 $f=0$ 时,$F=1$
- 当 $f \to \infty$ 时,$F \to 0$
- 所有适应度落在 $(0,1]$ 区间,便于归一化

但这还不够。早期种群中可能有个体特别优秀,导致其适应度远高于其他个体,从而形成“超级个体垄断”,引发早熟收敛。怎么办?

引入归一化技术!常见手段包括:

  • Min-Max 归一化 :线性缩放到 $[0,1]$
  • Z-Score 标准化 :适用于正态分布
  • Softmax 加权 :用温度系数 $\beta$ 控制选择强度

推荐组合拳:先用 $1/(1+f)$ 转换,再 Min-Max 归一化,最后 Softmax 输出选择概率。流程如下:

flowchart LR
    A[Raw Objective Value f(x)] --> B{Is f(x) Minimized?}
    B -->|Yes| C[Fitness = 1 / (1 + f(x))]
    B -->|No| D[Fitness = f(x)]
    C --> E[Min-Max Normalize F]
    E --> F[Apply Softmax with β]
    F --> G[Selection Probabilities]

下面是完整的预处理函数(附带负值处理):

vector<double> preprocess_fitness(const vector<double>& obj_values) {
    int N = obj_values.size();
    vector<double> fitness(N);

    // Step 1: Convert minimization objective to fitness
    for (int i = 0; i < N; ++i) {
        fitness[i] = 1.0 / (1.0 + obj_values[i]);  // Minimization case
    }

    // Step 2: Shift if any negative (unlikely here, but generalizable)
    double min_fit = *min_element(fitness.begin(), fitness.end());
    if (min_fit < 0) {
        double shift = -min_fit + 1e-6;
        for (double& f : fitness) f += shift;
    }

    // Step 3: Min-Max normalization
    double max_fit = *max_element(fitness.begin(), fitness.end());
    double range = max_fit - min_fit;
    if (range > 1e-8) {
        for (double& f : fitness) {
            f = (f - min_fit) / range;
        }
    }

    return fitness;
}

这套流程兼顾了通用性和稳定性,哪怕以后换成其他目标函数也能无缝衔接。


三大遗传操作:选择、交叉、变异,谁才是真正的主角?

如果说种群是军队,那么三大遗传操作就是指挥官下达的三道军令:优胜劣汰、重组创新、随机试探。

🎯 选择:谁才有资格当爹妈?

选择操作的本质是从当前种群中挑出优质个体作为“父母”,用于繁衍下一代。主流方法有两种:

轮盘赌选择(Roulette Wheel Selection)

每个个体被选中的概率与其适应度成正比,就像一个按比例划分的转盘。实现起来也不难:
1. 计算总适应度
2. 归一化得到选择概率
3. 构建累积概率数组
4. 随机抽样查找对应索引

核心代码如下:

std::vector<int> roulette_wheel_select(const std::vector<double>& fitness, int num_parents) {
    int pop_size = fitness.size();
    std::vector<double> probabilities(pop_size);
    double total_fitness = std::accumulate(fitness.begin(), fitness.end(), 0.0);

    for (int i = 0; i < pop_size; ++i) {
        probabilities[i] = fitness[i] / total_fitness;
    }

    std::vector<double> cum_prob(pop_size);
    cum_prob[0] = probabilities[0];
    for (int i = 1; i < pop_size; ++i) {
        cum_prob[i] = cum_prob[i - 1] + probabilities[i];
    }

    std::vector<int> selected_parents;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> dis(0.0, 1.0);

    for (int p = 0; p < num_parents; ++p) {
        double r = dis(gen);
        auto it = std::upper_bound(cum_prob.begin(), cum_prob.end(), r);
        int idx = std::distance(cum_prob.begin(), it);
        selected_parents.push_back(idx);
    }

    return selected_parents;
}

注意这里用了 std::upper_bound 实现二分查找,将时间复杂度从 $O(n)$ 降到 $O(\log n)$,对大种群很友好。

锦标赛选择(Tournament Selection)

每次随机抽取 $k$ 个个体比赛,选出最强的那个。优点是不需要全局归一化,抗干扰能力强,尤其适合并行环境。

std::vector<int> tournament_select(const std::vector<double>& fitness, 
                                   int num_parents, int k = 2) {
    int pop_size = fitness.size();
    std::vector<int> selected;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, pop_size - 1);

    for (int p = 0; p < num_parents; ++p) {
        int best_idx = dis(gen);
        double best_fit = fitness[best_idx];

        for (int c = 1; c < k; ++c) {
            int candidate = dis(gen);
            if (fitness[candidate] > best_fit) {
                best_fit = fitness[candidate];
                best_idx = candidate;
            }
        }
        selected.push_back(best_idx);
    }
    return selected;
}

$k$ 的取值很有讲究:太小(如1)等于随机选,太大(接近 $N$)又会导致早熟。一般推荐 $k=2\sim5$,平衡探索与开发。


🔀 交叉:基因重组的艺术

交叉操作模拟生物交配,通过交换父代基因片段生成新个体。对于实数编码,常用策略有:

算术交叉(Arithmetic Crossover)

$$
\begin{aligned}
\mathbf{c}_1 &= \alpha \cdot \mathbf{p}_1 + (1 - \alpha) \cdot \mathbf{p}_2 \
\mathbf{c}_2 &= \alpha \cdot \mathbf{p}_2 + (1 - \alpha) \cdot \mathbf{p}_1
\end{aligned}
$$

$\alpha$ 可以是常数(如0.5),也可以随机生成。代码实现如下:

void arithmetic_crossover(const std::vector<double>& p1,
                          const std::vector<double>& p2,
                          std::vector<double>& c1,
                          std::vector<double>& c2,
                          double alpha = 0.5) {
    c1.resize(p1.size());
    c2.resize(p2.size());
    for (size_t i = 0; i < p1.size(); ++i) {
        c1[i] = alpha * p1[i] + (1 - alpha) * p2[i];
        c2[i] = alpha * p2[i] + (1 - alpha) * p1[i];
    }
}
启发式交叉(Heuristic Crossover)

如果知道哪个父代更好,可以朝着它的方向“推进一步”:

$$
\mathbf{c} = \mathbf{p} {\text{better}} + r \cdot (\mathbf{p} {\text{better}} - \mathbf{p}_{\text{worse}})
$$

这种策略在接近最优解时加速效果明显。

实际使用中,还要控制交叉概率 $p_c$(通常设为0.7~0.9),并不是每对父母都要交叉:

if (dis(gen) < pc) {
    crossover(parent1, parent2, child1, child2);
} else {
    child1 = parent1;
    child2 = parent2;
}

🌀 变异:给进化加点“意外之喜”

如果没有变异,种群很快就会陷入同质化。变异的作用就是引入随机扰动,保持多样性。

两种常见方式:

均匀变异

某个基因位直接替换成区间内的随机值:

x_i' = lb + \text{rand()} \times (ub - lb)
高斯变异

叠加一个小幅度的正态噪声:

x_i' = x_i + N(0, \sigma)

后者更符合自然界突变规律,适合精细调整:

void gaussian_mutate(std::vector<double>& individual, 
                     double pm, double sigma = 0.1,
                     double lb = -5.12, double ub = 5.12) {
    std::normal_distribution<> norm(0.0, sigma);
    std::random_device rd;
    std::mt19937 gen(rd());

    for (auto& x : individual) {
        if (static_cast<double>(rand()) / RAND_MAX < pm) {
            x += norm(gen);
            if (x < lb) x = lb;
            if (x > ub) x = ub;
        }
    }
}

建议采用 自适应变异机制 :初期 $p_m$ 设高些(如0.1),鼓励探索;后期逐步降低(如0.01),专注开发。


迭代控制:什么时候该收手?

无限循环可不是个好主意。我们必须设定合理的终止条件,既要防止浪费资源,又要避免提前退出。

常用的判据有:

  1. 最大代数 :最简单的控制方式
  2. 精度阈值 :最优解连续多代无显著改进
  3. 适应度方差过低 :种群趋于同质化,可能已早熟

我们可以设计一个综合判断器:

bool check_termination(const std::vector<double>& best_fitness_history,
                       int current_gen, int max_gen,
                       double tolerance = 1e-6, int stable_gens = 5) {
    if (current_gen >= max_gen) return true;

    int history_size = best_fitness_history.size();
    if (history_size < stable_gens + 1) return false;

    double last_best = best_fitness_history[history_size - stable_gens - 1];
    bool no_improvement = true;
    for (int i = 1; i <= stable_gens; ++i) {
        double diff = fabs(best_fitness_history[history_size - i] - last_best);
        if (diff > tolerance) {
            no_improvement = false;
            break;
        }
    }

    return no_improvement;
}

还可以加入方差监控,实时预警早熟风险:

double calculate_fitness_variance(const std::vector<double>& fitness_values) {
    int n = fitness_values.size();
    double sum = std::accumulate(fitness_values.begin(), fitness_values.end(), 0.0);
    double mean = sum / n;
    double sq_diff_sum = 0.0;
    for (double f : fitness_values) {
        sq_diff_sum += (f - mean) * (f - mean);
    }
    return sq_diff_sum / n;
}

一旦检测到方差骤降,立刻提升变异率或切换搜索策略,实现“自我救赎”。


参数调优:别靠猜,要靠实验!

GA的效果极度依赖参数设置。盲目试错效率低下,我们应该系统化地进行实验分析。

以 Rastrigin 10D 为例,考察以下参数的影响:

参数 测试水平
种群大小 $N_p$ 20, 50, 100, 200
交叉概率 $p_c$ 0.6, 0.8, 0.9, 1.0
变异概率 $p_m$ 0.01, 0.05, 0.1, 0.2

每组跑30次独立实验,统计平均收敛代数、最终误差、成功率。你会发现:

  • $N_p=20$:容易早熟,成功率仅60%
  • $N_p=50$:平衡较好,多数能在200代内收敛
  • $N_p=100$:几乎必成功,但速度下降
  • $N_p=200$:收益递减,性价比不高

经验法则:$N_p = 5d \sim 10d$,即维度的5~10倍。

至于 $p_c$ 和 $p_m$,推荐范围分别是:
- $p_c$: 0.7 ~ 0.95(中等敏感)
- $p_m$: 0.01 ~ 0.05(高度敏感)

特别是 $p_m$,稍大一点就能跳出陷阱,稍小一点就可能停滞。因此强烈建议使用 动态调节机制

$$
p_m^{(g)} = p_{m,\max} - (p_{m,\max} - p_{m,\min}) \cdot \frac{g}{G_{\max}}
$$

前期大胆探索,后期谨慎开发,完美!


C++框架集成:打造你的专属优化引擎

为了提升复用性和可维护性,不妨封装一个 GeneticAlgorithm 类:

class GeneticAlgorithm {
public:
    GeneticAlgorithm(int dim, int popSize, double lb, double ub, 
                    double pc = 0.8, double pm = 0.1, int maxGen = 1000);

    std::vector<double> run();

private:
    void initializePopulation();
    void evaluateFitness();
    std::vector<std::vector<double>> selectParents();
    void applyCrossover(std::vector<std::vector<double>>& offspring);
    void applyMutation(std::vector<std::vector<double>>& offspring);
    bool checkTermination(int gen);

    int dimension, populationSize;
    double lowerBound, upperBound;
    double crossoverProb, mutationProb;
    int maxGenerations;

    std::vector<std::vector<double>> population;
    std::vector<double> fitnessValues;
    std::mt19937 rng;
};

主循环清晰明了:

std::vector<double> GeneticAlgorithm::run() {
    initializePopulation();
    for (int gen = 0; gen < maxGenerations; ++gen) {
        evaluateFitness();
        auto parents = selectParents();
        std::vector<std::vector<double>> offspring;
        applyCrossover(offspring);
        applyMutation(offspring);
        population = std::move(offspring);

        if (checkTermination(gen)) break;
    }
    return get_best_individual();
}

配合 Makefile 自动化构建:

CXX = g++
CXXFLAGS = -O3 -std=c++17 -Wall
TARGET = ga_solver.exe
OBJS = main.o GeneticAlgorithm.o

$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) -o $@ $^

%.o: %.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

clean:
    rm -f *.o $(TARGET)

项目结构建议:
- GeneticAlgorithm.h/cpp
- main.cpp
- fitness.h (放 Rastrigin 等目标函数)

模块化设计让你轻松更换目标函数或遗传策略,真正实现“一次搭建,处处可用”。


可视化你的进化之旅:从CSV到GIF动画

光看数字不够直观?那就把优化过程画出来吧!

在每代评估后记录最优个体坐标:

void GeneticAlgorithm::evaluateFitness() {
    static int generation = 0;
    std::ofstream logFile("ga_log.csv", std::ios::app);

    // ... 计算适应度 ...

    logFile << generation++ << ",";
    for (double x : bestIndividual) logFile << x << ",";
    logFile << bestFit << "\n";
    logFile.close();
}

然后用 Python + Matplotlib 生成动图:

import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.animation import FuncAnimation

data = pd.read_csv("ga_log.csv", header=None)
gens = data[0]
x1 = data[1]
x2 = data[2]

fig, ax = plt.subplots()
line, = ax.plot([], [], 'b-o', markersize=4)
point, = ax.plot([], [], 'ro', markersize=6)

def animate(i):
    line.set_data(x1[:i+1], x2[:i+1])
    point.set_data(x1[i], x2[i])
    return line, point

ani = FuncAnimation(fig, animate, frames=len(gens), blit=True, interval=200)
ani.save('ga_evolution.gif', writer='pillow')

最终你会看到一条蜿蜒的路径,不断跳跃、试探,最终精准命中原点🎯——那一刻,你会真切感受到“进化”的力量。


总结:GA不是银弹,但它是你工具箱里最锋利的刀

遗传算法不能解决所有优化问题,但它在以下场景表现卓越:
- 目标函数不可导、非线性、多峰值
- 解空间离散或混合型
- 黑箱优化(只知道输入输出)
- 初期缺乏良好初值

通过本次实战,你已经掌握了:
✅ Rastrigin 函数建模
✅ 实数编码与种群初始化
✅ 适应度评价与归一化
✅ 三大遗传操作的C++实现
✅ 迭代控制与防早熟策略
✅ 完整框架搭建与可视化

下一步,你可以尝试:
🔧 将算法迁移到神经网络超参优化
🔧 引入多目标机制(NSGA-II)
🔧 结合局部搜索(如梯度下降)做成混合算法

记住:最好的学习方式就是动手改代码、跑实验、看结果。现在,就去编译你的第一个GA程序吧!💻✨

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:遗传算法是一种模拟自然选择与遗传机制的全局优化方法,广泛应用于复杂问题的近似最优解搜索。本文介绍了一个基于C++实现的遗传算法项目,旨在求解具有多个局部极小值的Rastrigin函数全局最小值问题。通过初始化种群、适应度评估、选择、交叉、变异等核心步骤,算法迭代优化直至收敛。项目包含可执行文件、源码及可视化资源,帮助理解算法运行过程与参数调优策略,是学习遗传算法原理与工程实现的典型实战案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值