本文同样研究[上一篇]中离散的物流选址问题,考虑展示禁忌搜索算法求解该问题,问题介绍描述同样参考上一篇。
1 背景介绍
禁忌搜索算法(Tabu Search,TS)是局部搜索算法的推广,是人工智能在组合优化算法中的一个成功应用。Glover在1986年首次提出这一概念,进而形成一套完整的算法。禁忌搜索算法的特点就是采用了禁忌技术,所谓的禁忌就是禁止重复前面的工作。为了回避局部邻域搜索陷入混沌,禁忌搜索算法采用一个禁忌表记录下已经到达的可行解,在下一次搜索中,利用禁忌表中的信息不再或有选择地搜索这些点,以此来跳出局部最优点。禁忌搜索算法思路简单,应用性强,不受函数性质控制,是一种人工智能算法。
2 代码设计
代码设计同样分为两大模块,一是根据选址方案计算最小总费用,二是使用禁忌搜索寻找最优选址方案。
(1)由于考虑的问题相同,前面的参数设置部分就参考上一篇直接给出。
import pandas as pd
import numpy as np
import pulp
data = pd.read_csv(r'transcost.csv')
demand = [729, 630, 321, 293, 251, 573, 207, 732, 481, 783]
capacity = [4500, 3500, 5000, 3000, 2500]
fix_cost_lst = [304, 281, 459, 292, 241]
dic = {0:'A',1:'B',2:'C',3:'D',4:'E'}
(2)第一大模块同样直接给出代码,直接来的童鞋可以参考上一篇。
def Get_Translist(pop,data,dic):
transcost = [list(data[dic[i]]) for i in range(len(pop)) if pop[i] == 1]
return transcost
def Cal_total_capacity(location,capality):
total_capacity_lst = [capality[i] for i in range(len(location)) if location[i] == 1]
return sum(total_capacity_lst)
def fixpop(location,capacity,demand):
while sum(demand) > Cal_total_capacity(location, capacity):
ind = np.random.randint(0,len(location))
location[ind] = 1 if location[ind] == 0 else 1
return location
def transportation_problem(costs, x_max, y_max):
row = len(costs)
col = len(costs[0])
prob = pulp.LpProblem('Transportation Problem', sense=pulp.LpMinimize)
var = [[pulp.LpVariable(f'x{i}{j}', lowBound=0, cat=pulp.LpInteger) for j in range(col)] for i in range(row)]
flatten = lambda x: [y for l in x for y in flatten(l)] if type(x) is list else [x]
prob += pulp.lpDot(flatten(var), costs.flatten())
for i in range(row):
prob += (pulp.lpSum(var[i]) <= x_max[i])
for j in range(col):
prob += (pulp.lpSum([var[i][j] for i in range(row)]) == y_max[j])
prob.solve()
return {'objective':pulp.value(prob.objective), 'var': [[pulp.value(var[i][j]) for j in range(col)] for i in range(row)]}
def Cal_cost(location):
pop = fixpop(location,capacity,demand)
fix_cost = 0
capacity_new = []
for i in range(len(pop)):
if pop[i] == 1:
fix_cost += fix_cost_lst[i]
capacity_new.append(capacity[i])
translist = Get_Translist(pop, data, dic)
costs = np.array(translist)
max_plant = capacity_new
max_cultivation = demand
res = transportation_problem(costs, max_plant, max_cultivation)
return res['var'],res["objective"] + fix_cost
(3)第二部分是采用禁忌搜索寻找最优方案。首先依据当前的选址方案产生邻域,邻域的产生方法是对每个工厂依次进行抛弃/使用(0/1)的变换。由于len(location)== 5,故邻域解的个数是5,即对 location 中的每一个值进行变换后加入到邻域集合location_new_lst 中。
随后用 Cal_cost(location) 计算每一个邻域方案的值 ,并找出最小的3个值及其对应的选址方案组合成禁忌参考列表。
def Neibor(location):
location_new_lst = [] #存储当前location的所有邻域
for i in range(len(location)):
location_copy = location.copy()
location_copy[i] = 0 if location_copy[i] == 1 else 1 #0-1变换
location_copy = fixpop(location_copy, capacity, demand) #检验修正location
location_new_lst.append(location_copy) #添加到location_new_lst
cost_value = [Cal_cost(location)[1] for location in location_new_lst] #计算每一个领域的最小费用,添加到列表
ind_lst = find_min(3,cost_value) #寻找cost_value中最小的3个值所对应的索引,函数在下面给出
best_value_lst = [cost_value[i] for i in ind_lst] #添加最小值的3个值到best_value_lst
pop_new_lst_select = [location_new_lst[i] for i in ind_lst] #添加最小值的3个值所对应的选址方案到pop_new_lst_select
ref_lst = list(zip(best_value_lst,pop_new_lst_select)) #组合成禁忌参考列表
return ref_lst
def find_min(n,lst):
lst_copy = lst.copy()
ind = []
while len(ind) < n:
for i in range(len(lst_copy)):
if lst_copy[i] == min(lst_copy):
ind.append(i)
lst_copy[i] = float('inf')
return ind[:n]
ref_lst 的 含义是“location 的所有邻域中对应的最优值及其选址方案”,它是下一步更新禁忌表及寻优选择的关键。
(4)更新禁忌表。首先选择邻域中最优的选址方案(即ref_lst[0][1]),如果它不在禁忌表中,那么将它添加到禁忌表;如果在,考虑特赦准则,即比较 ref_lst[0][0] 和当前最优值(Valueold),对禁忌表进行恰当更新。详细的更新规则请参考大佬的禁忌搜索算法过程。
def update(ref_lst,Tabu_lst,Valueold):
mark = True
while mark:
if (ref_lst[0][1] not in Tabu_lst): #如果ref_lst中的最优方案不在禁忌表中
Tabu_lst.insert(0, ref_lst[0][1]) #在禁忌表的第0位插入之
del Tabu_lst[-1] #同时删除禁忌表的最后一个禁忌元素
mark = False
else:
if ref_lst[0][0] < Valueold: #考虑特赦准则,如果ref_lst中的最优方案在禁忌表中,但是该方案对应的最优值优于目前搜索的最优值,
#则删除Tabu_lst中的该方案并在第0位添加该方案
Tabu_lst.remove(ref_lst[0][1])
Tabu_lst.insert(0, ref_lst[0][1])
mark = False
else: #如果如果ref_lst中的最优方案在禁忌表中,同时该方案对应的最优值劣于目前搜索的最优值
print('Tabu works') #则禁忌表生效,删除第一个元素,继续考虑ref_lst后面的元素
del ref_lst[0]
mark = True
return ref_lst[0][0],ref_lst[0][1],Tabu_lst #返回邻域中的最优值、对应选址和更新后的禁忌表
(5)下面是主函数部分。
location = [0,0,1,0,0] #设置初始选址方案
Valueold = Cal_cost(location)[1] #计算初始方案的最优值
Tabu_lst = [None,None,None] #设置初始禁忌表
print(0,' minvalue:',Valueold,',location:', location,'\n',' Tabu List:', Tabu_lst)
print('='*80)
count = 0
Value_lst = [] #记录每一次寻找过程中的最优值
location_lst = [] #记录每一次寻找过程中的最优选址方案
while count < 10: #迭代10次
count += 1
ref_lst = Neibor(location)
Valuenew, location_new, Tabu_lst = update(ref_lst,Tabu_lst,Valueold) #更新禁忌表,返回邻域的最优值和对应的选址方案
print(count,' minvalue:',Valuenew,',location:', location_new,'\n',' Tabu List:', Tabu_lst)
print('='*80)
Value_lst.append(Valuenew)
location_lst.append(location_new)
location = location_new
if Valuenew < Valueold: #更新当前搜索的最优值
Valueold = Valuenew
#打印输出结果
opt = [(i,j) for i,j in zip(Value_lst,location_lst) if i == min(Value_lst)][0]
trans_schedule = Cal_cost(opt[1])[0]
print(' The best value of Tabu search is: {}'.format(opt[0]),'\n',
'The optimal location schedule is: {}'.format(opt[1]),'\n',
'The optimal transport schedule is: {}'.format(trans_schedule)
)
3 运行结果
0 minvalue: 31424.0 ,location: [0, 0, 1, 0, 0]
Tabu List: [None, None, None]
================================================================================
1 minvalue: 19328.0 ,location: [0, 1, 1, 0, 0]
Tabu List: [[0, 1, 1, 0, 0], None, None]
================================================================================
2 minvalue: 15513.0 ,location: [0, 1, 1, 1, 0]
Tabu List: [[0, 1, 1, 1, 0], [0, 1, 1, 0, 0], None]
================================================================================
3 minvalue: 13770.0 ,location: [1, 1, 1, 1, 0]
Tabu List: [[1, 1, 1, 1, 0], [0, 1, 1, 1, 0], [0, 1, 1, 0, 0]]
================================================================================
4 minvalue: 13562.0 ,location: [1, 1, 0, 1, 0]
Tabu List: [[1, 1, 0, 1, 0], [1, 1, 1, 1, 0], [0, 1, 1, 1, 0]]
================================================================================
Tabu works
5 minvalue: 13803.0 ,location: [1, 1, 0, 1, 1]
Tabu List: [[1, 1, 0, 1, 1], [1, 1, 0, 1, 0], [1, 1, 1, 1, 0]]
================================================================================
Tabu works
6 minvalue: 14011.0 ,location: [1, 1, 1, 1, 1]
Tabu List: [[1, 1, 1, 1, 1], [1, 1, 0, 1, 1], [1, 1, 0, 1, 0]]
================================================================================
7 minvalue: 13770.0 ,location: [1, 1, 1, 1, 0]
Tabu List: [[1, 1, 1, 1, 0], [1, 1, 1, 1, 1], [1, 1, 0, 1, 1]]
================================================================================
8 minvalue: 13562.0 ,location: [1, 1, 0, 1, 0]
Tabu List: [[1, 1, 0, 1, 0], [1, 1, 1, 1, 0], [1, 1, 1, 1, 1]]
================================================================================
Tabu works
9 minvalue: 13803.0 ,location: [1, 1, 0, 1, 1]
Tabu List: [[1, 1, 0, 1, 1], [1, 1, 0, 1, 0], [1, 1, 1, 1, 0]]
================================================================================
Tabu works
10 minvalue: 14011.0 ,location: [1, 1, 1, 1, 1]
Tabu List: [[1, 1, 1, 1, 1], [1, 1, 0, 1, 1], [1, 1, 0, 1, 0]]
================================================================================
The best value of Tabu search is: 13562.0
The optimal location schedule is: [1, 1, 0, 1, 0]
The optimal transport schedule is:
[[0.0, 0.0, 0.0, 0.0, 0.0, 573.0, 0.0, 0.0, 481.0, 783.0],
[729.0, 630.0, 0.0, 0.0, 0.0, 0.0, 207.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 321.0, 293.0, 251.0, 0.0, 0.0, 732.0, 0.0, 0.0]]
从结果可以发现禁忌搜索从第三步开始就进行循环迭代了,迭代周期是【3-4-5-6】,故算法寻找到的最优选址是[1, 1, 0, 1, 0] ,最优值和运输方案如结果所示。
4 一些想法
目前该问题的退火算法和禁忌搜索都写完了,可以看出禁忌搜索的原理还是相对简单,实现起来较容易,但求解大规模的规划问题可能就不那么理想,用遗传算法求解也写过,但确实是效果不好。个人认为遗传算法比较适合基因长度较长的大规模组合优化问题,对于本例中的小规模(选址范围只有5个工厂),每一代的最优个体比较容易在交叉变异中被破坏,因此适宜的基因片段不容易遗传到子代,自然搜索的目的就不明确,收敛效果很差。目前还在学习蚁群算法,等弄懂原理之后再来这里胡乱写点东西吧。虽然学习时间比学校少很多,但还是会抓住时间来学习的,任何幸福都来自平凡的努力和坚持,大家加油!