中山大学计算机学院
人工智能
本科生实验报告
2022学年春季学期
课程名称:Artificial Intelligence
@夜忆星辰
一、 实验题目
高级搜索
- 模拟退火算法
- 遗传算法
二、 实验内容
1.算法原理
-
模拟退火算法
模拟退火数学原理
模拟退火算法从某一较高初温出发,伴随温度参数的不断下降,结合一定的概率突跳特性在解空间中随机寻找目标函数的全局最优解,即在局部最优解能概率性地跳出并最终趋于全局最优。
这里的“一定的概率”的计算参考了金属冶炼的退火过程,这也是模拟退火算法名称的由来。将温度T当作控制参数,目标函数值f视为内能E,而固体在某温度T时的一个状态对应一个解,然后算法试图随着控制参数T的降低,使目标函数f(内能E)也逐渐降低,直至趋于全局最小值(退火中低温时的最低能量状态),就像金属退火过程一样。
从上面我们知道,会结合概率突跳特性在解空间中随机寻找目标函数的全局最优解,那么具体的更新解的机制是什么呢?如果新解比当前解更优,则接受新解,否则基于Metropolis准则判断是否接受新解。接受概率为:(1)初始的温度T(0)应选的足够高,使的所有转移状态都被接受。初始温度越高,获得高质量的解的概率越大,耗费的时间越长。
(2)退火速率,即温度下降,最简单的下降方式是指数式下降:
T(n) = α \alphaαT(n) ,n =1,2,3,…
其中α \alphaα是小于1的正数,一般取值为0.8到0.99之间。使的对每一温度,有足够的转移尝试,指数式下降的收敛速度比较慢。
(3)终止温度
如果温度下降到终止温度或者达到用户设定的阈值,则退火完成。 -
遗传算法
遗传算法是一类随机优化算法,但它不是简单的随机比较搜索,而是通过对染色体的评价和对染色体中基因的作用,有效地利用已有信息来指导搜索有希望改善优化质量的状态。
标准遗传算法主要步骤可描述如下:
① 随机产生一组初始个体构成初始种群。
② 计算每一个体的适配值(fitnessvalue,也称为适应度)。适应度值是对染色体(个体)进行评价的一种指标,是GA进行优化所用的主要信息,它与个体的目标值存在一种对应关系。
③ 判断算法收敛准则是否满足,若满足,则输出搜索结果;否则执行以下步骤。
④ 根据适应度值大小以一定方式执行复制操作(也称为选择操作)。
⑤ 按交叉概率pc执行交叉操作。
⑥ 按变异概率pm执行变异操作。
⑦ 返回步骤②。
2.伪代码
模拟退火算法:
def SA():
begin = list(range(0,N)) # 初始化一个解
begin.append(begin[0])
Tx = T0
while Tx > T1:
count += 1
Loop_times: #循环迭代
new = new_solution() # 获取一个新解
detaT = old_dis - new_dis
if detaT < 0:
rand = 随机数(0,1)
if rand < exp(detaT / Tx):
接受新解
else:
直接接受新解
Tx = Tx * alpha
遗传算法:
while count < gen: # 迭代gen次
count += 1
get possible # 获取概率表
part = possible 的最大20% # 选取 20% 概率最大的亲本保留下来
把part加入child
index = list(range(popsize))
news from (generation,possible,80%)# 轮盘转 80%
把news加入child
通过概率值对child进行变异,获取新解
generation = child # 完成一次迭代,孩子作为新的亲本
3.关键代码展示(带注释)
无论是模拟退火算法还是遗传算法,都有部分相同的操作:
def read():
global N
with open("ch130.txt", "r") as f:
# with open("eil101.txt", "r") as f:
for line in f.readlines():
line = line.strip('\n') # 去掉列表中每一个元素的换行符
data = line.split() # 将字符串转为列表
x = float(data[1])
y = float(data[2])
map.append((x,y))
N = len(map)
以浮点数读取坐标信息并存在 map 当中
# 两个坐标之间的距离
def distance(a, b):
x1 = map[a][0]
x2 = map[b][0]
y1 = map[a][1]
y2 = map[b][1]
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
# 环形路程总长
def circle_distance(a):
sum = 0
for j in range(N-1):
sum += distance(a[j], a[j + 1])
sum += distance(a[N-1], a[0])
return sum
要算一个环形回路,只需要简单的算出两点之间坐标距离,在利用整体循环一次既能得到总距离。
如何去产生新解,对于遗传算法和模拟退火算法,还是有一些不同的。遗传算法由两个亲代去产生一个新解或者两个新解,而模拟退火算法只需要一个初始解不断往后更新解,所以运用于遗传算法的交叉算子无法运用于模拟退火算法。而对于单个染色体变异如部分倒转,基因突变这个都是可以接受的,在模拟退火算法中,为了随机性,采取的是染色体变异的部分倒转,基因突变当仅改变少数对的时候,迭代次数过大,时间代价较长,遗传算法中同样包含部分倒转变异,如下:
def part_reverse(father):
num = len(father)
ran = np.random.choice(num, 2, replace=False) # 产生两个不同(false)随机数
ran.sort()
r = ran[1]
l = ran[0]
part_reverse_father = father[0:l]
tmp = list(reversed(father[l:r]))
part_reverse_father.extend(tmp)
part_reverse_father.extend(father[r:num])
return part_reverse_father
T0 = 100000
T1 = 0.00001
alpha = 0.985
Loop_times = 200
这部分是模拟退火算法的初始参数,T0是初始温度,T1是最终温度,alpha是降温系数,Loop_times是内部循环迭代次数。接下来看核心代码
while Tx > T1:
for i in range(Loop_times):
length = len(begin)
tmp = begin[0:length-1]
new = part_reverse(tmp) # 倒置产生新解
new.append(new[0])
En0 = circle_distance(begin)
En1 = circle_distance(new)
detaT = En0 - En1 # 距离差值
if detaT < 0:
rand = random.uniform(0, 1) # 产生(0,1)之间随机数
if detaT / Tx <= 709 and rand < exp(detaT / Tx): # 判断是否接受新解
begin = new
else:
begin = new
Tx = Tx * alpha # 降温一次
new_distance = circle_distance(begin) # 获取路径长度
path.append(new_distance)
伪代码中已经详细讲明核心代码算法,上面也有注释,只要通过变异产生新解,再由公式判断是否接受即可。
接下来看看遗传算法的不同之处:
先看看初始化数据,参数设置后面实验结果分析中讨论
popsize = 400 # 种群规模
gen = 10001 # 进化代数
pc = 0.8 # 交叉率
pr = 0.25 # 反转率
pm = 0.4 # 变异率
N = 0 # 城市数目
map_distance = [] # 邻接矩阵存储距离
begin = [] # 一个初始随机列表
sum_distance = 0 # 距离和
generation = [] # 种群矩阵
show_distance = [] # 距离曲线展示
def initial(): # 初始化种群并获取距离与概率信息
global map_distance,begin,sum_distance,generation
for i in range(N):
for j in range(i,N):
map_distance[i][j] = map_distance[j][i] = (( dx[i]- dx[j]) ** 2 + (dy[i] - dy[j]) ** 2) ** 0.5
begin = list(range(0,N))
np.random.shuffle(begin) # 随机打乱
sum_distance = circle_distance(begin)
generation = [0] * popsize
for i in range(popsize): # 建立初始种群
generation[i] = my_shuffle(begin)
# print(generation[i])
def my_shuffle(my_list): # 固定起点打乱
tmp = my_list.copy()
np.random.shuffle(tmp)
tmp.append(tmp[0])
return tmp
该部分首先初始化一个解begin,再用函数my_shuffle打乱加入亲本generation之中,并获取邻接矩阵的信息
def infomation(ge): # generation = [] # 种群矩阵
path = []
fit = [] # 适应性函数取 f=1/distance
for i in ge:
len = circle_distance(i)
fit.append(1/len)
sum_f = sum(fit) # 计算概率值(轮盘赌法)
possible = []
# possible.append(fit[0]/sum_f)
for i in range(popsize):
# possible.append(possible[i-1] + fit[i]/sum_f)
possible.append(fit[i] / sum_f)
# possible.append(1)
return possible
选取适应性函数应当满足当距离越小,f越大,这里采用距离的倒数1/d作为适应性函数,并生成一个概率表。
如何获取后代是我们所关心的,下面介绍几种常用的选择算子:
- 轮盘赌选择(Roulette Wheel Selection):是一种回放式随机采样方法。每个个体进入下一代的概率等于它的适应度值与整个种群中个体适应度值和的比例。选择误差较大。
- 随机竞争选择(Stochastic Tournament):每次按轮盘赌选择一对个体,然后让这两个个体进行竞争,适应度高的被选中,如此反复,直到选满为止。
- 最佳保留选择:首先按轮盘赌选择方法执行遗传算法的选择操作,然后将当前群体中适应度最高的个体结构完整地复制到下一代群体中。
- 无回放随机选择(也叫期望值选择Excepted Value Selection):根据每个个体在下一代群体中的生存期望来进行随机选择运算。方法如下:
(1) 计算群体中每个个体在下一代群体中的生存期望数目N。
(2) 若某一个体被选中参与交叉运算,则它在下一代中的生存期望数目减去0.5,若某一个体未 被选中参与交叉运算,则它在下一代中的生存期望数目减去1.0。
(3) 随着选择过程的进行,若某一个体的生存期望数目小于0时,则该个体就不再有机会被选中。 - 确定式选择:按照一种确定的方式来进行选择操作。具体操作过程如下:
(1) 计算群体中各个个体在下一代群体中的期望生存数目N。
(2) 用N的整数部分确定各个对应个体在下一代群体中的生存数目。
(3) 用N的小数部分对个体进行降序排列,顺序取前M个个体加入到下一代群体中。至此可完全确定出下一代群体中M个个体。 - 无回放余数随机选择:可确保适应度比平均适应度大的一些个体能够被遗传到下一代群体中,因而选择误差比较小。
- 均匀排序:对群体中的所有个体按期适应度大小进行排序,基于这个排序来分配各个个体被选中的概率。
- 最佳保存策略:当前群体中适应度最高的个体不参与交叉运算和变异运算,而是用它来代替掉本代群体中经过交叉、变异等操作后所产生的适应度最低的个体。
- 随机联赛选择:每次选取几个个体中适应度最高的一个个体遗传到下一代群体中。
- 排挤选择:新生成的子代将代替或排挤相似的旧父代个体,提高群体的多样性。
经过自己的测试研究,我认为轮盘赌选择是一个比较好的方法,兼顾了时间与随机性,为了跳出局部最优解,应当具备随机搜索能力,但是直接使用轮盘赌选择可能会错失当前最优解,为了解决这个问题,结合联赛选择法,直接保存部分适应函数值靠前的,剩下的在进行轮盘赌选择操作。如下:
def getop(possible):
dic = {}
for i in range(popsize):
dic[i] = possible[i]
res_list = sorted(dic.items(), key=lambda e: e[1])
a = int(0.1*popsize) # 保留原有 10% 种群
max_num_index = [one[0] for one in res_list[::-1][:a]]
return max_num_index
如何变异已经在模拟退火算法中出现部分,这里讨论一些常见的交叉算子:
-
单点交叉(Single-point crossover)
单点交叉通过选取两条染色体,在随机选择的位置点上进行分割并交换右侧的部分,从而得到两个不同的子染色体。单点交叉是经典的交叉形式,与多点交叉或均匀交叉相比,它交叉混合的速度较慢 -
两点交叉(Two-points crossover)
两点交叉是指在个体染色体中随机设置了两个交叉点,然后再进行部分基因交换。 -
多点交叉(Multi-point crossover)
与两点交叉类似 -
顺序交叉(Order Crossover,OX)
在两个父代染色体中随机选择起始和结束位置,将父代染色体1该区域内的基因复制到子代1相同位置上,再在父代染色体2上将子代1中缺少的基因按照顺序填入。另一个子代以类似方式得到。 -
循环交叉(Cycle Crossover,CX)
在两个父代染色体中随机选择几个位置,位置可以不连续,先在父代染色体2中找到父代染色体1被选中基因的位置,再用父代染色体2中其余的基因生成子代,并保证位置对应,将父代染色体1中被选择的基因按顺序放入子代剩余位置中。另一个子代以类似方式得到。
单点交叉:
def cross(father, mother): # father,mother为两条dna路径
num = len(father)
numlist = list(range(num))
idex = random.choice(numlist) #随机找一个节点,从该节点开始交换
son1 = father[0:idex]
son2 = mother[0:idex]
for i in range(idex,num): # 避免dna中有重复的路径
if father[i] in son1:
son1.append(mother[i])
son2.append(father[i])
else:
son1.append(father[i])
son2.append(mother[i])
return son1,son2
CX算子:
def CX(father, mother): # 交叉繁殖:CX
cycle = [] #交叉点集
start = father[0]
cycle.append(start)
end = mother[0]
while end != start:
cycle.append(end)
end = mother[father.index(end)]
child = father[:]
cross_points = cycle[:]
if len(cross_points) < 2 :
cross_points = random.sample(father, 2)
k = 0
for i in range(len(father)):
if child[i] in cross_points:
continue
else:
for j in range(k, len(mother)):
if mother[j] in cross_points:
continue
else:
child[i] = mother[j]
k = j + 1
break
return child
OX算子:
def OX(father, mother):
num = len(father)
ran = np.random.choice(num, 2, replace=False) # 产生两个不同(false)随机数
ran.sort()
r = ran[1]
l = ran[0]
tmp = mother[l:r] # 交叉的基因片段
child = []
id = 0
for g in father:
if id == l:
child.extend(tmp) # 插入基因片段
id += 1
if g not in tmp:
child.append(g)
id += 1
return child
准备好了读取,初始化,变异函数,最后就是遗传算法核心代码部分:
def evolution():
global generation
count = 0
while count < gen: # 迭代gen次
count += 1
possible = infomation(generation) # 获取概率表
part = getop(possible) # 选取 20% 概率最大的亲本保留下来
child = []
l = len(part)
for i in range(l):
child.append(generation[part[i]])
index = list(range(popsize))
news = random.choices(index, weights = possible, k = int(0.85 * popsize)) # 轮盘转 85%
newsnum = len(news)
for i in range(newsnum):
child.append(generation[news[i]])
m = int(popsize*0.45)
np.random.shuffle(child)
for i in range(m):
ran = np.random.choice(len(generation[0]), 2, replace=False) # 产生两个不同(false)随机数
j = i + m - 1
length = len(child[0])
son1 = child[i][0:length - 1] # 不取child中和起点重复的最后一个元素
son2 = child[j][0:length - 1]
if np.random.random() < pc: # possible_cross 交换概率
# son1, son2 = cross(son1, son2)
son1 = CX(son1,son2)
son2 = CX(son1,son2)
if np.random.random() < pr: # possible_reverse 易位概率
son1 = part_reverse(son1)
son2 = part_reverse(son2)
if np.random.random() < pm: # possible_mutate 变异概率
son1 = mutate(son1)
son2 = mutate(son2)
son1.append(son1[0]) # 回到起点,首尾相连
son2.append(son2[0])
child[i] = son1
child[j] = son2
path = []
for i in range(int(l/2)):
child.append(generation[part[i]])
generation = child # 完成一次迭代,孩子作为新的亲本
for i in range(popsize):
path.append(circle_distance(generation[i]))
min_path_len = min(path) # 查找最短路径长度并放入show_distance展示
为了尽可能的保留较优的解,首先在child中加入10%的最优解,再由轮盘赌选择得到85%的子代,由这95%的子代去参与变异,再加上原有的5%最优解,这样可以使得最优解既可以被保留,又有参与变异,可能跳出局部最优解。
三、实验结果及分析
模拟退火算法
算法 | 数据 | 时间 | 最短距离 | 实际距离 | 误差百分比 |
---|---|---|---|---|---|
TSP_SA | ch130.txt | 125.4 | 6110 | 6530 | 6.87% |
TSP_SA | ch130.txt | 128.4 | 6110 | 6580 | 7.69% |
TSP_SA | ch130.txt | 124.0 | 6110 | 6467 | 5.84% |
TSP_SA | eil101.txt | 75.6 | 629 | 674 | 7.15% |
TSP_SA | eil101.txt | 90.1 | 629 | 676 | 7.47% |
TSP_SA | eil101.txt | 126.7 | 629 | 663 | 5.41% |
-
ch130.txt
-
eil101.txt
几种算子下的遗传算法 -
遗传算法单点交叉算子
-
ch130.txt
算法 | 数据 | 时间 | 最短距离 | 实际距离 | 误差百分比 |
---|---|---|---|---|---|
TSP_GA | ch130.txt | 3328 | 6110 | 6876 | 12.53% |
TSP_GA | ch130.txt | 1055 | 6110 | 6863 | 12.32% |
TSP_GA | ch130.txt | 2410 | 6110 | 6820 | 11.62% |
TSP_GA | eil101.txt | 1324 | 629 | 704 | 11.92% |
TSP_GA | eil101.txt | 1228 | 629 | 691 | 9.86% |
TSP_GA | eil101.txt | 1073 | 629 | 700 | 11.3% |
-
ch130.txt
-
eil101.txt
-
遗传算法CX算子
算法 | 数据 | 时间 | 最短距离 | 实际距离 | 误差百分比 |
---|---|---|---|---|---|
TSP_GA | ch130.txt | 2056 | 6110 | 6573 | 7.58% |
TSP_GA | ch130.txt | 1356 | 6110 | 6421 | 5.09% |
TSP_GA | ch130.txt | 1272 | 6110 | 6407 | 4.86% |
TSP_GA | eil101.txt | 828 | 629 | 658 | 4.61% |
TSP_GA | eil101.txt | 2052 | 629 | 671 | 6.68% |
TSP_GA | eil101.txt | 1210 | 629 | 668 | 6.20% |
-
ch130.txt
-
eil101.txt
-
遗传算法OX算子
算法 | 数据 | 时间 | 最短距离 | 实际距离 | 误差百分比 |
---|---|---|---|---|---|
TSP_GA | ch130.txt | 1445 | 6110 | 6713 | 9.87% |
TSP_GA | ch130.txt | 1311 | 6110 | 6516 | 6.64% |
TSP_GA | ch130.txt | 1205 | 6110 | 6479 | 6.03% |
TSP_GA | eil101.txt | 1443 | 629 | 677 | 7.63% |
TSP_GA | eil101.txt | 1470 | 629 | 683 | 8.59% |
TSP_GA | eil101.txt | 1942 | 629 | 667 | 6.04% |
-
ch130.txt
-
eil101.txt
结果分析
-
模拟退火算法与遗传算法的比较
根据实验结果来看,模拟退火算法在时间方面占据很大优势,几乎是遗传算法时间的十倍,主要原因是遗传算法为了跳出局部最优解,需要有大量的种群基础,进行较多的变异操作,还要有足够多的后代数。模拟退火算法前期会上下波动,而后快速收敛,而优化后的遗传算法上下波动很小,基本一开始就快速收敛于一个局部最优解,这只需要1000代左右,而后为了跳出局部最优解,需要大量的时间迭代到10000代,从误差率来看,模拟退火算法误差大多在 6-7% 左右,最后能到 5.5%,遗传算法用CX算子大多误差在 5-6% 左右,最优可达4.5%,具有更好的解,更迭代数继续上升到 50000代预期可在 3%左右的误差,但是时间消费成本过大,所以此处采取10000代,接受一个 5%-6% 的误差解。 -
模拟退火算法因子设置
当城市数目较少,迭代次数不需要太高,初始温度可以设置小一些,终止温度一般需要设置小一些,可能在0.1,当城市数目较多时,需要把初始温度设置高一点,终止温度低一些,使得迭代更加充分寻找更优解。降温系数一般在0.98-0.99左右,降温太快不能充分搜索更优解就结束了。 -
遗传算法因子设置
popsize = 400 # 种群规模
gen = 10001 # 进化代数
pc = 0.8 # 交叉率
pr = 0.25 # 反转率
pm = 0.4 # 变异率
# 对于种群规模,设置太小的话不能充分得到更多的解,不够随机,设置太大的话,会快速收敛于局部最优解,迭代次数较大时会增加很多时间消耗,种群规模一般取300-700左右即可。
# 交叉的目的是为了在下一代产生新的个体,通过交叉操作,遗传算法的搜索能力得到了很大的提高,所以交叉率可以设置大一些,一般在0.7-0.9,太小影响搜索能力。
# 反转率以及变异率本质都是为了跳出局部最优解,变异率不宜设置过大,过大会经常破坏原有基因不易修复,太小不能很好跳出局部最优解,即便跳出,数目不够也容易被淘汰,一般设置在0.2-0.4 -
交叉算子比较
由实验结果来看,CX>OX>两点交叉
CX 与 OX 都有较好的随机性能跳出局部最优解,误差大多在 5-7% ,而普通两点交叉的随机性较弱,常常陷入局部最优解而无法跳出,误差达到了 10-12%。至于时间上都比较久,都要在1000-2000s左右,得到解的误差与时间同样与初始化的随机解有关,随机性较强。
四、 参考资料
https://blog.csdn.net/qq_34798326/article/details/79013338
https://blog.csdn.net/weixin_48241292/article/details/109468947
https://blog.csdn.net/huyiqiuuuu/article/details/110917656
https://blog.csdn.net/weixin_44343282/article/details/108300200
https://blog.csdn.net/xieju0605/article/details/109609413
https://blog.csdn.net/ucinmireux/article/details/80727707
https://blog.csdn.net/u013950379/article/details/87936999
https://blog.csdn.net/sandalphon4869/article/details/102928456
https://blog.csdn.net/dickdick111/article/details/85109091
https://blog.csdn.net/u010743448/article/details/108445588
https://www.jianshu.com/p/ae5157c26af9