变邻域搜索(Variable Neighbourhood Search, VNS)解决TSP问题
本文是之前禁忌算法解决TSP问题的延伸,工作也是在上一篇代码的基础上进行的,传送门:
什么是变邻域
在启发式局部搜索算法中,需要通过某种方法产生当前解的邻域解(解的一个集合),以往的搜索算法产生邻域的方法只有一种,而且是固定的。顾名思义,变邻域搜索就是,通过多个不同的邻域方法产生不同的邻域解的集合。
Variable Neighbourhood Descent, VND
在介绍VNS之前,先介绍一下VND,Variable Neighbourhood Descent也是一种VNS的方法
在搜索的过程中,遍历不同邻居方法产生的解的集合,如果在某个集合中最优邻域解优于最优解,就使用该解生成新的不同邻域解的集合,从头开始遍历这些邻域解方法,反之,继续从下一个方法产生的邻域解集合中寻找,直到遍历完所有的解集合。
Basic Variable Neighbourhood Search,BVNS
基本的变邻域搜索的方法,一般是一下步骤:
主要就是三个步骤,Shaking,在邻域中产生随机解,Local Search,使用生成的随机解利用一种Local Search的方法产生一个解,Move or not,判断最优解和上一步得到的解的效果,决定是否移动到下一个邻域空间,还是从第一个邻域空间使用最优解重复上述过程。
General Variable Neighbourhood Search,GVNS
一般的变邻域搜索的方法是使用VND代替上图的Local Search的过程:
因为Local Search使用的是VND的方法,所以在算法中需要两个不同邻域空间的集合,在每个集合中,产生不同邻域解集合的方法可以是相同的。
产生邻域解集合的方法
直接上代码,三种方法
#随机交换两个城市,最多产生max_num个解
def find_neighbour_zero(path, weights, max_num):
solution_neighbours = []
for i in range(0, max_num):
exchange = random.sample(range(1, len(path)-1), 2)
temp_path = copy.deepcopy(path)
temp_path[exchange[0]] = path[exchange[1]]
temp_path[exchange[1]] = path[exchange[0]]
if temp_path not in solution_neighbours:
cost = fitness(temp_path, weights)
solution_neighbours.append([temp_path, cost])
return solution_neighbours
#随机翻转某个区间,产生最多max_num个邻居解
def find_neighbour_one(path, weights, max_num):
solution_neighbours = []
for i in range(0, max_num):
# 随机选择两个端点, 不改变先后顺序
endpoints = random.sample(range(1, len(path)-1), 2)
endpoints.sort()
temp_path = copy.deepcopy(path)
temp_path[endpoints[0]:endpoints[1]] = list(reversed(temp_path[endpoints[0]:endpoints[1]]))
if temp_path not in solution_neighbours:
cost = fitness(temp_path, weights)
solution_neighbours.append([temp_path, cost])
return solution_neighbours
#随机找两个城市放到序列最前面,产生最多max_num个邻居解
def find_neighbour_two(path, weights, max_num):
solution_neighbours = []
for i in range(0, max_num):
# 随机选择两个city, 不改变先后顺序
endpoints = random.sample(range(1, len(path)-1), 2)
endpoints.sort()
temp_path = copy.deepcopy(path)
temp_path.pop(endpoints[0])
temp_path.pop(endpoints[1] - 1)
temp_path.insert(1, path[endpoints[0]])
temp_path.insert(2, path[endpoints[1]])
if temp_path not in solution_neighbours:
cost = fitness(temp_path, weights)
solution_neighbours.append([temp_path, cost])
return solution_neighbours
搜索过程
下面把上面的伪代码变成python
def variable_neighbourhood_search(edge_points, weights, iters, neighbour_num, neighbour_func_sets, k_max=3, l_max=3):
#绘图部分
pyplot.close()
fig = pyplot.figure()
path_fig = fig.add_subplot(1, 2, 1)
cost_fig = fig.add_subplot(1, 2, 2)
path_fig.axis("equal")
path_fig.set_title('Best Path')
cost_fig.set_title('Best Cost')
cost_fig.set_xlabel('iterations')
cost_fig.set_ylabel('fitness')
pyplot.subplots_adjust(wspace= 0.5)
pyplot.ion()#打开交互
path_fig.scatter([i[0] for i in edge_points], [i[1] for i in edge_points], s=2 ,color='red')
pyplot.pause(0.001)
first_path = [i for i in range(0, len(weights))]
first_path.append(0)
best_solution = [first_path, fitness(first_path,weights)]
cost_history = list()
cost_history.append(best_solution[1])
for it in range(0, iters):
#更新绘图
cost_fig.plot(cost_history, color='b')
path_fig.plot([edge_points[p][0] for p in best_solution[0]], [edge_points[p][1] for p in best_solution[0]], color='b', linewidth=1)
pyplot.pause(0.001)
path_fig.lines.pop(0)
k = 0
while k < k_max:
#shaking
s_1 = random.sample(neighbour_func_sets[k](best_solution[0], weights, neighbour_num), 1)[0]
#local_search return x_0
x_0 = copy.deepcopy(s_1)
l = 0
while l < l_max:
neighbour_solution = neighbour_func_sets[l](x_0[0], weights, neighbour_num)
neighbour_solution.sort(key= lambda x: x[1])
x_1 = neighbour_solution[0] #findbestsolution
if x_1[1] < x_0[1]:
x_0 = x_1
l = 1
else:
l = l + 1
#move or not
if x_0[1] < best_solution[1]:
best_solution = x_0
k = 1
else:
k = k + 1
cost_history.append(best_solution[1])
print("iterations: %d, best cost:%.2f"%(it, best_solution[1]))
path_fig.plot([edge_points[p][0] for p in best_solution[0]], [edge_points[p][1] for p in best_solution[0]], color='b', linewidth=1)
pyplot.savefig('result.jpg')
return best_solution
上面代码就完全按照伪代码的思路写的,调用的话就直接:
neighbourhood_funcs = [find_neighbour_zero, find_neighbour_one, find_neighbour_two]
variable_neighbourhood_search(edge, weights, opt.iters, opt.num, neighbourhood_funcs)
neighbourhood_funcs 是个函数的集合,用来产生不同的邻域解集合的,代码的剩余部分和上一篇文章相同,再来个传送门:
搜索过程的动图就不放了,看一下最终结果:
再和上一篇禁忌搜索的结果对比一下:
TIPS
之前我在哪里查的,说sample函数不改变元素的先后顺序,然而,他的确改变了。所以先排个序,在翻转区间的那个邻域函数中,必须小的在前面,不然翻转没有效果。
然后就是在删除这两个元素的代码中,删除第一元素后,第二个原本的索引就变了,所以要减一,不然你会发现,图里面有些city会重复或者不经过了,哀,调试了半天才发现这个问题…
至此,局部搜索的算法介绍就告一段落了,下面的研究内容是演化算法,不见不散。