[关闭]
@evilking 2018-04-30T12:01:24.000000Z 字数 8758 阅读 1937

NLP

遗传算法

遗传算法(Genetic Algorithm)遵循『适者生存』、『优胜劣汰』的原则,是一类借鉴生物界自然选择和自然遗传机制的随机化搜索算法。

遗传算法模拟一个人工种群的进化过程,通过 选择(Selection)交叉(Crossover) 以及 变异(Mutation) 等机制,在每次迭代中都保留一组候选个体,重复此过程,种群经过若干代进化后,理想情况下其适应度达到近似最优的状态。

自从遗传算法被提出以来,其得到了广泛的应用,特别是在函数优化、生产调度、模式识别、神经网络、自适应控制等领域,遗传算法发挥了很大的作用,提高了一些问题求解的效率。

遗传算法大致过程如下图所示:

遗传算法

本算法提出的时间比较早,而且得到了广泛的应用,所以资料相比于其他算法会多很多,所以本篇重点在源码分析上。

https://www.zhihu.com/question/23293449 对于算法说明部分,读者可以参考知乎上的前三个回答,都讲的通俗易懂。

参考 中提供的几篇博客讲的都挺好的,读者可以对比的去理解。


源码分析

这里分析了两个例子的源码,下载地址为:https://github.com/evilKing/GA

此源码是笔者 fork 其他网友的代码,github出了问题,需要读者自己去创建工程,并按 readme.md 上的目录结构组织文档了

数学函数求极值

首先看 com.ga.math.GA 类,此类解决数学函数求极值的问题,其中用到了遗传算法中的实数编码。

求函数 的最小值:

将实数进行二进制编码,首先得确定需要多少位二进制能编码出变量的范围。

其中变量定义如下:

通过上述公式就能把二进制串映射到变量的取值范围上。

有一点需要注意,这里是双变量 ,需要分别将 编码后直接二进制字符串首尾拼接,计算适应度函数值时再拆分开。


先看入口函数:

  1. public static void main(String args[]) {
  2. GA Tryer = new GA();
  3. //产生初始种群
  4. Tryer.ipop = Tryer.initPop();
  5. //迭代次数
  6. for (int i = 0; i < 100000; i++) {
  7. //选择
  8. Tryer.select();
  9. //交叉
  10. Tryer.cross();
  11. //变异
  12. Tryer.mutation();
  13. Tryer.generation = i;
  14. }
  15. //计算最优个体的适应度函数值
  16. double[] x = Tryer.calculatefitnessvalue(Tryer.beststr);
  17. }

上述过程都比较简单,先初始化一个种群,即随机初始化一系列 23 位的二进制串(染色体);然后迭代的选择、交叉、变异;最后计算最优个体的适应度函数值。

根据具体的目标设置适应度函数,比如这里是要求函数 的最小值,那该数学函数就成了个体的适应度函数,该函数值小的就应该被淘汰,或者用函数值较大的个体代替。

  1. private String initChr() {
  2. String res = "";
  3. for (int i = 0; i < GENE; i++) {
  4. if (Math.random() > 0.5) {
  5. res += "0";
  6. } else {
  7. res += "1";
  8. }
  9. }
  10. return res;
  11. }

以一定概率随机初始化一条二进制串。其中这里 01 的设置并不影响最后的结果。

  1. /**
  2. * 轮盘选择
  3. * 计算群体上每个个体的适应度值;
  4. * 按由个体适应度值所决定的某个规则选择将进入下一代的个体;
  5. */
  6. private void select() {
  7. // 所有染色体适应值
  8. double evals[] = new double[ChrNum];
  9. // 各染色体选择概率
  10. double p[] = new double[ChrNum];
  11. // 累计概率
  12. double q[] = new double[ChrNum];
  13. double F = 0; // 累计适应值总和
  14. for (int i = 0; i < ChrNum; i++) {
  15. // 计算适应度函数值
  16. evals[i] = calculatefitnessvalue(ipop[i])[2];
  17. // 记录下种群中的最小值,即最优解
  18. if (evals[i] < bestfitness){
  19. bestfitness = evals[i];
  20. bestgenerations = generation;
  21. beststr = ipop[i];
  22. }
  23. // 所有染色体适应值总和
  24. F = F + evals[i];
  25. }
  26. //轮盘赌
  27. for (int i = 0; i < ChrNum; i++) {
  28. p[i] = evals[i] / F;
  29. if (i == 0)
  30. q[i] = p[i];
  31. else {
  32. //计算 p[i] 的累积概率
  33. q[i] = q[i - 1] + p[i];
  34. }
  35. }
  36. /**
  37. * 对每个染色体使用轮盘赌选择,所以是两重 for 循环
  38. */
  39. for (int i = 0; i < ChrNum; i++) {
  40. double r = Math.random();
  41. if (r <= q[0]) {
  42. ipop[i] = ipop[0];
  43. } else {
  44. // 适应度低的以一定概率被适应度高的给取代
  45. for (int j = 1; j < ChrNum; j++) {
  46. if (r < q[j]) {
  47. ipop[i] = ipop[j];
  48. }
  49. }
  50. }
  51. }
  52. }

上述过程做了两步:

其中计算个体适应度的方法如下:

  1. private double[] calculatefitnessvalue(String str) {
  2. //二进制数前23位为x的二进制字符串,后23位为y的二进制字符串
  3. int a = Integer.parseInt(str.substring(0, 23), 2);
  4. int b = Integer.parseInt(str.substring(23, 46), 2);
  5. // 将二进制编码转换为实数
  6. double x = a * (6.0 - 0) / (Math.pow(2, 23) - 1); //x的基因
  7. double y = b * (6.0 - 0) / (Math.pow(2, 23) - 1); //y的基因
  8. //需优化的函数,也即是适应度值
  9. double fitness = 3 - Math.sin(2 * x) * Math.sin(2 * x)
  10. - Math.sin(2 * y) * Math.sin(2 * y);
  11. double[] returns = { x, y, fitness };
  12. return returns;
  13. }

计算适应度时,这里需要先将二进制串解码,转成成实数值,然后带入公式计算函数值。

种群的选择操作就完成了,可以看到其中通过计算适应度值,淘汰了适应度较低的个体,剩下的都是相对较高的个体.

其中需要注意,目标是计算函数的最小值,则说明函数值越小,适应度就越高。

  1. /**
  2. * 交叉操作 交叉率为60%,平均为60%的染色体进行交叉
  3. */
  4. private void cross() {
  5. String temp1, temp2;
  6. //对相邻两染色体进行交叉
  7. for (int i = 0; i < ChrNum; i++) {
  8. if (Math.random() < 0.60) {
  9. //pos位点前后二进制串交叉
  10. int pos = (int)(Math.random()*GENE)+1;
  11. //染色体交叉
  12. temp1 = ipop[i].substring(0, pos) + ipop[(i + 1) % ChrNum].substring(pos);
  13. temp2 = ipop[(i + 1) % ChrNum].substring(0, pos) + ipop[i].substring(pos);
  14. //更新染色体
  15. ipop[i] = temp1;
  16. ipop[(i + 1) / ChrNum] = temp2;
  17. }
  18. }
  19. }

上述是对相邻的两条染色体,以一定的概率执行交叉,随机生成一个交叉点,然后交换两条染色体的两个部分。

算法没有具体的实现准则,读者可以随机挑选两条染色体进行交叉。

下面再看变异:

  1. /**
  2. * 基因突变操作 1%基因变异
  3. */
  4. private void mutation() {
  5. for (int i = 0; i < 4; i++) {
  6. //使用随机数可以实现随机选择染色体和染色体的基因位
  7. int num = (int) (Math.random() * GENE * ChrNum + 1);
  8. int chromosomeNum = (int) (num / GENE) + 1; // 染色体号
  9. int mutationNum = num - (chromosomeNum - 1) * GENE; // 基因号
  10. if (mutationNum == 0)
  11. mutationNum = 1;
  12. chromosomeNum = chromosomeNum - 1;
  13. if (chromosomeNum >= ChrNum)
  14. chromosomeNum = 9;
  15. String temp;
  16. String a; //记录变异位点变异后的编码
  17. if (ipop[chromosomeNum].charAt(mutationNum - 1) == '0') { //当变异位点为0时
  18. a = "1";
  19. } else {
  20. a = "0";
  21. }
  22. //当变异位点在首、中段和尾时的突变情况
  23. if (mutationNum == 1) {
  24. temp = a + ipop[chromosomeNum].substring(mutationNum);
  25. } else {
  26. if (mutationNum != GENE) {
  27. temp = ipop[chromosomeNum].substring(0, mutationNum -1) + a
  28. + ipop[chromosomeNum].substring(mutationNum);
  29. } else {
  30. temp = ipop[chromosomeNum].substring(0, mutationNum - 1) + a;
  31. }
  32. }
  33. //记录下变异后的染色体
  34. ipop[chromosomeNum] = temp;
  35. }
  36. }

这步的处理是随机选择四个染色体,每个染色体中随机选择一个基因位进行变异(变异就是将 "0" 变成 "1",将 "1" 变成 "0")。


经过若干次迭代之后,能得到较优的解;虽然不一定能保证得到的是全局最优解,也有可能得到的依然是局部最优解,这是这类数值解法的问题。

遗传算法能比较快的收敛到最优解,若增大种群中的个体数量,可以更好的找到全局最优解,当然计算效率也降低了。

上面有两个核心的问题,第一是将实数变量编码成 0 1 字符串供遗传算法使用,第二是将目标函数值作为适应度函数.


复制图像

下载地址:https://github.com/JgyXyyxy/GA

项目目标是给定一幅图像 A,然后随机创建一幅图像 B,并利用遗传算法将 B 调整成 A,达到复制图像的目的。

程序的主要步骤为:

像素要应用遗传算法也需要先 编码 ;用 ColorFormat 类中的 boolean[] toBinaryGene(Color color) 方法将 rgb 颜色编码成了 24 位的 0 1 数组,以实现遗传算法所需要的编码要求;同时解码就用 Color binary2Color(boolean[] binary) 方法将 0 1 数组转换为颜色对象。详细过程可以参看源码。

rgb 都是 范围的整数值


程序入口为 test.java ChooseImage(),我们直接看菜单 openItem 按钮的点击事件:

  1. // 选择一幅目标图片
  2. String name = chooser.getSelectedFile().getPath();
  3. icon = new ImageIcon(name);
  4. //读取图片属性
  5. picAtri = new PicAtri(name);
  6. ......
  7. //启动线程开始复制
  8. startRecover();

下面看 startRecover() 方法:

  1. private void startRecover() throws IOException {
  2. //最大迭代次数
  3. count = Integer.parseInt(textfield.getText());
  4. //目标图像的长宽
  5. int width = picAtri.getWidth();
  6. int height = picAtri.getHigh();
  7. //每个像素设置的种群数量
  8. int popsize = 16;
  9. // 整幅图像转换为 0 1 字符串后的长度
  10. double sum = 0;
  11. int sumBestFitness = 0;
  12. //目标图像的颜色数组
  13. Color[] targetColors = picAtri.getColor();
  14. sum = 24*targetColors.length;
  15. //创建一幅空白图像,在这幅图像上生成目标图像
  16. String image = createBlankImage(width,height);
  17. PicAtri createdPic = new PicAtri(image);
  18. //实时显示每步迭代后的效果
  19. ShowImage createdShow = new ShowImage(createdPic);
  20. //对每个像素应用遗传算法
  21. GeneticAlgorithm[] createdPionts = new GeneticAlgorithm[width*height];
  22. int index = 0;
  23. //初始化
  24. for (;index<width*height;index++) {
  25. //每个像素的适应度函数值是考察与目标像素的差距
  26. Fitness fitness = new Fitness(targetColors[index]);
  27. Population population = new Population(popsize,fitness);
  28. createdPionts[index] = new GeneticAlgorithm(population,createdShow);
  29. }
  30. int maxIterNum = count;
  31. int generation = 1;
  32. while (generation <= maxIterNum){
  33. for (int i = picAtri.getMinY();i<height;i++){
  34. for (int j = picAtri.getMinX();j<width;j++){
  35. index = j + i*width;
  36. //每个像素对应的种群进行选择、交叉、变异
  37. createdPionts[index].evolve(createdPic,j,i);
  38. sumBestFitness +=createdPionts[index].getBestScore();
  39. }
  40. }
  41. //更新图像,查看迭代效果
  42. createdPic.writePic();
  43. createdShow.updatePic();
  44. //图像复制的精度
  45. double percent = sumBestFitness/sum;
  46. }
  47. }

上面的过程都是比较清晰的,主要是对图像的每个像素都应用遗传算法进行选择、交叉、变异。

对单个像素而言,我们看分析下遗传算法的过程,看 GeneticAlgorithm 类的 evolve() 方法:

  1. public void evolve(PicAtri atri, int x, int y) {
  2. List<Chromosome> childChromosomeList = new ArrayList<Chromosome>();
  3. //这里设置迭代种群数量次交叉操作
  4. while (childChromosomeList.size() < population.getPopSize()) {
  5. // 在交叉前,选择哪两个染色体时,做了选择操作
  6. //即此时只选择适应度强的染色体进行后续操作,而自然淘汰了适应度差的染色体
  7. Chromosome p1 = getParentChromosome();
  8. Chromosome p2 = getParentChromosome();
  9. //交叉操作比较常规,随机选择一个交叉点进行交换
  10. List<Chromosome> children = Chromosome.crossover(p1, p2);
  11. if (children != null) {
  12. for (Chromosome chro : children) {
  13. //保证种群数量不变
  14. if (childChromosomeList.size()<population.getPopSize()) {
  15. childChromosomeList.add(chro);
  16. }else{
  17. break;
  18. }
  19. }
  20. }
  21. }
  22. population.setPopulation(childChromosomeList);
  23. //变异操作也比较常规,对每条染色体以一定的概率进行变异,变异位置随机
  24. mutation();
  25. //计算迭代一轮后新的适应度函数值
  26. population.caculteScore();
  27. //得到适应度最高的染色体更新图像
  28. Chromosome bestChromosome = population.getBestChromosome();
  29. bestScore = bestChromosome.getScore();
  30. Color color = ColorFormat.binary2Color(bestChromosome.getGene());
  31. atri.setPointAlpha(x,y,color);
  32. }

重点关注种群分数的计算:

  1. public void caculteScore() {
  2. //计算每条染色体的适应度函数值
  3. for (Chromosome chromosome:population){
  4. chromosome.setScore(fitness.calculateFittness(chromosome.getGene()));
  5. }
  6. //下面就是统计适应度最高、最低 以及 平均值
  7. bestFitness = population.get(0).getScore();
  8. worstFitness = population.get(0).getScore();
  9. totalFitness= 0;
  10. for (Chromosome chro : population) {
  11. if (chro.getScore() >= bestFitness) { //设置最好基因值
  12. bestFitness = chro.getScore();
  13. bestChromosome = chro;
  14. }
  15. if (chro.getScore() < worstFitness) { //设置最坏基因值
  16. worstFitness = chro.getScore();
  17. }
  18. totalFitness += chro.getScore();
  19. }
  20. averageFitness = totalFitness / popSize;
  21. //因为精度问题导致的平均值大于最好值,将平均值设置成最好值
  22. averageFitness = averageFitness > bestFitness ? bestFitness : averageFitness;
  23. }

我们其实只需要适应度最高的染色体,用来更新图像。

下面重点看下适应度函数的计算,查看 Fitness.calculateFittness() 方法:

  1. public double calculateFittness(boolean[] srcGene) {
  2. double fit = 0;
  3. //获得目标染色体
  4. boolean[] targetGene = ColorFormat.toBinaryGene(target);
  5. //对比当前染色体与目标染色体相同的基因有多少,以此作为适应度值
  6. for (int i = 0;i<24;i++){
  7. if (srcGene[i] == targetGene[i])
  8. fit=fit+1;
  9. }
  10. return fit;
  11. }

其中适应度值越高,则说明当前像素与目标像素越接近,图像复制越成功,这就是我们最终实现的目标。


再回头看上面的过程,其实需要注意的就两处,第一是如何编码来表示像素,使之能应用遗传算法,第二是如何设置适应度函数,这与我们要实现的目标有关。

至于一遗传算法的选择、交叉、变异等操作,实现起来其实大同小异。


小结

遗传算法的理论部分网上已经有非常多的资料了,读者可以先阅读笔者推荐的几篇博客;

上面的两个例子是为了说明实数型如何编码;怎么应用遗传算法,适应度函数如何设置;这也是我们应用遗传算法做具体需求时需要考虑的核心问题。

在其他应用领域,大多是用遗传算法来做模型的参数估计,可以对参数进行编码,将模型对测试语料库的评估值作为适应度,淘汰掉使测试结果效果不太好的参数,这样可以快速的得到最优的参数估计值。


参考

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注