一起来AI——白话详解模拟退火算法与python实践

发表于2019/11/30,最后编辑于2019/12/3

退火,大自然的一种普通自然现象。然而,人类是如此善于从大自然中学习、模仿,从而出现了各式各样的算法。现在,让我们一起走进模拟退火算法。


介绍

模拟退火(Simulated annealing, SA),what is it? 首先,这是一个在竞赛中比较常用的算法,ACM的题目中能用它来找一个局部解,进而进行下一步求解。在数学建模比赛中,则常常利用模拟退火作为求解组合策略的方法,这也是一个能够作为基准线的算法。同时,作为人工智能的一种较基础的优化与搜索算法,学习模拟退火,学习它的思想,有助于对更高级的算法的理解与掌握。

小知识时间

模拟退火的名称及灵感源于冶金术中的退火工艺,最早是由S.Kirkpatrick等人于1983年提出相关算法。在热力学上,退火现象指物体逐渐降温的物理现象,温度愈低,物体的能量状态愈低。液体在缓慢降温时,就会达到最低能量状态:结晶。但是,如果降温过程过急过快,便成了淬炼,会导致不是最低能态的非晶形状态。 1 ^1 1
一起来AI——白话详解模拟退火算法与python实践_第1张图片

退火

gkd,进入正题!

简单来说,模拟退火算法是一种智能算法,它是人工智能(Artificial Intelligence)下的计算智能(Computational Intelligence)部分的一个算法。说得玄乎,但其实它就是一种基于概率优化的搜索办法(说到概率与暴力…没错,就是它,蒙特卡洛!)。它的目标是基于全局的搜索,尽可能地跳出局部最优值,找到全局最优值。

对比

相较于数学求解常用的梯度下降、牛顿法等算法,模拟退火算法通过退火的方法来寻找全局最优解,求解时更不容易陷入局部最优解中。此外,模拟退火是一种元启发式算法(metaheuristics algorithm),它通用于各类组合优化和函数计算的场景中,且相关算法编码复杂度较低,在特定的领域能解决数学优化解决不了的问题。当然,模拟退火也有不足之处,求解速度慢,玄学的过程导致求解答案无法保证最优等等,都是模拟退火的弊病。
一起来AI——白话详解模拟退火算法与python实践_第2张图片

牛顿法求极值


核心思想

core

原始的模拟退火其实是很简单的,让我们试着用一句话来表达它吧!模拟退火是从初始状态出发,不停地通过随机新状态,来寻找更优状态的搜索和优化算法。

一起来AI——白话详解模拟退火算法与python实践_第3张图片

模拟退火求解过程

状态可能是类似函数中自变量的取值,路径规划中途径的城市等。当然了,盲目地随机就不需要大费周章写一篇文章啦hhh。模拟退火的精髓在于随机过程是由一个概率值所控制的。评价状态是否优良,通常是由问题的求解目标决定的,如上,求解目标即为函数值、道路总长度。当新状态比当前状态更优时,毫无疑问,接受新状态。而当新状态更差时,模拟退火并不会直接舍弃新状态,而是通过计算出一个概率,然后随机一个值,当该值小于计算出的概率值时,即接受新状态,反之,舍弃。

取一个值,等同于概率抽样的原理是:随机取一个值,假如把取值范围划成100份,那么令某个范围抽取概率为p,如p为0.3时,取值范围是[0, 1)中的[0,0.3),那么,只要取到的值落在这一区间上,便是命中p概率。

还有一点没说到的是,这个概率值该如何计算?这个简单,背公式就好了!
p = e − Δ E K T p = e^{-\frac{\Delta E}{KT}} p=eKTΔE
让我们一个个参数进行剖析。p是我们要求的概率,这个概率的计算是一个自然指数计算。上方的 Δ E \Delta E ΔE代表了前文提到的问题评价指标的变化,比如新旧状态函数值( y n e w − y o l d y_{new} - y_{old} ynewyold)的变化。下方的K是一个常数,可忽略。而T是退火温度,这是一个很核心的概念。让我们进一步看一下这个T:当退火时,T会逐渐变小,因此,p会逐渐变小,所以这也是一个收敛的过程。当退火刚开始时,状态的转移比较随意,算法更容易通过全局搜索找到最优解。当退火到了后期时,状态不容易转移,也就防止了已找到的最优解范围由于随机而“暴毙”。

那么,显而易见的,还有几个参数会影响着我们的“杀手锏”,比如说,每次随机中温度的降火速率,每个温度下会随机多少次,温度为多少时停止降火,以及当温度多久不变化时退出随机过程

一起来AI——白话详解模拟退火算法与python实践_第4张图片

模拟退火过程

总而言之,模拟退火融合了蒙特卡洛抽样,搜索策略,以及概率分布的思想。


代码实操

有的同学喜欢理解抽象概念后自己实现,而像我这种比较懒的就乐于“抄作业”,在这里,我先给出整体的框架设计,然后再贴上代码,并尽量附上详细的注释。

代码框架是以自己粗浅的项目经验构思的,仅供批评,不供参考。

模拟退火是个比较简单的算法,我们力求通过一个函数来实现所有功能(一main到底)。

  • 一开始,我们先初始化参数,一般是随机的。然后设置一个用于记录最佳历史状态的参数。
  • 然后,我们对于每个温度进行数次迭代,在每次迭代中,计算函数值差值以及相应概率后,通过随机取值判断是否命中概率。
  • 当迭代数完成后,如果状态发生变化,则降温,否则,如果长时间不降温,则退出退火过程,此外,达到一定条件,如最低温度或是最大迭代次数,也应退出退火过程。

接下来贴上丑丑的代码~注意这里使用的几个超参数都是函数,通过这种方法,能大大增加算法的鲁棒性,同时,更易于进行元启发式算法接口的调用。

import numpy as np
import matplotlib.pyplot as plt
'''
模拟退火优化算法
'''
class SAoptimizer:
    def __init__(self):
        super().__init__()

    def optimize(self, f, ybound=(-np.inf, np.inf), initf=np.random.random, randf=np.random.random,
            t=10000, alpha=0.98, stop=1e-1, iterPerT=1, l=1):
        '''
        :param f: 目标函数,接受np.array作为参数 :param ybound: y取值范围 
        :param initf: 目标函数的初始权值函数,返回np.array :param alpha: 退火速率  
        :param iterPerT: 每个温度下迭代次数 :param t: 初始温度 :param l:新旧值相减后的乘数,越大,越不容易接受更差值
        :param stop: 停火温度 :param randf: 对参数的随机扰动函数,接受现有权值,返回扰动后的新权值np.array
        '''
        #初始化
        y_old = None
        while y_old == None or y_old < ybound[0] or y_old > ybound[1]:
            x_old = initf()
            y_old = f(x_old) 
        y_best = y_old
        x_best = np.copy(x_old)       
        #降温过程
        count = 0
        while(t > stop):
            downT = False
            for i in range(iterPerT):
                x_new = randf(x_old)
                y_new = f(x_new)    
                if y_new > ybound[1] or y_new < ybound[0]:
                    continue
                #根据取最大还是最小决定dE,最大为旧值尽可能小于新值
                dE = -(y_old - y_new) * l
                if dE < 0: 
                    downT = True
                    count = 0
                else: count += 1
                if self.__judge__(dE, t):
                    x_old = x_new
                    y_old = y_new
                    if y_old < y_best:
                        y_best = y_old
                        x_best = x_old
                    #绘图
                    # if (count % 50 == 0):
                    #     plt.scatter(x_old[0], x_old[1])
            if downT:
                t = t * alpha
            #长时间不降温
            if count > 1000: break
        self.weight = x_best
        return y_best
    
    def __judge__(self, dE, t):
        '''
        :param dE: 变化值\n
        :t: 温度\n
        根据退火概率: exp(-(E1-E2)/T),决定是否接受新状态
        '''
        if dE < 0:
            return 1
        else:
            p = np.exp(-dE / t)
            import random
            
            if p > np.random.random(size=1):
                return 1
            else: return 0

接下来是我自己用来生成合适的超参数函数的元函数

def initRandom(constraint):
    #产生随机初始值,由列表表示权值的个数,每个权值的上下界由一个元组表示
    #ex.constraint = [(1,20),(13, 21)], 返回np.array
    weight = np.array([])
    for i in constraint:
        weight = np.append(weight, (i[1] - i[0]) * np.random.random() + i[0])
    return weight

def changeWeight(constraint, changeRange, now, bias=0):
    '''产生扰动后的权值,返回np.array
    :param constraint: 权值约束 :param changeRange: 权值扰动的范围
    :param bias: 权值扰动的偏好方向,eg: 0.1代表更倾向偏大的值20%
    :param now: 现有权值
    '''
    result = np.copy(now)
    for index in range(len(result)):
        delta = (np.random.random() - 0.5 + bias) * changeRange
        while (delta + result[index] > constraint[index][1] or
            delta + result[index] < constraint[index][0]):
            delta = (np.random.random() - 0.5 + bias) * changeRange
        result[index] += delta
    return result

例题求解

来尝试几个例子吧! 2 ^2 2

test1问题是对一个f方程进行搜索,寻找函数最大值。

这是一个很直白的优化问题,将方程作为优化目标,传入相应初始化和参数扰动函数即可。

def test1():
    #单变量非线性方程优化
    f = lambda x:x + 10 * np.sin(5 * x) + 7 * np.cos(4 * x)
    myConstraint = [(0, 9)] #约束
    init = lambda :initRandom(myConstraint) #随机初始化函数
    randf = lambda now:changeWeight(myConstraint, 3, now) #扰动函数

    temp = np.linspace(0, 10, 1000)
    plt.plot(temp, f(temp))
    sa = SAoptimizer()
    ans = sa.optimize(f, initf=init, randf=randf, l=1) #调用优化器
    plt.scatter(sa.weight, ans)
    print(sa.weight, ans)
运行结果

test2问题是在点集中寻找“质心”,即离所有点的距离最近的点。

这其实就是稍微变种的函数求解问题。在这里,方程变成了距离之和,参数的扰动变成了质心的坐标变化,做出相应代码改动即可。

def test2():
    #到点集的最短距离
    dots_x = [0, 1.5, 1.5, -2, -4, -5, 2, 4, 5, -2, -3 ,
        3, -1.5, -2.5, 0.1]
    dots_y = [0, 10, 12, 11 ,-8 ,2 ,-1.5 ,-2.5 ,1 ,-2 ,
        8 ,6 ,0 ,0 ,0.2]
    plt.scatter(dots_x, dots_y)
    #这里的评估指标,也就是质心到点集的距离需要通过f函数计算得出
    def f(weight):
        tot = 0
        for i in zip(dots_x, dots_y):
            tot += np.sqrt(
                (weight[0] - i[0]) ** 2 + (weight[1] - i[1]) ** 2)
        return tot
    myConstraint = [(-5, 5), (-8, 12)]
    init = lambda :initRandom(myConstraint)
    randf = lambda now: changeWeight(myConstraint, 1, now)
    
    sa = SAoptimizer()
    ans = sa.optimize(f, initf=init, randf=randf, stop=0.01, iterPerT=1)
    plt.scatter(sa.weight[0], sa.weight[1])
    print(sa.weight, ans)
运行结果

test3则是经典的旅行商问题(tsp),即如何找到一段路,经过所有的城市,最后返回起点。

那么,只需要把回路的距离当成优化目标,设计相应的扰动策略即可。在这里我们使用随机交换两座城市的扰动策略。

def test3():
    #tsp问题
    cities = np.array([[0.9695,0.6606,0.5906,0.2124,0.0398,0.1367,0.9536,0.6091,0.8767,0.8148,0.3876,0.7041,0.0213,0.3429,0.7471,0.4606,0.7695,0.5006,0.3124,0.0098,0.3637,0.5336,0.2091,0.4767,0.4148,0.5876,0.6041,0.3213,0.6429,0.7471],
    [0.6740,0.9500,0.5029,0.8274,0.9697,0.5979,0.2184,0.7148,0.2395,0.2867,0.8200,0.3296,0.1649,0.3025,0.8192,0.6500,0.7420,0.0229,0.7274,0.4697,0.0979,0.2684,0.7948,0.4395,0.8867,0.3200,0.5296,0.3649,0.7025,0.9192]])
    plt.scatter(cities[0], cities[1])
    init = lambda :cities #参数为城市序列
    #这里需要自定义扰动函数
    def randf(now):
        import time
        new = np.copy(now)
        size = new.shape[1]
        while 1:
            city1 = np.random.randint(size)
            city2 = np.random.randint(size)
            if city1 != city2: break
        temp = np.copy(new[:, city1])
        new[:, city1] = new[:, city2]
        new[:, city2] = temp
        return new

    def f(weight):
        size = weight.shape[1]
        dist = 0
        for i in range(size - 1):
            dist += np.sqrt(np.sum(
                (weight[:, i + 1] - weight[:, i]) ** 2))
        # dist += np.sqrt(np.sum(
                # (weight[:, size - 1] - weight[:, 0]) ** 2))
        return dist

    sa = SAoptimizer()
    ans = sa.optimize(f, initf=init, randf=randf, stop=1e-5, t=1e3, alpha=0.99, l=10, iterPerT=1)
    for i in range(sa.weight.shape[1] - 1):
        plt.plot([sa.weight[0, i], sa.weight[0, i + 1]],
            [sa.weight[1, i], sa.weight[1, i + 1]])
    # plt.plot([sa.weight[0, 0], sa.weight[0, sa.weight.shape[1] - 1]],
        # [sa.weight[1, 0], sa.weight[1, sa.weight.shape[1] - 1]])
    print(ans)

if __name__ == '__main__':
    test1()
    # test2()
    # test3()
    pass

运行结果


扩展知识

你已经学会了很多,还可以做些什么呢?

我们可以考虑一下改进算法: 3 ^3 3

  • 设计合适的状态产生函数,使其根据搜索进程的需要表现出状态的全空间分散性或局部区域性。
  • 设计高效的退火策略,减少计算量。
  • 避免反复搜索状态(禁忌搜索)。
  • 采用并行搜索。
  • 设计合适的算法终止准则。

我们也可以考虑通过增加某些环节而实现改进,如:

  • 增加升温或重升温过程。在算法进程的适当时机,将温度适当提
    高,从而可激活各状态的接受概率,以调整搜索进程中的当前状
    态,避免算法在局部极小解处停滞不前。
  • 增加补充搜索过程。即在退火过程结束后,以搜索到的最优解为
    初始状态,再次执行模拟退火过程或局部性搜索。
  • 对每一当前状态,采用多次搜索策略,以概率接受区域内的最优
    状态,而非标准SA的单次比较方式。
  • 结合其他搜索机制的算法,如遗传算法、混沌搜索等。

附录与资料

  1. https://blog.csdn.net/weixin_40562999/article/details/80853354 ,参考模拟退火算法历史部分内容
  2. https://zhuanlan.zhihu.com/p/47375952 ,数学解析相关知识
  3. https://github.com/guofei9987/scikit-opt/blob/master/sko/SA.py ,模拟退火库实现
  4. http://xingozd.lofter.com/post/3ba3b9_79911d0 ,参考优化内容
  5. https://zhuanlan.zhihu.com/p/33184423 ,参考例子

系列感言

  为什么我要写这么 篇文章?这是个有趣的问题。从个人的角度,是记录,是锻炼,是交流。从更广的角度来看,我写这些不是为了造轮子,而是为了写得更明白、透彻。这个系列将立足于把算法讲得更通俗易懂,摆脱过多的数学证明与形式化表达,尽力为需要的人贡献一份自己的力量。

写在最后

  • For you: 我们通过简单的背景概述、原理阐述,一起了解了模拟退火。然后呢,也通过代码实现和问题求解实践了这个算法。最后,给出了进一步优化的方法。
  • For me: 嘛,一直以来都想着把自己学的知识整理成文章分享出来,结果上次写一半半途而废了hhhh。不管怎么说,这次又开了个新坑,相信我能坚持下去的!~~

然后,有什么技术和非技术上的不足大家可以在评论区讨论呀下次继续写其他智能算法!最后,求个赞~~

转载请在前言部分注明链接及作者

你可能感兴趣的:(计算智能,人工智能AI,模拟退火,Python,白话详解,计算智能)