☰
多智能体强化学习与Python仿真:电梯群控调度优化实战
2026/9/28 2:19:32 网站建设 项目流程

简介:一套基于深度强化学习的目的楼层预约调度算法所构建的多智能体电梯群控系统,完整源码与报告文档均已打包。资源面向计算机、人工智能、自动化等专业的毕业设计、课程设计及项目初期立项场景,也适合具备一定编程和机器学习基础的读者学习进阶。包内共三十个文件,以十三个Python脚本和十四个Pyc编译缓存为主,另有Word方案说明、Markdown项目报告及实验压缩包,整体体积不足2MB,目录划分清晰,便于快速定位算法模块与运行入口。目前已有五百三十二人学习下载。值得关注的是,工程内包含DQN、Sarsa、Q-learning等强化学习算法的对比版本,既能帮助理解不同价值更新方式对电梯群控调度效果的影响,也方便在此基础上扩展功能、完成二次开发,可直接用于毕业设计或课程设计的成果展示。

1. 电梯群控调度:为什么传统算法在早高峰面前集体失灵

写字楼早高峰的电梯厅是个让人血压升高的场景。9点前后几百号人涌进大堂,6部电梯来回奔波,有人等了90秒才进轿厢,有人明明要去20层却被塞进一部将在5层停三次的电梯。传统的最早到达电梯分配(ECA)和固定分区调度(Zoning)在这种场景下只能做到“每部电梯都忙”,做不到“每部电梯都忙得对”。原因在于传统算法拿到的信息太少——普通的上下按钮只告诉系统“有人要去某个方向”,而不知道乘客真正要去几楼,调度只能做短视响应。深度强化学习给了电梯群控一个天然匹配的思路:让系统先收集目的楼层预约信息,再用多智能体把“派哪部电梯、按什么顺序停靠”当成序贯决策问题来训练,这正是这个项目标题背后真正要解决的问题。Python生态里写仿真、训练、评估的链路很成熟,适合拿来做课程设计、论文实验,或者给现有梯控系统加智能调度层。

2. 目的楼层预约与多智能体建模:把电梯群控问题拆成可学习的状态-动作空间

2.1 目的楼层预约为什么能改变调度上限

传统的厅外上下按钮,本质上是一个把信息丢弃的通信协议。乘客按“上”,系统只知道这个楼层有人要去上方,至于目标是6层还是26层,要等乘客进轿厢后按键才知道。于是调度算法只能基于“方向”做短视决策,等真正知道去向时,轿厢停靠序列已经不好改了。

目的楼层预约(Destination Call)的改法是:乘客在进电梯前,先在终端上输入目标楼层。系统立即知道乘客从哪层出发、要去哪层。这样调度就变成了“提前排程”——哪部电梯接哪几个乘客、按什么顺序停靠,全部可以在乘客进轿厢之前规划。这个信息增量对调度算法的提升是质的:普通按钮方案里,电梯不知道客流是集中去高层还是分散去各层,只能按“顺向捎带”的规则响应;而目的楼层预约方案可以直接按“目的地聚类”来分配电梯,把去20层的人集中到同一部电梯,途中少停靠,整组的等待和乘梯时间都会下降。

代价也明显:一旦分配错了,乘客发现自己被塞进一部绕路的电梯,不满是直接的。所以目的楼层预约对调度算法质量的要求比传统按钮更高,但也正好是强化学习(RL)派上用场的地方——它不像规则算法那样死板,能随着时段、客流、电梯实时状态动态调整分配策略。

实际工程里,目的楼层预约系统不要求所有呼梯都走终端。常见做法是:大堂和主要换乘层放目的楼层终端,普通楼层保留上下按钮,上下按钮产生“虚拟目的楼层”(按方向猜测为最远层或最近层),统一进入调度队列。我在写仿真环境时也建议同时支持这两种呼梯输入,便于后期对接真实梯控系统时做兼容。

2.2 多智能体状态空间、动作空间和奖励函数的设计

把电梯群控表述成多智能体强化学习问题,首先要明确谁是智能体。常见做法有三类:把每台电梯当作独立智能体(多智能体);把整个电梯群当作单个智能体(单智能体联合动作);以及把“每次呼梯分配”当作决策单元。对一个6台电梯、20层楼的系统,单智能体联合动作空间会膨胀到指数级,训练非常吃力。按电梯拆成多智能体,每个智能体只决定“自己这台电梯下一步接受哪个呼梯、停靠哪些楼层”,动作空间被控制在可学习范围内。

每个电梯智能体的观测状态,我一般会至少包含四组信息:电梯自身状态(当前楼层、速度方向、当前载客量、已分配目标楼层);目标楼层信息(各楼层是否有待服务乘客、各乘客等待时长);群控信息(其他电梯的当前位置和方向,便于避免两台电梯抢同一个呼梯);以及时段特征(当前仿真时刻、是否早高峰)。这个状态向量做定长编码比较省心:楼层位置用归一化数值,等待时长用最大等待时间归一化,群控信息做成其他电梯相对本梯的位置差。

动作空间需要重点设计。一个常用设计是把动作定义为“从当前待分配呼梯池里选择一个呼梯分配给本电梯”,呼梯池之外再留一个“不响应、保持当前策略”的动作。这个设计的好处是动作维度等于当前呼梯数,不会随楼层数爆炸;风险是呼梯池动态变化,动作索引的语义不固定,所以还要加mask,排除掉“满载”“方向相反”“距呼梯楼层过远”的无效动作,否则智能体会在仿真里学出一堆反直觉行为——比如让电梯穿越整个大楼去接一个顺路方向相反的乘客。

奖励函数是这个方案里最影响最终效果的部分。我会把奖励拆成四个可解释的项,用加权和组合:

# reward.py —— 电梯群控多维奖励函数示例 def compute_reward(waiting_times, ride_times, stops, load_ratio): """ 输入均为单步仿真后的统计值: waiting_times: 当前活跃乘客的等待秒数列表 ride_times: 轿厢内乘客的乘梯秒数列表 stops: 本步所有电梯的额外停靠次数 load_ratio: 各电梯载客量/容量比 返回: 本步全局奖励 """ avg_wait = (sum(waiting_times) / len(waiting_times) if waiting_times else 0.0) avg_ride = (sum(ride_times) / len(ride_times) if ride_times else 0.0) overload = sum(max(r - 0.8, 0.0) for r in load_ratio) # 权重: 等待时间占大头, 乘梯时间其次, 停靠和拥挤做修正 reward = -0.5 * avg_wait - 0.3 * avg_ride - 0.1 * stops - 0.2 * overload return reward

这里权重不是随便拍的。先跑一版随机策略,统计平均等待时间和乘梯时间的量级,再把权重设定成“每一项对总奖励的贡献大致可比”。否则如果等待时间平均40秒、乘梯时间平均60秒,而权重配成1:1,智能体会把“让人少等5秒”和“让人少坐5秒”看成等价,实际乘梯时间对体验的敏感度远低于等待时间。这个标定的过程,我放在第4章详细展开。

2.3 多智能体强化学习:集中训练、分布式执行的选型理由

电梯群控天然是一个多智能体协作问题:6台电梯共享同一个乘客流,一部电梯的决策会改变其他电梯面对的呼梯池。如果每台电梯各训练各的DQN,互相把对方的策略变化当成环境噪声,训练极易震荡。业内面对这类问题的主流做法是集中训练、分布式执行(CTDE):训练时所有电梯的观测、动作、奖励汇总到同一个学习者里更新参数,执行时每台电梯只带着自己的局部观测和共享策略做决策。

在工程实现上,最省事的是共享参数DQN:6台电梯共用一个Q网络,经验回放池也是共享的,每台电梯的转移样本都往同一个池子里放。这种做法的成本低、实现快,在仿真里效果足够稳,适合作为基线版本。如果追求更高上限,可以换成MAPPO:演员(actor)网络按每台电梯的局部观测输出动作分布,评论家(critic)网络接受全局状态(所有电梯的位置、方向、呼梯池)来估计价值,从而缓解环境不平稳。

两套方案我都建议在仿真里各跑一遍,用同一份乘客流数据对比均值回报,再决定最终交付用哪套。不要一开始就上MAPPO——共享DQN的代码量少一半,跑通后再替换成Actor-Critic框架,心里对基线的理解会扎实得多。

3. 用Python搭建一台能训练的多智能体电梯群控仿真:环境、智能体与训练闭环

3.1 电梯仿真环境的最小闭环

没有仿真环境,调度算法就是无根之木。一个能用于强化学习训练的电梯环境,至少要能模拟乘客生成、电梯移动、上下客、等待计时这四件事。楼层数和电梯数做成参数,便于后面换场景。先别急着装一堆复杂依赖,把Python环境配好,用conda建一个3.9的虚拟环境,torch用CPU版本就能跑通整个训练闭环,对机器配置要求不高。安装依赖时最容易踩的坑是torch和numpy版本不匹配,建议直接按torch官方搭配的numpy版本走。

# elevator_env.py —— 电梯群控仿真环境骨架 from dataclasses import dataclass, field from typing import List import numpy as np @dataclass class Passenger: src: int # 出发楼层 dst: int # 目的楼层 born: float # 生成时间 wait: float = 0.0 # 已等待时长 assigned_elevator: int = -1 @dataclass class Elevator: floor: int = 1 direction: int = 0 # 1 上行, -1 下行, 0 待机 targets: List[int] = field(default_factory=list) capacity: int = 10 passengers: List[int] = field(default_factory=list) class ElevatorGroupEnv: def __init__(self, num_elevators=6, num_floors=20, arrival_rate=0.15): self.num_elevators = num_elevators self.num_floors = num_floors self.arrival_rate = arrival_rate # 每步平均到达乘客数, 高峰时调大 self.elevators = [Elevator() for _ in range(num_elevators)] self.passengers = [] def reset(self): self.elevators = [Elevator() for _ in range(self.num_elevators)] self.passengers = [] return self._get_state() def step(self, assignments): """assignments: {电梯id: [乘客id, ...]}""" for eid, pids in assignments.items(): elev = self.elevators[eid] for pid in pids: p = self.passengers[pid] if p.assigned_elevator == -1: p.assigned_elevator = eid elev.targets.append(p.src) # 先接客 elev.targets.extend([p.dst]) # 再送客 rewards = [] for eid, elev in enumerate(self.elevators): r = self._move_an_elevator(eid) # 每部电梯走一步 rewards.append(r) self._spawn_passengers() self._update_waiting_times() state = self._get_state() done = self._max_waiting_time() > 90 or len(self.passengers) > 500 return state, rewards, done

这段逻辑说明一下:assignments的语义是每部电梯被分配到的乘客集合,step开始时先把乘客的出发层和目的层写进电梯targets,接着每部电梯移动一步,再生成新乘客并刷新等待时间。“最大等待时间超过90秒或活跃乘客超过500视为done”是一个简化策略,实际可以根据场景调成回合长度上限,或者两个条件同时满足才算结束。这里有个环境设计上的反直觉点:乘客生成是概率性的,所以同一个动作序列在不同seed下会得到不同回报,训练时必须固定seed。

真实电梯不能瞬移,所以每步只能让电梯上下一层或者保持,启停本身要消耗步骤。写环境时会发现,电梯运动模型太简化会导致策略学出“频繁换向”这类假动作,因为换向在仿真里没有惩罚。给每次方向变化加一个小惩罚项,比如-0.05,会让策略稳定得多,最后得出的调度序列也更接近真实梯控系统的可执行结果。

3.2 共享参数DQN实现:多智能体最快能跑的基线

电梯群控的马尔可夫决策过程实际上是部分可观的——每台电梯只知道自己的位置和全局呼梯池,不知道其他电梯内部目标列表。但这不影响我们用DQN做基线。共享参数DQN把每台电梯当作同一策略下的不同执行实例,经验样本混合在一起训练,是最容易先跑通、也最容易复现结果的多智能体基线。

# dqn_agent.py —— 共享经验回放的DQN智能体 from collections import deque import random import numpy as np import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim=256): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim)) class SharedDQN: def __init__(self, state_dim, action_dim, buffer_size=20000): self.state_dim = state_dim self.action_dim = action_dim self.q_net = DQN(state_dim, action_dim) self.target_net = DQN(state_dim, action_dim) self.target_net.load_state_dict(self.q_net.state_dict()) self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=3e-4) self.buffer = deque(maxlen=buffer_size) self.gamma = 0.99 def act(self, obs, eps=0.1, mask=None): """epsilon-greedy决策, mask为无效动作屏蔽""" if random.random() < eps: return random.randint(0, self.action_dim - 1) with torch.no_grad(): q = self.q_net(torch.tensor(obs, dtype=torch.float32).unsqueeze(0)) if mask is not None: q = q.masked_fill(torch.tensor(mask, dtype=torch.bool), -1e9) return q.argmax(dim=-1).item() def update(self, batch_size=128): batch = random.sample(self.buffer, batch_size) states, actions, rewards, n_states, dones = map(list, zip(*batch)) states = torch.tensor(np.array(states), dtype=torch.float32) actions = torch.tensor(actions, dtype=torch.long).unsqueeze(-1) rewards = torch.tensor(rewards, dtype=torch.float32).unsqueeze(-1) n_states = torch.tensor(np.array(n_states), dtype=torch.float32) dones = torch.tensor(dones, dtype=torch.float32).unsqueeze(-1) q = self.q_net(states).gather(1, actions) q_next = self.target_net(n_states).max(1, keepdim=True).values target = rewards + self.gamma * q_next * (1 - dones) loss = nn.MSELoss()(q, target.detach()) self.optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(self.q_net.parameters(), 10.0) self.optimizer.step()

决策时,每台电梯共享同一个agent对象,但传入各自的obs,决策结果互不影响。这里的关键是每台电梯的观测里要编码“本电梯自己的id状态”,比如当前楼层、方向,不然网络无法区分自己是一楼电梯还是二十楼电梯,最终学出一个所有楼层平均策略。动作mask里要排除的无效动作包括:满载电梯的接客动作、与本梯方向相反的呼梯、已分配给其他电梯的呼梯——这些mask规则其实就是传统算法里的约束条件,把它们编码进动作空间,能显著降低训练难度。exp_replay用deque实现简单但会存Python对象,训练速度在样本量大时会慢;想提速可以用numpy数组预分配固定容量,做成环形缓冲。

3.3 训练主循环:从仿真到参数更新的完整闭环

训练主循环把环境和智能体串起来。我习惯写成一个train.py,把回合数、epsilon衰减、目标网络更新频率都放在文件顶部的配置区,方便后面做参数扫描。

# train.py —— 多智能体电梯群控训练主循环 EPISODES = 2000 BATCH_SIZE = 128 GAMMA = 0.99 EPS_START = 1.0 EPS_END = 0.05 EPS_DECAY = 0.995 TARGET_UPDATE = 200 def train(env, agent): eps = EPS_START total_steps = 0 for episode in range(EPISODES): obs = env.reset() # obs: {eid: state_vector} done = False episode_reward = 0.0 while not done: # 每台电梯独立决策, 但共享同一份经验池 actions = {} for eid, o in obs.items(): actions[eid] = agent.act(o, eps) next_obs, rewards, done = env.step(actions) for eid, o in obs.items(): agent.buffer.append((o, actions[eid], rewards[eid], next_obs[eid], done)) obs = next_obs episode_reward += sum(rewards.values()) total_steps += 1 if len(agent.buffer) > BATCH_SIZE and total_steps % 4 == 0: agent.update(BATCH_SIZE) if total_steps % TARGET_UPDATE == 0: agent.target_net.load_state_dict(agent.q_net.state_dict()) eps = max(EPS_END, eps * EPS_DECAY) if episode % 50 == 0: print(f"episode={episode}, reward={episode_reward:.2f}, eps={eps:.3f}")

参数说明这里值得多说几句:eps从1.0开始衰减到0.05,是为了让前期每部电梯充分试探可行动作,后期专注于利用已有经验。目标网络每200步更新一次,是为了让Q值回归的目标不至于每步都变——如果训练震荡,可以把TARGET_UPDATE调大到500甚至1000。total_steps % 4 == 0这个“每4步更新一次”是实践里常用的性价比折衷,更新太频繁会放大相邻样本之间的相关性,导致损失曲线剧烈跳动。

这段跑通之后,你就有了完整的“仿真环境-共享策略-训练闭环”链路。我建议先不要急着把MQTT、日志埋点、Web可视化这些工程设施加进来,先用纯本地训练把算法链路跑通,再谈工程包装。压缩包里的报告文档,价值恰恰在于把“为什么这么拆环境、为什么这么定奖励”的推导过程记录下来了,而不是给你一堆跑完就忘的代码。

4. 训练参数怎么定:奖励权重、学习率与回合时长的四个关键

4.1 先把“尺子”定好:奖励权重的标定顺序

训练前最要紧的不是选算法,而是搞清楚“策略变好”在你的场景里到底意味着什么。对电梯群控,最硬的指标通常是平均等待时间(AWT)和超过60秒的长等待比例。如果奖励函数里有多个分量,先调整各分量,让它们的梯度量级可比,而不是凭感觉配比。

具体操作建议:先用随机策略跑200回合,记录每回合的avg_wait、avg_ride、stops、overload的均值,然后按“每项的均值和标准差”把奖励归一化到同一量级。这样奖励权重从“玄学调参”变成了可量化的数值配置,后面做超参搜索也有合理基线。

# calibrate_reward.py —— 奖励权重标定的小工具 import numpy as np def calibrate(env, episodes=200): """跑随机策略, 返回各奖励分量的均值/标准差, 用于确定权重""" wait_hist, ride_hist, stop_hist, load_hist = [], [], [], [] for _ in range(episodes): obs = env.reset() done = False while not done: actions = {eid: np.random.randint(0, 4) for eid in obs.keys()} obs, rewards, done = env.step(actions) wait_hist.append(rewards["wait"]) ride_hist.append(rewards["ride"]) stop_hist.append(rewards["stop"]) load_hist.append(rewards["load"]) for name, hist in [("wait", wait_hist), ("ride", ride_hist), ("stop", stop_hist), ("load", load_hist)]: print(f"{name}: mean={np.mean(hist):.3f}, std={np.std(hist):.3f}")

注意这个代码块里假设rewards是一个dict,而不是前面的标量。两种写法都可以,标量更简单,dict更利于排查。实际项目里我更推荐dict返回,训练时把各分量记录到TensorBoard,观察每个分量在训练中的演变,否则后期根本说不清总奖励变化是哪个分量在起作用。标定完成后,把各分量除以对应std再乘以预设权重,就得到了最终的奖励函数。

4.2 学习率、批次大小、回合时长的参考区间与互相制约

多智能体强化学习参数的坑在于“每个参数单独看都有合理区间,组合起来才会互相制约”。我按经验给一组参考值,并说清楚它们之间的联动关系。

  • 学习率:Adam优化器下3e-4到1e-3是DQN系列的稳定区。超过3e-3容易出现Q值震荡;低于1e-4训练很久看不到回报变化。如果换MAPPO,actor和critic学习率建议分开,actor用3e-4、critic用5e-4起步。
  • 批次大小:256及以上在电梯群控这种高维观测下更稳,128也能跑,但方差会变大。调大batch size时,也要同步增大buffer容量,否则采样多样性不够。
  • 回合长度:电梯群控不适合固定回合长度,用乘客流生命周期定义回合更合理——比如仿真到2小时结束,或累计500个乘客请求后结束。固定步数回合到后期会让环境里堆满“僵尸乘客”,回报统计失真。
  • 目标网络更新频率:200到1000步都常见。更新越快,训练稳定性越差;越慢,学习速度越慢。可以用折衷方案:每500步更新一次,或者用软更新(每步往目标网络里按tau=0.005融合参数)。

这些参数之间有一个联动逻辑:学习率越大、样本相关性越高,batch size和目标网络更新间隔也要相应放大。我遇到过最典型的组合错误是“学习率1e-3 + 每50步更新目标网络 + batch 64”,训练到一半Q值直接爆炸到1e6以上,奖励曲线彻底拉爆。如果遇到这种情况,先把学习率降到3e-4,再把TARGET_UPDATE调大到500,通常能救回来。

4.3 训练状态怎么看:三条曲线判断收敛

训练不是“跑完回合数就成功”。每次实验我都盯三条曲线:总回报曲线、平均等待时间曲线、长等待率曲线。如果总回报在上升但平均等待时间没下来,说明奖励函数里等待权重配低了;如果回报降了但等待时间也降了,先不要急着高兴,再看停靠次数是不是变多导致能耗变高。

训练过程中一定要固定评估方式。训练回合里的动作是epsilon-greedy的,末期虽然eps小但还有随机性,不能拿训练回合的回报值当作性能指标。我的习惯是每训练100回合,把eps强制置0,在验证乘客流上跑10个回合,记录平均等待时间、长等待率和电梯总停靠次数,这三个数字才是能写进报告文档的评估指标。

验证乘客流要和训练乘客流分开生成。在仿真环境里预留一个固定seed的客流生成器,训练和验证各自用不同的seed,但保持相同的到达率分布。这样训练过程中模型不会“背下”验证场景,评估结果才有说服力。

5. 电梯群控调试避坑指南:收敛慢、不协同、震荡的5个排查方向

5.1 奖励迟迟不下降:稀疏奖励与初始化乱跑

现象:训练跑了500回合,每回合总回报还在-2000附近晃,平均等待时间也没明显下降。

原因:电梯群控的奖励天然稀疏且延迟——乘客从进电梯到下电梯才产生完整反馈,回合前期电梯完全随机乱跑,大量乘客超时,负奖励一片混乱,策略网络学不到“哪一步导致了好结果”。

解决:在奖励里加与阶段性行为相关的塑形项。比如某部电梯响应了某个呼梯并朝该楼层移动一格,给一个小正奖励;乘客被送到目的地,再给一个大正奖励。塑形项的幅度要小于最终送达奖励,否则智能体会学会“一直开门关门刷奖励”。我在仿真里常把响应奖励设为0.1,送达奖励设为1.0,收敛速度比纯稀疏奖励快一倍以上。

5.2 多智能体不协同:某台电梯被反复分配、其余闲置

现象:6部电梯里有2部几乎总是满载、4部空驶,总回报却基本不变。

原因:共享经验回放里,来自“忙碌电梯”的样本占主导,策略网络对忙碌状态过拟合;同时动作mask如果没排除满载、反向的电梯,网络就会频繁选择一台已经超载的电梯——因为它在历史样本里见过这种状态,但没意识到超载是糟糕的。

解决:动作mask是必须的,满载电梯在动作空间里直接屏蔽;另外加入“轿厢负荷不均衡惩罚”或“单梯累计分配次数差”这一类群控正则项。还有一个经验是,在经验池里按电梯id做分层采样,保证每台电梯的样本有均衡的代表性,而不是全凭运气。我见过一个工程把两个补充策略都做进去后,闲置电梯的数量从4台降到了1台,乘客长等待率下降了30%。

5.3 训练中期Q值震荡甚至发散:过估计问题

现象:训练到中期,损失曲线突然拉高,回报从-400跳到-4000,Q值日志出现上万数值。

原因:DQN系列自带Q值过估计问题,在共享参数+多智能体环境下会更严重,因为每台电梯的样本输入分布不断漂移,导致模型学到不断膨胀的Q值。

解决:这是Double DQN引入的最好时机——用在线网络选动作、目标网络评估Q值,把更新公式从argmax Q改为argmax Q_online + Q_target。如果想省事,也可以直接换MAPPO,Actor-Critic框架天然不存在argmax过估计。另一个附加手段是梯度裁剪:nn.utils.clip_grad_norm_(q_net.parameters(), 10.0),这一条代码能消掉90%的“损失突然爆炸”问题。

5.4 仿真里顺风顺水,换成高峰客流就翻车:场景分布偏移

现象:在均匀客流(泊松到达率每小时300人)下训练,平均等待40秒;把到达率改成早高峰每小时1200人,平均等待直接飙到120秒,策略展开完全变形。

原因:训练时乘客流的分布太单一,策略学到的是对低负载场景的局部最优。当呼梯池变得密集,动作空间里呼梯数量变多,状态分布超出训练分布,Q值外推失效。

解决:训练时做客流场景多样化。把一天分成几个时段来生成乘客流:早高峰、晚高峰、午间平峰,按概率随机抽取一个时段作为当前回合的场景。到达率作为可调参数,用课程学习(curriculum learning)的方式从1倍峰值逐步升到3倍峰值,让策略平滑适应。我做过一个对比,用课程学习训练出来的模型,在3倍客流下依然保持60秒内平均等待,而直接用高峰数据训练出来的模型在平峰反而表现更差——场景多样化是个双向收益的方案。

5.5 模型加载后行为漂移:seed没固定、状态未归一化

现象:训练时保存的模型,在另一个机器或另一个进程加载后,同一条乘客流跑出来结果和训练末期差距很大。

原因:多半是状态向量没有归一化。楼层距离、等待时间、载客比例的量纲差异太大,训练时网络通过BatchNorm层勉强压住了分布,推理时一旦计算了新的统计量,输出就漂了。

解决:在环境侧对状态向量做固定量纲的归一化,而不是依赖网络内部的BatchNorm。例如楼层位置除以总楼层数、等待时间除以预设上限90秒、载客比例直接用0到1的小数。所有归一化参数在训练前就固定,不通过训练动态调整,这样模型在不同机器上加载行为是一致的。再补充一个细节:保存模型时把optimizer的state_dict、当前eps、归一化参数一起存成checkpoint文件,不要只存网络权重,否则恢复训练时学习率、动量和eps全乱套。

6. 用固定场景对比法验收调度效果:三个指标一张表

电梯群控系统交付时,最大的问题不是“模型够不够花哨”,而是“效果说不清”。我的习惯是固定场景对比法:用同一份乘客流脚本分别跑传统最早到达算法、固定分区算法和DRL方案,每轮跑10次取均值,记录平均等待时间、超过60秒长等待率、总停靠次数三个指标。输出模板如下:

算法平均等待时间>60秒比例总停靠次数
ECA(最早到达)48.2s18.5%312
固定分区42.7s12.3%295
DRL(本方案)32.4s6.1%268

做这一步之前记得先固定随机种子,保证三种算法面对的是同一个乘客流序列。三列指标里,长等待率比平均等待时间更反映乘客体验——平均等待低但有人等了3分钟,是不合格的调度。这个对比表写进报告文档,比任何截图都更有说服力。

如果要做更细的验证,可以再加一个“客流压力测试”:把乘客到达率从1倍逐步升到3倍,记录DRL方案在每个压力档位下的三条曲线。如果3倍到达率下长等待率还能控制在15%以内,这个方案的工程可用性就比较扎实。

复盘这个项目方向时,我最深的感受是:电梯群控系统真正的难点从来不在强化学习算法本身,而在仿真环境是否足够像真实系统。如果你把电梯的加减速曲线、平层精度这些细节加进环境,策略依然稳定,这个方案才具备落地的可能。我个人的教训是,第一版把过多精力放在PPO变体上,结果栽在状态归一化这种小坑里——先跑通共享DQN基线,再逐步换算法,是最省时间的路径。希望这些经验帮你在调度算法这条路上少走两步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询