本文内容均以求最小化问题为背景进行阐述(代码实例除外)

算法简介

模拟退火算法是一种通用的全局优化算法,为具有NP复杂性的问题提供有效的近似解,其克服了其他优化过程容易陷入局部最小的缺陷和对初值的依赖,目前被广泛的应用于生产调度、控制工程、机器学习、神经网络、模式识别、图像处理、离散/连续变量的结构优化问题等领域。它能有效地求解常规优化方法难以解决的组合优化问题和复杂函数优化问题。

模拟退火算法的思想

模拟退火算法在搜索区间随机选择点,再利用Metropolis抽样准则,使随机选择的点逐渐收敛于局部最优解。Metropolis算法中一个重要控制参数是温度,该参数控制了随机过程向局部或全局最优解移动的快慢。
Metropolis是一种有效的重点抽样法,其为:系统从一个能量状态变化到另一个状态时,相应的能量从E1变化为E2,其概率为:
在这里插入图片描述

如果E2<E1,系统接受此状态;否则,以一个随机的概率接受或丢弃此状态。状态2被接受的概率为
在这里插入图片描述

模拟退火算法的流程

模拟退火是一个双层嵌套循环,外层是温度T的迭代,内层是每个温度下的迭代次数(为保证每一个温度下搜索过程的彻底)。

以最小化问题为例,模拟退火算法的流程如下:

  • (1)初始化:初始温度T0,初始解X0,每个温度T下的迭代次数L;
  • (2)对k = 1,… ,L做(3)——(6)步;
  • (3)产生新解X’;
  • (4)计算增量f(X’) - f(X),其中f(X) 这里可以理解为优化问题的目标函数;
  • (5)当Δf(x) < 0时,直接接受X’作为新解,否则以概率exp(-Δf(x)/T)接收机X’作为新的当前解;
  • (6)如果满足终止条件,则输出当前解为最优解,结束程序;
  • (7)T逐渐减小,且T→0,然后转(2)。


    流程图
    图来自于智能优化算法及其MATLAB实例,其中ΔE
    模拟退火算法流程图

说点“人话”

模拟退火算法是用来求解最优化的算法,也就是求解函数的最大值最小值问题。其通过迭代来寻找最值,每次迭代过程中随机生成一个新的可行解(不是完全随机的,每个问题有一个生成函数来产生新的可行解,对于不同的问题,这个生成函数是不一样的,对于你要解决的问题可以通过查找具有权威性的相关文献来确定生成函数),通过判断新生成的可行解的目标函数值当前的最优解的目标函数值的大小来进行后续的计算(此处以求解最小值问题为例),当新生成的可行解的目标函数值小于当前最优解的目标函数值时,我们认为新生成的可行解更优,所以我们对当前的最优解进行替换,而后继续进行迭代;当新生成的可行解的目标函数值大于当前最优解的目标函数时,我们不能直接放弃这个可行解,而是以一定的概率接受这个可行解(优化问题不是只有最值点,还有极值点,当出现大于的情况直接放弃,就很大的可能会出现找到的是极值点而非最值点),只有这样才有跳出极值点的可能性,而这里的概率也就是Metropolis抽样准则(当然你也可以选择其他的接受准则,但该方法是最常用的),可以看到Metropolis抽样准则的函数表达式是与两目标函数的差值以及温度T(在实际应用中T就是我们迭代循环的一个参数,也就是说,实际T就相当于迭代次数或者时间t)有关的。T一定时,两目标函数的差值越小,P越大,我们越倾向于接受新生成的可行解;而两目标函数的差值一定时,T越大,P越小,我们越不易接受这个新生成的可行解,也就体现为在搜索前期,我们可以接受差别很大的两个解,也就是全局搜索,而后期就更倾向于在局部进行搜索,这样即考虑了极值点排除了局部最优解的情况,也保证了算法后期可以收敛。

算法实例

求解一元函数

在这里插入图片描述

先随机选择一组初始解的取值,然后进行迭代,生成一组新的可行解,计算后一组解与前一组解的目标函数差值,当两者的差值为负时,接受新生成的可行解为最优解,继续进行迭代;否则,结合当前温度计算可接受概率,并生成一个0~1的随机数,若概率大于该随机数,则接受当前新生成的可行解,否则放弃这个解。如此不断迭代,直到满足终止条件。

仿真过程如下:
在这里插入图片描述

clear all;
close all;
clc;
D = 10;         %变量维数
Xs = 20;        %上限
Xx = -20;       %下限
L = 200;        %马尔可夫链长度
K = 0.998;      %衰减参数
S = 0.01;       %步长因子    后面用来生成新解用的
T = 100;        %初始温度
YZ = 1e-8;      %容差
P = 0;          %Metropolis过程中总接受点

PreX = rand(D,1)*(Xs-Xx) + Xx;
PreBestX = PreX;
PreX = rand(D,1) * (Xs -Xx) + Xx;
BestX = PreX;

%%%%%%退火迭代过程%%%%%
deta = abs(func1(BestX)-func1(PreBestX));
while (deta > YZ) && (T>0.001)
    T = K*T;
    for i = 1:L
        NextX = PreX + S * (rand(D,1) * (Xs-Xx)+ Xx);
        for ii = 1:D
            if NextX(ii) > Xs | NextX(ii) <Xx      % 依次遍历新解的每一个值,哪个超出了解的定义域,则重新生成该解
                NextX(ii) = PreX(ii) + S * (rand * (Xs-Xx)+ Xx);
            end
        end
        if (func1(BestX) > func1(NextX))
            PreBestX = BestX;
            BestX = NextX;
        end
        if (func1(PreX) - func1(NextX) > 0 )
            PreX  = NextX;
            P = P+1;
        else
            changer = -1*(func1(NextX) - func1(PreX))/T;
            p1 = exp(changer);
            if p1 >rand
                PreX = NextX;
                P = P +1;
            end
        end
    trace(P + 1) = func1(BestX);
    end
    deta = abs(func1(BestX) - func1(PreBestX));
end
disp('最小值在点:');
BestX
disp('最小值为:');
func1(BestX)
figure
plot(trace(2:end))
xlabel('迭代次数')
ylabel('目标函数值')
title('适应度进化函数')
#!/usr/bin/env python
# coding: utf-8

import random
import math
import matplotlib.pyplot as plt
import matplotlib

# matplotlib默认不支持中文,下面的otf文档要放在代码执行文件下(可以自己更换别的字体)
zhfont1 = matplotlib.font_manager.FontProperties(fname="SourceHanSansSC-Bold.otf")
# 详参考菜鸟教程:https://www.runoob.com/matplotlib/matplotlib-label.html

#定义目标函数
def func1(X):
    result = 0
    for x in X:
        result += x*x
    return result

#初始化变量
D = 10         #变量维度
Xs = 20        #上限
Xx = -20       #下限
L = 500        #马尔科夫链的长度,即内循环的次数
K = 0.998      #温度衰减因子
S = 0.01       #生成新解的步长因子
T = 100        #初始温度
P = 0          #metropolis过程中总接受的点
YZ = 1e-8      #容差
PreX = [0] *D      #用来存储当前解
trace = []         #用来存储每个温度下的最优值
BestX = [0]*D      #用来存储最优解

for index in range(len(PreX)):
    PreX[index] = random.random()*(Xs-Xx) +Xx
PreBestX = PreX      #上一个最优解

for index in range(len(BestX)):
    BestX[index] = random.random()*(Xs-Xx) +Xx

# # 退火迭代过程
deta = abs(func1(BestX)-func1(PreBestX))    #计算两次目标函数的差值(模拟退火的能量差)
while(deta > YZ) and (T>0.001):             #进行循环操作,此处有两个终止条件:deta小于所能接受的最小值,温度小于所能接受的最小值
    T = K *T                                #退温
    for i in range(L):                      #同一温度下的多次搜索
        NextX = [e + S*(random.random()*(Xs-Xx)+ Xx) for e in PreX]      #基于当前解生成新解
        for ii in range(D):                           #判断新生成的解是否符合上下界约束,不符合则重新生成
            if NextX[ii] > Xs or NextX[ii] < Xx:
                NextX[ii] = PreX[ii] + S*(random.random()*(Xs-Xx)+Xx)
        if (func1(BestX) > func1(NextX)):      #如果当前目标函数值小于最优目标函数值,则将最优赋给上一个最优,下一个为最优
            PreBestX = BestX
            BestX = NextX
        if (func1(PreX) - func1(NextX) > 0):       #正常的找到一个更小的解
            PreX = NextX
            P += 1
        else:
            changer = -1*(func1(NextX) -func1(PreX))/T        #当下一个生成的解更大时,以一定的概率接受这个新解
            p1 = math.exp(changer)
            if p1 > random.random():
                PreX = NextX
                P += 1
    trace.append(func1(BestX))           #将一次温度下搜索出的最优值加入trace
    deta = abs(func1(BestX) - func1(PreBestX))          #计算deta,进入下次循环


# # 画图,输出最后结果
print('最小值在点')
print(BestX)
print('最小值为')
print(func1(BestX))
plt.plot(trace[2:])
plt.xlabel('迭代次数', fontproperties=zhfont1)
plt.ylabel('目标函数值', fontproperties=zhfont1)
plt.title('适应度进化函数', fontproperties=zhfont1)

上述matlab代码来自电子工业出版社《智能优化算法及其MATLAB实例》第二版,python代码根据MATLAB代码进行改写。


求解二元函数

在这里插入图片描述
仿真过程如下:
在这里插入图片描述

#!/usr/bin/env python
# coding: utf-8

import random
import matplotlib.pyplot as plt
import math
 
# 定义目标函数
def func2(x,y):
    return 5 * math.cos(x*y) + x * y + y*y*y

# 变量上下界
XMAX = 5
XMIN = -5
YMAX = 5
YMIN = -5

L = 200     #马尔科夫链长度
K = 0.99    #温度衰减因子
S = 0.02    #生成新解的步长因子
T = 100     #初始温度
YZ = 1e-8   #容差
P = 0       #metropolis过程中总接受的点

#随机生成一组初始解
PreX = random.random() * (XMAX - XMIN) + XMIN
PreY = random.random() * (YMAX - YMIN) + YMIN
#上一组初始解
PreBestX = PreX    
PreBestY = PreY
#随机生成一组最优解
BestX = random.random() * (XMAX - XMIN) + XMIN
BestY = random.random() * (YMAX - YMIN) + YMIN


trace = []      #用来存储每个温度下的最优值
deta = abs(func2(BestX,BestY) - func2(PreBestX,PreBestY))       #计算两目标函数的差值
while(deta > YZ) and (T > 0.001):              # 开始循环迭代
    T = K * T   #退温过程
    for i in range(L):          # 每个温度下进行多次搜索,以确保搜索彻底
        p = 0;                  
        while p == 0:           # 基于当前解生成新解
            NextX = PreX + S * (random.random() * (XMAX - XMIN) + XMIN)
            NextY = PreY + S * (random.random() * (YMAX - YMIN) + YMIN)
            if (NextX >= XMIN and NextX <= XMAX and NextY >= YMIN and NextY <= YMAX):      # 新解的约束判断,新解不满足约束时就要一直重新生成
                p = 1
        if (func2(BestX,BestY) > func2(NextX,NextY)):       # 用当前的最优值与新生成的可行解的目标函数值进行比较
            PreBestX = BestX                                #书上的代码是这么写的,但是我感觉好像这里很奇怪呢
            PreBestY = BestY                                #好像是每生成一个新解都进行比较,后面的接受不接受并不是直接影响是否接受新生成解的目标函数值,
            BestX = NextX                                   #而是通过间接的影响新解的生成来影响目标函数值的变化(这里注释写的稀烂,不过我好像懂什么意思了,不知道你懂没有)
            BestY = NextY
        if(func2(PreX,PreY) - func2(NextX,NextY) > 0):
            PreX = NextX
            PreY = NextY
            P += 1
        else:
            changer = -1*(func2(NextX,NextY) - func2(PreX,PreY))/T         #Metropolis准则,判断是否接受当前生成的新可行解
            p1 = math.exp(changer)
            if p1> random.random():
                PreX = NextX 
                PreY = NextY
                P = P+ 1
                
        trace.append(func2(BestX,BestY))                     #将经过当前温度搜索后的最优值存储进trace
        deta = abs(func2(BestX,BestY) - func2(PreBestX,PreBestY))


print('最小值在点:')
print(BestX,BestY)
print('最小值为:')
print(func2(BestX,BestY))            
plt.plot(trace[2:])
plt.xlabel('number of iterations')
plt.ylabel('objective value')
plt.title('adaptation value evolutionary curve')

求解TSP问题

在这里插入图片描述
仿真过程如下:
在这里插入图片描述

#!/usr/bin/env python
# coding: utf-8

import math
import random
import matplotlib.pyplot as plt

def func3(route,city):     # 这里的route是一个表示路线的数组
    length = 0
    n = len(city)
    for i in range(n-1):
        length += math.sqrt((city[route[i]][0] - city[route[i+1]][0])**2 +
                       (city[route[i]][1] - city[route[i+1]][1])**2)
    length += math.sqrt((city[route[-1]][0] - city[route[0]][0])**2 +
                       (city[route[-1]][1] - city[route[0]][1])**2)
    return length

# 用dict来表示city的坐标
C = {1:[1304,2312],2:[3639,1315],3:[4177,2244],4:[3712,1399],5:[3488,1535],6:[3326,1556],
     7:[3238,1229],8:[4196,1044],9:[4312,790],10:[4386,570],11:[3007,1970],12:[2562,1756]
    ,13:[2788,1491],14:[2381,1676],15:[1332,695],16:[3715,1678],17:[3918,2179],18:[4061,2370]
    ,19:[3780,2212],20:[3676,2578],21:[4029,2838],22:[4263,2931],23:[3429,1908],24:[3507,2376]
    ,25:[3394,2643],26:[3439,3201],27:[2935,3240],28:[3140,3550],29:[2545,2357],30:[2778,2826]
    ,31:[2370,2975]}        #31个省会城市坐标

n = len(C)        #TSP问题的规模,即城市数目
T = 100 * n
L = 100
K = 0.99
l = 1         # 统计迭代次数
total_route_length = []
route = random.sample(range(1,n+1), n)       # 随机生成一条路线
total_route_length.append(func3(route,C))# 每次迭代后的路线长度
plt.figure(1)
while T > 0.001:
    for i in range(L):
        len1 = func3(route,C)
        # 随机置换前面生成的路线中的两个城市的顺序
        p1 = math.floor(0 + n * random.random())
        p2 = math.floor(0 + n * random.random())
        while p1 == p2:
            p1 = math.floor(0 + n * random.random())
            p2 = math.floor(0 + n * random.random())
        temp_route = route
        temp_city = temp_route[p1]
        temp_route[p1] = temp_route[p2]
        temp_route[p2] = temp_city
        len2 = func3(temp_route,C)
        delta_e = len2 - len1
        if delta_e < 0:
            route = temp_route
        else:
            if math.exp(-delta_e/T) > random.random():
                route = temp_route
    l = l+1
    total_route_length.append(func3(route,C))
    T = K*T
    for i in range(n-1):
        plt.plot([C[route[i]][0],C[route[i+1]][0]],[C[route[i]][1],C[route[i+1]][1]],'bo-')
        plt.ion()
    plt.plot([C[route[-1]][0],C[route[0]][0]],[C[route[-1]][1],C[route[0]][1]],'bo-')
    plt.ioff()  # 关闭hold状态  
    plt.pause(0.005)
plt.figure(2)
plt.plot(total_route_length)
plt.xlabel('num of interation')
plt.ylabel('objective value')
plt.title('Fitness evolution curve')
plt.show()

模拟退火求TSP时,产生新解的方法

TSP问题如何产生新解

对上述代码进行拓展,用生成随机数的方式来随机选择生成新解的方法

#!/usr/bin/env python
# coding: utf-8

import math
import random
import matplotlib.pyplot as plt

def func3(route,city):     # 这里的city是一个表示路线的数组
    length = 0
    n = len(city)
    for i in range(n-1):
        length += math.sqrt((city[route[i]][0] - city[route[i+1]][0])**2 +
                       (city[route[i]][1] - city[route[i+1]][1])**2)
    length += math.sqrt((city[route[-1]][0] - city[route[0]][0])**2 +
                       (city[route[-1]][1] - city[route[0]][1])**2)
    return length

def gen_new_route(route):
    n = len(route)
    p1 = 0.33   #使用交换法产生新路径的概率
    p2 = 0.33   #使用移位法产生新路径的概率
    r = random.random()
    if r < p1:        # 使用交换法产生新路径
        c1 = math.floor(0 + n * random.random())
        c2 = math.floor(0 + n * random.random())
        while c1 == c2:
            c1 = math.floor(0 + n * random.random())
            c2 = math.floor(0 + n * random.random())
        temp_route = route
        temp_city = temp_route[c1]
        temp_route[c1] = temp_route[c2]
        temp_route[c2] = temp_city
    elif r < p1 + p2:        # 使用移位法产生新路径
        c1 = math.floor(0 + n * random.random())
        c2 = math.floor(0 + n * random.random())
        c3 = math.floor(0 + n * random.random())
        C = [c1,c2,c3]
        C.sort()
        c1,c2,c3 = C[0],C[1],C[2]
        temp1 = route[0:c1]
        temp2 = route[c1:c2]
        temp3 = route[c2:c3]
        temp4 = route[c3:]
        temp_route = temp1 + temp2+ temp3 + temp4
        
    else:       # 使用倒置法产生新路径
        c1 = math.floor(0 + n * random.random())
        c2 = math.floor(0 + n * random.random())
        if c1 > c2:
            temp = c2
            c2 = c1
            c1 = temp
        temp1 = route[0:c1]
        temp2 = route[c1:c2]
        temp3 = route[c2:]
        temp_route = temp1 + temp2 + temp3
    return temp_route

# 用dict来表示city的坐标
C = {1:[1304,2312],2:[3639,1315],3:[4177,2244],4:[3712,1399],5:[3488,1535],6:[3326,1556],
     7:[3238,1229],8:[4196,1044],9:[4312,790],10:[4386,570],11:[3007,1970],12:[2562,1756]
    ,13:[2788,1491],14:[2381,1676],15:[1332,695],16:[3715,1678],17:[3918,2179],18:[4061,2370]
    ,19:[3780,2212],20:[3676,2578],21:[4029,2838],22:[4263,2931],23:[3429,1908],24:[3507,2376]
    ,25:[3394,2643],26:[3439,3201],27:[2935,3240],28:[3140,3550],29:[2545,2357],30:[2778,2826]
    ,31:[2370,2975]}        #31个省会城市生活

n = len(C)        #TSP问题的规模,即城市数目
T = 100 * n
L = 100
K = 0.99
l = 1         # 统计迭代次数
total_route_length = []
route = random.sample(range(1,n+1), n)       # 随机生成一条路线
total_route_length.append(func3(route,C))# 每次迭代后的路线长度
plt.figure(1)
while T > 0.001:
    for i in range(L):
        len1 = func3(route,C)
        # 随机选择三种生成新路的一种方式来生成新路线
        temp_route = gen_new_route(route)
        len2 = func3(temp_route,C)
        delta_e = len2 - len1
        if delta_e < 0:
            route = temp_route
        else:
            if math.exp(-delta_e/T) > random.random():
                route = temp_route
    l = l+1
    total_route_length.append(func3(route,C))
    T = K*T
    for i in range(n-1):
        plt.plot([C[route[i]][0],C[route[i+1]][0]],[C[route[i]][1],C[route[i+1]][1]],'bo-')
        plt.ion()
    plt.plot([C[route[-1]][0],C[route[0]][0]],[C[route[-1]][1],C[route[0]][1]],'bo-')
    plt.ioff()  # 关闭hold状态  
    plt.pause(0.005)
plt.figure(2)
plt.plot(total_route_length)
plt.xlabel('num of interation')
plt.ylabel('objective value')
plt.title('Fitness evolution curve')
plt.show()

参考资料

  • https://zhuanlan.zhihu.com/p/382426984
  • 电子工业出版社《智能优化算法及其MATLAB实例》第二版
  • 清风数学建模模拟退火算法(https://www.bilibili.com/video/BV1hK41157JL/?spm_id_from=333.337.search-card.all.click)
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐