【算法】遗传算法及其 Java 实现
遗传算法及其 Java 实现
一、遗传算法介绍
人类的 DNA 是由 ATGC 这四种碱基对的排列组合形成的。随着环境的变化和人类的变异,最适合环境的基因被遗传了下来,形成了现在的人类种群
遗传算法也是借助这个思想,不过,进化的对象成了程序
对于人类来说,组成 DNA 的单位是 ATGC 碱基对,而对于程序来说,组成其的最小单位就是01二进制序列了
遗传算法就是通过对 01 二进制序列的不断变异和筛选,选出最适合当前条件的子代,从而动态的获取我们想要的结果
与动态规划、回溯这些确定算法不同,遗传算法存在一定的随机性,所以这也是一种动态的算法。其好处在于相较于确定算法在基数较大的情况下时间复杂度会成倍增长,遗传算法的时间复杂度在海量数据面前是相对较小的
二、遗传算法流程
这里我引用一个最简单的遗传算法的例子
例: 求下列二元二次函数在指定数字范围内的最大值
1、个体编码
遗传算法操作的对象是 01 二进制串,所以不管我们输入的对象是什么,都需要将其序列化为 01 二进制串
这里我们的输入是 [1,7]
范围内的整数,所以我们每个数都可以轻松的用三位二进制表示。为了将 x1 x2 用同一个基因序列表示,我们将两个三位二进制拼接,使用六位二进制表示我们的基因序列
例:001 101 -> x1:1 x2:5
数据结构我们选用 boolen 数组,成员变量还包括适应度 score
这里我们为了讨巧,将数字范围限定在 [1,7]
内,所以二元变量的输入可以用六位二进制表示
对于其他问题,范围还是要根据具体情况来讨论
public class Chromosome implements Codec {private boolean[] gene;private int geneSize;private double score;
}
2、初始群体产生
种群繁衍的前提是有初始种群的产生,初始种群的推荐数目是在 20-100 个,这里我们使用配置文件的形式,设置初始种群以及基因序列的长短
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uzjov04x-1646374687134)(https://gitee.com/faro/images/raw/master/img/20220303131532.png)]
public class GeneticAlgorithm {// 种群private List pop;Properties pro = PropertyUtil.getProperty("ga_simple");// 种群大小private int POP_SIZE = Integer.valueOf(pro.getProperty("POP_SIZE"));// 基因长短private int CHROMOSOME_SIZE = Integer.valueOf(pro.getProperty("CHROMOSOME_SIZE"));public GeneticAlgorithm() {pop = new ArrayList<Chromosome>();for (int i = 0; i < POP_SIZE; i++) {pop.add(new Chromosome(CHROMOSOME_SIZE));}}
}
初始种群的个体组成是没有限制的,我们选择随机生成的方式
public class Chromosome implements Codec {// ...public Chromosome(int size) {if (size<0) size=0;geneSize=size;gene = new boolean[geneSize];init();}/*** 初始化基因序列*/private void init() {for (int i = 0; i < geneSize; i++) {if (Math.random()<0.5d) gene[i]=true;}}
}
3、适应度计算
适应度计算对于不同问题是有差异的
对于我们这个问题,适应度就是方程计算的结果,而对于路径规划问题,适应度可能就是路径和或者形式时间
- calculateScore() // 计算总群总适应度- calculateChromosomeScore() // 计算个体适应度- changeX() // 反序列化 x1x2- decoder() // 解码基因序列,这个示例中即二进制转十进制- changeY() // 通过 x1x2 计算函数值,即适应度在这个问题上的体现
- calculateScore()
/*** 计算种群适应度*/
private void calculateScore() {if (pop==null || pop.size()==0) {pop = new ArrayList<Chromosome>();init();}// 因为 totalScore 记录的是当前群体适应度总和// 所以每次计算适应度之前,都必须清空前一代群体的适应度总和totalScore = 0;for (Chromosome chromosome : pop) {chromosome.setScore(calculateChromosomeScore(chromosome));totalScore+= chromosome.getScore();bestScore = Math.max(bestScore, chromosome.getScore());worstScore = Math.min(worstScore,chromosome.getScore());}averageScore = totalScore/POP_SIZE;// 因为精度问题,要注意 averageScore 会不会大于 bestScoreif (averageScore>bestScore) averageScore = bestScore;
}
- calculateChromosomeScore()
这里比较麻烦的可能就是这部分的位运算了,可能有读者一开始看会看不太懂
这里changeX()
算出的是 x1x2 由01二进制序列转换而来的数字
比如说 x1=5 ,x2=4
,那么 x1x2 的转码表示为 101 100
,changeX()
计算的结果就是 44
而我们希望从这个 44 中,剥离出 x1 和 x2 的值
x&57
,即 101 100 & 111 000 -> 101 000
,右移三位即为 000 101
,即 x1 的值 5
同理 x&7 -> 101 100 & 000 111 -> 100
即 x2 的值 4
private int calculateChromosomeScore(Chromosome chromosome) {int x = changeX(chromosome);// 借助位运算,获取 x1,x2 的值return changeY((x&56)>>3,x&7);
}
- changeX()
@Override
int changeX(Chromosome chromosome) {if (chromosome!=null) return Integer.valueOf(chromosome.decoder());return 0;
}
- decoder()
/*** 解码器* 这里是将基因序列解码为数字* @return*/
@Override
public String decoder() {if (gene==null || gene.length==0) return "0";int sum = 0;for (int i = 0; i < gene.length; i++) {sum = sum<<1;if (gene[i]) sum+=1;}return String.valueOf(sum);
}
- changeY()
@Override
int changeY(int x1,int x2) {return x1*x1 + x2*x2;
}
4、选择适应度较高的父代
这里我们使用轮盘赌法,获取种群中相对较优的父代,此外,还要保证每次获得的父代个体,其适应度都要大于平均适应度
如果有读者对轮盘赌法还不了解,可以翻阅这篇文章:轮盘赌法原理及其 java 实现
/*** 使用轮盘赌法,获取相对较好的父类个体* 这里在选择个体的时候要注意:* 1、个体被选择的概率要遵循轮盘赌法* 2、个体的适应度要大于平均适应度* 轮盘赌法相关文章:https://blog.csdn.net/weixin_44062380/article/details/123255853* @return*/
private Chromosome getParentChromosome() {if (pop==null || pop.size()==0) return null;int iterCount = 0;while (true) {double slice = totalScore * Math.random();double sum = 0d;for (Chromosome chromosome : pop) {sum+=chromosome.getScore();// 一定要保证个体适应度大于平均适应度if (sum>slice && chromosome.getScore()>averageScore) {return chromosome;}}iterCount++;// 防止迭代次数过高// 迭代次数超过阈值,返回种群最优个体if (iterCount>400) {return pop.stream().max(Comparator.comparing(Chromosome::getScore)).get();}}
}
这里有用到 while(true)
,这是十分危险的
如果出现个体全为最优解,avgScore == bsetScore 这种特别极端情况,程序就会阻塞
所以我这里使用记录迭代次数的方式,防止阻塞发生
虽说这种情况的可能性十分十分的低(要么是初始状态即全为最优解,或者某次变异之后种群恰好达到最优解,在种群大小为40的情况下,这种情况发生的概率为 2^240 的倒数,即接近0.000…(24个0)…0001 的概率,这比你连续三次中一亿大奖的概率还低)。甚至我们的代码都不是标准的工程代码,但是,一个合格的程序员就是要做到防患于未然
5、生成新子代(交叉运算)
在我们上一步获取了两个相对较好父代,这里,我们就要使用交叉运算的方法让两个父代结合
举个例子,女性的基因是 AATT ,男性的基因是 GGCC ,AA是短腿基因,TT 是双眼皮基因,GG 是肥胖基因,CC 是高鼻梁基因。也就是说 ,女方是短腿但是双眼皮,男方是肥胖但是高鼻梁。二者结合后,其后代基因可能是 TTCC,即产生了双眼皮高鼻梁的良好后代
同样的,遗传算法也是这个原理,不过由于无法判断二进制串那部分是显性基因那部分是隐性基因,所以遗传出来的后代还是存在一定的随机性
- evolve() // 种群进化- getParentChromosome() // 轮盘赌法筛选父代最优- genetic() // 父代交叉运算形成子代
- evolve()
/*** 生成新种群*/
private void evolve() {List<Chromosome> newPop = new ArrayList<>();while (newPop.size()<POP_SIZE) {Chromosome p1 = getParentChromosome();Chromosome p2 = getParentChromosome();if (p1==null || p2==null) continue;List<Chromosome> children = Chromosome.genetic(p1, p2);for (Chromosome child : children) {newPop.add(child);}}pop.clear();pop=newPop;popCount++;
}
- genetic()
/*** 两个父代进行交叉运算,获取新的两个子代* @param parent1* @param parent2* @return*/
public static List<Chromosome> genetic(final Chromosome parent1,final Chromosome parent2) {List<Chromosome> children = new ArrayList<>();if (parent1==null || parent2==null ||parent1.gene.length!=parent2.gene.length) {return children;}int geneSize = parent1.geneSize;Random random = new Random();// 获取随机父代位交换位置int l = random.nextInt(geneSize);int r = random.nextInt(geneSize);if (l>r) {int tmp = l;l=r;r=tmp;}// 这里最好使用深拷贝Chromosome child1 = clone(parent1);Chromosome child2 = clone(parent2);boolean[] gene1 = child1.getGene();boolean[] gene2 = child2.getGene();for (int i = l; i <=r ; i++) {gene1[i]=!gene1[i];gene2[i]=!gene2[i];}children.add(child1);children.add(child2);return children;
}
6、子代变异
有的时候,单凭种群的持续迭代,是要很久才能出结果的
但是如果通过变异,就可能一下子产生比较良好的个体
现实的例子就有空间站实验中会尝试让植物受宇宙射线的辐射,从而产生 DNA 变异一快速获取想要的目标个体
当然,子代变异必须有个限度,如果每个子代变异幅度过大,步骤4中的父代筛选的作用就会被冲淡,同时,过度的变异也肯能导致出现意想不到的结果
所以说,对于子代变异,我们要控制在一个合适的范围内,这个范围包括 1、子代产生变异的概率 2、子代基因变异的范围
// 个体变异概率
private double MUTATION_RATE=Double.valueOf(pro.getProperty("MUTATION_RATE"));
// 最大变异长度
private int MAX_MUTATION_NUM=Integer.valueOf(pro.getProperty("MAX_MUTATION_NUM"));
这里我根据别人长期实验的结果给出这个程序的合适变异概率和变异长度
**变异概率:**0.001
**最大变异长度:**染色体基因长度的一半,这个示例中即 3
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0ESBUvZ-1646374687135)(https://gitee.com/faro/images/raw/master/img/20220304132336.png)]
- GeneticAlgorithm/mutation()
/*** 种群染色体发生变异*/
private void mutation() {for (Chromosome chromosome : pop) {if (Math.random()<MUTATION_RATE) {System.out.println("在" + popCount +"代发生了变异!");chromosome.mutation((int) (MAX_MUTATION_NUM*Math.random()));}}
}
- Chromosome/mutation()
public void mutation(int mutationNum) {for (int i = 0; i < mutationNum; i++) {int at =(int) Math.random() * geneSize;gene[at] = !gene[at];}
}
7、迭代
仅仅一次的进化,很难得到最优的结果,所以上述的过程要不断重复,直到指定迭代次数为止
public void conductGA() {init();for (int i = 0; i < ITER_NUM ; i++) {// 计算种群适应度calculateScore();print();// 种群遗传// - 筛选较优父代(轮盘赌算法)// - 交叉运算evolve();// 种群变异mutation();}
}
三、代码测试
一开始测试的时候,因为基因序列不长,数字范围也很短,所以经常是初始情况下就会产生有最佳适应度的个体
这里我们 mock 一个有烂数据的初始化示例
public void conductGA() {//init();mockBadPop(); // 初始化烂种群for (int i = 0; i < ITER_NUM ; i++) {// 计算种群适应度calculateScore();print();// 种群遗传// - 筛选较优父代(轮盘赌算法)// - 交叉运算evolve();// 种群变异mutation();}
}//***/*** 生成较坏的父代*/
private void mockBadPop() {System.out.println("0、初始化坏种群");popCount++;for (int i = 0; i < POP_SIZE; i++) {Chromosome chromosome = new Chromosome(CHROMOSOME_SIZE);boolean[] gene = chromosome.getGene();gene[0]=false;gene[1]=false;pop.add(chromosome);}
}
迭代次数设置为 50,种群大小设置为 40
- 测试结果如下
可以看到,因为数据范围较小,最佳适应度出现了急速收敛,不过我们的总适应度整体上是呈现上升趋势的
所以代码的问题应该不是很大
四、完整代码
完整代码我传到 github 上了,有需要的同学可以自取
https://github.com/wcgzgj/ga-PathPlanning/tree/main/src/main/java/ga_simple_planning
五、小结
我本职是搞开发的,本来对算法也没多少兴趣,奈何毕设一定要和算法搭边,这才花了些天研究了一下
不过遗传算法的应用不可能就这么简单的,在后面两天里,我还对带来遗传算法在应急路径规划上的应用,敬请期待
除此之外,我没有写 UI,数据展示可能就没有那么直观了,感兴趣的同学,可以写个 swing 窗口,甚至可以把这段代码插入到 web 中,再用 h5 绘制图表,这里就看各位的自由发挥了。
【算法】遗传算法及其 Java 实现相关推荐
- java和c 的rsa加密算法_RSA算法签名技术Java与C++统一(加密解密结果一样)
RSA算法签名技术Java与C++统一 (加密解密结果一样) 源代码下载地址:http://www.doczj.com/doc/64f44a94a0116c175f0e484d.html/produc ...
- 基数排序算法(基于Java实现)
title: 基数排序算法(基于Java实现) tags: 基数算法 基数排序算法原理及代码实现: 一.基数排序算法的原理 基数排序属于"分配式排序",又称"桶子法&qu ...
- 选择排序算法(基于Java实现)
title: 选择排序算法(基于Java实现) tags: 选择算法 选择排序算法原理及代码实现: 一.选择排序算法的原理 选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间.但是选择 ...
- 插入排序算法(基于Java实现)
title: 插入排序算法(基于Java实现) tags: 插入算法 插入排序算法原理及代码实现: 一.插入排序算法的原理 首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间.初始已排序区间 ...
- 桶排序算法(基于Java实现)
title: 桶排序算法(基于Java实现) tags: 桶排序算法 桶排序算法的原理和代码实现 一.桶排序算法的原理 桶排序,顾名思义,会用到"桶",核心思想是将要排序的数据分到 ...
- 快速排序算法(基于Java实现)
title: 快速排序算法(基于Java实现) tags: 快速排序算法 快速排序算法的原理与代码实现: 一.快速排序算法的原理 快排算法的思想是: 如果需要排序数组中下标从p到r之间的一组数据,我们 ...
- java回文数算法for_【Java】【每日算法/刷穿 LeetCode】9. 回文数(简单)
首页 专栏 java 文章详情 0 [每日算法/刷穿 LeetCode]9. 回文数(简单) 宫水三叶发布于 今天 15:30 题目描述 判断一个整数是否是回文数.回文数是指正序(从左向右)和倒序(从 ...
- Java Jvm 中的垃圾回收机制中的思想与算法 《对Java的分析总结》-四
Java中的垃圾回收机制中的思想与算法 <对Java的分析总结>-四 垃圾回收机制 中的思想与算法 引用计算法 给对象中添加一个引用计数器,每当一个地方引用它的时候就将计数器加1,当引用失 ...
- 插入排序算法 java_排序算法实现-插入排序(Java版本)
原标题:排序算法实现-插入排序(Java版本) 插入排序(英语:Insertion Sort)是一种简单直观的排序算法.它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到 ...
最新文章
- IOS开发笔记17-Object-C中的继承
- php中需要注意的问题
- freeRtos学习笔(1)内核剪裁
- typescript赋值
- 信息学奥赛一本通(1114:白细胞计数)
- 目录浏览器对话框控件 c# 1614822374
- 【React深入】深入分析虚拟DOM的渲染原理和特性
- pep3评估报告解读_首次公布!PISA全球胜任能力评估报告出炉,有何新启示?
- 招商银行王龙:金融科技银行数据架构设计的13条守则(含PPT)
- 分类算法python程序_分类算法——k最近邻算法(Python实现)(文末附工程源代码)...
- 通用快速检测邮件故障思路方法(二)
- scanf和getch函数的区别
- android模拟器 adb 命令,安卓模拟器下 使用ADB命令
- vue 移动端剪裁
- python spacy 安装超时_安装spacy失败
- Java 生成Word文档 — 简单示例
- 原创 | 王欣:多维深耕打造数字化银行
- 递归算法的原理(js)
- Laravel 模型中 $hidden 的作用
- java hgetall_详解Java使用Pipeline对Redis批量读写(hmsethgetall)
热门文章
- java(某人在玩游戏的时候输入密码112233后成功进入游戏(输错3次则被强行退出),要求用程序实现密码验证的过程。)
- 今天除夕夜了 祝大家新年快乐 恭喜发财
- cnn识别不定长英文验证码
- 赵小楼《天道》《遥远的救世主》深度解析(63)什么是强势文化?什么是弱势文化?怎么无所用,无所不用?
- 云呐:固定资产核算系统包括哪些功能
- 一文简要概述Seata AT与TCC的区别
- 小程序直播房间列表读取php,微信小程序直播管理 - 实时同步微信管理后台的直播数据,本地储存与管理直播信息 – 基于ThinkPHP和Bootstrap的极速后台开发框架...
- ERROR in : Cannot determine the module for class xxxx Add to the NgModule to fix it.
- 招商银行香港一卡通的猫腻
- oracle 日常管理小节