chinatown_1 / run_evolve.py
Torther's picture
Core research: deterministic simulator, evolutionary strategy, genetic algorithm
cdf3b47 unverified
Raw
History Blame Contribute Delete
13.7 kB
"""进化入口:跑 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()