1、模拟退火算法
1.1算法基本思想
算法本身思想借鉴于传统的金属退火,在高温状态下由于分子运动剧烈,处于一种无序的状态,而随着温度冷却会渐渐趋于有序的状态
而模拟退火算法按照以下的步骤进行优化
①首先给出一个随机初始解,只要满足题设约束即可
②对随机解进行微小的扰动(扰动幅度及相关参数需要自行调试),那么会出现结果优于初始解,及结果不如初始解两种情况
对于前一种情况,我们选择完全接受该优化,而对于后一种情况我们根据Metropolis准则选择是否接受
Metropolis准则
P
=
{
1
E
j
≥
E
i
e
−
E
i
−
E
j
T
E
j
≤
E
i
P=\left\{ \begin{array}{l} 1&& {E_j \geq E_i}\\ e^{-\frac{E_i-E_j}{T}}&&{E_j \leq E_i} \end{array} \right.
P={1e−TEi−EjEj≥EiEj≤Ei
我们可以发现在高温环境下,根据Metropolis准则,即使扰动后的新解不如原始解,但是由于温度较高,此时接受新解的概率依然较大,因此在高温环境下,有很大的搜索最优解的空间,不易陷入局部最优解,而随着温度降低,接受非优解的概率不断下降,此时搜索逐渐趋于稳定。
③设置马尔科夫链长度为n(具体多少根据实际情况决定,但是迭代次数越多,搜索效果就越好),也就是根据上述步骤扰动n次后,结束在该温度下的搜索,进行退火操作,此时就需要设定退火速度,经典的有均匀退火,指数退火等等,而指数退火效果较好,我们这次代码示例采用指数退火,如设定 T ∗ = T ∗ 0.999 T^{*}=T*0.999 T∗=T∗0.999,接着在新的温度下进行新一轮的迭代。
④设定记录变量记录下优化过程中的最优解,最终输出优化结果
1.2实例运用(函数最值求解)
这里给出的函数是y=x*sin(10*x*pi)+2,x ∈ [ − 1 , 2 ] \isin[-1,2] ∈[−1,2]
#include<iostream>
#include<cmath>
#include<random>
#include<ctime>
using namespace std;
double max(double x,double y)
{
return x > y ? x : y;
}
double newT(double T)
{
int a;
a = T * 0.999;
return a;
}
double func(double i)
{
return i * sin(10 * 3.14159 * i) + 2;
}
int main()
{
double T, Tm, p,finalx;
double newans, ans, value;
int i, j, k;
T = 1000;
Tm = 0.01;
ans = ((rand() % 3000000) - 1000000)/1000000.0;
value = func(ans);
finalx = ans;
while (T > Tm)
{
for (i = 1;i <= 100;i++)
{
newans = ans + rand() % 50000 / 1000000.0 * pow(-1, rand() % 2);
while (newans < -1 || newans>2)
{
newans = ans + rand() % 50000 / 1000000.0 * pow(-1, rand() % 2);
}
if (func(newans) > func(ans))
{
ans = newans;
if (func(ans) > value)
{
finalx = ans;
}
value = max(func(ans),value);
}
else
{
p = exp((func(ans) - func(newans)) / T);
if (rand() % 1000000 / 1000000.0 <= p)
{
ans = newans;
}
}
}
T = newT(T);
}
cout << finalx << endl;
cout << value <<endl ;
}
没有什么特别的注意点,因此不再赘述。
2、遗传算法
遗传算法相比起模拟退火算法来说相对复杂一些,因为遗传算法需要涉及到一些种群,个体,染色体,交叉,变异的操作,而且遗传算法的
初始解也是一个解集,而非单个的初始解,通过评价函数对解集中解进行评估选择,淘汰一系列不好的解,物竞天择,最终达到优化的目的。
2.1算法基本思想
首先需要引入几个概念:1、种群:即所有解集
2、个体:即单个可行解
3、染色体:即每个个体对应的编码(在人为自行设定的编码规则下)
4、交叉:染色体编码根据某种方式进行交叉,如单点交叉,多点交叉等等
5、后代:通过原先解生成的新解
6、变异:后代以一定概率出现编码随机化的操作
7、评价函数:通过可行解的各个参数评价解的性能,如在传统达尔文理论中的生存能力,函数最值问题中的函数值等等
而遗传算法实质上就是秉持着物竞天择的原则,评价函数值越大的个体能够得到更好的生存空间,因此拥有更高繁衍后代的机会,通过淘汰
评价函数值较小的个体,从而优化整个种群
其中需要特别介绍的是在父母选择方面的一个原则,其实有很多选择方式,在示例代码中选择的是轮盘赌法,即按照评价函数值得比例占额,分配繁衍后代的机会,具体程序实现上的处理可以看源代码。
2.2实例运用(函数最值求解)
这里给出的函数是y=x*sin(10*x*pi)+2,x ∈ [ − 1 , 2 ] \isin[-1,2] ∈[−1,2]
#include <iostream>
#include <cmath>
#include <cstdlib>
#include <ctime>
using namespace std;
#define pi 3.14159
typedef struct
{
double point;
double value;
}Individual;
double efunc(double x)
{
return exp(2*x*sin(10.0*pi*x)+2.0);
}
double func(double x)
{
return x*sin(10.0*pi*x)+2.0;
}
int main()
{
Individual initial[200],offspring[200],parent[400];
int i,j,f;
double k;
double totalvalue,sumvalue;
double best[200],top;
srand(time(0));
totalvalue=0;
sumvalue=0;
for (i=1;i<=100;i++)
{
offspring[i].point=rand()%30000/10000.0-1.0;
offspring[i].value=func(offspring[i].point);
}//生成初始种群数据
top=0;
//选择合适的父代交叉生成下一代(轮盘赌)
for (f=1;f<=100;f++)
{
totalvalue=0;
for (i=1;i<=100;i++)
{
totalvalue=totalvalue+efunc(offspring[i].point);
}
j=1;
while (j<=200)
{
k=rand()%30000/30000.0;
sumvalue=0;
for (i=1;i<=100;i++)
{
sumvalue=sumvalue+efunc(offspring[i].point)/totalvalue;
if (k<=sumvalue)
{
parent[j]=offspring[i];
j++;
break;
}
}
}
for(i=1;i<=100;i++)//生成下一代
{
offspring[i].point=(parent[i].point+parent[i+100].point)/2.0;
offspring[i].value=func(offspring[i].point);
if (offspring[i].value>top)
{
top=offspring[i].value;
}
}
}
std::cout<<top<<endl;
return 0;
}
2.3分析比较
其实实现过程中,遗传算法的编码交叉过程偷了懒,采用了取均值的方式而不是染色体交叉的方式,这就导致了种群个体的集中,而无法向外拓展,不过在本例中影响并不明显。
需要特别比较的是两个评价函数
double efunc(double x)
{
return exp(2*x*sin(10.0*pi*x)+2.0);
}
double func(double x)
{
return x*sin(10.0*pi*x)+2.0;
}
在最初选取评价函数时我选择了func进行轮盘赌选择操作,而实际结果相当不好,而换用了efunc函数后效果明显改善,原因是函数值差距过小并不能完全体现出在大规模种群中的生存优势,即使有一定优势,在1000甚至10000的规模下也很难被正确选择,因此efunc的作用就是扩大生存能力的评价,体现更优解的优势,让种群个体能够向其靠拢。