File size: 5,490 Bytes
719b42f | 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 | """Outils partagés par les méthodes d'apprentissage : évaluation en parallèle sur plusieurs cœurs."""
from __future__ import annotations
import csv
import os
import time
from collections import Counter
import numpy as np
from .. import bots, scenarios, shot
from ..vocab import ctx_of
_BOT = {}
def _bot(key):
if key not in _BOT:
_BOT[key] = bots.make(key, path="__aucun__") # cerveau vierge, on remplace les poids
return _BOT[key]
def _scenes(bot, seeds):
scenes = [scenarios.make(int(s)) for s in seeds]
obs = np.stack([bot.obs(w, s, t) for w, s, t in scenes])
return scenes, obs
def run_outs(bot, scenes, outs):
"""Tire avec les sorties de cerveau données ; renvoie récompenses et étiquettes."""
R, L = np.zeros(len(scenes)), []
for i, ((w, s, t), o) in enumerate(zip(scenes, outs)):
sh = bot.shot_from(o, ctx_of(w, s, t), t)
res = shot.simulate(w, s, sh.g, sh.z, t)
R[i], lab = scenarios.reward(res, w, s, t, bot.ally_penalty)
L.append(lab)
return R, L
# ---------------------------------------------------------------- tâches envoyées aux cœurs
def task_eval_flats(args):
"""Évalue plusieurs cerveaux (vecteurs de poids) sur les mêmes situations."""
key, flats, seeds = args
bot = _bot(key)
scenes, obs = _scenes(bot, seeds)
out = np.zeros((len(flats), len(seeds)))
labels = Counter()
for k, f in enumerate(flats):
bot.policy.set_flat(f)
R, L = run_outs(bot, scenes, bot.brain_out(obs))
out[k] = R
labels.update(L)
return out, labels
def task_rollout(args):
"""Pour PPO : joue des tirs avec une politique gaussienne (exploration)."""
key, flat, log_std, seeds, rseed = args
bot = _bot(key)
bot.policy.set_flat(flat)
rng = np.random.default_rng(rseed)
scenes, obs = _scenes(bot, seeds)
mean = bot.policy(obs)
act = mean + np.exp(log_std) * rng.standard_normal(mean.shape)
R, L = run_outs(bot, scenes, np.tanh(act))
return obs, act.astype(np.float32), R, L
SANS_FIN = 10 ** 9 # nombre d'itérations utilisé par --sans-limite
class Pool:
"""Petit enrobage de multiprocessing (fonctionne aussi sous Windows)."""
def __init__(self, workers: int):
self.workers = max(1, workers)
self.pool = None
if self.workers > 1:
import multiprocessing as mp
self.pool = mp.get_context("spawn").Pool(self.workers)
def map(self, fn, jobs):
return self.pool.map(fn, jobs) if self.pool else list(map(fn, jobs))
def close(self, force=False):
if self.pool:
if force:
self.pool.terminate()
else:
self.pool.close()
self.pool.join()
def split(arr, n):
n = max(1, min(n, len(arr)))
return [a for a in np.array_split(np.asarray(arr), n) if len(a)]
def eval_flats(pool, key, flats, seeds):
"""Matrice des récompenses (cerveaux x situations)."""
jobs = [(key, chunk, seeds) for chunk in split(flats, pool.workers * 2)]
res = pool.map(task_eval_flats, jobs)
labels = Counter()
for _, l in res:
labels.update(l)
return np.vstack([r for r, _ in res]), labels
def validate(pool, key, flat, n=len(scenarios.VAL_SEEDS)):
"""Mesure un cerveau sur les 300 cartes de contrôle (toujours les mêmes)."""
seeds = scenarios.VAL_SEEDS[:n]
jobs = [(key, [flat], chunk) for chunk in split(seeds, pool.workers)]
res = pool.map(task_eval_flats, jobs)
R = np.concatenate([r[0] for r, _ in res])
labels = Counter()
for _, l in res:
labels.update(l)
return {
"recompense": float(R.mean()),
"cible": labels["cible"] / n,
"touche": (labels["cible"] + labels["ennemi"]) / n,
"allie": labels["allié"] / n,
}
class Logger:
COLS = ["iter", "secondes", "recompense_entrainement", "recompense_controle",
"precision_cible", "precision_ennemi", "tirs_allies"]
def __init__(self, key, resume):
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.makedirs(os.path.join(root, "logs"), exist_ok=True)
self.path = os.path.join(root, "logs", f"{key}.csv")
self.offset = 0 # en reprise, on continue la numérotation des itérations
if resume and os.path.exists(self.path):
with open(self.path) as f:
rows = list(csv.reader(f))[1:]
self.offset = int(rows[-1][0]) if rows else 0
if not resume or not os.path.exists(self.path):
with open(self.path, "w", newline="") as f:
csv.writer(f).writerow(self.COLS)
self.t0 = time.time()
def log(self, it, train_r, v):
with open(self.path, "a", newline="") as f:
csv.writer(f).writerow([it + self.offset, round(time.time() - self.t0, 1), round(train_r, 4), round(v["recompense"], 4),
round(v["cible"], 4), round(v["touche"], 4), round(v["allie"], 4)])
def line(key, it, total, train_r, v, best, extra=""):
bar = "█" * int(20 * v["touche"]) + "·" * (20 - int(20 * v["touche"]))
total = "∞" if total >= SANS_FIN else total
return (f"[{key:10s}] it {it:4d}/{total} entraîn. {train_r:+.3f} │ contrôle : {bar} "
f"{100 * v['touche']:5.1f}% touché, {100 * v['allie']:4.1f}% alliés │ record {100 * best:5.1f}% {extra}")
|