简介:面向计算机相关专业学生与开发者,这份资源围绕深度强化学习在多智能体电梯群控系统中的应用展开,重点实现目的楼层预约调度算法,适合作为毕设、课设或项目立项演示。包内共30个文件,包含13个Python源码、14个编译生成的pyc缓存文件、1份doc方案说明、1份md报告文档及1个附加实验结果压缩包,整体约1.94MB,源码按不同算法版本划分目录,便于对照学习。已有529人学习下载。通过DQN、Sarsa、Q-Learning等经典强化学习算法的对比实现,读者可以理解调度决策的建模流程,并借助报告文档快速把握设计思路,也可在此基础上扩展功能用于实际场景。资源代码经测试运行通过,适合初学者进阶,也便于教师用于课堂演示。
1. 电梯调度为什么值得用深度强化学习
传统电梯的响应方式是乘客在厅外按上行/下行,轿厢就近接单;遇到办公楼下班高峰,电梯会频繁换向、反复折返,候梯时间被拉得很长。目的楼层预约调度把决策点提前到乘客进梯之前:乘客先输入目的楼层,系统统一分配电梯,这样群控模块能看到全局请求再决定每部轿厢的任务序列。这个分配问题很难用静态规则求全局最优,因为请求是陆续到达的,轿厢位置、方向、载荷每时每刻都在变化,本质上是一个序贯决策问题。深度强化学习正好覆盖这类场景:每部电梯作为一个智能体,共享整栋楼的电梯状态,通过奖励信号学习“当前请求该分配给谁”。这套资源把 Q-Learning、Sarsa、Sarsa(λ)、DQN 四个迷宫基线放在同一套环境里做对照,再延伸到多智能体电梯群控,适合课程设计、毕业设计,也适合想从迷宫 Demo 迁到真实调度问题的开发者。
2. 四个迷宫基线:Q-Learning、Sarsa、Sarsa(λ) 与 DQN 对照实现
2.1 迷宫环境与状态动作定义
压缩包根目录下的2_Q_Learning版本(对比用)、3_Sarsa_maze对比用、4_Sarsa_lambda_maze、DQN四个目录,共享同一套maze_env.py,差别全部收敛在各自的RL_brain.py和run_this.py里。这是刻意的工程解耦:环境不变,算法随便换,这样对比结论才干净。
迷宫用二维数组描述,0 表示通道、1 表示墙体,起点和终点各占一个格子。智能体每回合从起点出发,撞墙不移动并给一个负向小惩罚,到达终点拿到正奖励并结束回合。状态是当前格子的横纵坐标,动作是上下左右四个离散动作。这套建模和电梯群控的对应关系很直观:格子坐标相当于楼层与电梯编号的组合,动作数量相当于可选择的楼层方向或轿厢候选集。我在复现这类环境时,习惯让step()直接返回[row, col]形式的列表而不是二元组,这样后续接 DQN 时输入就是定长向量,省掉一层格式转换。
def step(self, action: int): # 动作映射: 0=上, 1=下, 2=左, 3=右 move = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)} new_pos = (self.pos[0] + move[action][0], self.pos[1] + move[action][1]) # 撞墙判定: 位置不更新, 奖励取 -0.1 if not self.is_wall(new_pos): self.pos = new_pos done = (self.pos == self.goal) reward = 1.0 if done else -0.01 state = [self.pos[0], self.pos[1]] return state, reward, done这段代码把位置更新、碰撞判定、终止信号合并到同一个 step 接口里,便于 run_this.py 里的训练循环统一调用。done判定放在坐标更新之后,保证到达终点那一帧就能拿到终止信号。reward在非终点回合给-0.01,目的是让智能体减少绕路,而不是逼它走绝对最短路径。
调参时要注意撞墙惩罚的量级。如果撞墙奖励设成-1,智能体很快就会学到“站在原地不动”的消极策略,因为原地踏步每步只损失0.01,比撞墙挨1.0惩罚划算得多。这是个非常容易踩的坑,迷宫小看不出影响,状态一复杂就会放大。
2.2 Q-Learning 与 Sarsa 的更新差异
Q-Learning 和 Sarsa 的核心代码差异只有一两行,学习出来的策略风格却完全不同。
Q-Learning 属于离线策略(off-policy)更新,目标值直接取下一状态所有动作里的最大 Q 值:
# Q-Learning: off-policy 更新 q_predict = self.q_table[state, action] q_target = reward + gamma * self.q_table[next_state, :].max() self.q_table[state, action] += lr * (q_target - q_predict)Sarsa 属于在线策略(on-policy)更新,目标值要用当前策略在下一状态实际执行的动作来计算:
# Sarsa: on-policy 更新 q_predict = self.q_table[state, action] next_action = self.choose_action(next_state) q_target = reward + gamma * self.q_table[next_state, next_action] self.q_table[state, action] += lr * (q_target - q_predict)差别就在q_target这一行:Q-Learning 对next_state取全部动作的最大值,没有二次采样,更新偏乐观;Sarsa 要先按当前 ε-greedy 策略挑一遍next_action,再用这个动作对应的 Q 值做目标,更新偏保守。放在迷宫场景里,Q-Learning 在墙角附近表现得更激进,愿意贴着墙边走;Sarsa 会绕开危险区域,收敛慢但过程稳定。
这个区别直接复制到了电梯调度场景:请求密集时,Q-Learning 风格的电梯更愿意改变当前方向去接远层乘客,Sarsa 风格的电梯则倾向保守完成当前任务再响应新请求。想直观验证差异,把两套算法在同一个迷宫上跑 300 个回合,对比累计奖励曲线,Q-Learning 前 50 回合起伏明显,Sarsa 的曲线后段更平滑。
2.3 Sarsa(λ) 与资格迹
Sarsa(λ) 在 Sarsa 基础上引入资格迹(eligibility trace),让一个回合里经过的一连串状态都能从最终奖励中获益,而不是只更新最后一步的状态动作对。实现上需要维护一张与 Q 表同 shape 的表 E,每走一步先给当前状态动作对叠加一个增量,再在更新 Q 表时按折扣系数回传:
# 资格迹表: 与 q_table 同shape, 每回合开始清零 self.E = np.zeros_like(self.q_table) # 每一步的更新逻辑 self.E[state, action] = self.E[state, action] + 1 td_error = reward + gamma * q_next - q_predict self.q_table += lr * td_error * self.E self.E *= gamma * self.lambda_decay资格迹的实际效果是让“走过这条路”这件事本身获得价值累积。lambda_decay取 0 时算法退化为标准 Sarsa,取 1 时接近蒙特卡洛,区间内取值控制的是回溯长度。对于电梯这种连续楼层轨迹,λ 取 0.9 左右能让较早做出的转向决策也分摊到后续等待时间缩短带来的奖励。迷宫状态量小,Sarsa(λ) 收益不明显,但把同样的逻辑迁移到请求序列较长的电梯调度里,它能让一次正确的分配决策影响到后续多个时间步的状态更新。
2.4 DQN 用神经网络替换 Q 表的本质
DQN 用神经网络逼近 Q 函数,迷宫这种小状态空间下未必比查表快,它的价值在状态膨胀之后才体现。电梯群控的状态里包含每部电梯的位置、方向、载荷、未响应请求列表,组合起来是天文数字,Q 表根本存不下。DQN 通过经验回放和 Target Network 两个机制让神经网络在时序相关的数据上稳定训练。
经验回放把每次转移存进循环缓冲区,训练时随机取样,打断相邻样本之间的相关性。电梯运行时序性极强,前后两个请求可能只隔 1 秒,直接按时间顺序学习会导致网络反复在局部模式上震荡。Target Network 的作用是给 TD 目标一个相对稳定的锚点:每隔固定步数把主网络参数复制过去,在两次同步之间,目标值不会跟着主网络每步变化,训练稳定性明显改善。
# 经验回放核心逻辑 if len(self.memory) > self.batch_size: batch = random.sample(self.memory, self.batch_size) for state, action, reward, next_state, done in batch: target = reward if not done: target = reward + gamma * self.target_net(next_state).max() loss = MSE(self.eval_net(state)[action], target) self.optimizer.zero_grad() loss.backward() self.optimizer.step() # 每隔 C 步同步一次 target 网络 if self.learn_step_counter % self.target_replace_iter == 0: self.target_net.load_state_dict(self.eval_net.state_dict())target_replace_iter通常取 100 到 200 步,太小起不到稳定作用,太大则目标和当前网络差距过远,训练初期的收敛路径会变慢。在电梯群控里我会结合经验池容量一起调:经验池 10000、批量 32、同步间隔 100,是实验收敛又快又稳的初始组合。
2.5 基线运行命令与选型参考
四个目录各自独立运行,环境会弹出迷宫可视化窗口,右侧显示 Q 表变化或学习曲线:
cd 2_Q_Learning版本\(对比用\) python run_this.py cd ../3_Sarsa_maze对比用 python run_this.py cd ../4_Sarsa_lambda_maze python run_this.py cd ../DQN python run_this.py要注意目录名里的括号在 shell 里需要转义,建议直接在 IDE 里打开对应目录运行,避免路径解析问题。四个基线跑完,可以按下面这张表做选型判断:
| 算法 | 更新方式 | 对探索的敏感度 | 迷宫收敛速度 | 电梯场景适配度 |
|---|---|---|---|---|
| Q-Learning | off-policy | 低 | 快但波动 | 请求密集时可能过度激进 |
| Sarsa | on-policy | 高 | 慢但稳定 | 在线实时调度更稳 |
| Sarsa(λ) | 资格迹回传 | 中 | 中等 | 长时间决策链场景 |
| DQN | 神经网络+经验池 | 中 | 慢 | 状态空间大时首选 |
这张表的判断依据是算法本身的更新特性,不是绝对结论。电梯群控系统里最终选了 DQN 作为主算法,不是因为其他三个不能用,而是因为状态空间大到 Q 表无法枚举,神经网络是唯一能继续扩展的路线。保留三个表格方法的价值在于对照:用迷宫验证算法实现没有 bug,再迁移到 DQN,排查问题时会从容很多。
3. 从迷宫到电梯:多智能体电梯群控的状态建模与 DQN 改型
3.1 系统整体框架与消息流
电梯群控和迷宫最大的不同在于实体数量:迷宫只有一个 agent 在格子上移动,电梯系统里有 M 部电梯、N 个楼层、随时到达的外呼请求。多智能体框架下,每个电梯 agent 有自己的策略网络,但观察到的全局状态是共享的。调度系统的主循环可以拆成四个模块:请求生成模块、群控协调模块、电梯执行模块、训练更新模块。
请求生成模块模拟乘客在楼层输入目的楼层,产生一条记录并打上时间戳。群控协调模块汇总当前所有未完成的请求,拼接全局状态向量。每个电梯 agent 根据状态向量评估“这个请求由我来接”的价值。电梯执行模块负责把分配到的请求转成运行指令,更新位置和载荷。训练更新模块在每个决策周期结束后收集转移数据,存入经验池并触发网络参数更新。
这套结构的核心是状态集中、决策分布:每个电梯看到的信息一样,但决策由各自网络独立产生,避免了单一控制器的单点瓶颈。
3.2 状态编码:把楼层请求变成定长向量
电梯群控不能直接把楼层坐标丢给神经网络,存在两个问题:电梯数量 M 是可变的,请求列表长度也不固定。常见做法是设定最大电梯数和最大排队请求数,用填充对齐输入长度。
以 20 层楼、4 部电梯为例,状态向量按三段拼接:
- 电梯特征段:每部电梯当前楼层(除以楼高归一化)、运行方向编码(-1 下行 / 0 静止 / 1 上行)、轿厢载荷百分比、已分配目的楼层的 one-hot 位图;
- 请求特征段:每条等待请求的来源楼层、目的楼层、已等待步数、请求人数,不足最大请求数时补零;
- 全局特征段:当前仿真时刻、所有电梯的平均楼层分布、未分配请求总数。
# 状态向量拼接示例 def build_state(env, elevators, requests, max_elev=4, max_req=16): state = [] # 每部电梯: [归一化楼层, 方向, 载荷, 目的楼层位图(20维)] for e in elevators: floor_feat = e.current_floor / env.num_floors direction_feat = {-1: 0.0, 0: 0.5, 1: 1.0}[e.direction] floor_mask = np.zeros(env.num_floors) for dest in e.destinations: floor_mask[dest] = 1.0 state.extend([floor_feat, direction_feat, e.load_ratio]) state.extend(floor_mask.tolist()) # 请求: [来源楼层, 目的楼层, 等待步数, 人数], 不足补零 for i in range(max_req): if i < len(requests): r = requests[i] state.extend([r.src / env.num_floors, r.dst / env.num_floors, r.wait_steps / 100.0, r.num_passengers / env.max_capacity]) else: state.extend([0.0, 0.0, 0.0, 0.0]) return np.array(state, dtype=np.float32)build_state的价值是把异构数据统一成固定维度向量。楼层特征归一化到[0,1]区间是为了让神经网络不同特征的尺度一致;目的楼层位图保留电梯已分配任务的完整信息,比只存“当前目标楼层”信息量大得多。补零策略要配合 mask 使用,否则网络会学到“零向量请求”是正常信号,干扰分配决策。
3.3 动作空间:先预约后分配的二级调度
电梯群控的动作设计有两种思路:一种是逐层控制,输出每部电梯下一步的运动方向,让智能体在楼层间连续决策;另一种是预约式调度,动作定义为把某个请求分配给某部电梯。资源里采用的是预约式,因为它把决策粒度放在“请求”而不是“轿厢移动”上,和目的楼层预约调度的业务逻辑完全对齐。
预约式调度的动作空间大小等于候选电梯数量 M,如果是二元决策,可以展开成 2M 个输出。网络结构上,共享层提取全局调度特征,输出层按电梯数量分组,每组两个 logits 表示接管与不接管。
class ElevatorPolicyNet(nn.Module): def __init__(self, state_dim, num_elevators, hidden=256): super().__init__() self.shared = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), ) # 输出维度: 电梯数 * 2, 每组表示 [不接管, 接管] self.head = nn.Linear(hidden, num_elevators * 2) def forward(self, state): feat = self.shared(state) logits = self.head(feat).view(-1, 2) return logits共享层让不同电梯共用特征提取参数,输出层按电梯展开成独立决策。view 操作把[batch, elev*2]重排成[batch, elev, 2],方便后续按电梯分组计算 softmax。电梯数量变化时只需要改num_elevators,网络结构不用动。
调度主循环采用固定时间窗批量分配:
while simulation_running: collect_new_destination_requests() # 收集新请求 if time_since_last_assign >= time_window: # 到达时间窗阈值 state = build_state(...) # 拼接全局状态 logits = policy_net(state) # 每个电梯输出接管分数 request_id, elevator_id = greedy_assign(logits) push_to_elevator_task_queue(request_id, elevator_id)时间窗累积请求再批量分配的好处是,系统能看到更多请求,分配结果更接近全局优化;代价是乘客等待分配决策的延迟增加。实时分配则相反,响应快但容易陷入局部抢占,A 电梯刚被派去接 5 层请求,6 层的请求马上又把 B 电梯调走,整体路径效率不高。工程上常用 2 到 3 秒的时间窗折中,既能累积到一定数量的请求,又不至于让乘客感知到明显的决策延迟。
4. 奖励整形与多智能体协作:调度实验的关键参数
4.1 奖励函数拆解:不能只惩罚平均等待时间
迷宫环境的奖励是稀疏的,走到终点才有正值,而电梯调度如果也用稀疏奖励,训练初期网络基本学不到方向。必须做奖励整形,让智能体在每一步都能获得反馈。核心权衡项有四个:乘客平均等待时间、长候梯惩罚、电梯能耗、成功送达奖励。平均等待时间反映整体服务水平,但只优化它会导致少数乘客被无限期搁置;长候梯惩罚专门对付这种情况,等待超过阈值的乘客单独累计惩罚量。
def compute_reward(env, elevators, requests, prev_energy): # 平均等待时间: 所有未完成请求的等待均值 wait_times = [r.wait_time for r in requests.active] mean_wait = np.mean(wait_times) if wait_times else 0.0 # 长候梯惩罚: 超过 60 秒的部分额外累加 long_wait_penalty = sum(max(0, r.wait_time - 60) for r in requests.active) # 能耗: 运行距离增量 + 启动次数 energy = sum(e.distance_ratio for e in elevators) # 归一化累计距离 start_penalty = sum(1 for e in elevators if e.just_started) reward = (-0.01 * mean_wait - 0.02 * long_wait_penalty - 0.005 * energy - 0.1 * start_penalty) return reward权重系数的量级决定了优化的优先级。mean_wait的系数设得比energy大,是因为乘客体验优先于能耗控制;start_penalty设成0.1是抑制电梯频繁启动转向,因为频繁换向在真实电梯里既耗电又影响舒适度。长候梯惩罚系数是整组实验里最容易出效果的一个:设成0.02时系统会在平均等待和最大等待之间找到平衡,设得过大,所有电梯都会被长候梯请求牵走,整体效率反而下降。
4.2 关键超参数配置参考表
多智能体 DQN 的超参数比迷宫版本多一层敏感性,下面这组参数来自实际调优过程,可以直接作为起点:
| 参数 | 建议值 | 调整方向说明 |
|---|---|---|
| 学习率 | 0.0005 | 1e-3 以上容易震荡,5e-4 是 DQN 在调度问题的稳妥起点 |
| 折扣因子 γ | 0.95 | 电梯决策影响时长,γ 太小时只看短期回报,忽略全局路径 |
| ε 初始 / 最小 | 1.0 / 0.05 | 前期探索要足够,否则容易被局部策略锁死 |
| ε 衰减步数 | 5000 | 衰减过快导致探索不足,过慢则收敛时间拉长 |
| 经验池容量 | 20000 | 电梯状态序列相关性强,水池太小样本多样性不足 |
| 批量大小 | 64 | 批量过小梯度噪声大,过大训练速度明显变慢 |
| 目标网络同步间隔 | 200 步 | 调度状态变化频率高,同步太快等于没有 target 网络 |
| 隐藏层维度 | 256 | 楼层数 20 以下 128 够用,复杂场景上 256 更稳 |
γ 取 0.95 而不是 0.99 是电梯场景的特殊选择:一次调度决策的影响大约持续 20 到 30 秒,折现到未来 30 步之后的奖励系数是0.95^30 ≈ 0.21,对当前决策仍有影响但不会过度放大远期收益。如果 γ 取 0.99,远期奖励未折现部分占比太高,网络会倾向于“为了未来可能出现的请求保持静止”,这是训练发散的一个隐藏原因。
4.3 共享策略与独立策略的取舍
多智能体电梯群控有两种典型的训练方式:共享策略网络和独立策略网络。共享策略指所有电梯复用同一个网络参数,优点是训练样本利用率高,数据量相当于把 M 部电梯的经验汇到一起;缺点是所有电梯学出同一种行为模式,请求密集时可能集体涌向同一个楼层。独立策略网络让每部电梯学出自己的调度风格,能产生角色分化,但每个网络只能拿到自己的局部经验,训练数据稀疏,收敛也慢。
工程上折中方案是把特征提取层共享、决策层独立,和 3.3 节里ElevatorPolicyNet的结构一致。共享层学习“什么样的整体状态有利于调度”,决策层学习“根据当前状态我这部电梯该接哪个请求”。训练时用集中式经验回放,所有电梯的经验都进入同一个池子,更新时按电梯编号分开取样本,这样既保留角色差异又不浪费数据。
5. 把实验结果跑出来:训练曲线、算法对比与验收检查单
5.1 复现实验的完整操作步骤
拿到压缩包后不要直接跑 DQN 目录,先按迷宫基线到群控系统的顺序依次验证。第一步,确认 Python 环境,建议直接用 Anaconda 创建干净环境,Python 3.8 对 PyTorch 和可视化库的兼容性最好:
conda create -n elevator python=3.8 conda activate elevator pip install numpy torch matplotlib第二步,依次运行四个迷宫目录的run_this.py,确认 Q 表能正常弹出、学习曲线稳步上升,这一步通过说明基础环境没问题。第三步,打开群控主系统的入口脚本,确认状态向量能打印出来,其中state的维度应该符合 3.2 节拼接后的长度,如果维度对不上,检查max_elev和max_req是否和自定义场景一致。第四步,跑短周期的训练,比如 500 步,确认经验池有数据写入、loss 在下降,再跑完整实验。
5.2 训练曲线怎么判读
训练曲线的横轴是训练步数,纵轴是累计奖励或平均等待时间。累计奖励上升不代表策略一定好在平均等待时间上,因为奖励函数里同时有能耗和长候梯惩罚,奖励上升可能是能耗下降贡献的。正确做法是把平均等待时间和长候梯占比分别画成独立曲线。平均等待时间呈下降趋势且伴随周期性波动,是正常的:请求到达本身有随机性,每个时间窗内的平均等待天然存在起伏。如果曲线出现突然跳水,优先检查奖励函数里的长候梯惩罚项,大概率是有乘客等待时间超过阈值,惩罚量级太大导致整体奖励骤降。
5.3 调参验证的一个具体技巧
判断多智能体有没有学到协作行为,不要只看总奖励,看“空跑率”。定义一部电梯在一个决策周期内没有接送任何乘客、只是空驶移动的里程占总里程的比例。训练前中期空跑率高是正常的,探索阶段电梯会频繁响应请求后又因为时间窗重分配而空跑;训练后期空跑率如果还超过 30%,说明分配策略已经被长候梯惩罚绑架,电梯都往同一个长候梯请求集中。
验证方法也很直观:在仿真器里只投放一个请求,观察系统最终分配给哪部电梯,以及该电梯是否先处理已有任务再去响应新请求。这个场景下最优策略是当前任务队列最短、距离请求楼层最近的电梯接管,如果网络把请求分配给远处空闲电梯,说明状态编码里的距离信息没有被网络有效利用,需要检查楼层归一化方式和位图编码是否在特征提取阶段被稀释。
本文还有配套的精品资源,点击获取