尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

城市交通网络平衡分析:从UE原理到Frank-Wolfe配流实现

城市交通网络平衡分析:从UE原理到Frank-Wolfe配流实现 简介黄海军的《城市交通网络平衡分析理论与实践》是一本聚焦城市交通网络建模与优化的专业文献面向交通工程、轨道交通及相关领域的研究者、规划师和高校师生旨在帮助读者理解交通网络平衡原理并应对拥堵、延误等城市交通顽疾为城市交通治理提供系统方法论。资源为单个PDF文件大小约8.1MB内容精炼完整已有608人学习关注。理论层面书中系统涵盖网络拓扑结构分析、交通流基本规律、出行时间与速度变化特征、通行能力约束等核心内容实践层面则介绍了交通监控系统、模拟仿真、优化算法、信息化平台和交通规划设计等落地手段。全书将理论推导与工程应用紧密结合适合作为交通网络平衡分析的参考书助力读者构建从基础概念到实际评估的系统知识框架。1. 城市交通网络平衡分析拿到 OD 矩阵后堵车是怎么算出来的如果你手里只有一张早高峰 OD 矩阵和一套路网想提前知道明天哪个路口会排长队交通分配就是那个把需求压到路网上、算出每条路流量的环节。这个环节在学术界有个更严格的名字城市交通网络平衡分析。黄海军的《城市交通网络平衡分析理论与实践》把这个环节的数学根子讲透了用户均衡UE假设每个人都只走当前最快的路系统最优SO假设有人统一调度两者之间的差距就是交通治理的空间。这本书适合两类人——一类是在 TransCAD、Emme 里点鼠标点到心虚的模型师另一类是想把商业软件当黑匣子拆开、自己写配流内核的研究者。下面我按“先立住概念、再交出可复现代码、最后补齐踩坑”的顺序把这个方向讲成一张能照着施工的图纸。2. 从用户均衡到系统最优模型假设与适用边界交通分配的外行版本是“哪条路短走哪条”听起来简单但一旦所有车都这么想最短的路会变堵然后有人改走次短路最后收敛到一个谁都不愿意再换路的状态。这个状态才是平衡。黄海军这套理论的核心是给这种“所有人互相博弈”的集体行为找一个严格数学定义。UE 和 SO 是这个定义的两端UE 描述自由选择下的现实SO 描述管制调度下的理想模型师必须在两端之间反复切换才能回答不同层面的问题。2.1 用户均衡每个司机都以为自己在走最快路用户均衡直接来自 Wardrop 第一原理同一 OD 对之间所有被使用的路径其行程时间相等并且小于等于任何未被使用路径的行程时间。翻译成人话没有一个人能靠单方面换路缩短行程时间大家谁都没动力再动系统就停了。光背定义不好消化我给个能用手算验证的例子。设 OD 需求 3000 pcu/h有两条平行路径A 路自由流 10 分钟、通行能力 1000 pcu/hB 路自由流 15 分钟、通行能力 2000 pcu/h。路阻函数用工程里最常见的 BPR 形式t_a(x_a) t_a^0 * (1 alpha * (x_a / cap_a)^beta)alpha 取 0.15beta 取 4。假设流量 x 走 A剩余 3000 - x 走 B均衡条件是两条路时间相等10 * (1 0.15 * (x / 1000)^4) 15 * (1 0.15 * ((3000 - x) / 2000)^4)这个方程没有手算解析解但用二分法卡几个值就能找到答案A 路约 1410 pcu/hB 路约 1590 pcu/h均衡行程时间约 15.9 分钟。我列一张表方便你对照路径自由流时间min通行能力pcu/h均衡流量pcu/h均衡时间minA101000141015.9B152000159015.9注意一个容易被质疑的细节A 路自由流快但容量小最后只分到 47% 的流量B 路自由流慢但容量大反而承担更多。这就是“个体理性导致整体次优”的第一层体现。在项目汇报里这张表对应的场景是平行通道分担失衡领导会问为什么快路没被塞满你把均衡时间算出来他才明白快路一旦塞满就不再快。2.2 系统最优为什么个体都理性路网反而没在最优如果把目标从“每个司机自主选择”换成“全网总行程时间最小”模型就切换到系统最优。SO 的数学表达是min Z_SO Σ_a x_a * t_a(x_a)这个目标函数直接用当前行程时间做最短路是解错的因为每个司机换路时不会考虑自己加入后对后面所有车造成的额外延误。正确的路阻应该是边际时间成本t_a(x_a) x_a * d t_a / d x_a也就是让每个使用者承担“我多走一个单位流量给全网带来的额外延迟”。BPR 函数代入后边际项比普通项多出一个大数所以 SO 分配出来的流量会往容量更宽的路段转移牺牲部分个体的短时利益来换整体效率。UE 和 SO 的总行程时间差距就是过度拥堵的量化值。拿上面两条路径继续算UE 全网总时间约 3000 * 15.9 47700 分钟SO 会引导更多车走 B 路总时间比 UE 低几个百分点。真实路网中这个差距通常在 2% 到 15% 之间取决于网络冗余度。做方案评估时如果你拿 UE 结果直接算“采取管理措施后节省了多少时间”等于假设司机真的服从调度。大多数项目里这个前提不成立所以评估限行、收费、信号优先这类政策时要么用 SO 做理想上界要么在 UE 基础上叠加行为响应模型。2.3 程序员的读法把均衡翻译成凸优化和最短路径迭代UE 不只是一个经济学假设它有一个漂亮的等价数学形式Beckmann 变换。找一个流量向量 x极小化Z(x) Σ_a ∫_0^{x_a} t_a(w) dw这个目标函数没有直观物理含义但它的梯度恰好是路段行程时间 t_a(x_a)而 UE 条件正好是它的一阶最优性条件也就是 KKT 条件。这是整本书里最值得反复读的一段。只要 t_a 是严格单调递增函数这个凸问题有唯一解Frank-Wolfe 算法就是顺着这个性质设计的。把理论翻译成程序我习惯走三步把 Wardrop 条件写成 KKT对每个 OD 对被使用路径的时间等于最短 OD 时间未使用路径的时间不低于它。把 BPR 路阻函数当作目标函数的梯度来源流量更新就等价于沿梯度方向逼近最优。用“反复求最短路径 沿方向走一小步”替代直接解非线性方程组因为真实路网有几千个节点纯解析解法在工程上不可行。这三步做完你会明白为什么配流软件的底层循环都长一个样算路段时间、全有全无分配、更新流量、检查收敛。下一章就按这个循环把最小代码写出来。3. 用 Frank-Wolfe 把理论跑成配流程序最小可复现实现很多模型师觉得 UE 配流是商业软件的内置黑盒其实它的迭代逻辑非常短。这一章给一个能直接在 Jupyter 里跑的最小实现NetworkX 负责最短路搜索SciPy 做一维线搜索剩下的路线逻辑全部手写方便你观察中间过程、改参数、调试奇怪结果。3.1 数据准备路网表、OD 表和 BPR 路阻参数先约定输入格式。路网表至少要有四列起始节点、终到节点、自由流时间、通行能力。BPR 参数 alpha 和 beta 可以放在边属性里方便按道路等级分开设置。OD 表就是起始、终到、需求量单位我统一用 pcu/h否则后面所有对比都会乱。参数常见初值作用踩坑提示alpha0.15拥堵项系数不同道路类型应分开标定beta4.0拥堵项指数beta 固定 4 时更容易拟合其他参数capacity实际通行能力决定曲线陡升位置双车道不是单车道简单乘 2demand分时段 OD输入规模全天揉成一张 OD 基本不能用gap 阈值1e-4收敛判据商业软件常用 0.1% 到 1%读 CSV 建立 NetworkX 有向图的代码import networkx as nx G nx.DiGraph() with open(network.csv, encodingutf-8) as f: for row in f: parts row.strip().split(,) if len(parts) 6: continue u, v parts[0], parts[1] t0, cap float(parts[2]), float(parts[3]) alpha, beta float(parts[4]), float(parts[5]) # flow 初始为 0后面每次迭代直接更新边属性 G.add_edge(u, v, t0t0, capcap, alphaalpha, betabeta, flow0.0)节点 ID 可以是字符串也可以是整数但 CSV 和 OD 表必须保持一致。如果 OD 表里出现路网不存在的节点NetworkX 会在最短路搜索时报 KeyError这个错误信息对新手不太友好后面避坑章会专门讲。3.2 UE 配流主循环从 AON 到收敛只需 60 行Frank-Wolfe 每次迭代做两件事按当前流量更新路段时间再做一次全有全无分配得到辅助流量然后沿两者的差方向走一步。先写路阻函数、全有全无分配和目标函数from scipy.optimize import minimize_scalar def edge_cost(e, flow): # BPR 路阻函数自由流时间 * (1 alpha * (流量/容量)^beta) return e[t0] * (1 e[alpha] * (flow / e[cap]) ** e[beta]) def set_times(G, flow): for u, v, e in G.edges(dataTrue): e[time] edge_cost(e, flow[(u, v)]) def all_or_nothing(G, flow, od): set_times(G, flow) aux {(u, v): 0.0 for u, v in G.edges()} shortest_total 0.0 for o, d, q in od: dist nx.dijkstra_path_length(G, o, d, weighttime) shortest_total q * dist path nx.dijkstra_path(G, o, d, weighttime) for a, b in zip(path[:-1], path[1:]): aux[(a, b)] q return aux, shortest_total def objective(G, flow): # Beckmann 目标函数积分用解析式展开beta1 出现在分母 z 0.0 for u, v, e in G.edges(dataTrue): cap, beta e[cap], e[beta] z e[t0] * (flow[(u, v)] e[alpha] * cap / (beta 1) * (flow[(u, v)] / cap) ** (beta 1)) return zedge_cost是 BPR 路阻all_or_nothing把每个 OD 的需求全压到当前最短路径上同时返回按最短路径时间算出的理想总时间为后面计算 gap 做准备objective是 Beckmann 目标函数注意积分展开后的系数beta 等于 4 时分母是 5。主循环如下flow {(u, v): 0.0 for u, v in G.edges()} aux, _ all_or_nothing(G, flow, od) for e in flow: flow[e] aux[e] for k in range(1, 200): aux, shortest_total all_or_nothing(G, flow, od) d {e: aux[e] - flow[e] for e in flow} # Frank-Wolfe 方向 def phi(lam): # 在当前流与辅助流之间试一个步长返回目标函数值 trial {e: flow[e] lam * d[e] for e in flow} return objective(G, trial) res minimize_scalar(phi, bounds(0, 1), methodbounded) lam res.x flow {e: flow[e] lam * d[e] for e in flow} sys_total sum(edge_cost(e, flow[e]) * flow[e] for u, v, e in G.edges(dataTrue)) gap (sys_total - shortest_total) / sys_total print(k, round(lam, 4), round(gap, 6)) if gap 1e-4: breakd是 Frank-Wolfe 方向phi(lam)做一维线搜索找一个让目标函数最小的步长。这里用scipy.optimize.minimize_scalar的有界搜索模式比手写黄金分割省事且更稳。输出里gap就是相对对偶间隙gap 降到 1e-4 以下认为收敛。商业软件默认阈值经常是 0.1% 到 1%自写程序建议至少放到 1e-4否则断面流量会差几个百分点。3.3 步长与收敛判据MSA 和线搜索该选哪个Frank-Wolfe 最经典的简化是固定步长 1/k这个方案叫 MSA代码只需替换一行lam 1.0 / k不需要线搜索。它的优点是稳定不会在目标函数不平滑时乱跳缺点是收敛慢。BPR 在容量附近很陡目标函数接近但并不是严格的二次型1/k 的衰减速度在后期会让 gap 卡在 1e-2 级别看起来很急。我的习惯是教学网络和刚跑通的骨架用 MSA因为行为容易解释真实路网第一版一定用线搜索等模型跑顺后再评估要不要换投影类算法。线搜索还能顺带检查目标函数写没写错——如果minimize_scalar返回的步长长期在 0.01 以下往往不是算法问题而是某个边容量小到离谱流量一上去路阻就爆了。相对对偶间隙的完整公式gap (Σ_a x_a * t_a(x_a) - Σ_rs q_rs * u_rs) / Σ_a x_a * t_a(x_a)其中 u_rs 是当前路段时间下 OD 对 rs 的最短路径时间。分子是“全网因为绕路多花的时间”分母是“当前实际总行程时间”。gap 高说明还有大量车没找到更短路径gap 低说明再重新分配也榨不出多少收益。第五个避坑点里 gap 卡住不降的问题就是从这个公式发端的。4. 把配流结果当工具用五个关键参数与校正顺序跑通最小实现后真正的工程挑战是让结果可信。这一章集中讲五个关键参数alpha、beta、capacity、OD 需求、收敛阈值。前四个决定模型的骨架最后一个决定你信不信这个骨架。校准时有个铁律先修路网再调容量最后才动 OD顺序乱了基本等于给自己挖坑。4.1 BPR 标定别把 0.15/4 当万能默认值BPR 里的 0.15 和 4 是教材默认值最初来自高速公路回归经验换到城市快速路、信号交叉口、匝道就不一定对。最务实的做法是找几段有线圈检测的路段拿到分时段流量和实际行程时间用最小二乘拟合 alpha 和 betafrom scipy.optimize import curve_fit def bpr(v, t0, cap, alpha, beta): return t0 * (1 alpha * (v / cap) ** beta) # volume 是观测流量序列travel_time 是同一时段的实际行程时间 popt, _ curve_fit(bpr, volume, travel_time, p0[10, 1000, 0.15, 4.0])volume和travel_time来自同一路段的历史检测数据。这里有个统计陷阱beta 对曲线尾部形状影响极大但日常观测数据往往集中在低饱和度区间beta 的辨识度很差。所以我一般固定 beta 4只标定 alpha、t0、cap。t0 用平峰自由流车速反推cap 用断面饱和流率实测alpha 只用拥堵时段的流量时间对拟合这样每个参数都有物理锚点不会出现“拟合出来 alpha 是负数”这种玄学结果。4.2 OD 矩阵配流结果一半的误差出在需求矩阵上很多第一次自建模型的人把注意力全放在算法上忽略配流对 OD 矩阵的敏感程度。OD 需求差 10%拥堵路段流量可能差 20% 以上因为 BPR 曲线在容量附近是非线性的流量一过 capacity 时间曲线直接抬头。常见做法是先按早高峰 1 小时、平峰 1 小时、晚高峰 1 小时分别做时段 OD而不是把全天 24 小时揉成一张表。如果只有 24 小时总量就先按调查得到的时间分布系数拆成时段需求。OD 矩阵还需要平滑相邻小区之间的需求往往会集中在少数几条路径原始 OD 直接分配会让某些路段流量比实测高 30%另一些路段是 0。这时可以引入随机项做 SUE 分配或者对 OD 做空间平滑。但千万别为了拟合断面流量而“拍脑袋调 OD”那是在给自己埋雷后面第 4.3 节会解释为什么。4.3 用 GEH 校验断面流量先修路网再修 OD配流完成后的第一件事是把模型断面流量和线圈检测值对一对。工程界常用 GEH 指标GEH sqrt( 2 * (q_est - q_obs)^2 / (q_est q_obs) )GEH 范围建议 5可接受5 ~ 10需检查周边路网编码与容量 10必须查 OD 或路网不能跳过调模型有个顺序也是血泪经验先查路网编码错没错再调通行能力和自由流时间最后才动 OD。很多人一上来就调 OD结果断面流量对上了但路径结构完全不合理回头想改回真实需求难如登天。路网是底子OD 是需求事实两者都不该为了拟合观测随意改动。遇到 GEH 大于 10 的断面我一般先打开路网看上下游有没有断边、方向反、节点拼错这类低级错误在自建模型里能占到一半以上。另外收敛阈值不要一上来就要求 1e-6。真实路网的观测误差本身就大于 5%把 gap 压到 1e-6 只会让模型吞掉大量噪声时间花在不必要的迭代上。我的做法是基础方案用 1e-4方案对比时统一用 1e-4只有写论文时才跑 1e-6。5. 配流避坑指南求解器不收敛到反常识结果五个高频翻车现场理论讲得再漂亮一跑真实路网就露馅。下面五个问题是我在项目里反复遇到的每个都按现象、原因、解决三段写你复现时大概率会撞上至少一个。5.1 现象迭代 200 次gap 还在 1% 附近锯齿状看起来结果已经“差不多”但阈值降到 1e-4 就是达不到gap 曲线像锯齿一样上下跳。原因通常是步长策略和路阻函数不匹配如果用 MSA1/k 衰减太慢后期每一步都在原地踏步如果用线搜索则可能是某个边容量设得过大路阻函数太平滑方向向量 d 已经接近零。解决方法是先改用线搜索再检查有没有容量小到离谱的边如果问题依旧就把 gap 定义里的最短路目标项加上转向惩罚不要让模型利用不现实的绕行路径。5.2 现象路段行程时间比自由流还低BPR 函数的输出永远大于等于自由流时间所以出现比自由流还低的结果一定是公式实现错了。最常见原因是在代码里写成了t0 * alpha * (flow / cap) ** beta少了括号里的1 其次是流量和容量单位不一致流量是 pcu/h容量标成了 veh/s数值上看起来像负数校正。解决写一个单元测试构造流量为 0 的边断言返回 t0构造流量等于 capacity 的边断言返回 t0 * (1 alpha)。这两行断言能挡住一半以上的低级错误。5.3 现象零流量路段永远是 0手算时明明该有车UE 的性质决定“未使用路径”流量就是 0但为什么一条和最短路径只差 0.1 分钟的平行边没车第一初始化全 0 后第一次 AON 选择了最短边之后线搜索步长如果一直小于 1较慢边始终分不到流量第二这其实是 UE 的角点解在真实世界很难出现因为没人能掌握精确到 0.1 分钟的路径时间。解决如果关心这类接近并列的路径就不要用纯 UE改用 SUE 或者给路径加一个感知误差项。UE 适合看拥堵主骨架不适合回答“为什么这条次路一辆车都没有”的微观问题。5.4 现象MSA 步长让流量像布朗运动收敛曲线拉不回来MSA 的 1/k 理论上能保证收敛但真实路网有大量非线性步长序列会带来长时间震荡。现象是前几十次迭代流量大幅摆动后面摆幅减小但 gap 纹丝不动。解决把固定步长换成线搜索后基本能恢复如果还想保留 MSA 的简洁可以用几何平均步长或对上次迭代流量做指数平滑。注意震荡本身是信息如果震荡幅度大到 10%说明路网存在两条几乎一样短的路模型在它们之间来回跳这时候该去看路径集而不是继续调参数。5.5 现象商业软件和自己写的程序结果差两成怀疑书错了自写 UE 收敛到 1e-4和商业软件默认结果差 20%第一反应通常是理论有问题。实际查下来大概率是三个差异叠加商业软件默认用 SUE 或带路径规模惩罚的模型BPR 参数和你设置的不一致收敛阈值不同。还有一个容易被忽略的因素是商业软件对零点流量做了平滑处理。解决先把两边路网、OD、BPR 参数、收敛阈值统一成完全一样的输入再用一个只有两条路径的小网络做对照实验。如果小网络一致、大网络不一致再去排查转向阻抗、收费、信号延误等附加逻辑。6. 进阶玩法把平衡配流接到交通仿真之前先做三件事当你把 UE 跑稳下一步不是马上接仿真软件而是先做三件常规验证否则仿真阶段翻车的代价会高出十倍。6.1 先做多模式均衡公交和小汽车的迭代反馈真实路网里公交和小汽车共享路权公交太慢会让更多人开小汽车小汽车变多又反过来让公交延误增加。常见做法是双层迭代先跑小汽车 UE得到各路段时间更新公交行程时间然后用 logit 模型调整模式分担比例重新生成 OD再跑配流直到分担比例变化小于 0.5%。注意 logit 的尺度参数需要单独标定否则分担比例对时间差过于敏感一次小小的拥堵改善就可能让公交分担率暴涨 10 个百分点。6.2 用 KKT 条件给求解器做体检路径时间相等吗UE 收敛后抽几个需求大的 OD 对枚举前若干条路径检查它们的时间是否真的相等。代码很短for path in nx.shortest_simple_paths(G, o, d, weighttime, cutoff5): tt sum(G[a][b][time] for a, b in zip(path[:-1], path[1:])) print(round(tt, 4), path)只要被使用的路径和枚举出的最短路径时间差超过 0.1%就说明收敛判据或路径加载逻辑还有问题。大路网枚举不了所有路径就退回去看 gapgap 到 1e-4 之后路径时间差通常在 0.05% 以内。6.3 做一张敏感性参数表扰动 OD 和容量看流量变化范围最后往 OD 需求上加 ±10% 扰动再把关键路段容量调高调低 10%各跑二十次配流把重点断面的流量变化范围记成一张表。这个表才是给决策者的交付物比单点结果更接近真实不确定性。我现在每个项目交付前都会跑一遍这个敏感性矩阵花二十分钟能挡掉后面一个月的扯皮。希望帮到你。本文还有配套的精品资源点击获取
返回列表