问题实例FT06
下面以FT06(有6个工件需要加工,有6台机器用于加工)作业车间调度问题为例,其中奇数列表示的是第几号机器(编码从0开始),偶数表示的是该工件在此机器上需要加工的时间是多少,每一行代表对应的每一个工件加工顺序。
6 6
2 1 0 3 1 6 3 7 5 3 4 6
1 8 2 5 4 10 5 10 0 10 3 4
2 5 3 4 5 8 0 9 1 1 4 7
1 5 0 5 2 5 3 3 4 8 5 9
2 9 1 3 4 5 5 4 0 3 3 1
1 3 3 3 5 9 0 10 4 4 2 1
Q-Learning
Q-Learning 整体算法
Q-Learning是价值学习的一种,详细原理在此不做解释,这里引用莫烦大神给出的算法伪代码:
那么,我们怎么样将它应用到作业车间调度里面呢?
- 其中最重要的一个就是Q表格的设计,这里将q_table设计为每个任务分别对应的每道工序情况下做出动作(选择工件)0-5的概率分别是多少,这里维度是7×7×7×7×7×7×6
- 状态State:
S中的每一个元素表示该元素对应下标任务的已加工工序个数,例如图中S[0]=3表示任务0已经完成了第3道工序的加工
- 动作Action:
选择加工的任务编号
- 奖励函数Reward:
if len(C) > 1 and C[-1] - C[-2] > 0: # C[-1] - C[-2] > 0 最后一个最大完工时间比倒数第二个大,得到的奖励少
R = 1 / (C[-1] - C[-2])
else:
R = 10
最终调度结果
从图中可以看出来,算法实在不断收敛的,但是最终和FT06案例的最优解(55)还有很大的差距,这是因为对于动作空间的设计太过简单,而且没有将机器的空闲时间利用起来,同一机器前后工序间的空闲时间太大,导致调度效果不理想,后续可以考虑使用调度规则来改善动作空间,并且将机器空闲时间考虑进去,得到更好的调度效果。
具体代码如下:
- 调度环境JSP.py
import numpy as np
import copy
class JspEnv:
def __init__(self, PT, Ma):
self.PT = PT
self.Ma = Ma
self.J_num = len(self.PT)
self.O_num = [len(self.PT[i]) for i in range(self.J_num)]
def reset(self):
"""
环境重置
C_m: 每个机器的加工结束时间
C_J: 每个任务的每道工序的完工时刻,未加工时为0
@return:
"""
self.C_m = [0 for _ in range(self.J_num)]
self.C_J = [[0 for _ in range(len(self.PT[j]))] for j in range(self.J_num)] # 每个工件的当前工序结束时间
def state_initial(self):
State_init = [0 for _ in range(self.J_num)]
State_term = self.O_num
return State_init, State_term
def scheduling(self, T_start, Job, O_num):
"""
调度,Job和O_num从0开始
@param T_start: 最早开工时刻
@param Job: 动作,选择加工的工件
@param O_num: 当前加工的第几道工序
@return: 最大完工时间(所有任务完成加工)
"""
self.C_m[self.Ma[Job][O_num] - 1] = T_start + self.PT[Job][O_num] # 机器上的加工结束时间
self.C_J[Job][O_num] = T_start + self.PT[Job][O_num] # 每个工序的结束时间
C_max = max(self.C_m)
return C_max
def job_selection(self, S, Q, epsilon):
"""
选择动作(工件)
弊端:只考虑了奖励值的大小,没有利用上机器的空闲时间idle
@param S: 每个任务工序的完工个数
@param Q: Q Table
@param epsilon: ε-greedy策略
@return: 选择的动作,即加工任务
"""
list_J = list(range(self.J_num))
J_undone = copy.copy(list_J) # 未完成加工的任务
for i in list_J: # 确定可以选择的工件
if S[i] == self.O_num[i]: # 工件i已经加工完成
J_undone.remove(i)
if np.random.random() < epsilon:
'''# max_v = max(np.delete(q_table[S[0]][S[1]][S[2]], [i]))
# A = list(q_table[S[0]][S[1]][S[2]]).index(max_v) # 选择其他工件,但是在Q初始为0情况下,此方法失效,因为所有值相同'''
A = J_undone[0]
if len(J_undone) > 1:
for j in J_undone: # 贪婪策略,选择奖励值大的
if Q[S[0]][S[1]][S[2]][S[3]][S[4]][S[5]][j] > Q[S[0]][S[1]][S[2]][S[3]][S[4]][S[5]][A]:
A = j
else:
A = np.random.choice(J_undone)
return A
- 主程序Q_learning_JSP.py
import numpy as np
from JSP import JspEnv
import copy
import matplotlib.pyplot as plt
from draw_gantt import GanttChart
from data_extract import load_txt
_, _, PT, Ma = load_txt("./lft06.txt", " ")
gantt_chart = GanttChart(PT, Ma)
env = JspEnv(PT, Ma)
State_init, State_term = env.state_initial()
dimension = copy.copy(env.O_num) # 各工件工序数集
for i in range(env.J_num):
dimension[i] += 1 # +1 是考虑S_next的时候会越界
dimension.append(env.J_num)
# q_table 含义:每个任务分别对应的每道工序情况下做出动作(选择工件)0-5的概率分别是多少
q_table = np.zeros(dimension) # Q初始化为0列表,其维度为dimension
alpha = 0.1
gamma = 0.9
epsilon = 0.8
episode_num = 10000
C_plot = []
C_mean = []
min_C = []
for e in range(episode_num):
S = State_init # 初始化S
O_list = []
C = []
env.reset()
start_list = []
while True:
A = env.job_selection(S, q_table, epsilon)
O_list.append(A) # 将加工任务添加到列表中
# 计算A对应的工序,然后计算其对应加工时间
O_sum = O_list.count(A) # 任务A已加工工序的个数
if O_sum == 1: # 该任务的第一道工序
Start = env.C_m[Ma[A][O_sum - 1] - 1]
else: # 机器上一工序的完工时刻和工件上一工序的完工时刻中的大那个
Start = max(env.C_m[Ma[A][O_sum - 1] - 1], env.C_J[A][O_sum - 2]) # 工序最早开工时间
start_list.append(Start)
C.append(env.scheduling(Start, A, O_sum - 1)) # 执行后的完工时间
S_next = copy.copy(S)
S_next[A] += 1 # 每个任务的工序的完工个数
if len(C) > 1 and C[-1] - C[-2] > 0: # C[-1] - C[-2] > 0 最后一个最大完工时间比倒数第二个大,得到的奖励少
R = 1 / (C[-1] - C[-2])
else:
R = 10
q_table[S[0]][S[1]][S[2]][S[3]][S[4]][S[5]][A] += alpha * (
R + gamma * np.max(q_table[S_next[0]][S_next[1]][S_next[2]][S_next[3]][S_next[4]][S_next[5]])
- q_table[S[0]][S[1]][S[2]][S[3]][S[4]][S[5]][A])
S = S_next
if S == State_term:
break
if e == episode_num - 1:
plt.figure(1)
C_J = env.C_J
print("工件顺序列表:", O_list) # 工件顺序列表
print("各工序完工时间:", C_J) # 各工序完工时间
print("开始时间列表:", start_list)
gantt_chart.draw_gantt(start_list, O_list, C_J)
if e % 100 == 0:
print("episode: {}/{}".format(episode_num, e))
C_plot.append(C[-1])
C_mean.append(np.mean(C_plot))
min_C.append(np.min(C_plot))
plt.figure(2)
plt.plot(C_plot[:], label="makeSpan of each episode")
plt.plot(C_mean[:], label="makeSpan of each episode with moving average")
plt.plot(min_C[:], label="min makeSpan of each episode")
plt.legend(loc="lower left")
plt.title('jsp-makeSpan')
plt.xlabel('episode')
plt.ylabel('time')
plt.show()
- 数据提取data_extract.py
def load_txt(txt_path, delimiter):
# ---
# 功能:读取只包含数字的txt文件,并转化为array形式
# txt_path:txt的路径;
# delimiter:数据之间的分隔符
# ---
array = []
with open(txt_path) as f:
data = f.readlines()
for line in data:
line = line.strip("\n") # 去除末尾的换行符
data_split = line.split(delimiter)
temp = list(map(int, data_split))
array.append(temp)
n, m = array[0][0], array[0][1] # n工件数, m机器数,
PT = []
MT = []
for i in range(1, len(array), 1):
mt = []
pt = []
for j in range(0, len(array[i]), 2):
mt.append(array[i][j])
pt.append(array[i][j + 1])
MT.append(mt) # 机器序号
PT.append(pt) # 对应机器上加工时间
return n, m, PT, MT
- 甘特图draw_gantt.py
import numpy as np
import copy
import matplotlib.pyplot as plt
class GanttChart:
def __init__(self, PT, Ma):
self.PT = PT
self.Ma = Ma
self.J_num = len(self.PT)
self.O_num = [len(self.PT[i]) for i in range(self.J_num)]
def color(self): # 甘特图颜色生成函数
color_ls = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
col = ''
for i in range(6): # 6种颜色数字字母组合
col += np.random.choice(color_ls)
return '#' + col
def draw_gantt(self, Start_list, O_list, C_J):
colors = [self.color() for i in range(self.J_num)]
self.Start_list = Start_list
num_list = []
for i, job in enumerate(O_list):
num_list.append(job)
op = num_list.count(job) # 工序
machine = self.Ma[job][op - 1] # 位置
plt.barh(y=machine, left=self.Start_list[i], width=self.PT[job][op - 1], height=0.5, color=colors[job],
label=f'job{job}')
plt.rcParams['font.sans-serif'] = ['SimHei'] # 显示中文标签
plt.rcParams['font.serif'] = ['KaiTi']
plt.rcParams['axes.unicode_minus'] = False
plt.title('jsp最优调度甘特图')
plt.xlabel('加工时间')
plt.ylabel('加工机器')
handles, labels = plt.gca().get_legend_handles_labels() # 标签去重
from collections import OrderedDict # :字典的子类,保留了他们被添加的顺序
by_label = OrderedDict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys())