在前两篇博客里面,我们重点讲解了利用随机搜索的方法解决车间调度问题,流程图如下:
在本篇博客中,我们将介绍如何利用遗传算法来解决车间调度问题。具体的算法流程图如下:
与上面流程图相对应的遗传算法的整体代码如下:
import random
1 """ pop是种群,种群中的每个个体是一个二元组,格式为:(该可行解的总完成时间, 可行解编码列表)""" 2 pop = [(ComputeStartTimes(g, I)[-1], g) for g in InitPopulation(ps, I)] 3 for it in range(1, mit+1):""" mit是迭代次数,由用户指定""" 4 # Random ordering of the population 5 random.shuffle(pop)"""把pop中各个个体的顺序打乱 """ 6 hpop = len(pop) / 2""" hpop是种群的一半""" 7 for i in range(hpop):""" 遍历种群的前半部分份""" 8 if random() < 0.3:"""若[0,1]之间的随机数 < 0.3(0.3是交叉概率,用户自行指定) """ 9 # Create two new elements 10 ch1 = Crossover(pop[i][1], pop[hpop + i][1], I)""" 通过交叉生成一个新解""" 11 ch2 = Crossover(pop[hpop + i][1], pop[i][1], I)""" 通过交叉生成一个新解""" 12 if random() < 0.1:"""若[0,1]之间的随机数 < 0.1(0.1是变异概率,用户自行指定) """ 13 ch1 = Mutation(ch1)""" 对第一个新解进行变异""" 14 if random() < 0.1:"""若[0,1]之间的随机数 < 0.1 """ 15 ch2 = Mutation(ch2)"""对第二个新解进行变异""" 16 pop.append((ComputeStartTimes(ch1, I)[-1], ch1))""" 将两个新解放回种群""" 17 pop.append((ComputeStartTimes(ch2, I)[-1], ch2)) 18 # Sort individuals in increasing timespan order and 19 # select only the best ones for the next iteration 20 pop.sort()""" 将种群中的染色体按总完成时间排序""" 21 pop = pop[:ps]""" 提取pop中的前ps个染色体""" 22 return pop[0]"""返回总完成时间最小的染色体 """
在上面的函数中Crossover函数就是那个对两个可行解进行交叉的函数。
2.交叉
交叉是遗传算法中的一个重要操作,它的目的是从已有的两个解Parent1和Parent2的编码中各自取出一部分来组合成两个新的解Offspring1和Offspring2,在车间调度中一种常见的交叉方法叫 Generalized Order Crossover方法(GOX),假设有三个工件A,B,C, 每个工件下面包含三道工序,根据这一信息我们可以利用上一节介绍的编码随机生成两个解如下:
我们可以用一个有序偶list(列表)来存储一个解,
这个有序偶list中的每个元素是一个有序偶,有序偶的第一个元素是工件号,第二个元素是工序号。
Parent1用列表可以表示为[(B,1),(A,1),(B,2),(B,3),(C,1),(A,2),(C,2),(C,3),(B,1),(A,3))]
有序偶的图示为
GOX交叉就是从Parent2中随机抽取其中的一部分把它移植到Parent1中生成新的染色体,比如,我们选取 “(A,2)(C,1)(A,3)(B,3)” 这个片段
我们首先随机选取Parent1中的一个位置,把这个“(A,2)(C,1)(A,3)(B,3)”片段插到该位置之后
同时,我们把原Parent1中与从Parent2中移植过来的部分中重复的有序偶删掉,即可以得到新的交叉后的染色体:
1 def Crossover(p1, p2, I)://p1,p2是两条待交叉的染色体,I存放工序和工件的信息(见编码部分) 2 """Crossover operation for the GA. Generalized Order Crossover (GOX).""" 3 def Index(p1, I)://这个函数接收两个参数p1和I,返回p1对应的有序偶list,n存储的是工件总数 4 ct = [0 for j in range(n)] 5 s = [] 6 for i in p1: 7 s.append((i, ct[i])) 8 ct[i] = ct[i] + 1 9 return s 10 idx_p1 = Index(p1, I) //p1的有序偶list,p1即上文的Parent2 11 idx_p2 = Index(p2, I) //p2的有序偶list,p2即上文的Parent1 12 nt = len(idx_p1) //p1的有序偶list的长度 13 i = randint(1, nt-1)//1到nt-1间的随机数 14 j = randint(0, nt-1)//0 到nt-1间的随机数 15 k = randint(0, nt-1)//k为Parent1插入Parent2时的插入点位置 16 //implant 相当于上面的“(A,2)(C,1)(A,3)(B,3)”即从Parent1抽取的片段 17 implant = idx_p1[j:min(j+i,nt)] + idx_p1[:i - min(j+i,nt) + j] 18 lft_child = idx_p2[:k] 19 rgt_child = idx_p2[k:] 20 for jt in implant://从Parent1删除与插入片段重复的有序偶 21 if jt in lft_child: lft_child.remove(jt) 22 if jt in rgt_child: rgt_child.remove(jt) 23 //Child:即相当于BABACABCCB 24 child = [ job for (job, task) in lft_child + implant + rgt_child ] 25 return child
除了交叉操作外,遗传算法中的另一个重要操作就是变异了,英文名字叫Mutation.变异操作通常发生在交叉操作之后,它的操作对象是交叉之后得到的新解。在本文中我们通过随机交换染色体的两个位置上的值来得到变异后的新解,变异操作的代码如下:
1 def Mutation(p)://p是解的编码 2 nt = len(p)//nt存放染色体的长度 3 i = randint(0, nt - 1)//i是0到nt-1之间的一个随机数 4 j = randint(0, nt - 1)//j是0到nt-1之间的一个随机数 5 m = [job for job in p]//将解p复制到临时变量m中 6 m[i], m[j] = m[j], m[i]//交换m中i和j位置的值 7 return m//返回m
变异操作的图示如下:
变异前的染色体: ABCABCABC
随机选取两个位置:ABCABCABC
变异后的染色体:AACABCBBC
在完成了遗传算法之后,我们还需要一个函数来显示最终的结果,结果显示函数如下所示:
1 def FormatSolution(s, C, I): 2 T = [0 for j in range(n)] 3 S = [[0 for t in I[j]] for j in range(n)]//n存储的是工件总数 4 for i in range(len(s)):"""遍历染色体""" 5 j = s[i]"""获得i的工件号j """ 6 t = T[j]"""获得i是j的第几道工序t""" 7 S[j][t] = C[i]"""将i的加工时间存到S的相应位置中""" 8 T[j] = T[j] + 1"""工件j的工序累加器+1 """ 9 return S
S中存放的是每道工序开始加工的时间,它的形式为:[[a,b,c],[d,e,f],[g,h,i]],每个子list代表一个工件的信息,子list中的字母代表这个工件下面各个工序开始加工的时间。
假设我们知道每道工序开始加工的时间,同时又知道每道工序所需要的机器号,我们就可以得到每台机器上工序的加工顺序,进而可以用软件画出调度的甘特图。
最后我们来介绍一下配置文件的读取问题,在之前的博客中我们已经介绍了一个车间调度问题的基本信息可以用一个已知条件表格来表示:
表格的第一行代表每道工序所使用的机器号,表格的第二行代表每道工序的加工时间。
在编程时我们要把这个已知条件表格用一个配置文件来存储,这个配置文件可以是一个记事本文件,后缀名是.txt。
下面我们给出上面那个表格对应的配置文件:
Init.txt
2 2 2 1 3 2 2 2 2 5 1 1
可以看到这个配置文件有三行,第一行中的第一个“2”表示有2个工件,第二个“2”表示有2台机器。第二行代表工件1的信息,有五个数字,它们的含义如下:
“2”表示工件1有2道工序,
“1”表示工件1的第一道工序需要在机器1上加工,
“3”表示工件1的第一道工序在机器"1"上加工的时间为3个时间单位
“2”表示工件1的第二道工序需要在机器2上加工
“2”表示工件1的第二道工序在机器2上的加工时间为2个时间单位
我们首先要写一个函数把配置文件的信息读到一个Instance类中,最终输出的类实例 I 应该有着如下的格式:
jobs=[[(1,3),(2,2)],[(2,5),(1,1)]]
(list中又嵌套两个子list,每个子list对应一个工件的信息,子list中的一个有序偶对应表格中的一列)
n=2(工件的个数)
m=3(机器的个数)
class Instance: """Class representing an instance of the JSP.""" def __init__(self, jobs, m): self.jobs = jobs self.n = len(jobs) # number of jobs self.m = m # number of machines def __getitem__(self, i): return self.jobs[i] def __len__(self): return len(self.jobs)
我们还需要写一个函数loadInstance函数来把配置文件txt中的信息读到Instance对象的对应的成员变量中即
I = LoadInstance("配置文件.txt");
loadInstance的python代码如下:
1 def LoadInstance(fname): 2 """Load instance file.""" 3 f = open(fname, 'r')"""打开配置文件 Init.txt""" 4 head = f.readline().split()"""读入第一行""" 5 n, m = int(head[0]), int(head[1])"""将工件数和机器数存在n和m两个变量中""" 6 I = []""" 构造上文中的“jobs列表”""" 7 for l in f:"""从文件中依次读入每行数据,一行对应一个工件""" 8 l = l.split() 9 if len(l) < 3: continue """如果少于3个数则跳过该行""" 10 ntasks = int(l[0])"""ntasks存放该工件包含的工序道数,Init每行对应一个工件"""" 11 I.append([])"""每行一个子list代表一个工件的信息""" 12 for j in range(ntasks): 13 mid = int(l[j*2+1]) 14 dur = int(l[j*2+2]) 15 I[-1].append((mid, dur))"""每个工件有多道工序,每个工序对应一个有序偶(机器数,加工时间)""" 16 return Instance(I, m)"""返回Instance类的一个实例"""