1 模拟退火法的理解

模拟退火法作为一种启发式搜索方法,与蒙特卡罗法、枚举法等盲目搜索方法相以区别的地方,即它可以利用搜索过程中的信息来改进搜索的策略,因此启发式搜索方法的特点就是,它有助于加速求解的过程,它可以找到较优解,但是却不一定能找到最优解,这个结论在之后的内容中再加以说明。

1.1 爬山法

爬山法作为一种局部寻优算法,主要的步骤:
(1)在解空间中随机生成一个初始解;
(2)在初始解的位置,分别向左邻域和右领域前进一步;
(3)比较在不同走法下的目标函数值,根据所需的最值要求,选择行进的方向;
(4)不断重复以上的操作,直到找到一个局部的极大值点,并结束搜索。
从以上图中,可看出,若初始解A点所选的位置直接影响最终寻优的结果,以图中状态为例,A点最终会寻找到局部最大值,而错过全局最大值。因此,这是爬山法的缺陷。分析爬山法陷入局部寻优的陷阱中,主要是因为其在找新解的时候,对于暂时不理想的解直接舍弃掉了,按照这个步骤循环,从寻解的范围越来越小,有点贪心算法的意思。因此,要解决爬山法的关键,就是以一定的概率接受不理想的新解。

1.2 求解函数最值问题思想
利用以上的思想,来求解函数最值问题:
(1)在初解xi附近随机找一个新解xj,对应的函数值为f(xj),分情况对比函数值f(xi)和f(xj);
(2)若f(xj) > f(xi),则接受新解,以新解代替原来的初解,继续循环下去;
(3)若f(xj) < f(xi),则以一个可接受的概率p来接受新解,并以新解代替原来的解,继续循环下去。概率p∈[0,1],当f(xi)和f(xj)的值越接近,则p越大。

因此,问题的关键是找到一个符合要求描述p的函数。由以上知,p正比于。
寻找一个函数,值域为[0,1],函数值y与|f(xj)-f(xi)|呈反比,则很容易想到指数函数

为继续构造函数,还需要对概率p进行更深一步的理解:
(1)接受新解的概率越大,则解在空间中搜索的范围越大;
(2)在整个解的搜索的过程中,前期解的搜索范围要尽可能的大一些,以避免陷入局部最优解,而在后期搜索范围则需要相应的小一些。
因此,构造的概率函数p应该是

其中Ct是关于时间t的函数。时间越长,Ct越大。
将上述函数代入求解函数最值的思路中,求解流程为:
(1)随机生成一个初解A,并计算解A对应的函数值f(A);
(2)在A位置附近随机生成一个解B,并计算解B的函数值f(B),对f(A)和f(B)比较的大小分情况讨论;
(3)若f(B) > f(A),则将解B赋给A,然后重复以上步骤;若f(B) ≤ f(A),那么计算接受B的概率

然后随机生成一个[0,1]之间的数r,若r<p,就将解B赋值给A,然后重复上面的步骤;否则继续执行步骤(2),然后继续下去。

在以上的操作中,有几个关键的地方需要特别注意:
(1)Ct函数如何进行设置
Ct函数采用模拟退火中特有的温度下降公式进行构造,其中温度下降公式为:

其中α的范围是[0,1],α取0.95,因此,时刻t的温度为:


​取

由上述可知,Ct是关于t的单调递增函数,因此以上构造的Ct函数符合要求。概率p的表达式为:

(2)搜索过程的循环以及结束条件
为了保证搜索过程的彻底性,在同一温度下,需要进行多次搜索;再达到设置的搜索次数后,之后再降低温度,重新再来新一轮的循环。
至于搜索过程的结束条件,一般有三种:
(a)达到了指定迭代的次数;(b)达到了指定的下降的温度;(c)搜索到的最优解在M次迭代后其值没有发生变化。

(3)如何产生新解,是最重要的部分
关于如何产生新解的方法,对于这个问题,只能具体问题具体分析。
以求解函数最值问题为例,新解的产生方法采用的是Matlab中自带的函数。产生新解的具体规则如下:
假设当前解为

首先生成一组随机数

其中y服从N(0,1),对y进行标准化处理,得

其中

接着进行以下操作:
(1)新解产生的计算公式:

(2)检查新解是否处于上下界内,

   (a)若满足条件

直接用新解赋值

(b)若满足条件

则令新解为

其中r是在区间[0,1]上均匀分布的随机数;
  (c)若满足条件

则令新解为

其中r是在区间[0,1]上均匀分布的随机数。

2 求解函数的最值问题算例

2.1 单个自变量的题目:
求函数y=sin(x)+cos(5x)在区间[-1,1]内的最大值。

(1)绘制函数

x=-1:0.01:1;
y=sin(x)+cos(5*x);
figure(1)
plot(x,y,'b');
hold on

(2)参数初始化

narvs =1; % 变量个数
T0 = 100; % 初始温度
T = T0; % 迭代中温度会发生改变,第一次迭代时温度就是T0
maxgen = 500; % 最大迭代次数
Lk = 200; % 每个温度下的迭代次数
alfa = 0.95; % 温度衰减系数
x_lb = -1; % x的下界
x_ub = 1; % x的上界

(3)生成新解

x0 = x_lb+(x_ub-x_lb)*rand(1,narvs);
h = scatter(x0,Obj_fun1(x0),'*r'); % scatter 是绘制二维散点图的函数,h是图形的句柄

其中Obj_fun1是该函数名

(4)保存中间值

iter_x = x0;
iter_y = Obj_fun1(x0);

(5)重点:模拟退火过程

for iter = 1:maxgen  %外循环,最大迭代次数
    for i=1:Lk  % 内循环,每个温度下的迭代次数
        %title(['当前温度:',num2str(T)])
        y0 = Obj_fun1(x0);  % 计算当前解的函数值
        
        y = randn(1,narvs);  % 生成1行narvs列的N(0,1)的随机数
        z = y/sqrt(sum(y.^2));  % 对y进行标准化处理
        x_new = x0 + z*T;     %采用Matlab自带的函数
        % 如果新解的位置超出了定义域,就对其进行调整,即与以上产生新解步骤相对应的操作
        for j =1:narvs
            if x_new(j) < x_lb(j)
                r = rand(1);
                x_new(j) = r*x_lb(j)+(1-r)*x0(j);
            elseif x_new(j) > x_ub(j)
                r =rand(1);
                x_new(j) = r*x_ub(j)+(1-r)*x0(j);
            end
        end
        x1 = x_new;
        y1 = Obj_fun1(x1);
       
        if y1 > y0 % 新解函数值大于当前解的函数值
            
            x0 = x1; %更新当前解为新解
            iter_x =[iter_x;x1];  % 将新找到的x1添加到中间结果中
            iter_y =[iter_y;y1];  % 将新找到的y1添加到中间结果中
        else
            p = exp(-(y0-y1)/T);  % 根据Metropolis准则计算一个概率
            
            if rand(1) < p % 生成一个随机数和这个概率进行比较,如果该随机数小于这个概率
                
                x0 = x1;
            end
        end
        
        h.XData = x0; % 更新散点图句柄的x轴的数据
        h.YData = y0; % 更新散点图句柄的y轴的数据
    end
    
    T = alfa * T;  % 温度下降
    pause(0.01)  % 暂停一段时间后再接着画图
end

(6)最佳解输出

[best_y,ind]=max(iter_y);  % 找到最大的y值,以及其在向量中的下标
best_x = iter_x(ind,:);  % 根据下标找到此时的x值
disp('最佳的位置是:');disp(best_x)
disp('此时最优值是:');disp(best_y)

pause(1)
h.XData = [];h.YData = [];  % 将原来的散点删除
scatter(best_x,best_y,'r');  % 在最大值处重新标上散点
title(['模拟退火找到的最大值为',num2str(best_y)])  % 加上图的标题

(7)仿真结果图

其中红色圆圈表示的是该函数在[-1,1]区间上的最值。

2.2 多个自变量题目:
求函数

在x1,x2∈[-15,15]内的最小值。

(1)绘制函数

figure 
x1 = -15:1:15;
x2 = -15:1:15;
[x1,x2] = meshgrid(x1,x2);
y = x1.^2 + x2.^2 - x1.*x2 - 10*x1 - 4*x2 + 60;
mesh(x1,x2,y)
xlabel('x1');  ylabel('x2');  zlabel('y');  % 加上坐标轴的标签
axis vis3d % 冻结屏幕高宽比,使得一个三维对象的旋转不会改变坐标轴的刻度显示
hold on  % 不关闭图形,继续在上面画图

(2)参数初始化

narvs = 2; % 变量个数
T0 = 100;   % 初始温度
T = T0; % 迭代中温度会发生改变,第一次迭代时温度就是T0
maxgen = 200;  % 最大迭代次数
Lk = 100;  % 每个温度下的迭代次数
alfa = 0.95;  % 温度衰减系数
x_lb = [-15 -15]; % x的下界
x_ub = [15 15]; % x的上界

(3)生成初始解

x0 = zeros(1,narvs);
for i = 1: narvs
    x0(i) = x_lb(i) + (x_ub(i)-x_lb(i))*rand(1);    
end
y0 = Obj_fun2(x0); % 计算当前解的函数值
h = scatter3(x0(1),x0(2),y0,'*r');  % scatter3是绘制三维散点图的函数(这里返回h是为了得到图形的句柄,未来我们对其位置进行更新)

(4)保存中间量

min_y = y0;     % 初始化找到的最佳的解对应的函数值为y0
MINY = zeros(maxgen,1); % 记录每一次外层循环结束后找到的min_y (方便画图)

(5)重点:模拟退火过程

for iter = 1 : maxgen  % 外循环, 我这里采用的是指定最大迭代次数
    for i = 1 : Lk  % 内循环,在每个温度下开始迭代
        y = randn(1,narvs);  % 生成1行narvs列的N(0,1)随机数
        z = y / sqrt(sum(y.^2)); % 对y值进行标准化处理
        x_new = x0 + z*T; % 根据新解的产生规则计算x_new的值
        % 如果这个新解的位置超出了定义域,就对其进行调整
        for j = 1: narvs
            if x_new(j) < x_lb(j)
                r = rand(1);
                x_new(j) = r*x_lb(j)+(1-r)*x0(j);
            elseif x_new(j) > x_ub(j)
                r = rand(1);
                x_new(j) = r*x_ub(j)+(1-r)*x0(j);
            end
        end
        x1 = x_new;    % 将调整后的x_new赋值给新解x1
        y1 = Obj_fun2(x1);  % 计算新解的函数值
        if y1 < y0    % 如果新解函数值小于当前解的函数值
            x0 = x1; % 更新当前解为新解
            y0 = y1;
        else
            p = exp(-(y1 - y0)/T); % 根据Metropolis准则计算一个概率
            if rand(1) < p   % 生成一个随机数和这个概率比较,如果该随机数小于这个概率
                x0 = x1;  % 更新当前解为新解
                y0 = y1;
            end
        end
        % 判断是否要更新找到的最佳的解
        if y0 < min_y  % 如果当前解更好,则对其进行更新
            min_y = y0;  % 更新最小的y
            best_x = x0;  % 更新找到的最好的x
        end
    end
    MINY(iter) = min_y; % 保存本轮外循环结束后找到的最小的y
    T = alfa*T;   % 温度下降
    pause(0.02)  % 暂停一段时间后(单位:秒)再接着画图
    h.XData = x0(1);  % 更新散点图句柄的x轴的数据(此时解的位置在图上发生了变化)
    h.YData = x0(2); % 更新散点图句柄的y轴的数据(此时解的位置在图上发生了变化)
    h.ZData = Obj_fun2(x0);  % 更新散点图句柄的z轴的数据(此时粒子的位置在图上发生了变化)
end

(6)寻找最佳位置

disp('最佳的位置是:'); disp(best_x)
disp('此时最优值是:'); disp(min_y)

pause(0.5) 
h.XData = [];  h.YData = [];  h.ZData = [];  % 将原来的散点删除
scatter3(best_x(1), best_x(2), min_y,'*r');  % 在最小值处重新标上散点
title(['模拟退火找到的最小值为', num2str(min_y)])  % 加上图的标题

%% 画出每次迭代后找到的最小y的图形
figure
plot(1:maxgen,MINY,'b-');
xlabel('迭代次数');
ylabel('y的值');
toc

(7)仿真结果

3 求解旅行商问题算例

3.1 单旅行商问题
给定38个城市的坐标位置,求解从起始城市出发访问每一座城市,且再次回到起始城市的最短回路。
产生路径新解的方法(三种):
(1)交换法:随机选择路径中两个节点,交换这两个点的位置;
(2)移位法:随机选择路径中三个节点,将前两个点之间的点移位到第三个点之后;
(3)倒置法:随机选择路径中两个节点,将这个点之间的顺序完全颠倒。

stsp.m代码

(1)载入城市坐标位置

coord = [11003.611100,42102.500000;11108.611100,42373.888900;11133.333300,42885.833300;11155.833300,42712.500000;11183.333300,42933.333300;11297.500000,42853.333300;11310.277800,42929.444400;11416.666700,42983.333300;11423.888900,43000.277800;11438.333300,42057.222200;11461.111100,43252.777800;11485.555600,43187.222200;11503.055600,42855.277800;11511.388900,42106.388900;11522.222200,42841.944400;11569.444400,43136.666700;11583.333300,43150.000000;11595.000000,43148.055600;11600.000000,43150.000000;11690.555600,42686.666700;11715.833300,41836.111100;11751.111100,42814.444400;11770.277800,42651.944400;11785.277800,42884.444400;11822.777800,42673.611100;11846.944400,42660.555600;11963.055600,43290.555600;11973.055600,43026.111100;12058.333300,42195.555600;12149.444400,42477.500000;12286.944400,43355.555600;12300.000000,42433.333300;12355.833300,43156.388900;12363.333300,43189.166700;12372.777800,42711.388900;12386.666700,43334.722200;12421.666700,42895.555600;12645.000000,42973.333300];
n = size(coord,1);  % 城市的数目
figure  % 新建一个图形窗口
plot(coord(:,1),coord(:,2),'o');   % 画出城市的分布散点图
hold on % 等一下要接着在这个图形上画图的

(2)计算距离矩阵

d = zeros(n);   % 初始化两个城市的距离矩阵全为0
for i = 2:n  
    for j = 1:i  
        coord_i = coord(i,:);   x_i = coord_i(1);     y_i = coord_i(2);  % 城市i的横坐标为x_i,纵坐标为y_i
        coord_j = coord(j,:);   x_j = coord_j(1);     y_j = coord_j(2);  % 城市j的横坐标为x_j,纵坐标为y_j
        d(i,j) = sqrt((x_i-x_j)^2 + (y_i-y_j)^2);   % 计算城市i和j的距离
    end
end
d = d+d';   % 生成距离矩阵的对称的一面

(3)参数初始化

T0 = 1000;   % 初始温度
T = T0; % 迭代中温度会发生改变,第一次迭代时温度就是T0
maxgen = 1000;  % 最大迭代次数
Lk = 500;  % 每个温度下的迭代次数
alpfa = 0.95;  % 温度衰减系数

(4)生成初解

path0 = randperm(n);  % 生成一个1-n的随机打乱的序列作为初始的路径
result0 = calculate_tsp_d(path0,d); % 调用我们自己写的calculate_tsp_d函数计算当前路径的距离

(5)保存中间量

min_result = result0;     % 初始化找到的最佳的解对应的距离为result0
RESULT = zeros(maxgen,1); % 记录每一次外层循环结束后找到的min_result (方便画图)

(6)重点:模拟退火过程

for iter = 1 : maxgen  % 外循环, 我这里采用的是指定最大迭代次数
    for i = 1 : Lk  %  内循环,在每个温度下开始迭代
        path1 = gen_new_path(path0);  % 调用我们自己写的gen_new_path函数生成新的路径
        result1 = calculate_tsp_d(path1,d); % 计算新路径的距离
        %如果新解距离短,则直接把新解的值赋值给原来的解
        if result1 < result0    
            path0 = path1; % 更新当前路径为新路径
            result0 = result1; 
        else
            p = exp(-(result1 - result0)/T); % 根据Metropolis准则计算一个概率
            if rand(1) < p   % 生成一个随机数和这个概率比较,如果该随机数小于这个概率
                path0 = path1;  % 更新当前路径为新路径
                result0 = result1; 
            end
        end
        % 判断是否要更新找到的最佳的解
        if result0 < min_result  % 如果当前解更好,则对其进行更新
            min_result = result0;  % 更新最小的距离
            best_path = path0;  % 更新找到的最短路径
        end
    end
    RESULT(iter) = min_result; % 保存本轮外循环结束后找到的最小距离
    T = alpfa*T;   % 温度下降       
end

(7)输出最优路径,作图

disp('最佳的方案是:'); disp(mat2str(best_path))  % 将数值矩阵转换为字符向量
disp('此时最优值是:'); disp(min_result)


best_path = [best_path,best_path(1)];   % 在最短路径的最后面加上一个元素,即第一个点(我们要生成一个封闭的图形)
n = n+1;  % 城市的个数加一个(紧随着上一步)
for i = 1:n-1 
    j = i+1;
    coord_i = coord(best_path(i),:);   x_i = coord_i(1);     y_i = coord_i(2); 
    coord_j = coord(best_path(j),:);   x_j = coord_j(1);     y_j = coord_j(2);
    plot([x_i,x_j],[y_i,y_j],'-b')    % 每两个点就作出一条线段,直到所有的城市都走完
%     pause(0.02)  % 暂停0.02s再画下一条线段
    hold on
end

%% 画出每次迭代后找到的最短路径的图形
figure
plot(1:maxgen,RESULT,'b-');
xlabel('迭代次数');
ylabel('最短路径');

toc

3.2 多旅行商问题

多个商品推销员要去若干个城市推销商品,这些推销员最开始都在同一个城市,他们被分配了不同的路径,在经过所有的城市后,大家都回到出发的城市。
首先需要思考的是:
假设有n+1个城市,第0号为起始城市,其他城市编号分别是1至n,一共有m个旅行商,那么生成的解应该是:
m-1个0 以及1至n组成的一个随机的排序。

需要注意的两点:
(1)产生初始解的时候,先用randperm函数生成1至n的随机排序,然后再次使用randi函数随机生成插入0的位置;
(2)在计算总的距离或总成本的时候,需要对解进行拆分,每一个旅行商经过的路径都可以保存在一个元胞数组中。

但是按照以下的代码运行,还是出现问题。

(1)生成初始解的代码

path0 = randperm(n);  % 城市的数量为n+1,生成一个1-n的随机打乱的序列作为初始的路径
for i = 1:salesman_num-1
    len = length(path0);
    ind = randi(len);
    path0 = [path0(1:ind),0,path0(ind+1:end)];
end

(2)对生成的解进行清理分割 clear_path.m

function [cpath,k] = clear_path(path,salesman_num)
    cpath = cell(salesman_num,1);  % 用来保存参与工作的每个旅行商经过的城市
    ind = find(path==0);  % 找到所有分割点的位置
    k =1;  % 初始化计数器,k表示参与工作的旅行商数量
    for i=1:salesman_num-1
        if i==1 % 如果是第一个分割点
            c = path(1:ind(i)-1); % 提取第一个旅行商所经过的城市
        else
            c = path(ind(i-1)+1:ind(i)-1);  % 提取中间的旅行商所经过的城市
        end
        if ~isempty(c)
            cpath{k}=c;  % 把c保存到元胞cpath的第k个位置
            k =k+1;
        end
    end
    c = path(ind(end)+1:end);
    if ~isempty(c)
        cpath{k}=c;
    else
        k = k-1;
    end
    cpath = cpath(1:k);  % 只保留元胞中前k个非空的部分
end

(3)计算当前解对应的成本 calculate_mtsp_cost.m

function [cost,every_cost]=calculate_mtsp_cost(cpath,k,d)
    % 要计算所有旅行商的路程长度的总和,需要首先计算每个旅行商的路程长度
    every_cost =zeros(k,1);  % 初始化每个旅行商的路程长度为0
    for i=1:k
        path_i =cpath{i};  % 第i个旅行商所经过的路线
        n = length(path_i); % 第i个旅行商所经过的城市数量
        for j =1:n
               if j == 1
                   every_cost(i)=every_cost(i)+d(1,path_i(j)); 
               else
                   every_cost(i)=every_cost(i)+d(path_i(j-1),path_i(j));
               end
        end
        every_cost(i)=every_cost(i)+d(path_i(end),1);
    end
    cost = sum(every_cost);  
end

(4)产生新解的方法 gen_new_path1.m

function path1 = gen_new_path1(path0,p1,p2)
    n = length(path0);
    r = rand(1);  % 随机生成一个[0,1]均匀分布的随机数
    if r < p1  % 使用交换法产生新路径
        c1 = randi(n);  % 生成1-n中的一个随机整数
        c2 = randi(n);  % 生成1-n中的一个随机整数
        path1 = path0;  % 将path0的值赋给path1
        path1(c1)=path0(c2); % 将path0第c2个位置的元素赋值给path1第c1个位置的元素
        path1(c2)=path0(c1); % 将path0第c1个位置的元素赋值给path1第c2个位置的元素
    elseif r < p1+p2
        c1 = randi(n);  % 生成1-n中的一个随机整数
        c2 = randi(n);  % 生成1-n中的一个随机整数
        c3 = randi(n);  % 生成1-n中的一个随机整数
        sort_c = sort([c1 c2 c3]);  % 对c1、c2、c3从小到大排序
        c1 = sort_c(1);c2 = sort_c(2);c3 = sort_c(3);
        temp1 = path0(1:c1-1);
        temp2 = path0(c1:c2);
        temp3 = path0(c2+1:c3);
        temp4 = path0(c3+1:end);
        path1 = [temp1 temp3 temp2 temp4];
    else
        % 采用倒置法产生新路径
        c1 = randi(n);  % 生成1-n中的一个随机整数
        c2 = randi(n);  % 生成1-n中的一个随机整数c2;
        if c1 > c2
            tem = c2;
            c2 = c1;
            c1 = tem;
        end
        tem1 = path0(1:c1-1);
        tem2 = path0(c1:c2);
        tem3 = path0(c2+1:end);
        path1 = [tem1 fliplr(tem2) tem3];
    end
end

参考

本文的内容参考于清风老师的数学建模模拟退火视频以及相应的网页内容。

模拟退火视频
多旅行商问题思路
多旅行商核心代码