作者:Logintern09

书接上回:多智能体进化算法求解带时间窗的VRP问题(python)。接下来我将从另一个角度详细的讲解带硬时间窗约束的VRP问题如何采用多智能体进化算法编程求解。不标题党,我在文末会附上完整python程序代码的下载链接。

VRP硬时间窗约束是硬性约束,较软时间窗约束,强制要求车辆必须在配送点的时间窗范围内到达,不能早于最早到达时间也不能晚于最晚到达时间。车辆从配送中心出发到第一个配送点的选择可能只有几个配送点的时间窗满足条件,把握这个特征进行解的生成可以有效地降低不可行解随机生成的可能性,加快算法的收敛速度。

还是以文献:《带时间窗VRP问题的多智能体进化算法》中的算例数据作为算法的输入参数进行多智能体进化算法求解VRP的算法有效性验证。

编程第一步得了解什么是带硬时间窗约束的VRP问题,以及多智能体进化算法的基本原理,我在上篇文章:多智能体进化算法求解带时间窗的VRP问题(python)中已经做了基本的介绍,更详细的介绍可以看看这方面的论文文献,此处不再赘述。

编程第二步考虑算法的可修改性,输入数据+算法参数要容易修改,一些基本模块要进行函数的封装,关键处的代码注释应该要有。至于代码的pythonic,我还做不到,还在学习探索中。

编程第三步考虑算法的有效性,至少要保证算法在求解一组算例时能得到正确的解。这个我用的文献:《带时间窗VRP问题的多智能体进化算法》中的算例数据作为我的算法输入参数,可以保证我编码的多智能体进化算法在求解这个算例时得到的解是正确的(手算验证正确+和文献计算结果对比正确),详情请看我在上篇博文:多智能体进化算法求解带时间窗的VRP问题(python)中最后的结论分析部分。

这篇博文主要详细讲讲编程的第二步,也就是如何造出来多智能体进化算法用于求解带硬时间窗约束的VRP问题,并且造出来的轮子还具有可修改可移植性。

1. 多智能体进化算法程序结构

我们先从宏观上认识一下算法的程序文件结构:

SVR案例python vrp python_机器学习

主程序文件是maea_algorithm.py,直接运行这个文件就能执行算法。input_params.py文件是读取VRP问题的输入数据input_data.xls表格转换成算法输入参数作用的,算法参数和VRP问题的成本费用参数一般也写在这个文件里。test.py是方便平时编码调试验证代码功能建立的。

2. 转化输入数据

我一般采用excel表格的形式保存VRP问题矩阵类型的输入数据,而将VRP问题成本费用等单个标量数据和算法需要的参数数据写在.py文件内方便调用。

我们结合文献:《带时间窗VRP问题的多智能体进化算法》中的算例数据来认识下input_data.xls表格每个sheet页签存放的数据内容。

算例的车辆行驶时间:

SVR案例python vrp python_数据_02

对应input_data.xlsx表格“车辆行驶时间”页签,其中“0”表示配送中心节点,“1”至“13”表示13个配送点:

SVR案例python vrp python_python_03

算例的每个配送点的需求配送量以及时间窗上下限:

SVR案例python vrp python_机器学习_04

配送点的配送量对应input_data.xlsx表格“配送量”页签:

SVR案例python vrp python_数据_05

配送点的需求时间窗对应input_data.xlsx表格“时间窗”页签,其中每个配送点的服务时间一致都为5分钟:

SVR案例python vrp python_算法_06

VRP问题的数据存储好后,要进行数据的转化,读取excel表格数据转换成程序变量用于调用。这部分的功能在input_params.py脚本中实现。

input_params.py内容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : Logintern09


import xlrd
import numpy as np
import os


# 从表格中提取数据
target_file_name = 'input_data.xls'
target_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), target_file_name)
data = xlrd.open_workbook(target_path)
sheet_name = '车辆行驶时间'
table = data.sheet_by_name(sheet_name)

supply_node_num = 1
demand_node_num = table.nrows-2  # 包含配送中心
travel_time_graph = np.zeros((demand_node_num + supply_node_num, demand_node_num + supply_node_num))

for i in range(demand_node_num + supply_node_num):
    travel_time_graph[i:] = list(map(float, (table.row_values(i+1)[1:])))

# 判断excel表格数据是否形成的对称矩阵
result_list = []
for i in range(demand_node_num):
    for j in range(demand_node_num):
        if travel_time_graph[i, j] == travel_time_graph[j, i]:
            result_list.append(True)
        else:
            result_list.append(False)
if False not in result_list:
    print('excel表格输入数据是对称矩阵')

supply_node_num = 1
supply_distance_graph = np.zeros((supply_node_num, demand_node_num))
for i in range(supply_node_num):
    supply_distance_graph[i:] = list(map(float, (table.row_values(i+1)[2:])))

# 需求点的时间窗
sheet_name = '时间窗'
table = data.sheet_by_name(sheet_name)
demand_time_window = np.zeros((2, demand_node_num+1))
for i in range(2):
    # demand_time_window的第一行表示各个需求节点的时间窗下限,第二行表示时间窗上限
    demand_time_window[i, 1:] = table.row_values(i+1)[1:]

# 需求点的服务时间
demand_service_time = table.row_values(3)[1:]
demand_service_time.insert(0, 0)

# 需求节点的需求量
demand_quantity = np.zeros((1, demand_node_num+1))
sheet_name = '配送量'
table = data.sheet_by_name(sheet_name)
demand_quantity[0, 1:] = table.row_values(1)[1:]


# 获取硬时间窗约束下车辆从配送中心出发能够到达的第一个配送点
def get_start_nodes():
    feasible_start_nodes = []
    arrive_time_list = travel_time_graph[0][1:]
    # 判断是否满足时间窗要求
    time_window_lower = demand_time_window[0][1:]
    time_window_upper = demand_time_window[1][1:]
    for i in range(demand_node_num):
        if (arrive_time_list[i] >= time_window_lower[i]) and (arrive_time_list[i] <= time_window_upper[i]):
            feasible_start_nodes.append(i + 1)
    if len(feasible_start_nodes) == 0:
        print("该组算例无解,退出算法运算")
        import sys
        sys.exit(1)
    return feasible_start_nodes


class Data_class:
    demand_node_num = demand_node_num
    travel_time_graph = travel_time_graph
    supply_node_num = supply_node_num
    supply_distance_graph = supply_distance_graph
    demand_time_window = demand_time_window
    demand_service_time = demand_service_time
    demand_quantity = demand_quantity  # 需求点的需求量
    #
    vehicle_capacity_max = 3  # 车辆的最大载重量
    feasible_start_nodes = get_start_nodes()

    # 多智能体进化算法参数
    net_scale = 7
    iter_times = 200  # 迭代次数
    balance_iter_times = 20  # 如果连续进化20代最优解不变算法停止
    self_study_iter = 10  # 自学习操作的迭代次数
    x1 = 0.4  # 最小总行驶时间个人偏好度
    x2 = 0.6  # 最小用户等待时间个人偏好度

3. 结合硬时间窗VRP问题特征进行编码

完整的多智能体进化算法求解硬时间窗约束的VRP问题打包程序会在文末附上下载链接。这节主要讲讲针对硬时间窗的约束特征如何进行算法的编码。

本篇博文开头有写道:VRP硬时间窗约束是硬性约束,较软时间窗约束,强制要求车辆必须在配送点的时间窗范围内到达,不能早于最早到达时间也不能晚于最晚到达时间。车辆从配送中心出发到第一个配送点的选择可能只有几个配送点的时间窗满足条件,把握这个特征进行解的生成可以有效地降低不可行解随机生成的可能性,加快算法的收敛速度。

针对这一特征,利用函数get_start_nodes()得到车辆离开配送中心只能到达的第一个配送点集合feasible_start_nodes,feasible_start_nodes作为全局变量参与算法解的生成。

# 获取硬时间窗约束下车辆从配送中心出发能够到达的第一个配送点
def get_start_nodes():
    feasible_start_nodes = []
    arrive_time_list = travel_time_graph[0][1:]
    # 判断是否满足时间窗要求
    time_window_lower = demand_time_window[0][1:]
    time_window_upper = demand_time_window[1][1:]
    for i in range(demand_node_num):
        if (arrive_time_list[i] >= time_window_lower[i]) and (arrive_time_list[i] <= time_window_upper[i]):
            feasible_start_nodes.append(i + 1)
    if len(feasible_start_nodes) == 0:
        print("该组算例无解,退出算法运算")
        import sys
        sys.exit(1)
    return feasible_start_nodes

多智能体进化算法的一般求解流程:

SVR案例python vrp python_机器学习_07

从整个算法求解流程来看,我们可以思考哪些地方可以利用硬时间窗的这个特征降低不可行解生成的概率。算法主程序函数如下:

def main():
    iter_times = Data_class.iter_times
    balance_iter_times = Data_class.balance_iter_times
    count = 0
    balance_count = 0
    value_list, solution_list = [], []
    # 生成初始可行智能体
    init_solution_graph = gen_init_solution()
    # 计算智能体的能量
    agent_power_graph = []
    for i in range(len(init_solution_graph)):
        agent_power = cal_agent_power(init_solution_graph[i])[1]
        agent_power_graph.append(agent_power)
    while count < iter_times:
        # 智能体的竞争行为
        agent_power_graph, init_solution_graph = agent_compete(agent_power_graph, init_solution_graph)
        # 智能体的自学习行为
        agent_power_graph, init_solution_graph = agent_self_study(agent_power_graph, init_solution_graph)
        count += 1
        print("count", count)
        # 存储解
        solution_seed = init_solution_graph[agent_power_graph.index(max(agent_power_graph))]
        total_time_cost = cal_agent_power(solution_seed)[0]
        value_list.append(total_time_cost)
        solution_list.append(solution_seed)
        # 如果连续进化20代最优解不变或者达到最大进化代数,则终止运算
        if (count > 1) and (total_time_cost == value_list[count-1]):
            balance_count += 1
        else:
            balance_count = 0
        if balance_count == balance_iter_times:
            break
    # 得到目标函数值最小的可行解
    minvalue = min(value_list)
    best_feasible_solution = solution_list[value_list.index(minvalue)]
    print("minvalue", minvalue)
    print("best_feasible_solution", best_feasible_solution)

初始可行解的生成+智能体的自学习行为,这两个部分可以利用硬时间窗车辆到达第一个配送点的可行解集合使算法尽可能的产生有效的可行解,减少利用车辆的载重量约束和配送点的时间窗约束判断解不可行的过程。

举例说明如下:

(1)初始可行解的生成过程

单个配送中心的智能体编码方案:为方便算法的执行,在编码过程中没有包括配送中心(配送中心用编号0表示),而对载质量约束和时间窗约束进行检验时需要将配送中心涵盖在上述智能体编码中以便检验。比如随机产生的智能体为1-4-5-7-9-6-3-4-2,则从左到右累加计算各个需求点的载质量,倘若载质量超过配送车辆最大载质量,则在前一客户需求点添加0,然后将累加后的载质量重置为0,再依次进行累加。比如添置后的结果为0-1-4-5-0-7-9-0-6-0-3-4-2-0,表示在此配送过程中需要3辆配送车辆,对应的路径分别为0→1→4→5→0、0→7→9→6→0、0→6→3→4→2→0。

结合上述的智能体编码方案,在某个节点前插入配送中心节点“0”后,并且该节点不可以作为车辆到达的第一个配送节点的前提下,考虑从车辆到达第一个配送点的可行解集合中任意选取一个还未被选中的节点作为新运输路径的第一个配送节点,而放弃掉原来初始随机生成路径的这个节点,这样做的好处是极大的利用硬时间窗VRP问题的特征增加生成可行解的概率。

(2)智能体的自学习行为

自学习的方法有2种,1种是通过交换编码的位置来提升能量,另1种是采用移动编码段的方式提升能量。自学习算子的学习方法见图1。

SVR案例python vrp python_数据_08

本文第一种自学习方法利用了硬时间窗车辆到达第一个配送节点有效集合的特征,第一种自学习方法主要是随机交换可行解编码内两个需求点的位置。本文算法此处的可行解是包含配送节点的,如果第一个配送点被交换出去了,为了新解也是可行解,最好在第一个配送节点有效集合内选择一个节点与该节点位置互换,函数self_study_method_1()第一种自学习方式就利用了这个特征。

def self_study_method_1(self_study_solution):
    feasible_start_nodes = Data_class.feasible_start_nodes
    node_sequence = list(set(self_study_solution) - {0})
    rand_seed_1 = random.choice(node_sequence)
    rand_seed_1_idx = self_study_solution.index(rand_seed_1)
    node_sequence.remove(rand_seed_1)
    rand_seed_2 = random.choice(node_sequence)
    rand_seed_2_idx = self_study_solution.index(rand_seed_2)
    if (rand_seed_1 in feasible_start_nodes) and (self_study_solution[rand_seed_1_idx-1] == 0):
        rand_seed_2 = random.choice(list(set(feasible_start_nodes) - {rand_seed_1}))
        rand_seed_2_idx = self_study_solution.index(rand_seed_2)
    if (rand_seed_2 in feasible_start_nodes) and (self_study_solution[rand_seed_2_idx-1] == 0):
        rand_seed_1 = random.choice(list(set(feasible_start_nodes) - {rand_seed_2}))
        rand_seed_1_idx = self_study_solution.index(rand_seed_1)
    self_study_solution_copy = deepcopy(self_study_solution)
    self_study_solution_copy[rand_seed_1_idx] = rand_seed_2
    self_study_solution_copy[rand_seed_2_idx] = rand_seed_1
    check_res = check_feasible_solution(self_study_solution_copy)
    return check_res, self_study_solution_copy

4. 下一步写作计划

论文中一般采用遗传算法与多智能体进化算法进行性能对比,论述多智能体进化算法在问题求解上的优势。接下来,我会出一系列关于VRP问题:遗传算法 VS 多智能体进化算法的文章。

5. 完整程序代码下载链接