Repository navigation
CooperateISO - #1498
CooperateISO#1498adrianhutter wants to merge 39 commits into
Conversation
drvinceknight
left a comment
There was a problem hiding this comment.
This looks really great, I enjoyed the preprint (congrats on the work)!
My requests are mainly stylistic as we tend to try and avoid inline comments (there are a number in the tests that I didn't comment on that could perhaps be moved to docstrings).
| @@ -1,4 +1,7 @@ | |||
| sphinx>=7.0.0,<9.0.0 | |||
There was a problem hiding this comment.
Can you help me understand why this change is needed? (This might be because of an upstream change.)
|
|
||
| # The theme to use for HTML and HTML Help pages. See the documentation for | ||
| # a list of builtin themes. | ||
| on_rtd = os.environ.get("READTHEDOCS", None) == "True" |
There was a problem hiding this comment.
Could you help me understand why this change? (It might be something got stale that I'm not remembering.)
| return opponent.history[-1] | ||
|
|
||
|
|
||
| # We describe memory-1 strategies as length-4 arrays, quantifying the probability of cooperation in the states [CC, CD, DC, DD]. |
There was a problem hiding this comment.
| # We describe memory-1 strategies as length-4 arrays, quantifying the probability of cooperation in the states [CC, CD, DC, DD]. |
| # Apply p_noise only to own strategy, not to opponent | ||
| # (the opponent strategy already includes noise effects). | ||
| own = my_strategy + p_noise * (1.0 - 2.0 * my_strategy) | ||
|
|
||
| # Flip CD/DC for opponent. | ||
| opp = opp_strategy[[0, 2, 1, 3]] | ||
|
|
||
| # Build the transition matrix. | ||
| trans_mat = np.array( | ||
| [ | ||
| own * opp, | ||
| own * (1.0 - opp), | ||
| (1.0 - own) * opp, | ||
| (1.0 - own) * (1.0 - opp), | ||
| ] | ||
| ).T | ||
|
|
||
| R, P, S, T = RPST | ||
| rewards = np.array([R, S, T, P], dtype=float) | ||
|
|
||
| # Don't include init state in summed rewards. | ||
| inv = np.linalg.inv(np.eye(4) - (1.0 - p_end) * trans_mat) | ||
|
|
||
| # Calculate expected reward, | ||
| reward = init_state @ (inv @ rewards - rewards) | ||
|
|
||
| # Avg. reward per step | ||
| return p_end * float(reward) / (1.0 - p_end) |
There was a problem hiding this comment.
Could you remove this inline comments and put the corresponding explanation in the docstring please.
| # Clamp to possible values, given noise | ||
| opp = np.clip(opponent, p_noise, 1.0 - p_noise) | ||
|
|
||
| # Setup initial state | ||
| init_state = np.zeros(4, dtype=np.float32) | ||
| init_state[init_state_idx] = 1.0 | ||
|
|
||
| # Define the objective function to minimize (negative reward) | ||
| def objective(params: np.ndarray) -> float: | ||
| return -get_reward(params, opp, init_state, p_end, p_noise, RPST) | ||
|
|
||
| x0 = np.array([0.5, 0.5, 0.5, 0.5]) | ||
| bounds = [(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)] | ||
| result = minimize( | ||
| objective, x0, method="L-BFGS-B", bounds=bounds, options={"maxiter": 50} | ||
| ) | ||
|
|
||
| # result.fun is the minimum loss (-reward), result.x are the optimal parameters | ||
| return -result.fun, result.x |
There was a problem hiding this comment.
I think we can remove the inline comments here
| # Track the opponent's rate of cooperation (numerator, denominator) for each state. | ||
| # Assume we have seen the opponent play following TfT once in each state, | ||
| # to make the opponent-model well-defined from the start. | ||
| self.ewma_CC = [1.0, 1.0] | ||
| self.ewma_CD = [1.0, 1.0] | ||
| self.ewma_DC = [0.0, 1.0] | ||
| self.ewma_DD = [0.0, 1.0] | ||
|
|
||
| # Initial cooperation probabilities (num / den) | ||
| self.opp_model = [1.0, 0.0, 1.0, 0.0] | ||
| self.my_policy = [1.0, 0.0, 1.0, 0.0] |
There was a problem hiding this comment.
Can you move these inline comments to the docstring of the __init__
Co-authored-by: Vince Knight <vince@vknight.org>
|
@drvinceknight Thanks a lot for review. I addressed all comments, please take another look. |
| self.opp_model[0] = pr_c | ||
| elif prev_state == (C, D): | ||
| pr_c = self._update_single_ewma(self.ewma_CD, opp_act) | ||
| self.opp_model[2] = pr_c |
There was a problem hiding this comment.
Just checking that the index of 2 is correct here, since the ordering is different than the ewma fields and in the _get_state_idx function (also why is _get_state_idx not used here?).
It might be clearer to use dictionaries with the prev_state tuples as keys for both opp_model and ewma_**.
| self.opp_model[2] = pr_c | ||
| elif prev_state == (D, C): | ||
| pr_c = self._update_single_ewma(self.ewma_DC, opp_act) | ||
| self.opp_model[1] = pr_c |
|
|
||
| Returns our expected reward per step. | ||
| """ | ||
| self._update_opponent_model(opponent) |
There was a problem hiding this comment.
If opponent.history is all that's used by these functions consider passing just that instead of the entire object
| Returns our expected reward per step. | ||
| """ | ||
| self._update_opponent_model(opponent) | ||
| state_idx = self._get_state_idx(opponent) |
| self.noise = self.match_attributes["noise"] | ||
| self.iso_instance.noise = self.noise | ||
|
|
||
| def _update_reward_history(self, opponent): |
There was a problem hiding this comment.
Could simplify by computing a dict of (action, action) --> reward in __init__, then this is just self.reward_history.append(reward_dict[state]). This eliminates the repeated unpackings and if/elif chain.
| players = [axl.Cooperator(), axl.Defector()] | ||
| tournament = axl.Tournament(players, turns=5, repetitions=3, noise=0.75) | ||
| tournament = axl.Tournament( | ||
| players, turns=5, repetitions=3, noise=0.75, seed=42 |
There was a problem hiding this comment.
Was this failing sometimes? Otherwise why set the seed?
| (C, D), | ||
| ( | ||
| C, | ||
| D, |
There was a problem hiding this comment.
Please remove the extra comma (and just below) so that the formatting is consistent
| return C | ||
| if len(self.history) == 1: | ||
| return opponent.history[-1] | ||
| if self.history[-2] == C: |
There was a problem hiding this comment.
What's the reason for only updating z and n_tft_would_c on cooperation?
| rewards = np.array([R, S, T, P], dtype=float) | ||
| inv = np.linalg.inv(np.eye(4) - (1.0 - p_end) * trans_mat) | ||
| reward = init_state @ (inv @ rewards - rewards) | ||
| return p_end * float(reward) / (1.0 - p_end) |
There was a problem hiding this comment.
Does this function break if p_end == 1 or p_noise == 1?
| Discovers the optimal response strategy (policy) against a fixed opponent | ||
| model by maximizing the expected reward from a given starting state. | ||
| """ | ||
| assert p_noise < 0.5 |
There was a problem hiding this comment.
Let's avoid the assert here so we don't break runs with p_noise >= 0.5
Adding 3 strategies (LongtermTfT, ISO, CooperateISO) from https://arxiv.org/abs/2303.03519.
Made a few changes relative to the versions used in the paper. Most importantly, ISO now uses
scipy.optimizeinstead of torch's Adam. This works equally well, is faster, and avoids having to import torch.Ran the CooperateISO implementation from this PR against all strategies in Axelrod-4.14.0, and verified that it outperforms EvolvedLookerUp2_2_2 and DBS at both noise levels 0% and 10%.
Fix Read the Docs build failures by updating documentation requirements and conf.py: