手撕:经典问题的遗传算法代码
1、遗传算法简介
1.1、基因的表达
1.2、新种群的诞生:交叉
1.3、天选的个体:变异
1.4、自然的选择:轮盘赌
2、算法框架搭建
3、完善每一步的算法实现
4、整体的调试
遗传算法简介
1.1 基因的表达
1、如何去编码
二级标题应用遗传算法的首要问题,就是进行染色体的编码,常用的方式,也是最基本的方式就是采用二进制进行编码,所以,我们首先需要根据实数表示的问题的解,如以下问题的求解:
将x1,x2进行二进制编码:
2、二进制编码的实现
首先是长度,即位数的确定,这个首先取决于我们对于问题解所要求的精度,eg:4位,那么解所在的定义域计算后的实数,再用二进制表达出来,至少需要的位数,便是其长度,
eg:x1的定义域为:-3~12.1,故有(12.1-(-3))*10000=151000
217<151000< 218-1,故为18位,同理,x2为15位,因此该问题解表达为二进制串后是一条33位的染色体。
3、解码
同理,解码时只需将33位的染色体前18位摘出来,转换位十进制数,便是x1,后15位则为x2。
1.2 新种群的诞生:交叉
在一个合适的交叉率的情况下,随机的从种群中选择选出染色体进行交叉,交叉点也是随机的,例如,种群一共十条染色体,那么随机如果选出4条,那么就有两次交叉,但是每一次的交叉点,是随机的,在33位的染色体上,随机的选择某一点作为交叉的点,互换染色体的片段,则完成一个新生,加入种群,当所有被选中的染色体都交叉完成后,就诞生出新的种群。
eg:
设交叉率P=0.7,即平均有70%的染色体会参与产生后代,同样,我们假设染色体还是10条,因此,需要产生十个随机数,赋予每条染色体,若是低于0.7的染色体,将会被选作交配对象,然后对每一次的交叉,随机产生一个1~32的数,例如13,作为交叉点,交换染色体片段,得到子代,和父代一起去接受下一步的天选:变异。
1.3 天选的个体:变异
作为模拟人类遗传,以及自然界的规律的遗传算法,变异必不可少,但是发生概率比较小,但是变异一般又是某个基因发生变异,因此,对于种群中的所有基因而言,它们都是带选择的对象,这里主要是我们把基因二进制化,因此带选择的对象就成了某一位二进制数了,即变异就是0翻转为1,或者1翻转为0。
eg:
假设基因变异的概率为0.01(一般都是小于此数),即种群平均有1%的基因发生变异,或者说每个基因有1%的可能变异,假设此时种群有十条染色体,一共330个基因,那么平均每次有3.3个基因发生变异,对每一个基因都产生一个随机数,若小于0.01,则该基因变异(翻转),产生新的染色体,加入种群,至于适不适合生存,咱看接下来来自大自然的选择:轮盘赌。
1.4、自然的选择:轮盘赌
大自然,在问题中既是我们要求的最优解,自然往往留下的总是优秀的那一批人,那么对于问题的求解,我们总是留下最好的一些解,再从中挑选最符合我们要求的那一个解,因此,检验这个染色体适不适合留下,依据就在于这个染色体对于问题的回答,是不是更好,比如我们的问题是求最大值,那么,染色体对于问题的回答自然是取得最大值的最应该留下,但是,自然选择充满了偶然性,有可能最优秀的,意外而亡,故只能说,越优秀,生存下去的可能就越大,故,采用一种轮盘赌的方式,来模拟大自然的选择,eg:
对每一个染色体都计算它对于问题的回答,即计算适值,然后计算每条染色体在转盘上所占的面积大小(概率大小),然后,每一条染色体都占有一定的面积,越优秀,面积越大,指针选到的几率也就越大,接下来选出10条染色体,作为新种群,淘汰掉未被选中的,故产生10个随机数,去在轮盘上转10次,当然,若是已经选中,则继续加转,直到选出10条不一样的染色体为止。至此,完成一次迭代。
2、算法框架搭建
框架:即我们在实现算法的过程中,都应该需要哪些步骤,哪些步骤适合封装为一个函数去操作,因此,我习惯首先写空函数,相当于写伪代码。
如下:
3、完善每一步的算法实现
首先是染色体,我设计为一个类,因为类有数据的保护性以及成员函数可对数据操作的方便的特点。
//染色体
class Chromosome
{
private:
double x1,x2; //染色体片段对应的十进制值x1、x2
string s1, s2, chro_s;//片段s1,s2,染色体chro_s
public:
Chromosome(double n1, double n2):x1(n1),x2(n2){}
void Encode(); //编码函数
float Eval(); //计算适值
string get_chro_s() { return chro_s; }
void input_chro_s(string s) { chro_s = s; }
void Decode(); //染色体解码
};
然后,是编码,解码的工作
//染色体编码函数
void Chromosome::Encode()
{
//对X1片段染色体编码
bitset<18> fragment1((x1 + 3) *(pow(2, 18)-1)/15.1);
s1 = fragment1.to_string();
//cout << s1 << endl;
//对X2片段染色体编码
bitset<15> fragment2((x2-4.1)*(pow(2, 15)-1)/1.7);
s2 = fragment2.to_string();
//cout << s2 << endl;
//片段拼接得到染色体字符串:chro_s
chro_s = s1 + s2;
cout << chro_s << endl;
//cout <<"以上运行Encode函数"<< endl;
}
//染色体解码函数
void Chromosome::Decode()
{
//片段一解码
//s.substr(pos, n) //截取s中从pos开始的n个字符的子串,并返回
bitset<18> t1(chro_s.substr(0, 18));
x1 = (t1.to_ulong()*15.1 / (pow(2, 18)-1) - 3);
//片段二解码
bitset<15> t2(chro_s.substr(18, 15));
x2 = (t2.to_ulong()*1.7 / (pow(2, 15)-1) + 4.1);
//cout << "x1=" << x1 << " x2=" << x2 << endl;
}
然后是计算适值:
//适值计算函数
float Chromosome::Eval()
{
Decode();
return(float)(21.5 + x1 * sin(4 * 3.14*x1) + x2 * sin(20 * 3.14*x2));
}
之后是交叉函数的编写
//交叉函数
void Crossover(vector<Chromosome> &c)
{
//1、选择交叉的染色体:产生随机数
vector<Chromosome> cross_chro;
for (int i = 0; i != Population_m; i++)
{
float p=(float)(rand() % (1001)) / 1000;
//cout << "p=" << p << endl;
if (p < Crossover_rate)//低于交叉率的染色体交叉
{
//记录染色体
cross_chro.push_back(c[i]);
//cout << "第" << i << "号染色体选作交叉" << endl;
//cout << "#" << i << " " << c[i].get_chro_s() << endl;
}
}
//2、子代存储
int cross_num = 0;
for (int j = 0; j < (cross_chro.size()/2); j++)
{
//cout << "第" << j+1 << "次交叉" << endl;
int Crossover_point = rand() % (Chro_len + 1);//产生一个随机的交叉点
//cout << "Crossover_point=" << Crossover_point << endl;
string temp;
temp = cross_chro[cross_num].get_chro_s();//做一个备份
cross_chro[cross_num].input_chro_s(cross_chro[cross_num].get_chro_s().replace(Crossover_point, Chro_len - Crossover_point, cross_chro[cross_num + 1].get_chro_s().substr(Crossover_point, Chro_len - Crossover_point)));
cross_chro[cross_num + 1].input_chro_s(cross_chro[cross_num + 1].get_chro_s().replace(Crossover_point, Chro_len - Crossover_point, temp.substr(Crossover_point, Chro_len - Crossover_point)));
//cout << cross_chro[cross_num].get_chro_s() << endl;
//cout << cross_chro[cross_num + 1].get_chro_s() << endl;
//cout << "***************************" << endl;
cross_num += 2;
}
//3、将子代染色体融入种群
for (int i = 0; i != (int)cross_chro.size() / 2 * 2; i++)
{
c.push_back(cross_chro[i]);
}
cross_chro.clear();
}
然后是变异函数
//变异函数
void Mutation(vector<Chromosome> &c)
{
vector<float> Gene_m_r;
//给每个基因产生随机数
for (int i = 0; i != Chro_len * c.size(); i++)
{
Gene_m_r.push_back((float)(rand() % (1001)) / 1000);
}
//遍历基因,检测变异基因
for (int i = 0; i != (Chro_len * c.size()); i++)
{
if (Gene_m_r[i] < Mutation_rate)
//计算基因位置
{
int Chromosome_num = i / Chro_len; //求染色体号数
int Gene_num = i - Chromosome_num * Chro_len;//求基因位置0-33
bitset<33> Comp(c[Chromosome_num].get_chro_s());//字符串转二进制串
Comp = Comp.flip(Gene_num); //基因变异
c[Chromosome_num].input_chro_s(Comp.to_string());//获得变异后的基因
//cout << "变异染色体号数:" << Chromosome_num << " 变异基因位置:" << Gene_num
// << " 变异后的染色体:" << c[Chromosome_num].get_chro_s() << endl;
}
}
Gene_m_r.clear();
}
最后是轮盘赌
//轮盘赌函数
void Roulette(vector<Chromosome> &c)
{
float Eval_sum = 0;
//计算适值总和
for (int i = 0; i != c.size(); i++)
{
Eval_sum += c[i].Eval();
}
vector<float> Pk;//累计概率的容器,需要清理内存
float pk = 0;
//计算累计概率
for (int i = 0; i != c.size(); i++)
{
pk+= c[i].Eval() / Eval_sum;
Pk.push_back(pk);
}
vector<float> chro_r;//需要清理内存
//产生新种群,染色体随机数
for (int i = 0; i != Population_m; i++)//新种群为10条染色体,故有十次轮盘指针值
{
chro_r.push_back((float)(rand() % (1001)) / 1000);
}
int num = 0,flag = 1;//染色体下标,轮盘有效标志位
int record[Population_m] = { 11,12,13,14,15,16,17,18 };//新种群下标值
vector<Chromosome> temp = c;//复制一份数据,进行种群更新,更新完需要清理内存
int k = Population_m;//初始轮盘次数=种群数量
for (int i = 0; i != k; i++)
{
for (int j = 0; j != Population_m; j++)
{
if (chro_r[i] <= Pk[j])//轮盘值小于累计概率区间,则输出该区间对应的染色体
{
for (int m = 0; m != Population_m; m++)
{
if (j == record[m])
{
flag = 0;
}
}
if (flag==0)//如果轮盘值选中的染色体已经被选中的,重新再选一次
{
chro_r.push_back((float)(rand() % (1001)) / 1000);//重新产生一个随机数
k++;
flag = 1;
break;
}
else//轮盘选中染色体
{
record[num] = j;
num++;
//**************************需要删除最后的几个元素*************************
c[num].input_chro_s(temp[j].get_chro_s());//应该需要删除多余的染色体
break;
}
}
}
}
for (int i = c.size(); i > Population_m; i--)
{
c.pop_back();
}
//清理内存
Pk.clear();
chro_r.clear();
temp.clear();
}
最后还需要增加一个计算当代最优值的函数
//输出当代最优值
float opt_value(vector<Chromosome> &c)
{
vector<float> value;
for (int i = 0; i != Population_m; i++)
{
value.push_back(c[i].Eval());
}
sort(value.begin(), value.end());
return value[Population_m-1];
value.clear();
}
4、整体的调试
自己写一个测试函数,把代码跑起来就行了
void test()
{
int n = N;
Population_init();//种群初始化完成,准备迭代
static float max=0;
static int m=0;
//开始迭代
while(n--)
{
Crossover(v_chro);//交叉
Mutation(v_chro);//变异
Roulette(v_chro);//轮盘
//输出轮盘选择后的染色体
for (int i = 0; i != v_chro.size(); i++)
{
cout << "#" << i << " " << v_chro[i].get_chro_s() << endl;
}
//计算最优值
float opt_Eval = opt_value(v_chro);
cout << "第" << N-n << "代最优解为:" << opt_Eval << endl;
if (opt_Eval > max)
{
max = opt_Eval;
m = n;
}
cout << "*******************完成第" << N - n << "次迭代*******************" << endl;
cout << endl;
}
cout << "第" << N - m << "代为最优解:" << max << endl;
}
然后输出后是这个样子:
来个小视频感受一下:
手撕遗传算法,详见博客