用基本Q-learning解决作业车间调度问题(JSP),以FT06案例为例

问题实例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-Learning伪代码

那么,我们怎么样将它应用到作业车间调度里面呢?

  1. 其中最重要的一个就是Q表格的设计,这里将q_table设计为每个任务分别对应的每道工序情况下做出动作(选择工件)0-5的概率分别是多少,这里维度是7×7×7×7×7×7×6
  2. 状态State:

在这里插入图片描述

S中的每一个元素表示该元素对应下标任务的已加工工序个数,例如图中S[0]=3表示任务0已经完成了第3道工序的加工

  1. 动作Action:

选择加工的任务编号

  1. 奖励函数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)还有很大的差距,这是因为对于动作空间的设计太过简单,而且没有将机器的空闲时间利用起来,同一机器前后工序间的空闲时间太大,导致调度效果不理想,后续可以考虑使用调度规则来改善动作空间,并且将机器空闲时间考虑进去,得到更好的调度效果

具体代码如下:

  1. 调度环境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
  1. 主程序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()
  1. 数据提取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
  1. 甘特图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())
  • 11
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值