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}")