diff --git a/main.ipynb b/main.ipynb index 4e13d9c..792eacd 100644 --- a/main.ipynb +++ b/main.ipynb @@ -95,6 +95,7 @@ "metadata": {}, "outputs": [], "source": [ + "from multiprocessing import Pool\n", "\n", "%load_ext blackcellmagic" ] @@ -152,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -162,7 +163,8 @@ "EXAMPLE_STACK_SIZE: Final[int] = 1000 # defines the game stack size for examples\n", "IMPOSSIBLE: Final[np.ndarray] = np.array([-1, -1], dtype=int)\n", "IMPOSSIBLE.setflags(write=False)\n", - "SIMULATE_TURNS: Final[int] = 70" + "SIMULATE_TURNS: Final[int] = 70\n", + "VERIFY_POLICY: Final[bool] = True" ] }, { @@ -454,22 +456,22 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "9.43 ms ± 1 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", - "1 s ± 179 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + "9.31 ms ± 1.67 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", + "831 ms ± 25.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] }, { "data": { "text/plain": "array([[[False, False, False, False, False, False, False, False],\n [False, False, False, False, False, False, False, False],\n [False, False, False, True, False, False, False, False],\n [False, False, True, False, False, False, False, False],\n [False, False, False, False, False, True, False, False],\n [False, False, False, False, True, False, False, False],\n [False, False, False, False, False, False, False, False],\n [False, False, False, False, False, False, False, False]]])" }, - "execution_count": 23, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -558,7 +560,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -593,7 +595,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -664,15 +666,15 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 14, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "177 µs ± 3.97 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n", - "29.7 µs ± 106 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n", - "31.2 µs ± 269 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" + "172 µs ± 7.68 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n", + "29.9 µs ± 1.08 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n", + "31.6 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" ] } ], @@ -748,7 +750,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -760,13 +762,13 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 16, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "95.1 ms ± 3.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + "89.4 ms ± 3.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" ] }, { @@ -874,7 +876,7 @@ "## An abstract reversi game policy\n", "\n", "For an easy use of policies an abstract class containing the policy generation / requests an action in an inherited instance of this class.\n", - "This class filters the policy to only propose valid actions. Inherited instance do not need to care about this." + "This class filters the policy to only propose valid actions. Inherited instance do not need to care about this. This super class also manges exploration and exploitation with the epsilon value." ], "metadata": { "collapsed": false @@ -882,7 +884,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -891,6 +893,20 @@ " A game policy. Proposes where to place a stone next.\n", " \"\"\"\n", "\n", + " def __init__(self, epsilon: float):\n", + " \"\"\"\n", + "\n", + " Args:\n", + " epsilon: the epsilon / greedy value. Should be between zero and one. Set the mixture of policy and exploration. One means only the policy is used. Zero means only random policies are used. All mixtures inbetween between are possible.\n", + " \"\"\"\n", + " if 0 > epsilon > 1:\n", + " raise ValueError(\"Epsilon should be between zero and one.\")\n", + " self._epsilon: float = epsilon\n", + "\n", + " @property\n", + " def epsilon(self):\n", + " return self._epsilon\n", + "\n", " @property\n", " @abc.abstractmethod\n", " def policy_name(self) -> str:\n", @@ -909,39 +925,179 @@ " \"\"\"\n", " raise NotImplementedError()\n", "\n", - " def get_policy(\n", - " self, boards: np.ndarray, epsilon: float = 1\n", - " ) -> tuple[np.ndarray, np.ndarray]:\n", + " def get_policy(self, boards: np.ndarray) -> np.ndarray:\n", + " \"\"\"Calculates the policy that should be followed.\n", + "\n", + " Calculates the policy that should be followed.\n", + " This function does include the usage of epsilon to configure greediness and exploration.\n", + "\n", + " Args:\n", + " boards: A set of boards that show the environment where the policy should be calculated for.\n", + "\n", + " Returns:\n", + " A vector of indices. Should be formatted as an array of the form [x, y]. The value [-1, -1] is expected if no turn is possible.\n", + " \"\"\"\n", " assert len(boards.shape) == 3\n", - " assert boards.shape == (BOARD_SIZE, BOARD_SIZE)\n", + " assert boards.shape[1:] == (BOARD_SIZE, BOARD_SIZE)\n", "\n", - " # todo possibly change this function to only validate the purpose turn and\n", + " if self.epsilon <= 0:\n", + " policies = np.random.rand(*boards.shape)\n", + " else:\n", + " policies = self._internal_policy(boards)\n", + " if self.epsilon < 1:\n", + " policies = policies * self.epsilon + np.random.rand(*boards.shape) * (\n", + " 1 - self.epsilon\n", + " )\n", "\n", - " policies = self._internal_policy(boards)\n", - " raw_policy = policies.copy()\n", - " if epsilon < 1:\n", - " policies = policies + np.random.rand(*boards.shape)\n", - "\n", - " # todo talk to team about backpropagation epsilon for greedy factor\n", + " # todo talk to team about backpropagation of score and epsilon for greedy factor\n", "\n", + " # todo possibly change this function to only validate the purpose turn and not all turns\n", " possible_turns = get_possible_turns(boards)\n", " policies[possible_turns == False] = -1.0\n", " max_indices = [\n", " np.unravel_index(policy.argmax(), policy.shape) for policy in policies\n", " ]\n", " policy_vector = np.array(max_indices)\n", - " max_policy = policy_vector\n", + " no_turn_possible_1 = np.all(policy_vector == 0, 1)\n", + " zero_pos = policies[:, 0, 0] == -1.0\n", " no_turn_possible = np.all(policy_vector == 0, 1) & (policies[:, 0, 0] == -1.0)\n", "\n", - " policy_vector[no_turn_possible] = IMPOSSIBLE\n", - " max_policy[no_turn_possible] = 0\n", - " return policy_vector, raw_policy" + " policy_vector[no_turn_possible, :] = IMPOSSIBLE\n", + " return policy_vector" ] }, { "cell_type": "markdown", "source": [ - "## A first policy" + "## A first policy\n", + "\n", + "To quantify the quality of a game AI there needs to be some benchmarks.\n", + "The easiest benchmark is to play against a random player.\n", + "The easiest player to use as a benchmark is the random player.\n", + "For this and testing purpose the random policy was implemented." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 18, + "outputs": [], + "source": [ + "class RandomPolicy(GamePolicy):\n", + " \"\"\"\n", + " A policy playing a random turn by setting epsilon to 0.\n", + " \"\"\"\n", + "\n", + " def __init__(self, epsilon: float):\n", + " _ = epsilon\n", + " super().__init__(epsilon=0)\n", + "\n", + " @property\n", + " def policy_name(self) -> str:\n", + " return \"random\"\n", + "\n", + " def _internal_policy(self, boards: np.ndarray) -> np.ndarray:\n", + " pass\n", + "\n", + "\n", + "rnd_policy = RandomPolicy(1)\n", + "assert rnd_policy.policy_name == \"random\"\n", + "assert rnd_policy.epsilon == 0\n", + "\n", + "rnd_policy_result = rnd_policy.get_policy(get_new_games(10))\n", + "assert np.any((5 >= rnd_policy_result) & (rnd_policy_result >= 3))" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Putting the game simulation together\n", + "Now it's time to bring all together for a proper simulation." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Playing a single turn\n", + "\n", + "The next function needed is used to request a policy, verify that the turn is legit and place a stone and turn enemy stones if possible." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.02 s ± 58.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", + "949 ms ± 43.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + }, + { + "data": { + "text/plain": "
", + "image/png": "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jaefurOfin/63rZt2/KP//iPufzyy3PvvffuuNx3332ZN29eLe+4tl2j0UhPT09+8YtfVDLvsMMOy2//9m/nE5/4RLZu3fqkr9f59rHf/OY388ADD+T1r399bRm6xeGHH56zzz47H//4x/f4c0C1dFSLfmI7HVUO/dSin9hOP5VDP7XoJ7br5n4q/kypG264IY8++mj+4A/+4Enb8te//vW58sor88d//MeVZBkZGcnDDz+cpHVq58c//vFs2bIly5Ytq2R+knziE5/I6aefnhe96EX5y7/8yyxevDjbtm3L17/+9axevTo/+MEPpj3D9r+H0dHR/Nd//VduuummrFq1KkuXLs2b3vSmaZ+/a4Zd9fb25hnPeEYl8+v2yU9+Mqeffnpe8IIX5P3vf38WL16cnp6e3HXXXfnhD3+YU089te6IXUNH7aSfnpxjVzpKR1VNP+2kn56cY1f6ST9VTT/tpJ+enGNX+qkL+qlZuKVLlzZf85rX7PFrd9xxRzNJ87777pv2HOeff34zyY7LnDlzmi984Qub//RP/zTts8fbtGlTc8WKFc2jjz66eeCBBzaf9axnNV/72tc2v/Wtb0377F3/Hnp7e5tHHHFE85WvfGXz05/+dHN0dHTa54/PsOtl0aJFlczfNce5555b6cxdbdq0qfm2t72tecwxxzQPOOCA5uzZs5svetGLmh/96EebW7dunfb5e/v//61vfauZpPnoo49Oe4YS6KjddXs/jc9RV0fV3U/NZr0dpZ9a9NPu9JN+2s5jqPrpp93pJ/20XTf2U6PZLPzV1QAAAADY7xT/mlIAAAAA7H8spQAAAAConKUUAAAAAJWzlAIAAACgcpZSAAAAAFTOUgoAAACAyllKAQAAAFA5SykAAAAAKmcpBQAAAEDlLKUAAAAAqJylFAAAAACVs5QCAAAAoHL/P+MeJ8jHvWnsAAAAAElFTkSuQmCC\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def single_turn(\n", + " current_boards: np, policy: GamePolicy\n", + ") -> tuple[np.ndarray, np.ndarray]:\n", + " \"\"\"Execute a single turn on a board.\n", + "\n", + " Places a new stone on the board. Turns captured enemy stones.\n", + "\n", + " Args:\n", + " current_boards: The current board before the game.\n", + " policy: The game policy to be used.\n", + "\n", + " Returns:\n", + " The new game board and the policy vector containing the index of the action used.\n", + " \"\"\"\n", + " policy_results = policy.get_policy(current_boards)\n", + "\n", + " # if the constant VERIFY_POLICY is set to true the policy is verified. Should be good though.\n", + " # todo deactivate the policy verification after some testing.\n", + " if VERIFY_POLICY:\n", + " assert np.all(moves_possible(current_boards, policy_results)), (\n", + " current_boards[(moves_possible(current_boards, policy_results) == False)],\n", + " policy_results[(moves_possible(current_boards, policy_results) == False)],\n", + " np.where(moves_possible(current_boards, policy_results) == False),\n", + " )\n", + " return do_moves(current_boards, policy_results), policy_results\n", + "\n", + "\n", + "%timeit single_turn(get_new_games(EXAMPLE_STACK_SIZE), RandomPolicy(1))\n", + "VERIFY_POLICY = False # type: ignore\n", + "%timeit single_turn(get_new_games(EXAMPLE_STACK_SIZE), RandomPolicy(1))\n", + "VERIFY_POLICY = True # type: ignore\n", + "plot_othello_boards(\n", + " single_turn(get_new_games(EXAMPLE_STACK_SIZE), RandomPolicy(1))[0][:8]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Simulate a stack of games\n", + "This function will simulate a stack of games and return an array of policies and histories." ], "metadata": { "collapsed": false @@ -951,63 +1107,58 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception in thread Thread-5 (_handle_workers):\n", + "Traceback (most recent call last):\n", + " File \"C:\\Program Files\\Python310\\lib\\threading.py\", line 1016, in _bootstrap_inner\n", + " self.run()\n", + " File \"C:\\Program Files\\Python310\\lib\\threading.py\", line 953, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\pool.py\", line 516, in _handle_workers\n", + " cls._maintain_pool(ctx, Process, processes, pool, inqueue,\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\pool.py\", line 340, in _maintain_pool\n", + " Pool._repopulate_pool_static(ctx, Process, processes, pool,\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\pool.py\", line 329, in _repopulate_pool_static\n", + " w.start()\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\process.py\", line 121, in start\n", + " self._popen = self._Popen(self)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\context.py\", line 336, in _Popen\n", + " return Popen(process_obj)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\popen_spawn_win32.py\", line 93, in __init__\n", + " reduction.dump(process_obj, to_child)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\reduction.py\", line 60, in dump\n", + " ForkingPickler(file, protocol).dump(obj)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\synchronize.py\", line 104, in __getstate__\n", + " h = context.get_spawning_popen().duplicate_for_child(sl.handle)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\popen_spawn_win32.py\", line 99, in duplicate_for_child\n", + " return reduction.duplicate(handle, self.sentinel)\n", + " File \"C:\\Program Files\\Python310\\lib\\multiprocessing\\reduction.py\", line 79, in duplicate\n", + " return _winapi.DuplicateHandle(\n", + "PermissionError: [WinError 5] Zugriff verweigert\n" + ] + } + ], "source": [ - "class RandomPolicy(GamePolicy):\n", - " @property\n", - " def policy_name(self) -> str:\n", - " return \"random\"\n", - "\n", - " def internal_policy(self, boards: np.ndarray) -> np.ndarray:\n", - " random_values = np.random.rand(*boards.shape)\n", - " return random_values\n", - " # return np.argmax(random_values, (1, 2))\n", - "\n", - "\n", - "rnd_policy = RandomPolicy()\n", - "assert rnd_policy.policy_name == \"random\"\n", - "rnd_policy_result = rnd_policy.get_policy(get_new_games(1))\n", - "assert np.any((5 >= rnd_policy_result) & (rnd_policy_result >= 3))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def single_turn(\n", - " current_boards: np, policy: GamePolicy\n", - ") -> tuple[np.ndarray, np.ndarray]:\n", - " policy_results = policy.get_policy(current_boards)\n", - "\n", - " assert np.all(moves_possible(current_boards, policy_results)), (\n", - " current_boards[(moves_possible(current_boards, policy_results) == False)],\n", - " policy_results[(moves_possible(current_boards, policy_results) == False)],\n", - " np.where(moves_possible(current_boards, policy_results) == False),\n", - " )\n", - "\n", - " return do_moves(current_boards, policy_results), policy_results\n", - "\n", - "\n", - "%timeit single_turn(get_new_games(100), RandomPolicy())\n", - "single_turn(get_new_games(100), RandomPolicy())[0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", + "from tqdm.notebook import tqdm\n", "\n", "\n", "def simulate_game(\n", " nr_of_games: int,\n", " policies: tuple[GamePolicy, GamePolicy],\n", ") -> tuple[np.ndarray, np.ndarray]:\n", + " \"\"\"Simulates a stack of games.\n", "\n", + " Args:\n", + " nr_of_games: The number of games that should be simulated.\n", + " policies: The policies that should be used to simulate the game.\n", + "\n", + " Returns:\n", + " A stack of board histories and actions.\n", + " \"\"\"\n", " board_history_stack = np.zeros((SIMULATE_TURNS, nr_of_games, 8, 8))\n", " action_history_stack = np.zeros((SIMULATE_TURNS, nr_of_games, 2))\n", " current_boards = get_new_games(nr_of_games)\n", @@ -1026,21 +1177,88 @@ " return board_history_stack, action_history_stack\n", "\n", "\n", - "%timeit simulate_game(100, (RandomPolicy(), RandomPolicy()))\n", - "simulate_game(10, (RandomPolicy(), RandomPolicy()))" + "simulation_results = simulate_game(1, (RandomPolicy(1), RandomPolicy(1)))" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, "outputs": [], - "source": [] + "source": [ + "\n", + "%timeit simulate_game(100, (RandomPolicy(1), RandomPolicy(1)))\n", + "# simulate_game(EXAMPLE_STACK_SIZE, (RandomPolicy(1), RandomPolicy(1)))" + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "outputs": [], + "source": [ + "policies_to_use = RandomPolicy(1), RandomPolicy(1)\n", + "with Pool(3) as pool:\n", + " results = pool.map(simulate_game, [100, policies_to_use])" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "is_executing": true + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "a = np.array(\n", + " [\n", + " [\n", + " [-1, -1, -1, -1, 0, 0, 0, 0],\n", + " [1, 1, -1, 1, 1, 0, 0, 0],\n", + " [1, 1, -1, 1, 1, 1, 0, 0],\n", + " [0, 1, -1, 1, 1, 1, 0, 0],\n", + " [0, 1, 1, 1, 1, 1, 0, 0],\n", + " [-1, 1, 1, 1, 1, 0, 0, 0],\n", + " [0, 0, 0, 1, 0, 0, 0, 0],\n", + " [0, 0, 0, 0, 0, 0, 0, 0],\n", + " ]\n", + " ],\n", + " dtype=int,\n", + ")\n", + "a" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "is_executing": true + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "RandomPolicy(1).get_policy(a)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pycharm": { + "is_executing": true + } + }, "outputs": [], "source": [ "import numpy as np\n", @@ -1291,7 +1509,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "pycharm": { + "is_executing": true + } + }, "outputs": [], "source": [ "plot_othello_boards(create_test_game()[-3:])" @@ -1300,7 +1522,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "pycharm": { + "is_executing": true + } + }, "outputs": [], "source": [ "array = create_test_game()" @@ -1321,7 +1547,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "pycharm": { + "is_executing": true + } + }, "outputs": [], "source": [] }