简介:这份资源面向计算机、人工智能、自动化等专业的在校学生与教师,提供一套基于深度强化学习的目的楼层预约调度算法与多智能体电梯群控系统设计源码及配套报告文档,可用于毕业设计、课程设计或项目立项演示。压缩包共30个文件,约1.94MB,以13个py源码文件与14个pyc缓存文件为主,另含1份doc方案说明、1份md项目报告及1个实验结果压缩包,覆盖DQN、Sarsa、Sarsa(lambda)、Q-Learning等多种强化学习算法的对比实现,并配有迷宫环境与运行入口脚本,便于理解多智能体调度策略的训练与评估流程。目前已有532人学习下载。读者可借此获得完整可运行的算法代码、实验对比数据与项目报告,快速掌握深度强化学习在电梯群控场景中的落地思路,也可在此基础上修改扩展,完成自己的毕设或课设任务。
1. 从「电梯永远等不来」说起:这套多智能体调度系统到底解决什么问题
早高峰写字楼里,最让人抓狂的不是挤不上电梯,而是明明有六部梯,你却眼睁睁看着三部空梯从面前掠过,直奔顶层。传统电梯群控用的是先来先服务调度算法或者固定分区策略,逻辑简单、可预测,但面对潮汐客流时几乎必然翻车——它不会「预判」,只会「响应」。这套 Python 基于深度强化学习的目的楼层预约调度算法,核心思路是把每部电梯当成一个独立智能体,让它们通过多智能体协同的方式,在乘客进入轿厢前就完成目的楼层分配和路径规划,从而把平均候梯时间压下来。
它适合谁?做楼宇自动化、智能交通调度、多智能体强化学习方向的研究生和工程师,以及想找一个「有真实物理约束、有明确评价指标、能跑出对比曲线」的深度强化学习落地项目的开发者。你不需要先成为强化学习专家,但需要能看懂 Python 类结构、会配环境、愿意调参。读完这篇,你能判断这个方向值不值得投入,也能照着把最小可运行版本跑起来。
2. 为什么目的楼层预约比传统群控更值得做:原理与选型理由
2.1 传统群控的三个死结
传统电梯群控系统的主流做法是「厅外呼叫 + 先来先服务」:哪部梯先空闲就派哪部,或者按固定分区把楼层分给不同电梯。这套逻辑在低峰期够用,但高峰期会暴露三个死结。
第一是信息缺失。乘客按下上行按钮时,系统只知道「有人要往上」,不知道他去几楼。于是电梯可能在 3 楼停一次、5 楼停一次、8 楼停一次,每次停靠都意味着一次加减速,能耗和时间全耗在启停上。第二是局部最优。每部电梯独立决策,A 梯觉得自己接这个乘客最合适,B 梯也这么想,结果两部梯同时响应同一个呼叫,另一片区域没人管。第三是无法预判。先来先服务本质是反应式的,它不会根据历史客流模式提前把空闲电梯调到高需求楼层待命。
目的楼层预约(Destination Dispatch)从根上改变了信息结构:乘客在厅外就输入目的楼层,系统拿到完整的 OD 信息后再分配电梯。这一步信息前置,让「同方向合并」「减少停靠次数」成为可能。而深度强化学习负责的,就是在多部电梯、多个乘客、动态到达的条件下,学出一个分配策略,而不是靠人工写死的规则。
2.2 多智能体建模:把每部电梯当成一个 Agent
为什么用多智能体而不是单智能体?因为电梯群控天然是分布式的:每部电梯有自己的状态(当前楼层、方向、载重、已分配任务),决策之间相互影响。如果强行用一个中心智能体控制所有电梯,动作空间会随电梯数量指数膨胀,训练极难收敛。
常见做法是把每部电梯建模为一个 Agent,共享一个策略网络(参数共享),但各自观测局部状态。这样既降低了单 Agent 的决策复杂度,又能通过共享参数让所有电梯学到协同行为。状态设计一般包含:电梯当前楼层、运行方向、轿厢内人数、已分配的目的楼层列表、各楼层等待人数。动作空间是「接受或拒绝当前待分配乘客」,或者「从候选乘客中选择一个接」。
奖励函数是这套系统的灵魂。我一般会这样设计:主奖励是负的乘客等待时间,辅以负的电梯启停次数、负的能耗,再加一个「超时惩罚」——如果某乘客等待超过阈值,给一个大的负奖励。这样训练出来的策略会主动避免让任何人等太久,而不是只优化平均值。
2.3 深度强化学习算法选型:为什么 DQN 和 PPO 是主流
标题里说的是「深度强化学习」,具体用哪个算法?从可复现性和稳定性看,离散动作空间下 DQN 系列(Nature DQN、Double DQN、Dueling DQN)是最常见的选择,因为电梯的「选哪个乘客」本质是离散选择。如果动作空间设计成连续或需要处理多步决策,PPO 更稳,训练曲线也更平滑。
选型时看三个指标:样本效率、训练稳定性、部署难度。DQN 样本效率一般,但实现简单、调参经验多;PPO 样本效率高一些,但超参敏感。对于电梯群控这种仿真环境可以无限采样、但真实部署要求策略稳定的场景,我倾向于先用 DQN 跑通 baseline,再用 PPO 对比。不要一上来就上 MADDPG 这类多智能体专用算法,除非你确认环境是完全合作且通信受限——电梯群控里电梯之间可以共享全局信息,参数共享的独立 PPO 往往就够了。
3. 从零搭起仿真环境:状态、动作、奖励的具体写法
3.1 用 Python 定义电梯与乘客的数据结构
先把物理对象抽象成类。电梯需要记录楼层、方向、载重、任务队列;乘客需要记录起始楼层、目的楼层、到达时间、等待时间。下面是最小可用的数据结构,不依赖任何强化学习库,先保证仿真逻辑正确。
import heapq from dataclasses import dataclass, field from typing import List @dataclass class Passenger: pid: int origin: int # 起始楼层 dest: int # 目的楼层 arrive_time: float # 到达时刻 wait_time: float = 0.0 assigned: bool = False @dataclass class Elevator: eid: int capacity: int = 8 current_floor: int = 1 direction: int = 0 # 1 上行, -1 下行, 0 空闲 passengers: List[Passenger] = field(default_factory=list) task_queue: List[int] = field(default_factory=list) # 待停靠楼层 def load(self) -> int: return len(self.passengers) def is_full(self) -> bool: return self.load() >= self.capacity这段代码的关键在于task_queue:它记录电梯接下来要停的楼层,调度算法的输出就是往这个队列里插入目的楼层。direction和current_floor构成电梯的物理状态,passengers列表用于计算载重和判断是否满载。参数上,capacity默认 8 人,实际项目里按电梯型号改;current_floor初始为 1 层,仿真时按实际建筑配置。
3.2 状态向量与动作空间的设计细节
状态设计直接决定策略能不能学到东西。我一般把全局状态和局部状态分开:全局状态给所有 Agent 共享,包括各楼层等待人数、当前时间;局部状态是每部电梯自己的信息。下面这个函数把状态拼成一个固定长度的向量,方便喂给神经网络。
import numpy as np def build_state(elevators, waiting_passengers, num_floors, current_time): # 全局特征:每层等待人数 + 时间归一化 floor_wait = np.zeros(num_floors) for p in waiting_passengers: floor_wait[p.origin - 1] += 1 global_feat = np.concatenate([floor_wait, [current_time / 3600.0]]) states = [] for e in elevators: # 局部特征:楼层、方向、载重、任务队列长度 local = np.array([ e.current_floor / num_floors, e.direction, e.load() / e.capacity, len(e.task_queue) / num_floors ]) states.append(np.concatenate([local, global_feat])) return np.array(states)floor_wait统计每层等待人数,是策略判断「哪里需求大」的核心依据。current_time归一化到小时,避免数值过大影响训练。局部特征里e.direction用 -1/0/1 表示,e.load()/e.capacity是载重比,len(e.task_queue)/num_floors反映电梯忙碌程度。注意状态向量长度必须固定,楼层数变化时要重新定义网络输入维度。
动作空间我建议用「候选乘客选择」而不是「楼层选择」:对每部电梯,从当前等待乘客中筛出「同方向且未满载」的候选,动作就是选第几个候选,或者选「不接」。这样动作维度可控,也天然满足物理约束。
3.3 奖励函数:让策略学会「别让人等太久」
奖励函数写不好,训练出来的策略会「摆烂」——比如所有电梯都拒绝接人,等待时间反而爆炸。下面这个奖励设计经过几轮调整,比较稳。
def compute_reward(elevators, waiting_passengers, served, dt, wait_threshold=60.0): reward = 0.0 # 1. 已服务乘客的等待时间惩罚 for p in served: reward -= p.wait_time * 0.01 # 2. 超时等待的额外惩罚 for p in waiting_passengers: if p.wait_time > wait_threshold: reward -= 1.0 # 3. 电梯启停能耗惩罚 for e in elevators: if e.direction != 0: reward -= 0.05 # 4. 成功送达的正奖励 reward += len(served) * 0.5 return rewardp.wait_time * 0.01把等待时间缩放到和正奖励同一量级,避免梯度被大数值主导。wait_threshold=60秒是超时阈值,超过就持续扣分,逼策略优先处理老乘客。e.direction != 0时扣 0.05,是鼓励电梯在空闲时停靠而不是空跑。len(served) * 0.5给送达行为正反馈。这几个系数需要按仿真步长调整,步长越大,能耗惩罚系数要越小。
提示:奖励尺度不统一是训练不收敛的头号原因。先把所有项缩放到 [-1, 1] 附近,再开始调网络。
4. 训练与调参:DQN 和 PPO 在电梯群控里的实操差异
4.1 用 DQN 跑通第一个可用策略
DQN 适合离散动作,电梯调度正好是「从候选里选一个」。下面是一个精简的训练循环,重点看经验回放和目标网络更新。
import torch import torch.nn as nn import random from collections import deque class QNet(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim) ) def forward(self, x): return self.net(x) def train_dqn(env, episodes=2000, gamma=0.95, lr=1e-3, batch_size=64, buffer_size=10000): state_dim = env.state_dim action_dim = env.action_dim q_net = QNet(state_dim, action_dim) target_net = QNet(state_dim, action_dim) target_net.load_state_dict(q_net.state_dict()) optimizer = torch.optim.Adam(q_net.parameters(), lr=lr) buffer = deque(maxlen=buffer_size) for ep in range(episodes): state = env.reset() done = False while not done: if random.random() < 0.1: # epsilon 探索 action = random.randrange(action_dim) else: with torch.no_grad(): action = q_net(torch.FloatTensor(state)).argmax().item() next_state, reward, done, _ = env.step(action) buffer.append((state, action, reward, next_state, done)) state = next_state if len(buffer) >= batch_size: batch = random.sample(buffer, batch_size) s, a, r, ns, d = zip(*batch) s = torch.FloatTensor(s); a = torch.LongTensor(a) r = torch.FloatTensor(r); ns = torch.FloatTensor(ns) d = torch.FloatTensor(d) q = q_net(s).gather(1, a.unsqueeze(1)).squeeze() with torch.no_grad(): q_next = target_net(ns).max(1)[0] target = r + gamma * q_next * (1 - d) loss = nn.MSELoss()(q, target) optimizer.zero_grad(); loss.backward(); optimizer.step() if ep % 50 == 0: target_net.load_state_dict(q_net.state_dict()) return q_netgamma=0.95是折扣因子,电梯调度是长序列决策,折扣不能太低,否则策略只看眼前。epsilon=0.1是固定探索率,实际项目里建议用从 1.0 线性衰减到 0.05 的策略。target_net每 50 个 episode 同步一次,这是稳定训练的关键,同步太频繁等于没目标网络。buffer_size=10000是经验回放池,太小会导致样本相关性过强。
4.2 PPO 版本:什么时候该换算法
如果 DQN 训练曲线震荡严重,或者你想把动作空间改成「连续分配权重」,就该换 PPO。PPO 的核心是裁剪的替代目标,防止策略更新步子太大。
def ppo_update(policy, optimizer, states, actions, old_log_probs, rewards, clip_eps=0.2, epochs=10): states = torch.FloatTensor(states) actions = torch.LongTensor(actions) old_log_probs = torch.FloatTensor(old_log_probs) returns = torch.FloatTensor(rewards) for _ in range(epochs): logits = policy(states) dist = torch.distributions.Categorical(logits=logits) new_log_probs = dist.log_prob(actions) ratio = torch.exp(new_log_probs - old_log_probs) surr1 = ratio * returns surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * returns loss = -torch.min(surr1, surr2).mean() optimizer.zero_grad(); loss.backward(); optimizer.step()clip_eps=0.2是 PPO 的经典值,控制新旧策略差异。epochs=10表示同一批数据重复训练 10 次,太多会过拟合。PPO 的优势是训练更稳,但需要先收集一整批轨迹再更新,实现比 DQN 复杂。我的经验是:如果仿真环境步进快、能采大量数据,PPO 更省心;如果环境慢、样本宝贵,DQN 的经验回放更划算。
4.3 关键参数对照表
| 参数 | DQN 推荐值 | PPO 推荐值 | 作用 |
|---|---|---|---|
| 学习率 | 1e-3 | 3e-4 | 太大震荡,太小收敛慢 |
| 折扣因子 | 0.95 | 0.99 | 电梯调度看中长期收益 |
| 批大小 | 64 | 256 | PPO 需要更大批次降方差 |
| 探索率 | 1.0→0.05 | 熵系数 0.01 | 控制探索强度 |
| 目标网络同步 | 每 50 轮 | 无 | DQN 特有 |
| 训练轮数 | 2000+ | 1000+ | 看奖励曲线是否平台 |
注意:这张表是起点不是终点。楼层数、电梯数、客流强度一变,最优参数就变,必须重新扫。
5. 避坑与排查:训练不收敛、策略摆烂、仿真失真的真实原因
5.1 奖励曲线一直不涨,先查状态归一化
现象:训练几千轮,平均奖励在 0 附近抖动,策略和随机没区别。原因:状态向量里楼层、时间、人数量纲差异大,神经网络被大数值特征主导。解决:把所有特征归一化到 [0,1] 或标准化到均值 0 方差 1,楼层除以总层数,时间除以最大仿真时长,人数除以最大等待人数。
5.2 所有电梯都拒绝接人,策略「摆烂」
现象:训练中期开始,电梯频繁选择「不接」,等待人数爆炸。原因:接人带来的即时负奖励(能耗、绕路)大于延迟的正奖励,策略学会了逃避。解决:提高送达正奖励权重,或者把等待时间惩罚改成「每步都扣」,让不接人的代价立刻显现。我一般会把超时惩罚从 1.0 提到 2.0 以上。
5.3 仿真里表现很好,换客流模式就崩
现象:训练时用均匀到达客流,测试时用早高峰潮汐客流,等待时间翻倍。原因:策略过拟合到训练分布,没见过突发客流。解决:训练时做域随机化,随机切换客流模式(均匀、潮汐、随机突发),每种模式跑若干 episode。这是提升泛化最有效的手段,没有之一。
5.4 多智能体之间「抢活」或「推诿」
现象:两部电梯同时响应同一乘客,或者都以为对方会接。原因:局部观测下 Agent 不知道别人的决策。解决:在状态里加入「其他电梯的当前任务队列摘要」,或者用集中训练分散执行的框架,训练时给 Critic 全局信息,执行时只用局部观测。
5.5 训练速度慢到无法调参
现象:一个 episode 要跑几秒,扫一轮参数要几天。原因:仿真用 Python 循环逐步模拟,每步都做大量列表操作。解决:把仿真步长调大(比如 1 秒一步而不是 0.1 秒),用 numpy 向量化楼层状态更新,或者把环境写成 gym 接口后用并行采样。别小看这一步,仿真速度提升 10 倍,调参效率就是 10 倍。
6. 进阶技巧:用课程学习和奖励塑形把候梯时间再压 20%
跑通 baseline 之后,真正拉开差距的是训练技巧。我踩过最深的坑是「一上来就用完整难度训练」,结果策略在早期根本学不到有效行为。后来改用课程学习:先让电梯少、乘客少、楼层低,策略学会基本接送;再逐步增加电梯数、乘客到达率、楼层数。具体做法是维护一个难度等级,每训练 N 轮且成功率超过阈值就升一级。
class Curriculum: def __init__(self): self.level = 0 self.levels = [ {"elevators": 2, "floors": 6, "arrival_rate": 0.05}, {"elevators": 3, "floors": 10, "arrival_rate": 0.10}, {"elevators": 4, "floors": 15, "arrival_rate": 0.20}, {"elevators": 6, "floors": 20, "arrival_rate": 0.35}, ] def get_config(self): return self.levels[self.level] def step(self, success_rate): if success_rate > 0.8 and self.level < len(self.levels) - 1: self.level += 1arrival_rate是每步每层新增乘客的概率,从 0.05 到 0.35 模拟从空闲到早高峰。success_rate可以用「平均等待时间低于阈值」的比例来算。课程学习的关键是升级条件不能太松,否则策略还没学会就被推到高难度,前功尽弃。
另一个技巧是奖励塑形。原始奖励只有等待时间和能耗,稀疏且延迟。我一般会加一个「潜力函数」:用当前所有乘客到目的楼层的总距离作为势能,奖励里减去势能变化。这样策略每走一步都有反馈,收敛快很多。公式是shaped_reward = reward + gamma * phi(next_state) - phi(state),phi用负的总剩余距离。
验证方法上,别只看平均等待时间。我习惯同时记录三个指标:平均等待时间、最长等待时间、电梯启停总次数。平均等待时间容易被大量短等待拉低,最长等待时间反映公平性,启停次数反映能耗。三个指标一起看,才能判断策略是真的好还是「偏科」。
最后说个习惯:每次改奖励函数或状态设计,先跑 200 轮看曲线趋势,别等跑完 5000 轮才发现方向错了。强化学习调参是体力活,但方向对了,剩下的就是耐心。希望帮到你。
本文还有配套的精品资源,点击获取