- Paper: Optimizing urban last mile delivery efficiency (Liu & Wang, 2025) - 5 algorithms: Static-VRPTW, TA-Greedy, ALNS-Base, T-ALNS, T-ALNS-RRD - v1 baseline + v2 calibrated experiments with full results - Tabu memory ablation study with convergence analysis - Comprehensive final report (FINAL_REPORT.md)
12 KiB
T-ALNS-RRD 复现优化计划
版本管理策略
目录组织
t_alns_rrd_reproduction/
├── configs/
│ ├── default.yaml # v1 原始配置(保留不动)
│ └── calibrated.yaml # v2 校准配置(新)
├── src/ # 核心代码(通过配置参数化,不复制)
│ └── ... # 只修改 bug,不破坏 v1
├── results/
│ ├── v1_baseline/ # v1 运行结果(已生成 → 移入此处)
│ │ ├── tables/
│ │ ├── figures/
│ │ └── logs/
│ └── v2_calibrated/ # v2 运行结果(新生成)
│ ├── tables/
│ ├── figures/
│ └── logs/
└── CHANGELOG.md # 记录版本变更
运行方式:通过配置切换版本
# v1: 原始(已完成的实验)
python3 src/experiments/run_main_comparison.py --config default
# v2: 校准后
python3 src/experiments/run_main_comparison.py --config calibrated
输出自动路由到对应目录:
output_dir = f"results/{config_name}/"
修改清单(6 项,按实施顺序)
修改 1:修复 Static 基线公平性 【P0 | ~10 行】
问题:static_vrptw.py:62 构建路径时只用 interval 0 的旅行时间,评估时却用时间依赖时间 → Static 被不公平地"惩罚"
修改文件: src/baselines/static_vrptw.py
改动内容:
# 原代码 (line ~60)
def _static_route_cost(self, nodes: list) -> float:
depart_time = self.ctx.op_start
for idx in range(1, len(nodes)):
i, j = nodes[idx - 1], nodes[idx]
tt = self.ctx.traffic.travel_time[i, j, 0] # ❌ 永远 interval 0
total += tt
# 新代码
def _static_route_cost(self, nodes: list) -> float:
"""使用所有时段的平均旅行时间,而非仅 interval 0"""
depart_time = self.ctx.op_start
for idx in range(1, len(nodes)):
i, j = nodes[idx - 1], nodes[idx]
# 对所有 12 个时段取平均 → 公平的"无交通感知"基线
avg_tt = np.mean([self.ctx.traffic.travel_time[i, j, h]
for h in range(self.ctx.n_intervals)
if not np.isinf(self.ctx.traffic.travel_time[i, j, h])])
tt = avg_tt
total += tt
同时需要:Static 成本评估时不加 λ₁ 和 λ₂ 惩罚(只有纯旅行时间),让它和 TA-Greedy 在评估维度上可比。
在 cost.py 的 evaluate_solution 中加一个参数:
def evaluate_solution(self, solution, problem_ctx, include_penalties=True):
# Static: include_penalties=False → 只算旅行时间
# 其他: include_penalties=True → 完整 Eq.1
预期效果:Static 成本从 ~7580 → ~2500-3500,TA-Greedy 改善从 70% → 15-25%
修改 2:CES 缩放校准 【P0 | ~5 行】
问题:data_generator.py:326 中 ρ = θ × γ,θ=1.0,γ∈[0,1] → CES 只有 ~25。论文 CES ~1300-2850。
修改文件: src/data_generator.py
改动内容:
# 原代码 (line ~326)
rho = self.congestion_scale * gamma # ρ ∈ [0, 1],太小
# 新代码
# ρ 应反映"拥堵带来的额外时间成本",量级应与 travel_time 可比
extra_time = base_time * (self.traffic_multipliers[h] - 1.0) # 拥堵造成的额外分钟
rho = self.congestion_scale * extra_time * gamma
# γ 作为缩放因子:[0, 1] × extra_time → ρ ∈ [0, 额外时间]
# θ 控制在 config 中设置
修改文件: configs/calibrated.yaml
traffic:
congestion_scale_theta: 50.0 # v1 用 1.0 → v2 用 50.0
预期效果:CES 从 ~25 → ~300-800 范围(取决于 θ 值,可通过 config 微调)
修改 3:数据集难度提升 【P1 | ~15 行配置 + 生成参数】
问题:47 客户/4 车/120kg/宽时间窗 → OTDR 100%,无优化压力
修改文件: configs/calibrated.yaml
改动内容:
problem:
n_customers: 60 # 47 → 60 (+28%)
vehicle_capacity_kg: 100 # 120 → 100 (更紧)
# 其余不变
customers:
demand_min_kg: 4 # 3 → 4
demand_max_kg: 15 # 12 → 15 (更多变异性)
time_window_categories:
morning:
earliest: 540
latest: 720
afternoon:
earliest: 780
latest: 960
evening:
earliest: 1020
latest: 1200
# 每个客户窗口随机 30-90 分钟(而非 60-150)
window_length_min: 30 # 新增
window_length_max: 90 # 新增
修改文件: src/data_generator.py(支持新配置参数)
# 在 _extract_params 中添加
self.tw_window_min = c.get("window_length_min", 60)
self.tw_window_max = c.get("window_length_max", 150)
# 在 generate_customers 中使用
window_len = self.rng.uniform(self.tw_window_min, self.tw_window_max)
预期效果:OTDR 从 100% → 75-90%,各算法间出现明显差异
修改 4:增加迭代数与事件频率 【P1 | 配置改动】
修改文件: configs/calibrated.yaml
alns:
max_iterations: 1000 # 500 → 1000 (对齐论文)
time_limit_sec: 600
stall_limit: 400 # 200 → 400
rrd:
rollout:
horizon_min_min: 30
horizon_max_min: 120
n_sim_min: 5 # 2 → 5
n_sim_max: 30 # 50 → 30
events:
urgency_threshold: 0.3 # 0.5 → 0.3 (更容易触发)
event_probability: 0.5 # 新增在顶层
event_check_interval: 5 # 10 → 5
同时修改: src/rrd/event_generator.py 让 E1 事件真正修改 travel_time 张量
def generate_traffic_incident(self, current_time, solution):
arc = ...
# 临时将受影响的弧的旅行时间乘以 3
i, j = arc
saved_times = self.ctx.traffic.travel_time[i, j, :].copy()
self.ctx.traffic.travel_time[i, j, :] *= 3.0
event._undo = lambda: setattr(self.ctx.traffic, 'travel_time', ...) # rollback
return event
修改 5:实验参数 → 30 seeds + 统计 【P2 | ~20 行】
修改文件: src/experiments/run_main_comparison.py
改动内容:
# 1. 接受 --config 命令行参数
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="default", help="Config name")
args = parser.parse_args()
# 2. 加载对应 config
config_path = Path(f"configs/{args.config}.yaml")
with open(config_path) as f: cfg = yaml.safe_load(f)
# 3. 输出到版本目录
output_dir = Path(f"results/{args.config}/")
# 4. n_seeds 从 config 读取
n_seeds = cfg.get("experiments", {}).get("random_seeds", 30)
新增: 统计检验
from scipy import stats
# 对每对算法做 paired t-test
for i in range(len(algorithms)):
for j in range(i+1, len(algorithms)):
t_stat, p_val = stats.ttest_rel(costs_i, costs_j)
print(f" {names[i]} vs {names[j]}: t={t_stat:.2f}, p={p_val:.4f}")
修改文件: requirements.txt
scipy>=1.10 # 新增
修改 6:实验结果版本隔离 【架构 | ~10 行】
修改文件: src/experiments/run_main_comparison.py
改动内容:
def run_experiment(config_name="default", ...):
output_dir = Path(f"results/{config_name}/")
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "tables").mkdir(exist_ok=True)
(output_dir / "figures").mkdir(exist_ok=True)
(output_dir / "logs").mkdir(exist_ok=True)
# 保存一份 config 副本到结果目录
import shutil
shutil.copy(f"configs/{config_name}.yaml", output_dir / "config_used.yaml")
# ... 后续所有输出路径都基于 output_dir
完整文件变更清单
| # | 文件 | 操作 | 内容 |
|---|---|---|---|
| 1 | configs/calibrated.yaml |
新建 | v2 完整配置(复制 default + 修改 6 个参数组) |
| 2 | src/baselines/static_vrptw.py |
修改 10 行 | 使用平均旅行时间 + 移除评估中的 λ 惩罚 |
| 3 | src/cost.py |
修改 5 行 | evaluate_solution 加 include_penalties 参数 |
| 4 | src/data_generator.py |
修改 8 行 | 客户生成支持 window_length_min/max,ρ 改用 extra_time |
| 5 | src/rrd/event_generator.py |
修改 15 行 | E1 事件真正修改 travel time 张量 |
| 6 | src/experiments/run_main_comparison.py |
修改 30 行 | 支持 --config,版本输出目录,统计检验 |
| 7 | src/visualization/plot_results.py |
修改 5 行 | 接受 output_dir 参数 |
| 8 | requirements.txt |
修改 1 行 | 添加 scipy |
| 9 | CHANGELOG.md |
新建 | 记录 v1/v2 差异 |
| 10 | results/v1_baseline/ |
移动 | 将已有 v1 结果移入子目录 |
不动的文件(v1 和 v2 共用):
src/problem.py (无 bug,不碰)
src/alns/* (无 bug,参数通过 config 控制)
src/tabu/* (无 bug,参数通过 config 控制)
src/rrd/dispatch.py (无 bug)
src/rrd/rollout.py (无 bug)
src/rrd/candidate_actions.py (无 bug)
实施步骤(6 步,约 1-2 小时)
Step 0: 版本快照 + 目录准备
cd t_alns_rrd_reproduction
# 0.1 创建 CHANGELOG
echo "# Changelog\n\n## v1.0-baseline (default)\n- 47 customers, 4 vehicles, 120kg\n- θ=1.0, 200 iter, 5 seeds, complete graph\n- Known issues: inflated Static baseline, low CES, OTDR=100%\n" > CHANGELOG.md
# 0.2 隔离 v1 结果
mkdir -p results/v1_baseline
mv results/tables results/figures results/logs results/v1_baseline/ 2>/dev/null
mkdir -p results/v1_baseline/{tables,figures,logs}
Step 1: 创建 calibrated.yaml
复制 configs/default.yaml → 修改以上所有参数
Step 2: 修改 static_vrptw.py + cost.py
修复 Static 基线公平性(两个文件,共约 15 行改动)
Step 3: 修改 data_generator.py
支持新客户窗口参数 + 修复 ρ 计算(一个文件,约 10 行改动)
Step 4: 修改 event_generator.py
让 E1 事件真正影响 travel time(一个文件,约 15 行改动)
Step 5: 修改 run_main_comparison.py + 依赖
支持 --config、版本目录、统计检验、scipy(一个文件 + requirements.txt)
Step 6: 生成 v2 数据 + 跑实验
# 6.1 重新生成校准数据集
python3 -c "from src.data_generator import DataGenerator; DataGenerator(config_path='configs/calibrated.yaml', seed=42).generate_all()"
# 6.2 跑 v2 实验(30 seeds × 1000 iter ~ 20-30 分钟)
python3 src/experiments/run_main_comparison.py --config calibrated
预期结果对比(v1 vs v2)
| 指标 | v1 当前 | v2 预期 | 变化原因 |
|---|---|---|---|
| Static Cost | 7580 | 2500-3500 | 用平均时间,移除评估偏差 |
| TA-Greedy Cost | 2250 | 2000-2500 | 更难的数据集 |
| OTDR (all algos) | 100% | 75-95% | 更紧时间窗 + 更多客户 |
| CES (all algos) | 22-30 | 300-800 | θ=50 + ρ 改用 extra_time |
| Static→TA drop | 70% | 15-25% | 基线公平后差距缩小 |
| ALNS→T-ALNS drop | 0.2% | 2-5% | 更多迭代让 Tabu 生效 |
| T-ALNS→RRD drop | 0.1% | 1-3% | 更多事件 + 事件真正改 travel time |
| n_seeds | 5 | 30 | 对齐论文 |
| p-values | 无 | < 0.05 | 添加 t-test |
风险控制
| 风险 | 缓解措施 |
|---|---|
| v2 改动破坏 v1 | 所有改动通过 config 参数化,不改 v1 逻辑路径 |
| 数据集变难导致无可行解 | 先保守收紧(n=55, Q=110),不行再松 |
| 跑 30 seeds 太慢 | 先用 10 seeds 快速验证,确认趋势正确后再跑全量 |
| CES 调到什么值合适 | 先设 θ=30 跑一轮看效果,再调 |
对比报告模板
完成后 results/ 目录结构:
results/
├── v1_baseline/
│ ├── tables/main_comparison.csv
│ ├── figures/fig1-7.png
│ ├── logs/convergence.npz
│ └── config_used.yaml
├── v2_calibrated/
│ ├── tables/main_comparison.csv
│ ├── figures/fig1-7.png
│ ├── logs/convergence.npz
│ └── config_used.yaml
└── comparison_report.md # 自动生成的 v1 vs v2 对比
对比报告包含:
- 两张表并排(v1 数值 vs v2 数值)
- 趋势一致性检查(每个算法的递进方向)
- 统计显著性(v2 的 t-test 结果)
- 结论:v2 更接近论文报告的质量标准