Solving flow shop scheduling problem with genetic algorithm

前言

这里要来说明如何运用GA求解flow shop 问题,以下将先对flow shop 问题做个简介,说明一下编码原则,接着对每个程序块进行说明。

什么是 flow shop 问题?

简单来说,flow shop 问题就是有 n 个工件以及 m 台机器,每个工件在机器上的加工顺序都一样,如下图所示,工件1先进入机器1加工,再到机器2加工,而工件2跟随着工件1的脚步,按照同样的机器顺序加工,其他工件以此类推。

python snowflake用法 python spiffworkflow_python

 因此假设现在有3个工件2台机器,每个工件在每台机器上的加工时间,如左下图所示,工件的加工顺序为先到机器A加工再到机器B上加工,假设得到的排程结果为: Job 1->Job 2->Job 3,因此可得到如右下图的甘特图

python snowflake用法 python spiffworkflow_spring_02

问题描述

本范例是一个具有20个工件的flow shop 问题,排程目标最小化的加权延迟 (Total weighted tardiness) ,工件信息如下图所示

python snowflake用法 python spiffworkflow_python_03

排程目标

由本文范例的目标为最小化的加权延迟 (Total weighted tardiness),因此除了必须知道每个工件在每台机器上的加工时间外,还必须知道每个工件的到期日及权重。

python snowflake用法 python spiffworkflow_算法_04

工件 i 的完工时间 (Completion time)、:工件 i 的到期日 (Due date)、:工件 i 的延迟时间(Tardiness time)、工件 i 的权重(Weight)

首先计算每个工件的延迟时间,如果提早做完,则延迟时间为0

python snowflake用法 python spiffworkflow_人工智能_05

计算所有工件的加权延迟时间总和,从公式我们可以知道,当工件的权重越大,我们要尽可能的准时完成那些权重较大的工件,不然会导致总加权延迟时间太大,对于这样的排程问题来说,这就不是一个好的排程

python snowflake用法 python spiffworkflow_算法_06

另外,这里提供了另一个版本的 flow shop 程序,跟本文主要的差別在于求解目标的不同,另一版本的目标为最小化总闲置时间 (Idle time),也就是上面范例甘特图中,灰色区域的部分,期望排出来的排程,可以尽可能减少总机器的闲置时间。

编码原则

这里的编码方式很简单,每個染色体就表示一组排程结果,因此,如果 flow shop 的问题中,共有五个工件要排,则每个染色体就由五个基因所組成,每个基因即代表某个工件,会通过list 來储存每个染色体,

如下面所示:

chromosome 1 => [0,1,2,3,4]

chromosome 2 => [1,2,0,3,4]

chromosome 3 => [4,2,0,1,3]

........

程序说明

这里主要针对程序中重要的步骤来说明,有些细节未放入,如有需要请参考完整程序码或范例代码

导入包

# importing required modules
import numpy as np
import time
import copy

初始设定

此处主要包含数据或是资料给定,以及一些参数上的设定

''' ================= initialization setting ======================'''
num_job=20 # 工件数量

p=[10,10,13,4,9,4,8,15,7,1,9,3,15,9,11,6,5,14,18,3]#每个工件的完工时间
d=[50,38,49,12,20,105,73,45,6,64,15,6,92,43,78,21,15,50,150,99]#每个工件的到期时间
w=[10,5,1,5,10,1,5,10,5,1,5,10,10,5,1,10,5,5,1,5]#每个工作的到期日
# raw_input is used in python 2
population_size=int(input('Please input the size of population: ') or 30)#种群规模,默认为30
crossover_rate=float(input('Please input the size of Crossover Rate: ') or 0.8) #交叉概率,默认为0.8
mutation_rate=float(input('Please input the size of Mutation Rate: ') or 0.1) 
#变异概率,默认为0.1
mutation_selection_rate=float(input('Please input the mutation selection rate: ') or 0.5)#变异选择概率,默认为0.5
num_mutation_jobs=round(num_job*mutation_selection_rate)#变异数量
num_iteration=int(input('Please input number of iteration: ') or 2000) 
# 最大迭代次数,默认为2000


start_time = time.time()

种群初始化

根据上述所设定的族群大小,通过随机的方式,产生初始族群

'''----- generate initial population -----'''
Tbest = 999999999999999  # 最小的总加权延迟时间
best_list, best_obj = [], []
population_list = []
makespan_record = []
for i in range(population_size):
    nxm_random_num = list(np.random.permutation(num_job))  # 对0到num_job-1之间的序列进行随机排序
    population_list.append(nxm_random_num)  # 添加到population_list中
    # population_list: 30次0到num_job-1的随机排序

for n in range(num_iteration):
    Tbest_now = 99999999999  # 到目前为止最小的总加权延迟时间

交插

这里的交插方式是通过指定位置的方式进行行交插,执行的步骤如下:

  1. 通过随机选择的方式,将基因数一半的位置设为固定不变,以下图例,共有六个工件进行排序,生成两个子代,在此选定2、5、6为工件顺序不变的位置。
  2. 将 Parent 1 工件不变的位置,复制到对应的 Child 2 ,接着将Child 2 与Parent 2 进行比较。
  3. 将 parent 2 与child2不重复的工件,依序填入 Child 2 剩余的位置,形成新的子代2。
  4. Child 1 的形成方式如 Child 2 所示。

python snowflake用法 python spiffworkflow_spring_07

 

'''-------- crossover --------'''
    parent_list = population_list[:]  # 亲本列表
    offspring_list = population_list[:]  # 子代列表
    S = list(np.random.permutation(population_size))  # 生成一个随机序列,选择亲本染色体进行交叉操作
    # 对0到population_size之间的序列进行随机排序

    for m in range(int(population_size / 2)):
        crossover_prob = np.random.rand()
        if crossover_rate >= crossover_prob:
            parent_1 = population_list[S[2 * m]][:]  # 亲本1为population_list的第S[2*m]个
            parent_2 = population_list[S[2 * m + 1]][:]  # 亲本2为population_list的第S[2*m+1]个
            child_1 = ['na' for i in range(num_job)]  # 初始化子代1
            child_2 = ['na' for i in range(num_job)]  # 初始化子代2
            fix_num = round(num_job/2)  # 工件个数的一半
            g_fix = list(np.random.choice(num_job, fix_num, replace=False))  # 从20个基因中随机选10个

            for g in range(fix_num):
                child_1[g_fix[g]] = parent_2[g_fix[g]]  # 亲本2中g_fix位的基因赋给子代1
                child_2[g_fix[g]] = parent_1[g_fix[g]]  # 亲本1中g_fix位的基因赋给子代2
            c1 = [parent_1[i] for i in range(num_job) if parent_1[i] not in child_1]  # 子代1中的空位需要的基因
            c2 = [parent_2[i] for i in range(num_job) if parent_2[i] not in child_2]  # 子代2中的空位需要的基因

            for i in range(num_job - fix_num):
                child_1[child_1.index('na')] = c1[i]  # 填补子代2中的空位需要的基因
                child_2[child_2.index('na')] = c2[i]  # 填补子代2中的空位需要的基因
            offspring_list[S[2 * m]] = child_1[:]
            offspring_list[S[2 * m + 1]] = child_2[:]

变异

此方法是通过基因位移的方式进行突变,突变方式如下:

  1. 依据 mutation selection rate 決定染色体中有多少百分比的基因要进行突变,假设每条染色体有六个基因, mutation selection rate 为0.5,则有3个基因要进行突变。
  2. 随机选定要位移的基因位,假设选定5、2、6 (在此表示该位置下的基因要进行突变)
  3. 进行基因移位,移位方式如图所示。

python snowflake用法 python spiffworkflow_算法_08

'''--------mutatuon--------'''
    for m in range(len(offspring_list)):
        mutation_prob = np.random.rand()
        if mutation_rate >= mutation_prob:
            m_chg = list(np.random.choice(num_job, num_mutation_jobs, replace=False))  # 从20个基因中随机选10个变异位置
            t_value_last = offspring_list[m][m_chg[0]]  # 保存子代第一个变异位置上的值
            for i in range(num_mutation_jobs - 1):
                offspring_list[m][m_chg[i]] = offspring_list[m][m_chg[i + 1]]  # 移位

            offspring_list[m][m_chg[num_mutation_jobs - 1]] = t_value_last  # 将第一个变异位置的值移至最后一个变异位置

 

适应度计算

计算每个染色体也就是每个排程结果的总加权延迟,并将其记录,以利于进行选择时能进行比较.

'''--------fitness value(calculate tardiness)-------------'''
    total_chromosome = parent_list[:] + offspring_list[:]  # 结合亲本和子代的染色体,形成总种群total_chromosome
    chrom_fitness, chrom_fit = [], []
    total_fitness = 0
    for i in range(population_size * 2):  # 对于每个亲本和子代染色体
        ptime = 0
        tardiness = 0
        for j in range(num_job):  # 对于每个工件
            ptime = ptime + p[total_chromosome[i][j]]  # 完工时间
            tardiness = tardiness + w[total_chromosome[i][j]] * max(ptime - d[total_chromosome[i][j]], 0)  # 总加权拖期时间
        chrom_fitness.append(1 / tardiness)
        chrom_fit.append(tardiness)
        total_fitness = total_fitness + chrom_fitness[i]

选择

这里选择轮盘赌 (Roulette wheel) 的选择机制

'''----------selection----------'''
    pk, qk = [], []

    for i in range(population_size * 2):  # 对于每个亲本和子代染色体
        pk.append(chrom_fitness[i] / total_fitness)
    for i in range(population_size * 2):
        cumulative = 0
        for j in range(0, i + 1):
            cumulative = cumulative + pk[j]
        qk.append(cumulative)

    selection_rand = [np.random.rand() for i in range(population_size)]

    for i in range(population_size):
        if selection_rand[i] <= qk[0]:
            population_list[i][:] = total_chromosome[0][:]
            break
        else:
            for j in range(0, population_size * 2 - 1):
                if selection_rand[i] > qk[j] and selection_rand[i] <= qk[j + 1]:
                    population_list[i][:] = total_chromosome[j + 1][:]

比较

先比较每个染色体的总加权延迟 (chrom_fit) ,找到的最好解 (Tbest_now) ,接着在跟目前为止找到的最好解 (Tbest) 进行比较,一旦有一个的解比目前为止找到的解还要好,就替代 Tbest 并记录此解所得到的排程结果。

'''----------comparison----------'''
    for i in range(population_size * 2):
        if chrom_fit[i] < Tbest_now:
            Tbest_now = chrom_fit[i]
            sequence_now = total_chromosome[i][:]

    if Tbest_now <= Tbest:
        Tbest = Tbest_now
        sequence_best = sequence_now[:]

    makespan_record.append(Tbest)

    job_sequence_ptime = 0
    num_tardy = 0
    for k in range(num_job):  # 对于每个工件
        job_sequence_ptime = job_sequence_ptime + p[sequence_best[k]]
        if job_sequence_ptime > d[sequence_best[k]]:
            num_tardy = num_tardy + 1

结果

等所有迭代次数结束后,在输出的所有迭代中找到的最好排程结果 (sequence_best)、它的总加权延迟时间、每个工件平均加权延迟时间、有多少工件延迟以及程序的执行时间

'''----------result----------'''
print("optimeal sequence", sequence_best)
print("optimeal value:%f" % Tbest)
print("average tardiness:%f" % (Tbest / num_job))
print("number of tardy:%d" % num_tardy)
print('the elapsed time:%s' % (time.time() - start_time))

import matplotlib.pyplot as plt
%matplotlib inline
plt.plot([i for i in range(len(makespan_record))],makespan_record,'b')
plt.ylabel('makespan',fontsize=15)
plt.xlabel('generation',fontsize=15)
plt.show()
Please input the size of population: 30
Please input the size of Crossover Rate: 0.8
Please input the size of Mutation Rate: 0.1
Please input the mutation selection rate: 0.5
Please input number of iteration: 2000
optimeal sequence [11, 4, 15, 16, 3, 0, 7, 19, 9, 10, 6, 12, 13, 1, 8, 17, 2, 18, 5, 14]
optimeal value:2235.000000
average tardiness:111.750000
number of tardy:12
the elapsed time:35.346625566482544

python snowflake用法 python spiffworkflow_spring_09

 

甘特图

'''--------plot gantt chart-------'''
import pandas as pd
import plotly.plotly as py
import plotly.figure_factory as ff
import plotly.offline as offline
import datetime

j_keys=[j for j in range(num_job)]
j_count={key:0 for key in j_keys}
m_count=0
j_record={}
for i in sequence_best:
   gen_t=int(p[i])
   j_count[i]=j_count[i]+gen_t
   m_count=m_count+gen_t
   
   if m_count<j_count[i]:
       m_count=j_count[i]
   elif m_count>j_count[i]:
       j_count[i]=m_count
   start_time=str(datetime.timedelta(seconds=j_count[i]-p[i])) # convert seconds to hours, minutes and seconds

   end_time=str(datetime.timedelta(seconds=j_count[i]))
   j_record[i]=[start_time,end_time]
       

df=[]
for j in j_keys:
   df.append(dict(Task='Machine', Start='2018-07-14 %s'%(str(j_record[j][0])), Finish='2018-07-14 %s'%(str(j_record[j][1])),Resource='Job %s'%(j+1)))

# colors={}
# for i in j_keys:
#     colors['Job %s'%(i+1)]='rgb(%s,%s,%s)'%(255/(i+1)+0*i,5+12*i,50+10*i)

fig = ff.create_gantt(df, colors=['#008B00','#FF8C00','#E3CF57','#0000CD','#7AC5CD','#ED9121','#76EE00','#6495ED','#008B8B','#A9A9A9','#A2CD5A','#9A32CD','#8FBC8F','#EEC900','#EEE685','#CDC1C5','#9AC0CD','#EEA2AD','#00FA9A','#CDB38B'], index_col='Resource', show_colorbar=True, group_tasks=True, showgrid_x=True)
py.iplot(fig, filename='GA_flow_shop_scheduling_tardyjob', world_readable=True)

python snowflake用法 python spiffworkflow_spring_10

 

Reference

  • António Ferrolho and Manuel Crisóstomo. “Single Machine Total Weighted Tardiness Problem with Genetic Algorithms”
  • N. Liu, Mohamed A. Abdelrahman, and Snni Ramaswamy. “A Genetic Algorithm for the Single Machine Total Weighted Tardiness Problem”