mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 23:02:03 +00:00
feat(*): Log live goal rate to TensorBoard during training
This commit is contained in:
@@ -107,6 +107,15 @@ var ball: RigidBody3D
|
|||||||
var opponent: Ship
|
var opponent: Ship
|
||||||
var attack_goal_position: Vector3
|
var attack_goal_position: Vector3
|
||||||
|
|
||||||
|
# Set directly by TrainingMode (_on_goal_scored / the timeout branch in
|
||||||
|
# _physics_process) at the same time as `done = true`. Deliberately NOT
|
||||||
|
# cleared in reset(): TrainingMode's _reset_episode() (which calls reset())
|
||||||
|
# runs synchronously, immediately after done is set, before the Sync node
|
||||||
|
# ever reads get_info()/get_done() for that terminal tick — clearing it here
|
||||||
|
# would wipe the value that read needs. Both call sites always overwrite
|
||||||
|
# (true on goal, false on timeout) rather than toggle, so no reset is needed.
|
||||||
|
var goal_scored_this_episode := false
|
||||||
|
|
||||||
var _ticks_since_ball_touch := 1 << 30 # large so the first touch always pays
|
var _ticks_since_ball_touch := 1 << 30 # large so the first touch always pays
|
||||||
|
|
||||||
|
|
||||||
@@ -135,6 +144,14 @@ func get_reward() -> float:
|
|||||||
return reward
|
return reward
|
||||||
|
|
||||||
|
|
||||||
|
# Symmetric across both self-play agents: reports whether this episode ended
|
||||||
|
# in a goal at all, not which team scored — a clean "goal rate" signal
|
||||||
|
# distinct from rollout/ep_rew_mean, which mixes this with dense shaping
|
||||||
|
# (ball chasing/touching). See train.py's GoalRateCallback.
|
||||||
|
func get_info() -> Dictionary:
|
||||||
|
return {"goal_scored": goal_scored_this_episode}
|
||||||
|
|
||||||
|
|
||||||
func get_action_space() -> Dictionary:
|
func get_action_space() -> Dictionary:
|
||||||
return {
|
return {
|
||||||
"thrust": {"size": 3, "action_type": "continuous"},
|
"thrust": {"size": 3, "action_type": "continuous"},
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ func _physics_process(_delta):
|
|||||||
for agent in _agents:
|
for agent in _agents:
|
||||||
agent.reward -= draw_penalty
|
agent.reward -= draw_penalty
|
||||||
agent.done = true
|
agent.done = true
|
||||||
|
agent.goal_scored_this_episode = false
|
||||||
_reset_episode()
|
_reset_episode()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -320,6 +321,7 @@ func _on_goal_scored(conceding_team: int) -> void:
|
|||||||
for agent in _agents:
|
for agent in _agents:
|
||||||
agent.reward += goal_reward if agent.ship.team != conceding_team else -goal_reward
|
agent.reward += goal_reward if agent.ship.team != conceding_team else -goal_reward
|
||||||
agent.done = true
|
agent.done = true
|
||||||
|
agent.goal_scored_this_episode = true
|
||||||
_reset_episode()
|
_reset_episode()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -89,7 +89,11 @@ real behaviour needs tens of millions of steps (hours on the 3090 box).
|
|||||||
|
|
||||||
Key curves: `rollout/ep_rew_mean` (should trend up), `rollout/ep_len_mean`
|
Key curves: `rollout/ep_rew_mean` (should trend up), `rollout/ep_len_mean`
|
||||||
(should trend *down* from 225 as goals end episodes early — 225 action steps
|
(should trend *down* from 225 as goals end episodes early — 225 action steps
|
||||||
= the 30s episode timeout).
|
= the 30s episode timeout), `rollout/goal_rate` (fraction of recent episodes
|
||||||
|
that ended in an actual goal rather than timing out as a draw — the live
|
||||||
|
signal for "is the policy actually finishing more episodes by scoring",
|
||||||
|
since `ep_rew_mean` mixes that with dense reward-shaping (ball chasing/
|
||||||
|
touching) and doesn't isolate it).
|
||||||
|
|
||||||
### Reward/observation tuning
|
### Reward/observation tuning
|
||||||
|
|
||||||
|
|||||||
+26
-3
@@ -16,7 +16,8 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from stable_baselines3 import PPO
|
from stable_baselines3 import PPO
|
||||||
from stable_baselines3.common.callbacks import CheckpointCallback
|
from stable_baselines3.common.callbacks import BaseCallback, CheckpointCallback
|
||||||
|
from stable_baselines3.common.utils import safe_mean
|
||||||
from stable_baselines3.common.vec_env.vec_monitor import VecMonitor
|
from stable_baselines3.common.vec_env.vec_monitor import VecMonitor
|
||||||
|
|
||||||
from cosmic_env import CosmicClashVecEnv
|
from cosmic_env import CosmicClashVecEnv
|
||||||
@@ -25,6 +26,27 @@ TRAINING_DIR = pathlib.Path(__file__).resolve().parent
|
|||||||
DEFAULT_GODOT_MACOS = "/Applications/Godot.app/Contents/MacOS/Godot"
|
DEFAULT_GODOT_MACOS = "/Applications/Godot.app/Contents/MacOS/Godot"
|
||||||
|
|
||||||
|
|
||||||
|
class GoalRateCallback(BaseCallback):
|
||||||
|
"""Logs rollout/goal_rate: the fraction of completed episodes in the
|
||||||
|
current ep_info_buffer that ended in an actual goal, vs. timing out as a
|
||||||
|
draw. rollout/ep_rew_mean mixes dense reward-shaping (ball chasing/
|
||||||
|
touching) with the sparse terminal goal reward, so it can trend up from
|
||||||
|
better shaping alone without the policy finishing more episodes by
|
||||||
|
actually scoring — this isolates that. Requires VecMonitor(...,
|
||||||
|
info_keywords=("goal_scored",)), which copies ShipAIController.get_info()
|
||||||
|
into each completed episode's info["episode"] dict (see
|
||||||
|
training_mode.gd's _on_goal_scored / timeout branch)."""
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _on_rollout_end(self) -> None:
|
||||||
|
if len(self.model.ep_info_buffer) == 0:
|
||||||
|
return
|
||||||
|
goal_rate = safe_mean([ep_info["goal_scored"] for ep_info in self.model.ep_info_buffer])
|
||||||
|
self.logger.record("rollout/goal_rate", goal_rate)
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -164,7 +186,7 @@ def main():
|
|||||||
speedup=args.speedup,
|
speedup=args.speedup,
|
||||||
**_curriculum_kwargs(args),
|
**_curriculum_kwargs(args),
|
||||||
)
|
)
|
||||||
env = VecMonitor(env)
|
env = VecMonitor(env, info_keywords=("goal_scored",))
|
||||||
|
|
||||||
if args.resume:
|
if args.resume:
|
||||||
model = PPO.load(
|
model = PPO.load(
|
||||||
@@ -204,11 +226,12 @@ def main():
|
|||||||
save_path=str(checkpoint_dir),
|
save_path=str(checkpoint_dir),
|
||||||
name_prefix="ppo",
|
name_prefix="ppo",
|
||||||
)
|
)
|
||||||
|
goal_rate_callback = GoalRateCallback()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model.learn(
|
model.learn(
|
||||||
args.timesteps,
|
args.timesteps,
|
||||||
callback=checkpoint_callback,
|
callback=[checkpoint_callback, goal_rate_callback],
|
||||||
tb_log_name=args.experiment,
|
tb_log_name=args.experiment,
|
||||||
reset_num_timesteps=not args.resume,
|
reset_num_timesteps=not args.resume,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user