Skip to content

Commit

Permalink
Add an error to stochastic fictitious play. (#215)
Browse files Browse the repository at this point in the history
- Adds documentation.
- Adds tests.

This is done by raising an error to the distribution calculation
function.

Closes #214
  • Loading branch information
drvinceknight committed Jul 26, 2023
1 parent 1fdf01c commit 9cb0a70
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 12 deletions.
34 changes: 26 additions & 8 deletions docs/how-to/use-stochastic-fictitious-play.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ Note that this process is stochastic::
>>> np.random.seed(1)
>>> play_counts_and_distributions = game.stochastic_fictitious_play(iterations=iterations)
>>> for play_counts, distributions in play_counts_and_distributions:
... row_play_counts, column_play_counts = play_counts
... row_distributions, column_distributions = distributions
... row_play_counts, column_play_counts = play_counts
... row_distributions, column_distributions = distributions
... print(row_play_counts, column_play_counts)
[0 0] [0 0]
[1. 0.] [1. 0.]
Expand All @@ -48,8 +48,8 @@ point for the algorithm::
>>> play_counts = (np.array([0., 500.]), np.array([0., 500.]))
>>> play_counts_and_distributions = game.stochastic_fictitious_play(iterations=iterations, play_counts=play_counts)
>>> for play_counts, distributions in play_counts_and_distributions:
... row_play_counts, column_play_counts = play_counts
... row_distributions, column_distributions = distributions
... row_play_counts, column_play_counts = play_counts
... row_distributions, column_distributions = distributions
... print(row_play_counts, column_play_counts)
...
[ 0. 500.] [ 0. 500.]
Expand All @@ -58,17 +58,17 @@ point for the algorithm::
[ 0. 999.] [ 0. 999.]
[ 0. 1000.] [ 0. 1000.]

A value of :code:`etha` and :code:`epsilon_bar` can be passed.
See the :ref:`stochastic-fictitious-play` reference section for more information. The default values for etha and epsilon bar are
A value of :code:`etha` and :code:`epsilon_bar` can be passed.
See the :ref:`stochastic-fictitious-play` reference section for more information. The default values for etha and epsilon bar are
:math:`10^-1` and :math:`10^-2` respectively::

>>> np.random.seed(0)
>>> etha = 10**-2
>>> epsilon_bar = 10**-3
>>> play_counts_and_distributions = game.stochastic_fictitious_play(iterations=iterations, etha=etha, epsilon_bar=epsilon_bar)
>>> for play_counts, distributions in play_counts_and_distributions:
... row_play_counts, column_play_counts = play_counts
... row_distributions, column_distributions = distributions
... row_play_counts, column_play_counts = play_counts
... row_distributions, column_distributions = distributions
... print(row_play_counts, column_play_counts)
...
[0 0] [0 0]
Expand All @@ -77,3 +77,21 @@ See the :ref:`stochastic-fictitious-play` reference section for more information
[498. 1.] [497. 2.]
[499. 1.] [498. 2.]


Note that for some large valued input matrices a numerical error can occur::

>>> A = np.array([[113, 65, 112], [141, 93, -56], [120, 73, -76]])
>>> B = np.array([[-113, -65, -112], [-141, -93, 56], [-120, -73, 76]])
>>> game = nash.Game(A, B)
>>> iterations = 500
>>> play_counts_and_distributions = tuple(game.stochastic_fictitious_play(iterations=iterations))
Traceback (most recent call last):
...
ValueError: The matrix with values ranging from -76 to 141...

This can usually addressed by an affine scaling of the matrices::

>>> A = A / 10
>>> B = B / 10
>>> game = nash.Game(A, B)
>>> play_counts_and_distributions = game.stochastic_fictitious_play(iterations=iterations, etha=etha, epsilon_bar=epsilon_bar)
3 changes: 1 addition & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
Welcome to Nashpy's documentation!
==================================

This is a Python library used for the computation of equilibria in 2 player
strategic form games.
A python library for 2 player games.

.. toctree::
:maxdepth: 2
Expand Down
19 changes: 18 additions & 1 deletion src/nashpy/learning/stochastic_fictitious_play.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def get_distribution_response_to_play_count(
-------
int
The action that corresponds to the best response.
Raises
------
ValueError
A value error is raised if the input matrix leads to a numerical error
when computing the logit distribution. Usually this can be addressed by
rescaling the input matrix for example A = A / 10.
"""
if np.sum(play_count) == 0:
strategies = play_count + 1 / len(play_count)
Expand All @@ -35,6 +42,16 @@ def get_distribution_response_to_play_count(
logit_choice = np.exp(etha**-1 * noisy_utilities) / np.sum(
np.exp(etha**-1 * noisy_utilities)
)
try:
assert np.all(logit_choice < np.inf)
except AssertionError:
raise ValueError(
f"""The matrix with values ranging from {np.min(A)} to {np.max(A)}
has entries with values that lead to
numeric errors in the calculation of the logit choice. This can
be caused by having absolute values too large. Rescaling your
matrix could fix this error."""
)
return logit_choice


Expand Down Expand Up @@ -72,7 +89,7 @@ def stochastic_fictitious_play(

yield play_counts, distributions

for repetition in range(iterations):
for _ in range(iterations):
distributions = [
get_distribution_response_to_play_count(
A=matrix,
Expand Down
34 changes: 33 additions & 1 deletion tests/unit/test_stochastic_fictitious_play.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""
Tests for stochastic fictitious learning
"""
import numpy as np
from hypothesis import given
from hypothesis.extra.numpy import arrays
import numpy as np
import pytest

from nashpy.learning.stochastic_fictitious_play import (
get_distribution_response_to_play_count,
Expand Down Expand Up @@ -38,6 +39,23 @@ def test_get_distribution_response_to_play_count_2():
assert np.sum(distribution_response) == 1


def test_get_distribution_response_to_play_for_bug_reported_by_user():
"""
This is a bug reported by email.
Captured here: https://github.com/drvinceknight/Nashpy/issues/214
"""
A = np.array([[113, 65, 112], [141, 93, -56], [120, 73, -76]])
np.random.seed(0)
etha = 10**-1
epsilon_bar = 10**-2
play_count = [1, 1, 1]
with pytest.raises(ValueError):
get_distribution_response_to_play_count(
A=A, play_count=play_count, epsilon_bar=epsilon_bar, etha=etha
)


def test_get_distribution_response_to_play_count_3():
np.random.seed(0)
M = np.array([[3, 2], [7, 6]])
Expand Down Expand Up @@ -136,3 +154,17 @@ def test_stochastic_fictitious_play_longrun_default_inputs():
assert np.sum(c_playcounts) == iterations
assert np.allclose(r_dist, np.array([0.35888645, 0.32741658, 0.31369697]))
assert np.allclose(c_dist, np.array([0.30257911, 0.3463743, 0.35104659]))


def test_bug_reported_by_user():
"""
This is a bug reported by email.
Captured here: https://github.com/drvinceknight/Nashpy/issues/214
"""
A = np.array([[113, 65, 112], [141, 93, -56], [120, 73, -76]])
B = np.array([[-113, -65, -112], [-141, -93, 56], [-120, -73, 76]])
iterations = 500
np.random.seed(0)
with pytest.raises(ValueError):
tuple(stochastic_fictitious_play(A=A, B=B, iterations=iterations))

0 comments on commit 9cb0a70

Please sign in to comment.