在前兩篇博客里面,我們重點講解了利用隨機搜索的方法解決車間調度問題,流程圖如下:
在本篇博客中,我們將介紹如何利用遺傳算法來解決車間調度問題。具體的算法流程圖如下:
與上面流程圖相對應的遺傳算法的整體代碼如下:
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類的一個實例"""