Download graphwar/learn/ppo.py from Foxgrad/graphwar-arena: direct link, hf CLI and curl.
- Browser
- Download file 4.94 kB
-
https://huggingface.co/Foxgrad/graphwar-arena/resolve/main/graphwar/learn/ppo.py
- Command line
-
hf download hf://Foxgrad/graphwar-arena/graphwar/learn/ppo.py
-
curl -L -o ppo.py https://huggingface.co/Foxgrad/graphwar-arena/resolve/main/graphwar/learn/ppo.py
4.94 kB
| """PPO (Proximal Policy Optimization) pour Irrational, Peace et 3D. | |
| Chaque « partie » d'entraînement est un seul tir : le bot voit la situation, choisit | |
| les coefficients de sa fonction, et reçoit sa récompense. Le réseau de politique donne | |
| une moyenne, on ajoute du bruit pour explorer, et on renforce ce qui a mieux marché que | |
| prévu par le réseau de valeur (le « critique »). | |
| Le réseau de valeur sert aussi en combat : il estime la chance de toucher chaque ennemi, | |
| et le bot vise celui où il se sent le plus sûr de lui. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from ..nets import MLP | |
| from .common import Logger, line, split, task_rollout, validate | |
| def _torch(): | |
| try: | |
| import torch | |
| return torch | |
| except ImportError as e: | |
| raise SystemExit("PPO a besoin de PyTorch : pip install torch") from e | |
| def _mlp(torch, sizes): | |
| layers = [] | |
| for i, (a, b) in enumerate(zip(sizes[:-1], sizes[1:])): | |
| layers.append(torch.nn.Linear(a, b)) | |
| if i < len(sizes) - 2: | |
| layers.append(torch.nn.Tanh()) | |
| return torch.nn.Sequential(*layers) | |
| def _to_flat(net): | |
| parts = [] | |
| for m in net: | |
| if hasattr(m, "weight"): | |
| parts += [m.weight.detach().cpu().numpy().T.ravel(), m.bias.detach().cpu().numpy().ravel()] | |
| return np.concatenate(parts).astype(np.float32) | |
| def _from_flat(torch, net, flat): | |
| i = 0 | |
| with torch.no_grad(): | |
| for m in net: | |
| if hasattr(m, "weight"): | |
| a, b = m.in_features, m.out_features | |
| m.weight.copy_(torch.tensor(flat[i:i + a * b].reshape(a, b).T)); i += a * b | |
| m.bias.copy_(torch.tensor(flat[i:i + b])); i += b | |
| def train_ppo(bot, pool, iters, rng, say=print, save=None, resume=False, val_every=5, best0=-1.0, | |
| batch=1536, epochs=8, minibatch=256, lr=1e-3, clip=0.2, ent=0.0, log_std0=-1.0): | |
| torch = _torch() | |
| torch.manual_seed(int(rng.integers(1 << 30))) | |
| sizes = bot.policy.sizes | |
| vsizes = [sizes[0], *bot.hidden, 1] | |
| pi, vf = _mlp(torch, sizes), _mlp(torch, vsizes) | |
| log_std = torch.nn.Parameter(torch.full((sizes[-1],), log_std0)) | |
| if bot.trained_iters > 0: # reprise | |
| _from_flat(torch, pi, bot.policy.flat) | |
| if bot.value is not None: | |
| _from_flat(torch, vf, bot.value.flat) | |
| opt = torch.optim.Adam([*pi.parameters(), *vf.parameters(), log_std], lr=lr) | |
| log = Logger(bot.key, resume) | |
| best = best0 | |
| def export(): | |
| bot.policy.set_flat(_to_flat(pi)) | |
| bot.value = MLP(vsizes, "linear", flat=_to_flat(vf)) | |
| for it in range(1, iters + 1): | |
| flat = _to_flat(pi) | |
| ls = log_std.detach().numpy().copy() | |
| seeds = rng.integers(0, 2 ** 31 - 1, batch) | |
| jobs = [(bot.key, flat, ls, chunk, int(rng.integers(1 << 30))) for chunk in split(seeds, pool.workers * 2)] | |
| res = pool.map(task_rollout, jobs) | |
| obs = torch.tensor(np.concatenate([r[0] for r in res]), dtype=torch.float32) | |
| act = torch.tensor(np.concatenate([r[1] for r in res]), dtype=torch.float32) | |
| rew = torch.tensor(np.concatenate([r[2] for r in res]), dtype=torch.float32) | |
| with torch.no_grad(): | |
| dist = torch.distributions.Normal(pi(obs), log_std.exp()) | |
| old_logp = dist.log_prob(act).sum(-1) | |
| adv = rew - vf(obs).squeeze(-1) | |
| adv = (adv - adv.mean()) / (adv.std() + 1e-8) | |
| n = len(rew) | |
| for _ in range(epochs): | |
| perm = torch.randperm(n) | |
| for k in range(0, n, minibatch): | |
| idx = perm[k:k + minibatch] | |
| dist = torch.distributions.Normal(pi(obs[idx]), log_std.exp()) | |
| logp = dist.log_prob(act[idx]).sum(-1) | |
| ratio = (logp - old_logp[idx]).exp() | |
| a = adv[idx] | |
| loss_pi = -torch.min(ratio * a, ratio.clamp(1 - clip, 1 + clip) * a).mean() | |
| loss_v = ((vf(obs[idx]).squeeze(-1) - rew[idx]) ** 2).mean() | |
| loss = loss_pi + 0.5 * loss_v - ent * dist.entropy().sum(-1).mean() | |
| opt.zero_grad() | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_([*pi.parameters(), *vf.parameters(), log_std], 0.5) | |
| opt.step() | |
| with torch.no_grad(): | |
| log_std.clamp_(-2.5, 0.5) | |
| if it % val_every == 0 or it == iters: | |
| export() | |
| v = validate(pool, bot.key, bot.policy.flat) | |
| log.log(it, float(rew.mean()), v) | |
| if v["touche"] > best: | |
| best = v["touche"] | |
| bot.trained_iters = it + log.offset | |
| save(bot, "best") | |
| say(line(bot.key, it + log.offset, iters + log.offset, float(rew.mean()), v, best, | |
| f"(bruit {float(log_std.detach().exp().mean()):.2f})")) | |
| export(); bot.trained_iters = iters + log.offset; save(bot, "last") | |
| return best | |