Files
2026-07-13 13:17:40 +08:00

98 lines
3.6 KiB
Python

from collections import defaultdict
import numpy as np
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
from ray.rllib.utils.metrics import ENV_RUNNER_RESULTS
class SelfPlayCallback(RLlibCallback):
def __init__(self, win_rate_threshold):
super().__init__()
# 0=RandomPolicy, 1=1st main policy snapshot,
# 2=2nd main policy snapshot, etc..
self.current_opponent = 0
self.win_rate_threshold = win_rate_threshold
# Report the matchup counters (who played against whom?).
self._matching_stats = defaultdict(int)
def on_episode_end(
self,
*,
episode,
env_runner,
metrics_logger,
env,
env_index,
rl_module,
**kwargs,
) -> None:
# Compute the win rate for this episode and log it with a window of 100.
main_agent = 0 if episode.module_for(0) == "main" else 1
rewards = episode.get_rewards()
if main_agent in rewards:
main_won = rewards[main_agent][-1] == 1.0
metrics_logger.log_value(
"win_rate",
main_won,
reduce="mean",
window=100,
)
def on_train_result(self, *, algorithm, metrics_logger=None, result, **kwargs):
win_rate = result[ENV_RUNNER_RESULTS]["win_rate"]
print(f"Iter={algorithm.iteration} win-rate={win_rate} -> ", end="")
# If win rate is good -> Snapshot current policy and play against
# it next, keeping the snapshot fixed and only improving the "main"
# policy.
if win_rate > self.win_rate_threshold:
self.current_opponent += 1
new_module_id = f"main_v{self.current_opponent}"
print(f"adding new opponent to the mix ({new_module_id}).")
# Re-define the mapping function, such that "main" is forced
# to play against any of the previously played modules
# (excluding "random").
def agent_to_module_mapping_fn(agent_id, episode, **kwargs):
# agent_id = [0|1] -> policy depends on episode ID
# This way, we make sure that both modules sometimes play
# (start player) and sometimes agent1 (player to move 2nd).
opponent = "main_v{}".format(
np.random.choice(list(range(1, self.current_opponent + 1)))
)
if hash(episode.id_) % 2 == agent_id:
self._matching_stats[("main", opponent)] += 1
return "main"
else:
return opponent
main_module = algorithm.get_module("main")
algorithm.add_module(
module_id=new_module_id,
module_spec=RLModuleSpec.from_module(main_module),
new_agent_to_module_mapping_fn=agent_to_module_mapping_fn,
)
# TODO (sven): Maybe we should move this convenience step back into
# `Algorithm.add_module()`? Would be less explicit, but also easier.
algorithm.set_state(
{
"learner_group": {
"learner": {
"rl_module": {
new_module_id: main_module.get_state(),
}
}
}
}
)
else:
print("not good enough; will keep learning ...")
# +2 = main + random
result["league_size"] = self.current_opponent + 2
print(f"Matchups:\n{self._matching_stats}")