发表于2019/11/30,最后编辑于2019/12/3
退火,大自然的一种普通自然现象。然而,人类是如此善于从大自然中学习、模仿,从而出现了各式各样的算法。现在,让我们一起走进模拟退火算法。
模拟退火(Simulated annealing, SA),what is it? 首先,这是一个在竞赛中比较常用的算法,ACM的题目中能用它来找一个局部解,进而进行下一步求解。在数学建模比赛中,则常常利用模拟退火作为求解组合策略的方法,这也是一个能够作为基准线的算法。同时,作为人工智能的一种较基础的优化与搜索算法,学习模拟退火,学习它的思想,有助于对更高级的算法的理解与掌握。
小知识时间
模拟退火的名称及灵感源于冶金术中的退火工艺,最早是由S.Kirkpatrick等人于1983年提出相关算法。在热力学上,退火现象指物体逐渐降温的物理现象,温度愈低,物体的能量状态愈低。液体在缓慢降温时,就会达到最低能量状态:结晶。但是,如果降温过程过急过快,便成了淬炼,会导致不是最低能态的非晶形状态。 1 ^1 1
gkd,进入正题!
简单来说,模拟退火算法是一种智能算法,它是人工智能(Artificial Intelligence)下的计算智能(Computational Intelligence)部分的一个算法。说得玄乎,但其实它就是一种基于概率优化的搜索办法(说到概率与暴力…没错,就是它,蒙特卡洛!)。它的目标是基于全局的搜索,尽可能地跳出局部最优值,找到全局最优值。
对比
相较于数学求解常用的梯度下降、牛顿法等算法,模拟退火算法通过退火的方法来寻找全局最优解,求解时更不容易陷入局部最优解中。此外,模拟退火是一种元启发式算法(metaheuristics algorithm),它通用于各类组合优化和函数计算的场景中,且相关算法编码复杂度较低,在特定的领域能解决数学优化解决不了的问题。当然,模拟退火也有不足之处,求解速度慢,玄学的过程导致求解答案无法保证最优等等,都是模拟退火的弊病。
core
原始的模拟退火其实是很简单的,让我们试着用一句话来表达它吧!模拟退火是从初始状态出发,不停地通过随机新状态,来寻找更优状态的搜索和优化算法。
状态可能是类似函数中自变量的取值,路径规划中途径的城市等。当然了,盲目地随机就不需要大费周章写一篇文章啦hhh。模拟退火的精髓在于随机过程是由一个概率值所控制的。评价状态是否优良,通常是由问题的求解目标决定的,如上,求解目标即为函数值、道路总长度。当新状态比当前状态更优时,毫无疑问,接受新状态。而当新状态更差时,模拟退火并不会直接舍弃新状态,而是通过计算出一个概率,然后随机一个值,当该值小于计算出的概率值时,即接受新状态,反之,舍弃。
取一个值,等同于概率抽样的原理是:随机取一个值,假如把取值范围划成100份,那么令某个范围抽取概率为p,如p为0.3时,取值范围是[0, 1)中的[0,0.3),那么,只要取到的值落在这一区间上,便是命中p概率。
还有一点没说到的是,这个概率值该如何计算?这个简单,背公式就好了!
p = e − Δ E K T p = e^{-\frac{\Delta E}{KT}} p=e−KTΔE
让我们一个个参数进行剖析。p是我们要求的概率,这个概率的计算是一个自然指数计算。上方的 Δ E \Delta E ΔE代表了前文提到的问题评价指标的变化,比如新旧状态函数值( y n e w − y o l d y_{new} - y_{old} ynew−yold)的变化。下方的K是一个常数,可忽略。而T是退火温度,这是一个很核心的概念。让我们进一步看一下这个T:当退火时,T会逐渐变小,因此,p会逐渐变小,所以这也是一个收敛的过程。当退火刚开始时,状态的转移比较随意,算法更容易通过全局搜索找到最优解。当退火到了后期时,状态不容易转移,也就防止了已找到的最优解范围由于随机而“暴毙”。
那么,显而易见的,还有几个参数会影响着我们的“杀手锏”,比如说,每次随机中温度的降火速率,每个温度下会随机多少次,温度为多少时停止降火,以及当温度多久不变化时退出随机过程。
总而言之,模拟退火融合了蒙特卡洛抽样,搜索策略,以及概率分布的思想。
有的同学喜欢理解抽象概念后自己实现,而像我这种比较懒的就乐于“抄作业”,在这里,我先给出整体的框架设计,然后再贴上代码,并尽量附上详细的注释。
代码框架是以自己粗浅的项目经验构思的,仅供批评,不供参考。
模拟退火是个比较简单的算法,我们力求通过一个函数来实现所有功能(一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
我们也可以考虑通过增加某些环节而实现改进,如:
为什么我要写这么 些 篇文章?这是个有趣的问题。从个人的角度,是记录,是锻炼,是交流。从更广的角度来看,我写这些不是为了造轮子,而是为了写得更明白、透彻。这个系列将立足于把算法讲得更通俗易懂,摆脱过多的数学证明与形式化表达,尽力为需要的人贡献一份自己的力量。
然后,有什么技术和非技术上的不足大家可以在评论区讨论呀下次继续写其他智能算法!最后,求个赞~~
转载请在前言部分注明链接及作者