一、题目
使用遗传算法求解 f(x)目标函数的最小值。
由题可知,f(x)是一个30元函数,即含有30个自变量。
二、原理
2.1 基础
个体:染色体带有特征的实体,本题中是指 f(x)的潜在解的二进制编码形式。
群体:个体的集合,群体的数量由用户确定。
目标函数:f(x)就是一个目标函数,我们想要得到该目标函数的最小值。
适应度:根据目标函数确定用于区分群体中个体好坏的度量方式。
基因:染色体的内部表现,在这指二进制位(0或者1)。
2.2 编码与解码
编码:
编码就是对问题潜在解进行“数字化”的方式。
在本题中,潜在解由30个自变量组成,我们需要将潜在解进行二进制编码。
如何确定单个自变量的二进制位数?
假设自变量的精度为小数点后2位,那么为了保证精度,需要将[-5, 5]的区间平均分为10×10^2份。
512 = 2^9 < [5-(-5)]×10^2 = 1000 < 2^10 = 1024
所以编码的单个自变量的二进制位数为10。
本题中,一个潜在解由30个自变量组成,因此,该潜在解由30个位数为10的二进制数拼接组成,即是一个位数为300二进制。
解码:
解码就是将编码的数进行实数化。
本题中,由于一个潜在解由30个自变量二进制数拼接而成,因此在解码前,需要对其进行裁剪操作。
a:自变量范围的下界(最小值)。
b:自变量范围的上界(最大值)。
x:自变量二进制转换为十进制的实数。
y:自变量解码后的真实值。
m:单个自变量二进制位数。
y = -5 + x × [ 5 - (-5)] / (2^10 - 1)
裁剪后通过上式对各个自变量二进制数进行解码。
2.3 适应度函数
适应度函数也称评价函数,是根据目标函数确定的,用于区分群体中个体好坏的标准。
适应度函数总是非负的,而目标函数可能有正有负,故需要在目标函数与适应度函数之间进行变换。
上式为本题的适应度函数,该式在区间内都是非负的,并且适应度函数的值越大,其目标函数的值越小,符合题意。
2.4 选择
选择实际上就是一个“优胜劣汰”的过程。适应度越大的个体将被更大概率的选中保留到下一代,适应度较小的个体更容易被淘汰。
在本次实验中,每次将保留适应度排在群体前50%的个体,适应度排在后50%的个体将被新的随机初始化的个体替换。
2.5 交叉
交叉是指有一定概率的对两个相互配对的染色体按某种方式相互交换其部分基因,从而形成两个新的个体。
这个概率是由用户确定的,一般为0.5。
本实验中,交叉的方法是将染色体依次两两配对,随机在一对染色体上选取一点分成两段,然后互换重组为新的两条染色体。
在交叉这一步,有更好的策略是只选取选择得到的适应度高的个体进行交叉,并且可以选择交叉后保留原个体。
2.6 变异
复制时可能(很小的概率)产生某些复制差错,变异产生新的染色体,表现出新的性状。
这个概率是由用户确定的,一般为0.01。
本实验中,在染色体上随机选取一位,翻转其二进制位。
三、程序实现
3.1 编码类
初始化群体中的染色体,实际上就是潜在解。潜在解由30个自变量组成,每个自变量二进制位数为10,所以一个潜在解的二进制位数为300。
/**
* 编码类:初始化群体,一个群体中有多条染色体。对染色体进行编码。
* 染色体即是个体。
*/
public class Code {
/**
* 随机初始化一条染色体。
* @param GENE 染色体的位数,位数是根据自变量x范围确定的。
* @return 返回二进制染色体。例如011000110
*/
public String codeSingle(int GENE){
String res = ""; // 定义为字符串型,这样子的话+就是在字符串尾部添加。
for(int i = 0; i < GENE; i++){
if(Math.random() < 0.5){ // 随机添加0或者1
res += 0;
}else{
res += 1;
}
}
return res;
}
/**
* 随机初始化一组染色体
* @param GENE 染色体的位数
* @param groupsize 种群中存在染色体的个数
* @return 返回所有二进制染色体
*/
public String[] codeAll(int GENE,int groupsize){
String[] iAll = new String[groupsize];
for(int i = 0; i < groupsize; i++){
iAll[i] = codeSingle(GENE);
}
return iAll;
}
}
3.2 解码类
将潜在解的二进制数拆分为30个二进制数,然后解码。
/**
* 解码类:对二进制染色体进行解码操作。
*/
public class Decode {
/**
* 单个染色体解码
* @param single 染色体字符串(二进制)
* @param GENE 染色体位数
* @return 自变量x的值
*/
public double[] decode(String single,int GENE) {
int[] a = new int[GAmain.NUM];
// 裁剪分段
for (int i = 0; i < GAmain.NUM; i++) {
a[i] = Integer.parseInt(single.substring(i * (GENE / 30), (i + 1) * (GENE / 30)), 2);
}
// 解码
double[] x = new double[GAmain.NUM];
for (int i = 0; i < GAmain.NUM; i++) {
x[i] = a[i] * (5 - (-5)) / (Math.pow(2, GENE / 30) - 1) - 5;
}
return x;
}
}
3.3 适应度类
将解码后的潜在解代入适应度函数中,求出个体的适应度,适应度越大,更有机会进入下一代。
import java.lang.Math;
/**
* 适应度类:将个体解码然后通过公式进行换算。
*/
public class Fitness {
/**
* 计算个体的适应度
* @param str 染色体二进制数
* @param GENE 染色体二进制位数
* @return 适应度的值
*/
public double fitSingle(String str, int GENE){
Decode decode = new Decode();
double[] x = decode.decode(str, GENE);
//适应度计算公式
double f1 = 0;
double f2 = 0;
double c1 = 20;
double c2 = 0.2;
double c3 = 2 * Math.PI;
for (int i = 0; i < 30; i++) {
f1 += Math.pow(x[i], 2) / 2;
f2 += Math.cos(c3 * x[i]) / 2;
}
double fitness = c1 * Math.exp(-c2 * Math.pow(f1, 0.5)) + Math.exp(f2);
return fitness;
}
/**
* 批量计算数组的适应度
* @param str 染色体数组
* @param GENE 染色体位数
* @return 适应度数组
*/
public double[] fitAll(String str[], int GENE){
double [] fit = new double[str.length];
for(int i = 0;i < str.length; i++){
fit[i] = fitSingle(str[i], GENE);
}
return fit;
}
/**
* 适应度最值(返回序号)
* @param fit 适应度数组
* @return 适应度最大的序号
*/
public int mFitNum(double fit[]){
double m = fit[0];
int n = 0;
for(int i = 0;i < fit.length; i++){
if(fit[i] > m){
m = fit[i]; // 最大值
n = i;
}
}
return n;
}
/**
* 适应度最值(返回适应度)
* @param fit 适应度数组
* @return 适应度最大的值
*/
public double mFitVal(double fit[]){
double m = fit[0];
for(int i = 0;i < fit.length; i++){
if(fit[i] > m){
m = fit[i]; // 最大值
}
}
return m;
}
}
3.4 选择类
选择进入下一代的染色体,将保留适应度排在群体前50%的个体,适应度排在后50%的个体将被新的随机初始化的个体替换。
/**
* 选择类
*/
public class Selection {
Code init = new Code();
Fitness fitness = new Fitness();
/**
* 选择
* @param group 群体数组
* @param GENE 染色体二进制数
* @return 新的二进制染色体数组
*/
public String[] RWS(String group[], int GENE){
String[] newgroup = new String[group.length];
double[] fit = fitness.fitAll(group, GENE); // 计算适应度数组
// 冒泡排序算法 fit和group数组进行小到大排序
for (int i = 0; i < fit.length; i++) {
for (int j = 0; j < fit.length-1-i; j++){
if (fit[j] > fit[j+1]){
double temp = fit[j];
fit[j] = fit[j+1];
fit[j+1] = temp;
String stringTemp = group[j];
group[j] = group[j+1];
group[j+1] = stringTemp;
}
}
}
// 根据适应度大小排序,排在前一半的被选中,后一半的被淘汰
for (int i = 0; i < fit.length; i++){
//适应度最大的个体直接继承
if(i > fit.length/2){
newgroup[i] = group[i];
}
else {
newgroup[i] = init.codeSingle(GENE); // 如果没被选中,被外来者取代
}
}
return newgroup;
}
}
3.5 交叉类
将染色体依次两两配对,随机在一对染色体上选取一点分成两段,然后互换重组为新的两条染色体。
需要设置交叉率,一般设置为0.5。
/**
* 交叉类:将染色体依次两两配对,随机在一对染色体上选取一点分成两段,然后互换重组为新的两条染色体。
*/
public class Cross {
Fitness fitness = new Fitness();
/**
*
* @param group 群体数组
* @param GENE 染色体二进制位数
* @param crossRate 交叉率
* @return
*/
public String[] cross(String[] group,int GENE,double crossRate){
String temp1, temp2;
int pos = 0;
double[] fit = fitness.fitAll(group, GENE);
int mFitNum = fitness.mFitNum(fit); // 计算适应度最大的染色体序号
String max = group[mFitNum];
for(int i = 0; i < group.length; i++){
if(Math.random() < crossRate){
pos = (int)(Math.random()*GENE) + 1; //交叉点
temp1 = group[i].substring(0, pos) + group[(i+1) % group.length].substring(pos); //%用来防止数组越界
temp2 = group[(i+1) % group.length].substring(0, pos) + group[i].substring(pos);
group[i] = temp1;
group[(i+1) % group.length] = temp2;
}
}
group[0] = max;
return group;
}
}
3.6 变异类
在染色体上随机选取一位,翻转其二进制位。
需要设置变异概率,这里取0.01,即100个个体会有一个个体发生变异。
/**
* 变异
*/
public class Mutation {
//替换String中的指定位
//str要改动的字符串
//num要改动的位(从0开始数)
//pos要把这一位改动成什么
public String replacePos(String str,int num,String pos){
String temp;
if(num == 0){
temp = pos + str.substring(1);
}else if(num == str.length()-1){
temp = str.substring(0, str.length() - 1) + pos;
}else{
String temp1 = str.substring(0, num);
String temp2 = str.substring(num + 1);
temp = temp1 + pos + temp2;
}
return temp;
}
public String[] mutation(String[] group,int GENE,double MP){
Fitness fitness = new Fitness();
double[] fit = fitness.fitAll(group,GENE);
int mFitNum = fitness.mFitNum(fit); //计算适应度最大的染色体序号
String max = group[mFitNum];
for(int i = 0; i < group.length * MP; i++){
int n = (int) (Math.random() * GENE * group.length ); //从[0,GENE * group.length)区间取随机数
int chrNum = (int) (n / GENE); //取得的染色体数组下标
int gNum = (int)(n % GENE); //取得的基因下标
String temp = "";
if(group[chrNum].charAt(gNum) == '0' ){
temp = replacePos(group[chrNum], gNum, "1");
}else{
temp = replacePos(group[chrNum], gNum, "0");
}
group[chrNum] = temp;
}
group[0] = max;
return group;
}
}
3.7 日志类
将结果记录到log.txt文件中。
import java.io.*;
/**
* 日志类,记录结果。
*/
public class Logger {
public static void log(String msg) {
try {
BufferedWriter out = new BufferedWriter(new FileWriter("log.txt", true));
out.write(msg);
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.8 主程序
public class GAmain {
public static final int groupsize = 100; //染色体数(群体中个体数)
public static final double MP = 0.01; //变异概率
public static final double CP = 0.5; //交叉概率
public static final int ITERA = 100000; //迭代次数
public static final int NUM = 30; // 群体中的个体数量
public static final int GENE = 10 * NUM; // 潜在解二进制位数
/**
* 输出适应度最大值,以及返回最优的个体,测试用
* @param str 完成
* @param group 群体数组
* @return 返回最大值的染色体序号
*/
public int outMax(String str,String[] group){
Fitness fitness = new Fitness();
double[] fit = fitness.fitAll(group, GENE);
double max = fitness.mFitVal(fit);
System.out.println(str+"后适应度最大值为"+max);
return fitness.mFitNum(fit);
}
// 程序入口
public static void main(String[] args) {
Code init = new Code();
Fitness fitness = new Fitness();
Selection rws = new Selection();
Cross cross = new Cross();
Mutation mutation = new Mutation();
Decode decode = new Decode();
GAmain ga = new GAmain();
String group[] = init.codeAll(ga.GENE, groupsize); //初始化
for(int i = 0; i < ITERA; i++){
group = rws.RWS(group, ga.GENE); //选择
group = cross.cross(group,ga.GENE,CP); //交叉
group = mutation.mutation(group, ga.GENE, MP); //变异
if (i % 200 == 0) {
double[] fit = fitness.fitAll(group, GENE);
int max = fitness.mFitNum(fit);
double result = -fitness.fitSingle(group[max], ga.GENE) + 20 + 2.71282;
String msg = i / 200 + " " + result + "\n";
Logger.log(msg);
System.out.println("目标函数结果为:" + result);
}
}
}
}
四、实验结果
4.1 Python可视化
读取记录下来的log.txt文件,对数据进行可视化绘图。
import os
import matplotlib.pyplot as plt
# 读取log.txt文件
with open(os.path.join("log.txt"), 'r') as fd:
dataList = fd.readlines()
dataList = [l[:-1] for l in dataList] # 把/n去掉
# 将数据放入list中
data = []
iters = []
for i in range(len(dataList)):
sample = dataList[i].split()
data.append(float(sample[1]))
iters.append(int(sample[0]))
# 绘图
plt.title("Result GA")
plt.plot(iters, data, color='blue')
plt.xlabel("iters")
plt.ylabel("result")
plt.savefig("Result_GA")
plt.show()
4.2 实验结果
横坐标是迭代次数,纵坐标是目标函数的结果。
增加迭代次数,求得该目标函数的最小值近似为:-3268995.485765472(不一定是正确答案,但应该是接近了)。
五、总结
在本次实验中,还尝试了赌轮盘+最佳保留的选择方法,将适应度最高的个体保留到下一代,然后其余个体通过概率进行选择。这个概率对应于适应度,适应度越大,概率越高。
但是实验发现,对于本题,赌轮盘+最佳保留的选择方法寻找最优解太慢。所以最终使用了每次将保留适应度排在群体前50%的个体,适应度排在后50%的个体将被新的随机初始化的个体替换的策略。