基于遗传算法的最优化仿真(Java)
本文旨在对遗传算法进行基础阐述,并采用Java语言开发相应的算法模型以解决类似于寻找最优路径的问题。
问题描述

如图所示,在X轴上分布着五个关键点(分别标记为x₁到x₅)。这些关键点的实际间距设定为L值。然而,在实际应用过程中,受测量误差的影响(如图中的黑色圆圈标注),每个关键点都会产生一系列测不准的采样点。为了在这些测不准的采样点中选择合适的数值以使相邻位置之间的距离尽可能接近L值,则可表述如下所给数学公式:
解决思路
我通过查阅资料了解到,在我的研究中遇到的问题与旅行商问题(TSP)具有高度相似性。在经典的TSP问题中通常涉及固定数量且已知位置的点集,并要求找到一条遍历所有这些点(每个点仅访问一次)且最终返回起点的回路路径中最优的一条(即总路径长度最短)。具体细节可参考维基百科旅行商问题一文。
遗传算法
遗传算法,顾名思义,可以联想到自然界种群繁衍、基因遗传的过程,实际上它也是借鉴进化生物学中的一些现象发展起来的(交叉,变异,选择以及遗传等等)维基百科遗传算法,是一种通过模拟生物进化过程搜索最优解的启发式搜索算法。
遗传算法的本质就是模仿自然界优胜劣汰、适者生存的过程。它往往从实际问题的解集出发通过一定的编码方式形成问题域中“基因”、“染色体”和“个体”的概念,进而确定初始种群(由一定数量的个体组成),然后根据问题域中的适应度函数(Fitness Function),通过一代代的选择(Selection)、交叉(Crossover)和变异(Mutation)等方式模拟这个种群的进化过程,最后逐渐进化出较好的个体(也就是解集中近似的最优解)。 将遗传算法应用到实际问题的流程大致如下:
1. 对实际问题的解集进行编码,使其可以对应生物遗传过程中“基因”、“染色体”和“个体”的概念。比如本题中,解集就是每个位置上的随机选一个测量点连起来的路径,这样我可以对测量点进行编号,使得每个测量点就代表了一个“基因”,然后一条路劲就代表了一条“染色体”,进而形成一个个体(每个个体只有一条“染色体”)。具体如下图所示:

2. 在问题域中设定适应度函数(Fitness Function)。通常实际问题都会预先设定好适应度函数,并且为了加快收敛速度,在本题中所采用的就是前面所述的数学公式:
3. 通过随机的方式生成初始种群(population)。如果为了提高收敛效率,则可以在生成初期加入一些表现较为优秀的个体作为初始群体。
4. 根据适应度函数分别执行选择操作(Selection)、交叉操作(Crossover)以及变异操作(Mutation),经过一定数量代数的遗传运算后即可得到近似的最优解。
以上是我的对遗传算法的基本理解。接下来将详细介绍每个步骤的具体实现方法(包括编码方式、个体的选择标准等),并附上相应的代码实现。(如需深入了解遗传算法的知识,请参考这里 知乎如何通俗易懂的理解遗传算法)
解题过程
1. 编码方式
为问题域中的解集建立编码机制以生成相应的基因型、染色体等描述。常见的两种编码策略是:一种采用实数值表示(例如0到n-1之间的数值),另一种则是将解集中的每个元素映射到二元组(0或1)来实现。基于此分析,在本研究中选择将每个位置上的测量点编号为0至n-1之间的整数。这种编号方法具有以下优势:第一,在本研究中选择将每个位置上的测量点编号为0至n-1之间的整数使得后续处理更加便捷;第二,在二维数组中只需新建一个矩阵即可完整地描述所有测量点的位置信息;第三,在初始化阶段可以通过随机算法生成初始测量点坐标值并将其存储在对应数组索引处以提高计算效率
//pointNum是位置数(本题中是5),realPointNum是每个位置上实际的测量点数
class GARandom {
private int pointNum,realPointNum;
public GARandom(int pointNum,int realPointNum) {
this.pointNum = pointNum;
this.realPointNum = realPointNum;
}
public double[][] randomInitPopulation(){
double[][] x = new double[pointNum][realPointNum];
for (int i = 0; i < x.length; i++) {
for (int j = 0; j < x[0].length; j++) {
if(i == j){
x[i][j] = i * 10;//将一组收敛值放入其中,用以后面测试算法的性能
}else{
x[i][j] = i * 10 + (i + 1)*Math.random();
}
}
}
return x;
}
}
2. 确定适应度函数
本题的目标函数旨在以尽可能小的距离间隔安排各节点的位置,并使这些间隔尽可能趋近于给定值L
在题目中存在5个测量点。即确定这5个测量点之间的最小间距为L。因此可以采用类似于TSP算法的方式,在此问题中可预先计算各相邻测定点对的距离并将这些数值存储在一个数组中这样就可以在此数组基础上对群体中的每一个染色体(即路径)进行适应度评估进而判断其优劣程度(这里指路径长度越短则适应度越高)。具体而言需先遍历所有测定点对之间的距离并累加得到总误差值这样的过程将有助于提高整个算法的有效性。另外为了实现这一功能我们可以编写如下的代码来完成这一功能
//求得每两个位置,所有点之间的距离
distance = new double[pointNum - 1][realPointNum][realPointNum];
for (int i = 0; i < pointNum - 1; i++) {
for (int j = 0; j < realPointNum; j++) {
for (int k = 0; k < realPointNum; k++) {
distance[i][j][k] = Math.abs(x[i + 1][k] - x[i][j] - L);
}
}
}
评价个体适应度的代码如下:
//根据先验条件求得个体适应度,并根据适应度求得单个个体的概率以及个体的累积概率
//以便选择阶段使用
private void evaluate(int[][] chromosome) {
int k = 0;
double len = 0;
double sumFitness = 0;
double[] tempFitness = new double[scale];
//根据距离数组求得每条路径的适应度,也就是和固定距离L的偏差的和
while (k < chromosome.length) {
for (int i = 0; i < chromosome[k].length - 1; i++) {
len += distance[i][chromosome[k][i]][chromosome[k][i + 1]];
}
fitness[k] = len;
len = 0;
k++;
}
//求总的适应度
for (int i = 0; i < scale; i++) {
tempFitness[i] = 10.0 / (fitness[i] + 1);//计算适应度,这里距离越小适应度越大,因此采用倒数的方式表示
sumFitness += tempFitness[i];
}
//根据适应度,转化成相应的单个个体概率和个体的累积概率,用于后面的轮盘赌选择策略
double tempP = 0;
for (int i = 0; i < scale; i++) {
ps[i] = tempFitness[i] / sumFitness;//单个个体概率
tempP += ps[i];
pc[i] = tempP;//个体累积概率
}
}
3. 确定初始种群
我采用了一种随机生成的方式进行参数设置。为了确保初始种群能够覆盖所有经过实数编号的测量点(即从0到n-1),因此,在前n个个体中,它们的染色体设计如下所示:

通过这种方式,在初始种群中安排前n个试件的基因序列全部为0、1一直到n-1的状态;以此最大限度地包含所有测定点,在避免随机生成时遗漏测定点的基础上实现均匀分布。其代码如下:
//生成父代种群的“染色体”,也就是随机选取每个位置上的点组成一个网络
//scale是种群规模,pointNum是位置数(x1-x5)
oldPopulation = new int[scale][pointNum];
for (int i = 0; i < scale; i++) {
for (int j = 0; j < pointNum; j++) {
if (i < realPointNum){
oldPopulation[i][j] = i;
}else{
oldPopulation[i][j] = rand.nextInt(realPointNum);
}
}
}
4. 选择(Selection)
设定初始种群后
//精英选择(选择上一代中fitness最好的个体,然后直接保存到下一代中)
private void selectMaxFitness() {
int maxId = 0;
double tempFitness = fitness[0];
//
for (int i = 1; i < scale; i++) {
if (tempFitness > fitness[i]) {
tempFitness = fitness[i];
maxId = i;
}
}
if (bestLength > tempFitness) {
bestLength = tempFitness;
bestGen = t;
System.arraycopy(oldPopulation[maxId], 0, bestChoice, 0, pointNum);
}
copyPopulation(0, maxId);
}
//轮盘赌选择策略
private void select() {
int j, selectId;
double r;
selectMaxFitness();//精英选择,保留上一代fitness最好的个体
for (int i = 1; i < scale; i++) {
r = rand.nextDouble();
for (j = 0; j < scale; j++) {
if (r <= pc[j]) {
break;
}
}
if (j < scale) {
selectId = j;
copyPopulation(i, selectId);
}
}
}
5. 交叉(Crossover)
在完成选择操作后, 就需要对这些个体进行染色体间的交叉作用, 目的是为了产生新的后代个体. 由于多种多样的交配方式可供选择, 在此我们采用了单点交配策略, 即通过随机生成一个介于0到1之间的数值, 当该数值低于预设的交配概率阈值时, 就会执行染色体交配操作. 这一步骤的关键在于随机选取一个基因位置作为分割点, 然后将位于该位置之后的所有基因片段进行交换. 其具体的实现代码如下所示:
//单点交叉
private void crossover() {
for (int k = 1; k < scale/2; k += 2) {
double pCrossoverTemp = rand.nextDouble();
//小于交叉概率时进行“染色体”交叉,将交叉索引(包括交叉索引处的元素)后的元素进行互换
if (pCrossoverTemp <= pCrossover) {
int tempCrossover;
int indexCrossover = 1 + rand.nextInt(pointNum - 1);//排除索引值为0的情况,整体交换没有意义
for (int i = indexCrossover; i < pointNum; i++) {
tempCrossover = newPopulation[k][i];
newPopulation[k][i] = newPopulation[k + 1][i];
newPopulation[k + 1][i] = tempCrossover;
}
}
}
}
显然这是最基本形式的交叉算子,在深入研究其他高阶操作之前,请参考这篇遗传算法相关文章链接遗传算法中几种交叉算子
6. 变异(Mutation)
在变异部分方面,我并未深入研究相关的文献资料,默认采用了基本的单点变异策略.
private void mutation() {
for (int i = 1; i < scale; i++) {
double pMutationTemp = rand.nextDouble();
if (pMutationTemp < pMutation) {
//随机选择变异位置
int mutationIndex = rand.nextInt(pointNum);
//将变异位置的值保存下来
int temp = newPopulation[i][mutationIndex];
//获得变异值
int mutationTemp = rand.nextInt(realPointNum);
//确保变异值和之前的值不一样
while (mutationTemp == temp) {
mutationTemp = rand.nextInt(realPointNum);
}
newPopulation[i][mutationIndex] = mutationTemp;
}
}
}
7. 重复操作
在确定初始种群群体的过程中,在筛选、重组以及突变操作后就形成了第一个子代群体。随后依次执行4至6号步骤进行迭代运算,在适应度函数的制约下其运行机制本质上模仿了生物自然选择与遗传变异的原理,并最终呈现出类似于自然演化的动态特性。值得注意的是,在大多数情况下这些方法只能逼近全局最优解。
代码测试
测试输入
初始群体规模被指定为30的具体数值;具体而言,在每个位置上实际测量点的数量被确定下来总共是1 个单位;在evolution代数方面则设定了一个较大的数值值即2 以保证算法具有充分的时间去完成整个寻优过程;相邻的位置之间的最小间距被精确地确定下来共计是1 单位长度;通过将交叉概率设置在较高的水平上达到了更好的全局搜索能力的同时又不会影响到整体收敛速度的表现;而变异率则被设置得相对较低以确保算法能够稳定地收敛到较优解的位置附近

测试结果

从图表中可以看出经过2000次选择、交叉和变异操作,在固定间距设置为10的情况下。该算法在第355代迭代时成功收敛于最短路径。这些结果与我预先输入到数组中的数据相对应。
从图表中可以看出经过2000次选择、交叉和变异操作,在固定间距设置为10的情况下。该算法在第355代迭代时成功收敛于最短路径。这些结果与我预先输入到数组中的数据相对应。
结语
这是我双十一期间的学习成就可以做记录。完整的代码资源在我的github上如需获取请访问这里
参考资料
- 了解遗传算法的简单明了说明及实际案例解析
- 基于Java语言实现旅行商问题(TSP)的经典案例研究
- 遗传算法原理及其实现方案解析
- 探讨多种交叉算子在遗传算法中的应用价值
- 旅行商问题(TSP)最优路径求解方法全解析
