Spaces:
Running
Running
File size: 13,695 Bytes
cdf3b47 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | """进化入口:跑 N 代遗传算法,输出每代最佳基因与适应度。
用法:.venv/bin/python run_evolve.py [--generations N] [--pop N] [--workers N]
"""
import argparse
import json
import os
from pathlib import Path
import time
from evolve.ga import GA
from status import EVOLVE_STATUS, write_status
from strategies.heuristic import GENES, make_default_genes
class Reporter:
"""把进化进度写成状态快照,供 monitor.py 实时展示。"""
def __init__(self, path, args, eval_seeds):
self.path = path
self.started = time.time()
self.gen_seconds = []
self.state = {
"kind": "evolve", "phase": "baseline",
"generation": 0, "generations": args.generations,
"done": 0, "total": args.pop, "history": [], "baseline": None,
"best_genes": None, "best_detail": None,
"default_genes": make_default_genes(),
"gene_bounds": {n: [lo, hi] for n, _d, lo, hi in GENES},
"config": {"pop": args.pop, "players": args.players,
"seeds": list(eval_seeds), "max_ticks": args.max_ticks,
"workers": args.workers, "prescreen": args.prescreen,
"live": args.live, "multistage": args.multistage,
"mid_ticks": args.mid_ticks,
"risk_lambda": args.risk_lambda,
"holdout_seeds": args.holdout_seeds,
"seed_rollover": args.seed_rollover,
"long_check_ticks": args.long_check_ticks},
}
self.flush()
def flush(self):
s = self.state
s["elapsed"] = time.time() - self.started
# ETA:已完成代的均速 × 剩余代 + 当前代剩余部分
if self.gen_seconds:
per_gen = sum(self.gen_seconds) / len(self.gen_seconds)
frac = (s["done"] / s["total"]) if s["total"] else 0
remain = per_gen * (s["generations"] - s["generation"] - frac)
s["eta"] = max(0.0, remain)
else:
s["eta"] = None
write_status(self.path, s)
def progress(self, done, total):
self.state["done"] = done
self.state["total"] = total
self.flush()
def generation_done(self, gen, best, avg, genes, detail, seconds):
self.gen_seconds.append(seconds)
s = self.state
# history 附带每代冠军的 detail(harvested/deposited/res/...)供前端画曲线
s["history"].append({"gen": gen, "best": best, "avg": avg,
"detail": detail or {}})
if s["best_genes"] is None or best >= max(h["best"] for h in s["history"]):
s["best_genes"] = genes
s["best_detail"] = detail
s["generation"] = gen + 1
self.flush()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--generations", type=int, default=8,
help="0 = 无限循环(手动/收敛停止)")
ap.add_argument("--patience", type=int, default=0,
help="连续 N 代 best 无提升则停止(0 = 不早停)")
ap.add_argument("--stop-file", default="results/evolve_stop",
help="手动停止标记文件(无限模式下存在即优雅停止)")
ap.add_argument("--pop", type=int, default=16)
ap.add_argument("--workers", type=int, default=0,
help="并行进程数(0=自动:保留 2 个 CPU 核心)")
ap.add_argument("--seeds", type=str, default="42,43,44,45")
ap.add_argument("--seed-pool", type=str, default="",
help="滚动种子池(逗号分隔,配合 --seed-rollover;"
"默认 = eval_seeds,即不滚动)")
ap.add_argument("--seed-rollover", type=int, default=0,
help="每 N 代从种子池滚动一批新种子(P0#13 过拟合缓解:"
"固定种子集合会让基因只适应几张地图;滚动后代间 best "
"不可直接比,收敛看 holdout 分数)")
ap.add_argument("--max-ticks", type=int, default=800)
ap.add_argument("--players", type=int, default=8,
help="每局玩家数(被测基因 + 分层对手)。baseline 与进化"
"必须用同一个值,否则分数不可比")
ap.add_argument("--prescreen", type=float, default=0.0,
help="预筛比例:先用 1 个种子跑全种群,仅前该比例的个体补齐"
"其余种子(0.5 约省 1.6x 时间,代价是偶尔漏掉一个好个体)")
ap.add_argument("--risk-lambda", type=float, default=0.0,
help="风险调整:选择按 fitness - λ×跨种子std 排序"
"(>0 惩罚偶尔爆高经常崩盘的策略)")
ap.add_argument("--holdout-seeds", type=str, default="",
help="独立验证种子(逗号分隔,不进选择压力):每代冠军"
"用这些种子复评并记录,防记忆过拟合固定种子")
ap.add_argument("--long-check-ticks", type=int, default=0,
help="每代冠军的长局复评时长(multistage 模式,固定种子):"
"看稳态指标(经济枯竭/侦察死区在 5k+ tick 才暴露,"
"进化压力内的短局看不到);0 = 关闭")
ap.add_argument("--long-check-seeds", type=str, default="77",
help="长局复评种子(默认 77,不进选择压力)")
ap.add_argument("--live", action="store_true",
help="线上环境模式:老玩家带兵出生(发育差)+ 偏远环带")
ap.add_argument("--multistage", action="store_true",
help="多阶段评估:0.5×短局(0→600) + "
"0.5×真实续局(600→600+mid-ticks)")
ap.add_argument("--mid-ticks", type=int, default=600,
help="多阶段中局时长")
ap.add_argument("--size", type=int, default=512,
help="live 模式世界大小(线上推断 4096+,取 768 更接近比例)")
ap.add_argument("--out", type=str, default="evolve_results.json")
ap.add_argument("--init-genes", default=None,
help="初始种群基因 JSON(warm start):围绕该基因扰动生成"
"初始种群(代替默认基因——v7 解封基因是已知好起点,"
"省去前 5-8 代的爬升)")
ap.add_argument("--status", default=EVOLVE_STATUS,
help="实时进度快照路径,供 monitor.py 读取;传空串关闭")
args = ap.parse_args()
if args.workers <= 0:
args.workers = max(1, (os.cpu_count() or 2) - 2)
eval_seeds = tuple(int(s) for s in args.seeds.split(","))
seed_pool = tuple(int(s) for s in args.seed_pool.split(",")) \
if args.seed_pool else eval_seeds
os.makedirs("results", exist_ok=True)
rep = Reporter(args.status, args, eval_seeds) if args.status else None
ga = GA(pop_size=args.pop, workers=args.workers, eval_seeds=eval_seeds,
max_ticks=args.max_ticks, seed=0, prescreen=args.prescreen,
num_players=args.players, live=args.live, size=args.size,
multistage=args.multistage, mid_ticks=args.mid_ticks,
risk_lambda=args.risk_lambda,
holdout_seeds=tuple(int(s) for s in args.holdout_seeds.split(","))
if args.holdout_seeds else (),
seed_pool=seed_pool, seed_rollover=args.seed_rollover,
long_check_ticks=args.long_check_ticks,
long_check_seeds=tuple(int(s) for s in
args.long_check_seeds.split(",")),
init_genes=(json.load(open(args.init_genes))
if args.init_genes else None),
progress=rep.progress if rep else None)
# 评估默认基因作为基线(必须与进化完全同配置:玩家数/种子/tick 数)
print("=== 评估默认基因(baseline)===")
t0 = time.time()
base_fit, base_detail = ga.evaluate_genes_by_seed(
make_default_genes(), eval_seeds)
print(f"baseline fitness: {base_fit:.1f} detail: {base_detail}")
print(f" ({time.time()-t0:.1f}s)")
if rep:
rep.state["baseline"] = base_fit
rep.state["baseline_detail"] = base_detail
rep.state["phase"] = "evolving"
rep.flush()
ga.init_population()
history = []
stop_file = Path(args.stop_file)
if stop_file.exists():
stop_file.unlink() # 启动时清旧标记
gens = args.generations if args.generations > 0 else None
gen = 0
best_overall = -1e9
best_overall_genes = None
best_overall_gen = None
best_overall_is_holdout = False
stale = 0
while gens is None or gen < gens:
t0 = time.time()
if rep:
rep.state["generation"] = gen
rep.state["done"] = 0
rep.flush()
best_fit, best_genes, avg = ga.evaluate(generation=gen, verbose=True)
dt = time.time() - t0
print(f"gen {gen}: best={best_fit:.1f} avg={avg:.1f} ({dt:.0f}s)")
if ga.last_holdout:
print(f" [holdout] {ga.last_holdout[0]:.1f}")
history.append({"gen": gen, "best": best_fit, "avg": avg,
"holdout": ga.last_holdout[0] if ga.last_holdout
else None,
"seeds": list(ga.eval_seeds),
"genes": best_genes})
# 跨代比较/落盘/早停的分数:滚动种子模式下代间 best 不可比
# (各代种子不同),必须用 holdout(固定种子纵向可比);
# 未配置 holdout 时退回代内 best(旧行为)
# A holdout score is the deployment selection metric whenever it is
# configured, regardless of whether seed rollover is also enabled.
# Using training ``best`` here made the checkpoint and final JSON pick
# a different genome than the champion printed at shutdown.
cmp_fit = ga.last_holdout[0] if ga.last_holdout else best_fit
# 最佳基因持续落盘(无限模式随时可取当前 best 部署)
if cmp_fit > best_overall:
best_overall = cmp_fit
best_overall_genes = dict(best_genes)
best_overall_gen = gen
best_overall_is_holdout = ga.last_holdout is not None
stale = 0
_out_p = args.out if args.out.startswith("results/") \
else os.path.join("results", args.out)
os.makedirs(os.path.dirname(_out_p), exist_ok=True)
with open(_out_p, "w") as f:
json.dump({"baseline_fitness": base_fit,
"best": best_overall, "gen": gen,
"best_is_holdout": best_overall_is_holdout,
"best_gen": best_overall_gen,
"best_genes": best_overall_genes,
"best_metric": best_overall,
"history": history[-200:]}, f, indent=2,
ensure_ascii=False)
else:
stale += 1
if rep:
best_idx = max(range(len(ga.fitness)), key=lambda i: ga.fitness[i])
rep.generation_done(gen, best_fit, avg, best_genes,
ga.last_details[best_idx] if ga.last_details else None,
dt)
# 停止条件:固定代数完成 / 收敛早停 / 手动停止文件
if gens is not None and gen >= gens - 1:
break
if args.patience and stale >= args.patience:
print(f"收敛早停:连续 {stale} 代无提升")
break
if stop_file.exists():
print("手动停止:检测到停止标记")
break
ga.next_generation()
gen += 1
ga.close()
if rep:
rep.state["phase"] = "done"
rep.flush()
# 输出最佳(滚动种子模式下按 holdout 选——代内 best 跨代不可比)
if any(h.get("holdout") is not None for h in history):
final_best = max(history, key=lambda h: h.get("holdout") or -1e9)
print("(按 holdout 分数选择冠军)")
else:
final_best = max(history, key=lambda h: h["best"])
print("\n=== 最佳基因 ===")
for name, val in final_best["genes"].items():
print(f" {name}: {val:.3f}")
out_path = args.out if args.out.startswith("results/") \
else os.path.join("results", args.out)
os.makedirs(os.path.dirname(out_path), exist_ok=True)
selection_metric = ("holdout" if any(h.get("holdout") is not None
for h in history) else "best")
final_metric = final_best.get(selection_metric, final_best.get("best"))
with open(out_path, "w") as f:
json.dump({"schema_version": 2,
"rules_version": "v0.14",
"sdk_version": "0.2.9",
"baseline_fitness": base_fit,
"best": final_metric,
"best_gen": final_best["gen"],
"best_genes": final_best["genes"],
"best_metric": final_metric,
"selection_metric": selection_metric,
"best_is_holdout": selection_metric == "holdout",
"history": history},
f, indent=2, ensure_ascii=False)
print(f"结果已保存: {out_path}")
if __name__ == "__main__":
main()
|