码迷,mamicode.com
首页 > 编程语言 > 详细

模拟退火算法(转自-ranjiewen的博客园!)

时间:2018-01-21 01:09:31      阅读:293      评论:0      收藏:0      [点我收藏+]

标签:就是   ini   public   using   准则   距离   注意   dual   comm   

 

     著名的模拟退火算法,它是一种基于蒙特卡洛思想设计的近似求解最优化问题的方法。

 

一点历史——如果你不感兴趣,可以跳过

     美国物理学家 N.Metropolis 和同仁在1953年发表研究复杂系统、计算其中能量分布的文章,他们使用蒙特卡罗模拟法计算多分子系统中分子的能量分布。这相当于是本文所探讨之问题的开始,事实上,模拟退火中常常被提到的一个名词就是Metropolis准则,后面我们还会介绍。


美国IBM公司物理学家 S.Kirkpatrick、C. D. Gelatt 和 M. P. Vecchi 于1983年在《Science》上发表了一篇颇具影响力的文章:《以模拟退火法进行最优化(Optimization by Simulated Annealing)》。他们借用了Metropolis等人的方法探讨一种旋转玻璃态系统(spin glass system)时,发觉其物理系统的能量和一些组合最优(combinatorial optimization)问题(著名的旅行推销员问题TSP即是一个代表例子)的成本函数相当类似:寻求最低成本即似寻求最低能量。由此,他们发展出以 Metropolis  方法为本的一套算法,并用其来解决组合问题等的寻求最优解。

 

几乎同时,欧洲物理学家 V.Carny 也发表了几乎相同的成果,但两者是各自独立发现的;只是Carny“运气不佳”,当时没什么人注意到他的大作;或许可以说,《Science》杂志行销全球,“曝光度”很高,素负盛名,而Carny却在另外一本发行量很小的专门学术期刊《J.Opt.Theory Appl.》发表其成果因而并未引起应有的关注。

 

Kirkpatrick等人受到Metropolis等人用蒙特卡罗模拟的启发而发明了“模拟退火”这个名词,因为它和物体退火过程相类似。寻找问题的最优解(最值)即类似寻找系统的最低能量。因此系统降温时,能量也逐渐下降,而同样意义地,问题的解也“下降”到最值。

 

一、什么是退火——物理上的由来

 

在热力学上,退火(annealing)现象指物体逐渐降温的物理现象,温度愈低,物体的能量状态会低;够低后,液体开始冷凝与结晶,在结晶状态时,系统的能量状态最低。大自然在缓慢降温(亦即,退火)时,可“找到”最低能量状态:结晶。但是,如果过程过急过快,快速降温(亦称「淬炼」,quenching)时,会导致不是最低能态的非晶形。

 

如下图所示,首先(左图)物体处于非晶体状态。我们将固体加温至充分高(中图),再让其徐徐冷却,也就退火(右图)。加温时,固体内部粒子随温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小(此时物体以晶体形态呈现)。

技术分享图片

似乎,大自然知道慢工出细活:缓缓降温,使得物体分子在每一温度时,能够有足够时间找到安顿位置,则逐渐地,到最后可得到最低能态,系统最安稳。

 

二、模拟退火(Simulate Anneal)

 

如果你对退火的物理意义还是晕晕的,没关系我们还有更为简单的理解方式。想象一下如果我们现在有下面这样一个函数,现在想求函数的(全局)最优解。如果采用Greedy策略,那么从A点开始试探,如果函数值继续减少,那么试探过程就会继续。而当到达点B时,显然我们的探求过程就结束了(因为无论朝哪个方向努力,结果只会越来越大)。最终我们只能找打一个局部最后解B。

 技术分享图片

模拟退火其实也是一种Greedy算法,但是它的搜索过程引入了随机因素。模拟退火算法以一定的概率来接受一个比当前解要差的解,因此有可能会跳出这个局部的最优解,达到全局的最优解。以上图为例,模拟退火算法在搜索到局部最优解B后,会以一定的概率接受向右继续移动。也许经过几次这样的不是局部最优的移动后会到达B 和C之间的峰点,于是就跳出了局部最小值B。

 

根据Metropolis准则,粒子在温度T时趋于平衡的概率为exp(-ΔE/(kT)),其中E为温度T时的内能,ΔE为其改变数,k为Boltzmann常数。Metropolis准则常表示为

Metropolis准则表明,在温度为T时,出现能量差为dE的降温的概率为P(dE),表示为:P(dE) = exp( dE/(kT) )。其中k是一个常数,exp表示自然指数,且dE<0。所以P和T正相关。这条公式就表示:温度越高,出现一次能量差为dE的降温的概率就越大;温度越低,则出现降温的概率就越小。又由于dE总是小于0(因为退火的过程是温度逐渐下降的过程),因此dE/kT < 0 ,所以P(dE)的函数取值范围是(0,1) 。随着温度T的降低,P(dE)会逐渐降低。

我们将一次向较差解的移动看做一次温度跳变过程,我们以概率P(dE)来接受这样的移动。也就是说,在用固体退火模拟组合优化问题,将内能E模拟为目标函数值 f,温度T演化成控制参数 t,即得到解组合优化问题的模拟退火演算法:由初始解 i 和控制参数初值 t 开始,对当前解重复“产生新解→计算目标函数差→接受或丢弃”的迭代,并逐步衰减 t 值,算法终止时的当前解即为所得近似最优解,这是基于蒙特卡罗迭代求解法的一种启发式随机搜索过程。退火过程由冷却进度表(Cooling Schedule)控制,包括控制参数的初值 t 及其衰减因子Δt 、每个 t 值时的迭代次数L和停止条件S。

 

总结起来就是:

  • f( Y(i+1) ) <= f( Y(i) )  (即移动后得到更优解),则总是接受该移动;
  • f( Y(i+1) ) > f( Y(i) )  (即移动后的解比当前解要差),则以一定的概率接受移动,而且这个概率随着时间推移逐渐降低(逐渐降低才能趋向稳定)相当于上图中,从B移向BC之间的小波峰时,每次右移(即接受一个更糟糕值)的概率在逐渐降低。如果这个坡特别长,那么很有可能最终我们并不会翻过这个坡。如果它不太长,这很有可能会翻过它,这取决于衰减 t 值的设定。

 

关于普通Greedy算法与模拟退火,有一个有趣的比喻:

    • 普通Greedy算法:兔子朝着比现在低的地方跳去。它找到了不远处的最低的山谷。但是这座山谷不一定最低的。这就是普通Greedy算法,它不能保证局部最优值就是全局最优值。
    • 模拟退火:兔子喝醉了。它随机地跳了很长时间。这期间,它可能走向低处,也可能踏入平地。但是,它渐渐清醒了并朝最低的方向跳去。这就是模拟退火。

     

      通过一个实例来编程演示模拟退火的执行。特别地,我们这里所采用的实例是著名的“旅行商问题”(TSP,Traveling Salesman Problem),它是哈密尔顿回路的一个实例化问题,也是最早被提出的NP问题之一。

     TSP是一个最常被用来解释模拟退火用法的问题,因为这个问题比较有名,我们这里不赘言重述,下面直接给出C++实现的代码:

  1 #include <iostream>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <algorithm>
  5 #include <stdio.h>
  6 #include <time.h>
  7 #include <math.h>
  8 
  9 #define N     30      //城市数量
 10 #define T     3000    //初始温度
 11 #define EPS   1e-8    //终止温度
 12 #define DELTA 0.98    //温度衰减率
 13 
 14 #define LIMIT 1000   //概率选择上限
 15 #define OLOOP 20    //外循环次数
 16 #define ILOOP 100   //内循环次数
 17 
 18 using namespace std;
 19 
 20 //定义路线结构体
 21 struct Path
 22 {
 23     int citys[N];
 24     double len;
 25 };
 26 
 27 //定义城市点坐标
 28 struct Point
 29 {
 30     double x, y;
 31 };
 32 
 33 Path bestPath;        //记录最优路径
 34 Point p[N];       //每个城市的坐标
 35 double w[N][N];   //两两城市之间路径长度
 36 int nCase;        //测试次数
 37 
 38 double dist(Point A, Point B)
 39 {
 40     return sqrt((A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y));
 41 }
 42 
 43 void GetDist(Point p[], int n)
 44 {
 45     for(int i = 0; i < n; i++)
 46         for(int j = i + 1; j < n; j++)
 47             w[i][j] = w[j][i] = dist(p[i], p[j]);
 48 }
 49 
 50 void Input(Point p[], int &n)
 51 {
 52     scanf("%d", &n);
 53     for(int i = 0; i < n; i++)
 54         scanf("%lf %lf", &p[i].x, &p[i].y);
 55 }
 56 
 57 void Init(int n)
 58 {
 59     nCase = 0;
 60     bestPath.len = 0;
 61     for(int i = 0; i < n; i++)
 62     {
 63         bestPath.citys[i] = i;
 64         if(i != n - 1)
 65         {
 66             printf("%d--->", i);
 67             bestPath.len += w[i][i + 1];
 68         }
 69         else
 70             printf("%d\n", i);
 71     }
 72     printf("\nInit path length is : %.3lf\n", bestPath.len);
 73     printf("-----------------------------------\n\n");
 74 }
 75 
 76 void Print(Path t, int n)
 77 {
 78     printf("Path is : ");
 79     for(int i = 0; i < n; i++)
 80     {
 81         if(i != n - 1)
 82             printf("%d-->", t.citys[i]);
 83         else
 84             printf("%d\n", t.citys[i]);
 85     }
 86     printf("\nThe path length is : %.3lf\n", t.len);
 87     printf("-----------------------------------\n\n");
 88 }
 89 
 90 Path GetNext(Path p, int n)
 91 {
 92     Path ans = p;
 93     int x = (int)(n * (rand() / (RAND_MAX + 1.0)));
 94     int y = (int)(n * (rand() / (RAND_MAX + 1.0)));
 95     while(x == y)
 96     {
 97         x = (int)(n * (rand() / (RAND_MAX + 1.0)));
 98         y = (int)(n * (rand() / (RAND_MAX + 1.0)));
 99     }
100     swap(ans.citys[x], ans.citys[y]);
101     ans.len = 0;
102     for(int i = 0; i < n - 1; i++)
103         ans.len += w[ans.citys[i]][ans.citys[i + 1]];
104     cout << "nCase = " << nCase << endl;
105     Print(ans, n);
106     nCase++;
107     return ans;
108 }
109 
110 void SA(int n)
111 {
112     double t = T;
113     srand((unsigned)(time(NULL)));
114     Path curPath = bestPath;
115     Path newPath = bestPath;
116     int P_L = 0;
117     int P_F = 0;
118     while(1)       //外循环,主要更新参数t,模拟退火过程
119     {
120         for(int i = 0; i < ILOOP; i++)    //内循环,寻找在一定温度下的最优值
121         {
122             newPath = GetNext(curPath, n);
123             double dE = newPath.len - curPath.len;
124             if(dE < 0)   //如果找到更优值,直接更新
125             {
126                 curPath = newPath;
127                 P_L = 0;
128                 P_F = 0;
129             }
130             else
131             {
132                 double rd = rand() / (RAND_MAX + 1.0);
133                 //如果找到比当前更差的解,以一定概率接受该解,并且这个概率会越来越小
134                 if(exp(dE / t) > rd && exp(dE / t) < 1)
135                     curPath = newPath;
136                 P_L++;
137             }
138             if(P_L > LIMIT)
139             {
140                 P_F++;
141                 break;
142             }
143         }
144         if(curPath.len < bestPath.len)
145             bestPath = curPath;
146         if(P_F > OLOOP || t < EPS)
147             break;
148         t *= DELTA;
149     }
150 }
151 
152 int main(int argc, const char * argv[]) {
153 
154     freopen("TSP.data", "r", stdin);
155     int n;
156     Input(p, n);
157     GetDist(p, n);
158     Init(n);
159     SA(n);
160     Print(bestPath, n);
161     printf("Total test times is : %d\n", nCase);
162     return 0;
163 }

注意由于是基于蒙特卡洛的方法,所以上面代码每次得出的结果并不完全一致。你可以通过增加迭代的次数来获得一个更优的结果。

我们这里需要说明的是,在之前的文章里,我们用求最小值的例子来解释模拟退火的执行:如果新一轮的计算结果更前一轮之结果更小,那么我们就接受它,否则就以一个概率来拒绝或接受它,而这个拒绝的概率会随着温度的降低(也即是迭代次数的增加)而变大(也就是接受的概率会越来越小)。

但现在我们面对一个TSP问题,我们如何定义或者说如何获取下一轮将要被考察的哈密尔顿路径呢?在一元函数最小值的例子中,下一轮就是指向左或者向右移动一小段距离。而在TSP问题中,我们可以采用的方式其实是很多的。上面代码中GetNext()函数所采用的方式是随机交换两个城市在路径中的顺序。例如当前路径为 A->B->C->D->A,那么下一次路径就可能是A->D->C->B->A,即交换B和D。而在文献【3】中,作者采样的代码如下(我们截取一个片段,完整代码请参考原文):

public class Tour{
    ... ...
    // Creates a random individual
    public void generateIndividual() {
        // Loop through all our destination cities and add them to our tour
        for (int cityIndex = 0; cityIndex < TourManager.numberOfCities(); cityIndex++) {
          setCity(cityIndex, TourManager.getCity(cityIndex));
        }
        // Randomly reorder the tour
        Collections.shuffle(tour);
    }
    ... ...
}

    可见作者的方法是把上一轮路径做一个随机的重排(这显然也是一种策略)。

TSP.data的数据格式如下,第一行的数字表示一个有多少座城市,第2至最后一行,每行有两个数字表示,城市的坐标(平面直角坐标系)。例如: 

20 80 
16 84 
23 66 
62 90 
11 9 
35 28 
最后请读者自行编译执行程序并观察分析输出的结果。

模拟退火算法(转自-ranjiewen的博客园!)

标签:就是   ini   public   using   准则   距离   注意   dual   comm   

原文地址:https://www.cnblogs.com/fushao2yyj/p/8322331.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!