diff --git a/week2_model_based/practice_vi.ipynb b/week2_model_based/practice_vi.ipynb
index e510370ba..973579327 100644
--- a/week2_model_based/practice_vi.ipynb
+++ b/week2_model_based/practice_vi.ipynb
@@ -1 +1,909 @@
-{"nbformat":4,"nbformat_minor":0,"metadata":{"language_info":{"name":"python","pygments_lexer":"ipython3"},"colab":{"name":"practice_vi.ipynb","provenance":[],"collapsed_sections":[]}},"cells":[{"cell_type":"markdown","metadata":{"id":"dDQKDe_d4TAR"},"source":["### Markov decision process\n","\n","This week methods are all built to solve __M__arkov __D__ecision __P__rocesses. In the broadest sense, the MDP is defined by how it changes the states and how rewards are computed.\n","\n","State transition is defined by $P(s' |s,a)$ - how likely you are to end at the state $s'$ if you take an action $a$ from the state $s$. Now there's more than one way to define rewards, but for convenience we'll use $r(s,a,s')$ function.\n","\n","_This notebook is inspired by the awesome_ [CS294](https://github.com/berkeleydeeprlcourse/homework/blob/36a0b58261acde756abd55306fbe63df226bf62b/hw2/HW2.ipynb) _by Berkeley_"]},{"cell_type":"markdown","metadata":{"id":"G793b49v4TAa"},"source":["For starters, let's define a simple MDP from this picture:\n","\n",""]},{"cell_type":"code","metadata":{"id":"JokMVpgS4TAb"},"source":["import sys, os\n","if 'google.colab' in sys.modules and not os.path.exists('.setup_complete'):\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/master/setup_colab.sh -O- | bash\n","\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/grading.py -O ../grading.py\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week2_model_based/submit.py\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week2_model_based/mdp.py\n","\n"," !touch .setup_complete\n","\n","# This code creates a virtual display to draw game images on.\n","# It won't have any effect if your machine has a monitor.\n","if type(os.environ.get(\"DISPLAY\")) is not str or len(os.environ.get(\"DISPLAY\")) == 0:\n"," !bash ../xvfb start\n"," os.environ['DISPLAY'] = ':1'"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"Q7psltI74TAc"},"source":["transition_probs = {\n"," 's0': {\n"," 'a0': {'s0': 0.5, 's2': 0.5},\n"," 'a1': {'s2': 1}\n"," },\n"," 's1': {\n"," 'a0': {'s0': 0.7, 's1': 0.1, 's2': 0.2},\n"," 'a1': {'s1': 0.95, 's2': 0.05}\n"," },\n"," 's2': {\n"," 'a0': {'s0': 0.4, 's2': 0.6},\n"," 'a1': {'s0': 0.3, 's1': 0.3, 's2': 0.4}\n"," }\n","}\n","rewards = {\n"," 's1': {'a0': {'s0': +5}},\n"," 's2': {'a1': {'s0': -1}}\n","}\n","\n","from mdp import MDP\n","mdp = MDP(transition_probs, rewards, initial_state='s0')"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"aptlOq3-4TAc"},"source":["We can now use the MDP just as any other gym environment:"]},{"cell_type":"code","metadata":{"id":"1P4Z0nyM4TAd","outputId":"adcd0062-07be-4233-ae0d-0bd5a3e3e3d0"},"source":["print('initial state =', mdp.reset())\n","next_state, reward, done, info = mdp.step('a1')\n","print('next_state = %s, reward = %s, done = %s' % (next_state, reward, done))"],"execution_count":null,"outputs":[{"output_type":"stream","text":["initial state = s0\n","next_state = s2, reward = 0.0, done = False\n"],"name":"stdout"}]},{"cell_type":"markdown","metadata":{"id":"LGRFfXCx4TAj"},"source":["but it also has other methods that you'll need for Value Iteration:"]},{"cell_type":"code","metadata":{"id":"m7HkluEM4TAj","outputId":"834673c1-4e15-4203-9332-d485169af928"},"source":["print(\"mdp.get_all_states =\", mdp.get_all_states())\n","print(\"mdp.get_possible_actions('s1') = \", mdp.get_possible_actions('s1'))\n","print(\"mdp.get_next_states('s1', 'a0') = \", mdp.get_next_states('s1', 'a0'))\n","print(\"mdp.get_reward('s1', 'a0', 's0') = \", mdp.get_reward('s1', 'a0', 's0'))\n","print(\"mdp.get_transition_prob('s1', 'a0', 's0') = \", mdp.get_transition_prob('s1', 'a0', 's0'))"],"execution_count":null,"outputs":[{"output_type":"stream","text":["mdp.get_all_states = ('s0', 's1', 's2')\n","mdp.get_possible_actions('s1') = ('a0', 'a1')\n","mdp.get_next_states('s1', 'a0') = {'s0': 0.7, 's1': 0.1, 's2': 0.2}\n","mdp.get_reward('s1', 'a0', 's0') = 5\n","mdp.get_transition_prob('s1', 'a0', 's0') = 0.7\n"],"name":"stdout"}]},{"cell_type":"markdown","metadata":{"id":"_5b-S1za4TAk"},"source":["### Optional: Visualizing MDPs\n","\n","You can also visualize any MDP with the drawing fuction donated by [neer201](https://github.com/neer201).\n","\n","You have to install graphviz for system and for python. \n","\n","1. * For ubuntu just run: `sudo apt-get install graphviz` \n"," * For OSX: `brew install graphviz`\n","2. `pip install graphviz`\n","3. restart the notebook\n","\n","__Note:__ Installing graphviz on some OS (esp. Windows) may be tricky. However, you can ignore this part alltogether and use the standart vizualization."]},{"cell_type":"code","metadata":{"id":"e8cTCCsw4TAl","outputId":"f64e32db-5e61-40b2-b6d2-0ac66ed70a91"},"source":["from mdp import has_graphviz\n","from IPython.display import display\n","print(\"Graphviz available:\", has_graphviz)"],"execution_count":null,"outputs":[{"output_type":"stream","text":["Graphviz available: True\n"],"name":"stdout"}]},{"cell_type":"code","metadata":{"id":"u6eXD2VV4TAn","outputId":"b51aaf24-875c-483e-bf12-fdd03290c716"},"source":["if has_graphviz:\n"," from mdp import plot_graph, plot_graph_with_state_values, plot_graph_optimal_strategy_and_state_values\n"," display(plot_graph(mdp))"],"execution_count":null,"outputs":[{"output_type":"display_data","data":{"image/svg+xml":"\n\n\n\n\n","text/plain":[""]},"metadata":{"tags":[]}}]},{"cell_type":"markdown","metadata":{"id":"rjmj8HqP4TAp"},"source":["### Value Iteration\n","\n","Now let's build something to solve this MDP. The simplest algorithm so far is __V__alue __I__teration\n","\n","Here's the pseudo-code for VI:\n","\n","---\n","\n","`1.` Initialize $V^{(0)}(s)=0$, for all $s$\n","\n","`2.` For $i=0, 1, 2, \\dots$\n"," \n","`3.` $ \\quad V_{(i+1)}(s) = \\max_a \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')]$, for all $s$\n","\n","---"]},{"cell_type":"markdown","metadata":{"id":"wjdZyDdo4TAs"},"source":["First, let's write a function to compute the state-action value function $Q^{\\pi}$, defined as follows:\n","\n","$$Q_i(s, a) = \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')].$$\n"]},{"cell_type":"code","metadata":{"id":"2Nol2GMV4TAu"},"source":["def get_action_value(mdp, state_values, state, action, gamma):\n"," \"\"\" Computes Q(s,a) according to the formula above \"\"\"\n","\n"," \n","\n"," return "],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"fpSRbBKd4TAu"},"source":["import numpy as np\n","test_Vs = {s: i for i, s in enumerate(sorted(mdp.get_all_states()))}\n","assert np.isclose(get_action_value(mdp, test_Vs, 's2', 'a1', 0.9), 0.69)\n","assert np.isclose(get_action_value(mdp, test_Vs, 's1', 'a0', 0.9), 3.95)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"qlU2gcRJ4TAw"},"source":["Using $Q(s,a)$ we now can define the \"next\" V(s) for value iteration.\n"," $$V_{(i+1)}(s) = \\max_a \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')] = \\max_a Q_i(s,a)$$"]},{"cell_type":"code","metadata":{"id":"2-N2MI_64TAx"},"source":["def get_new_state_value(mdp, state_values, state, gamma):\n"," \"\"\" Computes the next V(s) according to the formula above. Please do not change state_values in process. \"\"\"\n"," if mdp.is_terminal(state):\n"," return 0\n","\n"," \n"," \n"," return "],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"ltxcMRsf4TAx"},"source":["test_Vs_copy = dict(test_Vs)\n","assert np.isclose(get_new_state_value(mdp, test_Vs, 's0', 0.9), 1.8)\n","assert np.isclose(get_new_state_value(mdp, test_Vs, 's2', 0.9), 1.08)\n","assert np.isclose(get_new_state_value(mdp, {'s0': -1e10, 's1': 0, 's2': -2e10}, 's0', 0.9), -13500000000.0), \\\n"," \"Please ensure that you handle negative Q-values of arbitrary magnitude correctly\"\n","assert test_Vs == test_Vs_copy, \"Please do not change state_values in get_new_state_value\""],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"MOgbwGgX4TAy"},"source":["Finally, let's combine everything we wrote into a working value iteration algo."]},{"cell_type":"code","metadata":{"id":"2dc-cvDu4TAy"},"source":["# parameters\n","gamma = 0.9 # discount for the MDP\n","num_iter = 100 # maximum iterations, excluding initialization\n","# stop VI if new values are as close to old values (or closer)\n","min_difference = 0.001\n","\n","# initialize V(s)\n","state_values = {s: 0 for s in mdp.get_all_states()}\n","\n","if has_graphviz:\n"," display(plot_graph_with_state_values(mdp, state_values))\n","\n","for i in range(num_iter):\n","\n"," # Compute new state values using the functions you defined above.\n"," # It must be a dict {state : float V_new(state)}\n"," new_state_values = \n","\n"," assert isinstance(new_state_values, dict)\n","\n"," # Compute difference\n"," diff = max(abs(new_state_values[s] - state_values[s])\n"," for s in mdp.get_all_states())\n"," print(\"iter %4i | diff: %6.5f | \" % (i, diff), end=\"\")\n"," print(' '.join(\"V(%s) = %.3f\" % (s, v) for s, v in state_values.items()))\n"," state_values = new_state_values\n","\n"," if diff < min_difference:\n"," print(\"Terminated\")\n"," break"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"P3vdbCLH4TAy"},"source":["if has_graphviz:\n"," display(plot_graph_with_state_values(mdp, state_values))"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"i5vAfode4TAz"},"source":["print(\"Final state values:\", state_values)\n","\n","assert abs(state_values['s0'] - 3.781) < 0.01\n","assert abs(state_values['s1'] - 7.294) < 0.01\n","assert abs(state_values['s2'] - 4.202) < 0.01"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"7igSDGQD4TAz"},"source":["Now let's use those $V^{*}(s)$ to find optimal actions in each state:\n","\n"," $$\\pi^*(s) = argmax_a \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')] = argmax_a Q_i(s,a).$$\n"," \n","The only difference vs V(s) is that here instead of max we take argmax: find the action that leads to the maximum of Q(s,a)."]},{"cell_type":"code","metadata":{"id":"DcXjiEqr4TAz"},"source":["def get_optimal_action(mdp, state_values, state, gamma=0.9):\n"," \"\"\" Finds optimal action using formula above. \"\"\"\n"," if mdp.is_terminal(state):\n"," return None\n","\n"," \n","\n"," return "],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"VQp_e_V_4TAz"},"source":["assert get_optimal_action(mdp, state_values, 's0', gamma) == 'a1'\n","assert get_optimal_action(mdp, state_values, 's1', gamma) == 'a0'\n","assert get_optimal_action(mdp, state_values, 's2', gamma) == 'a1'\n","\n","assert get_optimal_action(mdp, {'s0': -1e10, 's1': 0, 's2': -2e10}, 's0', 0.9) == 'a0', \\\n"," \"Please ensure that you handle negative Q-values of arbitrary magnitude correctly\"\n","assert get_optimal_action(mdp, {'s0': -2e10, 's1': 0, 's2': -1e10}, 's0', 0.9) == 'a1', \\\n"," \"Please ensure that you handle negative Q-values of arbitrary magnitude correctly\""],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"-NiKxKkU4TA0"},"source":["if has_graphviz:\n"," display(plot_graph_optimal_strategy_and_state_values(mdp, state_values, get_action_value))"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"DIaa_EG64TA1"},"source":["# Measure agent's average reward\n","\n","s = mdp.reset()\n","rewards = []\n","for _ in range(10000):\n"," s, r, done, _ = mdp.step(get_optimal_action(mdp, state_values, s, gamma))\n"," rewards.append(r)\n","\n","print(\"average reward: \", np.mean(rewards))\n","\n","assert(0.40 < np.mean(rewards) < 0.55)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"5r8aWg-M4TA2"},"source":["### Frozen lake"]},{"cell_type":"code","metadata":{"id":"lu4XMKab4TA2"},"source":["from mdp import FrozenLakeEnv\n","mdp = FrozenLakeEnv(slip_chance=0)\n","\n","mdp.render()"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"xheVP-IK4TA2"},"source":["def value_iteration(mdp, state_values=None, gamma=0.9, num_iter=1000, min_difference=1e-5):\n"," \"\"\" performs num_iter value iteration steps starting from state_values. The same as before but in a function \"\"\"\n"," state_values = state_values or {s: 0 for s in mdp.get_all_states()}\n"," for i in range(num_iter):\n","\n"," # Compute new state values using the functions you defined above. It must be a dict {state : new_V(state)}\n"," new_state_values = \n","\n"," assert isinstance(new_state_values, dict)\n","\n"," # Compute the difference\n"," diff = max(abs(new_state_values[s] - state_values[s])\n"," for s in mdp.get_all_states())\n","\n"," print(\"iter %4i | diff: %6.5f | V(start): %.3f \" %\n"," (i, diff, new_state_values[mdp._initial_state]))\n","\n"," state_values = new_state_values\n"," if diff < min_difference:\n"," break\n","\n"," return state_values"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"YpUiTG0V4TA2"},"source":["state_values = value_iteration(mdp)"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"e853Et3q4TA3"},"source":["s = mdp.reset()\n","mdp.render()\n","for t in range(100):\n"," a = get_optimal_action(mdp, state_values, s, gamma)\n"," print(a, end='\\n\\n')\n"," s, r, done, _ = mdp.step(a)\n"," mdp.render()\n"," if done:\n"," break"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"HK8PwJd54TA3"},"source":["### Let's visualize!\n","\n","It's usually interesting to see, what your algorithm actually learned under the hood. To do so, we'll plot the state value functions and optimal actions at each VI step."]},{"cell_type":"code","metadata":{"id":"PdHigU9R4TA3"},"source":["import matplotlib.pyplot as plt\n","%matplotlib inline\n","\n","\n","def draw_policy(mdp, state_values):\n"," plt.figure(figsize=(3, 3))\n"," h, w = mdp.desc.shape\n"," states = sorted(mdp.get_all_states())\n"," V = np.array([state_values[s] for s in states])\n"," Pi = {s: get_optimal_action(mdp, state_values, s, gamma) for s in states}\n"," plt.imshow(V.reshape(w, h), cmap='gray', interpolation='none', clim=(0, 1))\n"," ax = plt.gca()\n"," ax.set_xticks(np.arange(h)-.5)\n"," ax.set_yticks(np.arange(w)-.5)\n"," ax.set_xticklabels([])\n"," ax.set_yticklabels([])\n"," Y, X = np.mgrid[0:4, 0:4]\n"," a2uv = {'left': (-1, 0), 'down': (0, -1), 'right': (1, 0), 'up': (0, 1)}\n"," for y in range(h):\n"," for x in range(w):\n"," plt.text(x, y, str(mdp.desc[y, x].item()),\n"," color='g', size=12, verticalalignment='center',\n"," horizontalalignment='center', fontweight='bold')\n"," a = Pi[y, x]\n"," if a is None:\n"," continue\n"," u, v = a2uv[a]\n"," plt.arrow(x, y, u*.3, -v*.3, color='m',\n"," head_width=0.1, head_length=0.1)\n"," plt.grid(color='b', lw=2, ls='-')\n"," plt.show()"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"j5plpt_64TA4"},"source":["state_values = {s: 0 for s in mdp.get_all_states()}\n","\n","for i in range(10):\n"," print(\"after iteration %i\" % i)\n"," state_values = value_iteration(mdp, state_values, num_iter=1)\n"," draw_policy(mdp, state_values)\n","# please ignore iter 0 at each step"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"rSIcOqdZ4TA4"},"source":["from IPython.display import clear_output\n","from time import sleep\n","mdp = FrozenLakeEnv(map_name='8x8', slip_chance=0.1)\n","state_values = {s: 0 for s in mdp.get_all_states()}\n","\n","for i in range(30):\n"," clear_output(True)\n"," print(\"after iteration %i\" % i)\n"," state_values = value_iteration(mdp, state_values, num_iter=1)\n"," draw_policy(mdp, state_values)\n"," sleep(0.5)\n","# please ignore iter 0 at each step"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"M5h0nvn14TA4"},"source":["Massive tests"]},{"cell_type":"code","metadata":{"id":"wmd8w9va4TA4"},"source":["mdp = FrozenLakeEnv(slip_chance=0)\n","state_values = value_iteration(mdp)\n","\n","total_rewards = []\n","for game_i in range(1000):\n"," s = mdp.reset()\n"," rewards = []\n"," for t in range(100):\n"," s, r, done, _ = mdp.step(\n"," get_optimal_action(mdp, state_values, s, gamma))\n"," rewards.append(r)\n"," if done:\n"," break\n"," total_rewards.append(np.sum(rewards))\n","\n","print(\"average reward: \", np.mean(total_rewards))\n","assert(1.0 <= np.mean(total_rewards) <= 1.0)\n","print(\"Well done!\")"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"UO53Ys_64TA5"},"source":["# Measure agent's average reward\n","mdp = FrozenLakeEnv(slip_chance=0.1)\n","state_values = value_iteration(mdp)\n","\n","total_rewards = []\n","for game_i in range(1000):\n"," s = mdp.reset()\n"," rewards = []\n"," for t in range(100):\n"," s, r, done, _ = mdp.step(\n"," get_optimal_action(mdp, state_values, s, gamma))\n"," rewards.append(r)\n"," if done:\n"," break\n"," total_rewards.append(np.sum(rewards))\n","\n","print(\"average reward: \", np.mean(total_rewards))\n","assert(0.8 <= np.mean(total_rewards) <= 0.95)\n","print(\"Well done!\")"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"ZpN6kg2a4TA6"},"source":["# Measure agent's average reward\n","mdp = FrozenLakeEnv(slip_chance=0.25)\n","state_values = value_iteration(mdp)\n","\n","total_rewards = []\n","for game_i in range(1000):\n"," s = mdp.reset()\n"," rewards = []\n"," for t in range(100):\n"," s, r, done, _ = mdp.step(\n"," get_optimal_action(mdp, state_values, s, gamma))\n"," rewards.append(r)\n"," if done:\n"," break\n"," total_rewards.append(np.sum(rewards))\n","\n","print(\"average reward: \", np.mean(total_rewards))\n","assert(0.6 <= np.mean(total_rewards) <= 0.7)\n","print(\"Well done!\")"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"qgcjyhTR4TA6"},"source":["# Measure agent's average reward\n","mdp = FrozenLakeEnv(slip_chance=0.2, map_name='8x8')\n","state_values = value_iteration(mdp)\n","\n","total_rewards = []\n","for game_i in range(1000):\n"," s = mdp.reset()\n"," rewards = []\n"," for t in range(100):\n"," s, r, done, _ = mdp.step(\n"," get_optimal_action(mdp, state_values, s, gamma))\n"," rewards.append(r)\n"," if done:\n"," break\n"," total_rewards.append(np.sum(rewards))\n","\n","print(\"average reward: \", np.mean(total_rewards))\n","assert(0.6 <= np.mean(total_rewards) <= 0.8)\n","print(\"Well done!\")"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"LnVRYtE64TA7"},"source":["### Submit to coursera\n","\n","If your submission doesn't finish in 30 seconds, set `verbose=True` and try again."]},{"cell_type":"code","metadata":{"id":"QW3Cft5w4TA7"},"source":["from submit import submit_assigment\n","submit_assigment(\n"," get_action_value,\n"," get_new_state_value,\n"," get_optimal_action,\n"," value_iteration,\n"," 'your.email@example.com',\n"," 'YourAssignmentToken',\n"," verbose=False,\n",")"],"execution_count":null,"outputs":[]}]}
\ No newline at end of file
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Markov decision process\n",
+ "\n",
+ "This week methods are all built to solve __M__arkov __D__ecision __P__rocesses. In the broadest sense, the MDP is defined by how it changes the states and how rewards are computed.\n",
+ "\n",
+ "State transition is defined by $P(s' |s,a)$ - how likely you are to end at the state $s'$ if you take an action $a$ from the state $s$. Now there's more than one way to define rewards, but for convenience we'll use $r(s,a,s')$ function.\n",
+ "\n",
+ "_This notebook is inspired by the awesome_ [CS294](https://github.com/berkeleydeeprlcourse/homework/blob/36a0b58261acde756abd55306fbe63df226bf62b/hw2/HW2.ipynb) _by Berkeley_"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For starters, let's define a simple MDP from this picture:\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys, os\n",
+ "if 'google.colab' in sys.modules and not os.path.exists('.setup_complete'):\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/master/setup_colab.sh -O- | bash\n",
+ "\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/grading.py -O ../grading.py\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week2_model_based/submit.py\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week2_model_based/mdp.py\n",
+ "\n",
+ " !touch .setup_complete\n",
+ "\n",
+ "# This code creates a virtual display to draw game images on.\n",
+ "# It won't have any effect if your machine has a monitor.\n",
+ "if type(os.environ.get(\"DISPLAY\")) is not str or len(os.environ.get(\"DISPLAY\")) == 0:\n",
+ " !bash ../xvfb start\n",
+ " os.environ['DISPLAY'] = ':1'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "transition_probs = {\n",
+ " 's0': {\n",
+ " 'a0': {'s0': 0.5, 's2': 0.5},\n",
+ " 'a1': {'s2': 1}\n",
+ " },\n",
+ " 's1': {\n",
+ " 'a0': {'s0': 0.7, 's1': 0.1, 's2': 0.2},\n",
+ " 'a1': {'s1': 0.95, 's2': 0.05}\n",
+ " },\n",
+ " 's2': {\n",
+ " 'a0': {'s0': 0.4, 's2': 0.6},\n",
+ " 'a1': {'s0': 0.3, 's1': 0.3, 's2': 0.4}\n",
+ " }\n",
+ "}\n",
+ "rewards = {\n",
+ " 's1': {'a0': {'s0': +5}},\n",
+ " 's2': {'a1': {'s0': -1}}\n",
+ "}\n",
+ "\n",
+ "from mdp import MDP\n",
+ "mdp = MDP(transition_probs, rewards, initial_state='s0')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can now use the MDP just as any other gym environment:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "initial state = s0\n",
+ "next_state = s2, reward = 0.0, done = False\n"
+ ]
+ }
+ ],
+ "source": [
+ "print('initial state =', mdp.reset())\n",
+ "next_state, reward, done, info = mdp.step('a1')\n",
+ "print('next_state = %s, reward = %s, done = %s' % (next_state, reward, done))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "but it also has other methods that you'll need for Value Iteration:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "mdp.get_all_states = ('s0', 's1', 's2')\n",
+ "mdp.get_possible_actions('s1') = ('a0', 'a1')\n",
+ "mdp.get_next_states('s1', 'a0') = {'s0': 0.7, 's1': 0.1, 's2': 0.2}\n",
+ "mdp.get_reward('s1', 'a0', 's0') = 5\n",
+ "mdp.get_transition_prob('s1', 'a0', 's0') = 0.7\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"mdp.get_all_states =\", mdp.get_all_states())\n",
+ "print(\"mdp.get_possible_actions('s1') = \", mdp.get_possible_actions('s1'))\n",
+ "print(\"mdp.get_next_states('s1', 'a0') = \", mdp.get_next_states('s1', 'a0'))\n",
+ "print(\"mdp.get_reward('s1', 'a0', 's0') = \", mdp.get_reward('s1', 'a0', 's0'))\n",
+ "print(\"mdp.get_transition_prob('s1', 'a0', 's0') = \", mdp.get_transition_prob('s1', 'a0', 's0'))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Optional: Visualizing MDPs\n",
+ "\n",
+ "You can also visualize any MDP with the drawing fuction donated by [neer201](https://github.com/neer201).\n",
+ "\n",
+ "You have to install graphviz for system and for python. \n",
+ "\n",
+ "1. * For ubuntu just run: `sudo apt-get install graphviz` \n",
+ " * For OSX: `brew install graphviz`\n",
+ "2. `pip install graphviz`\n",
+ "3. restart the notebook\n",
+ "\n",
+ "__Note:__ Installing graphviz on some OS (esp. Windows) may be tricky. However, you can ignore this part alltogether and use the standart vizualization."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Graphviz available: True\n"
+ ]
+ }
+ ],
+ "source": [
+ "from mdp import has_graphviz\n",
+ "from IPython.display import display\n",
+ "print(\"Graphviz available:\", has_graphviz)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "tags": []
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "if has_graphviz:\n",
+ " from mdp import plot_graph, plot_graph_with_state_values, plot_graph_optimal_strategy_and_state_values\n",
+ " display(plot_graph(mdp))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Value Iteration\n",
+ "\n",
+ "Now let's build something to solve this MDP. The simplest algorithm so far is __V__alue __I__teration\n",
+ "\n",
+ "Here's the pseudo-code for VI:\n",
+ "\n",
+ "---\n",
+ "\n",
+ "`1.` Initialize $V^{(0)}(s)=0$, for all $s$\n",
+ "\n",
+ "`2.` For $i=0, 1, 2, \\dots$\n",
+ " \n",
+ "`3.` $ \\quad V_{(i+1)}(s) = \\max_a \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')]$, for all $s$\n",
+ "\n",
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "First, let's write a function to compute the state-action value function $Q^{\\pi}$, defined as follows:\n",
+ "\n",
+ "$$Q_i(s, a) = \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')].$$\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_action_value(mdp, state_values, state, action, gamma):\n",
+ " \"\"\" Computes Q(s,a) according to the formula above \"\"\"\n",
+ "\n",
+ " \n",
+ "\n",
+ " return "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "test_Vs = {s: i for i, s in enumerate(sorted(mdp.get_all_states()))}\n",
+ "assert np.isclose(get_action_value(mdp, test_Vs, 's2', 'a1', 0.9), 0.69)\n",
+ "assert np.isclose(get_action_value(mdp, test_Vs, 's1', 'a0', 0.9), 3.95)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Using $Q(s,a)$ we now can define the \"next\" V(s) for value iteration.\n",
+ " $$V_{(i+1)}(s) = \\max_a \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')] = \\max_a Q_i(s,a)$$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_new_state_value(mdp, state_values, state, gamma):\n",
+ " \"\"\" Computes the next V(s) according to the formula above. Please do not change state_values in process. \"\"\"\n",
+ " if mdp.is_terminal(state):\n",
+ " return 0\n",
+ "\n",
+ " \n",
+ " \n",
+ " return "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test_Vs_copy = dict(test_Vs)\n",
+ "assert np.isclose(get_new_state_value(mdp, test_Vs, 's0', 0.9), 1.8)\n",
+ "assert np.isclose(get_new_state_value(mdp, test_Vs, 's2', 0.9), 1.08)\n",
+ "assert np.isclose(get_new_state_value(mdp, {'s0': -1e10, 's1': 0, 's2': -2e10}, 's0', 0.9), -13500000000.0), \\\n",
+ " \"Please ensure that you handle negative Q-values of arbitrary magnitude correctly\"\n",
+ "assert test_Vs == test_Vs_copy, \"Please do not change state_values in get_new_state_value\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, let's combine everything we wrote into a working value iteration algo."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# parameters\n",
+ "gamma = 0.9 # discount for the MDP\n",
+ "num_iter = 100 # maximum iterations, excluding initialization\n",
+ "# stop VI if new values are as close to old values (or closer)\n",
+ "min_difference = 0.001\n",
+ "\n",
+ "# initialize V(s)\n",
+ "state_values = {s: 0 for s in mdp.get_all_states()}\n",
+ "\n",
+ "if has_graphviz:\n",
+ " display(plot_graph_with_state_values(mdp, state_values))\n",
+ "\n",
+ "for i in range(num_iter):\n",
+ "\n",
+ " # Compute new state values using the functions you defined above.\n",
+ " # It must be a dict {state : float V_new(state)}\n",
+ " new_state_values = \n",
+ "\n",
+ " assert isinstance(new_state_values, dict)\n",
+ "\n",
+ " # Compute difference\n",
+ " diff = max(abs(new_state_values[s] - state_values[s])\n",
+ " for s in mdp.get_all_states())\n",
+ " print(\"iter %4i | diff: %6.5f | \" % (i, diff), end=\"\")\n",
+ " print(' '.join(\"V(%s) = %.3f\" % (s, v) for s, v in state_values.items()))\n",
+ " state_values = new_state_values\n",
+ "\n",
+ " if diff < min_difference:\n",
+ " print(\"Terminated\")\n",
+ " break"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if has_graphviz:\n",
+ " display(plot_graph_with_state_values(mdp, state_values))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(\"Final state values:\", state_values)\n",
+ "\n",
+ "assert abs(state_values['s0'] - 3.781) < 0.01\n",
+ "assert abs(state_values['s1'] - 7.294) < 0.01\n",
+ "assert abs(state_values['s2'] - 4.202) < 0.01"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now let's use those $V^{*}(s)$ to find optimal actions in each state:\n",
+ "\n",
+ " $$\\pi^*(s) = argmax_a \\sum_{s'} P(s' | s,a) \\cdot [ r(s,a,s') + \\gamma V_{i}(s')] = argmax_a Q_i(s,a).$$\n",
+ " \n",
+ "The only difference vs V(s) is that here instead of max we take argmax: find the action that leads to the maximum of Q(s,a)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_optimal_action(mdp, state_values, state, gamma=0.9):\n",
+ " \"\"\" Finds optimal action using formula above. \"\"\"\n",
+ " if mdp.is_terminal(state):\n",
+ " return None\n",
+ "\n",
+ " \n",
+ "\n",
+ " return "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "assert get_optimal_action(mdp, state_values, 's0', gamma) == 'a1'\n",
+ "assert get_optimal_action(mdp, state_values, 's1', gamma) == 'a0'\n",
+ "assert get_optimal_action(mdp, state_values, 's2', gamma) == 'a1'\n",
+ "\n",
+ "assert get_optimal_action(mdp, {'s0': -1e10, 's1': 0, 's2': -2e10}, 's0', 0.9) == 'a0', \\\n",
+ " \"Please ensure that you handle negative Q-values of arbitrary magnitude correctly\"\n",
+ "assert get_optimal_action(mdp, {'s0': -2e10, 's1': 0, 's2': -1e10}, 's0', 0.9) == 'a1', \\\n",
+ " \"Please ensure that you handle negative Q-values of arbitrary magnitude correctly\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if has_graphviz:\n",
+ " display(plot_graph_optimal_strategy_and_state_values(mdp, state_values, get_action_value))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Measure agent's average reward\n",
+ "\n",
+ "s = mdp.reset()\n",
+ "rewards = []\n",
+ "for _ in range(10000):\n",
+ " s, r, done, _ = mdp.step(get_optimal_action(mdp, state_values, s, gamma))\n",
+ " rewards.append(r)\n",
+ "\n",
+ "print(\"average reward: \", np.mean(rewards))\n",
+ "\n",
+ "assert(0.40 < np.mean(rewards) < 0.55)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Frozen lake"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from mdp import FrozenLakeEnv\n",
+ "mdp = FrozenLakeEnv(slip_chance=0)\n",
+ "\n",
+ "mdp.render()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def value_iteration(mdp, state_values=None, gamma=0.9, num_iter=1000, min_difference=1e-5):\n",
+ " \"\"\" performs num_iter value iteration steps starting from state_values. The same as before but in a function \"\"\"\n",
+ " state_values = state_values or {s: 0 for s in mdp.get_all_states()}\n",
+ " for i in range(num_iter):\n",
+ "\n",
+ " # Compute new state values using the functions you defined above. It must be a dict {state : new_V(state)}\n",
+ " new_state_values = \n",
+ "\n",
+ " assert isinstance(new_state_values, dict)\n",
+ "\n",
+ " # Compute the difference\n",
+ " diff = max(abs(new_state_values[s] - state_values[s])\n",
+ " for s in mdp.get_all_states())\n",
+ "\n",
+ " print(\"iter %4i | diff: %6.5f | V(start): %.3f \" %\n",
+ " (i, diff, new_state_values[mdp._initial_state]))\n",
+ "\n",
+ " state_values = new_state_values\n",
+ " if diff < min_difference:\n",
+ " break\n",
+ "\n",
+ " return state_values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "state_values = value_iteration(mdp)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "s = mdp.reset()\n",
+ "mdp.render()\n",
+ "for t in range(100):\n",
+ " a = get_optimal_action(mdp, state_values, s, gamma)\n",
+ " print(a, end='\\n\\n')\n",
+ " s, r, done, _ = mdp.step(a)\n",
+ " mdp.render()\n",
+ " if done:\n",
+ " break"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Let's visualize!\n",
+ "\n",
+ "It's usually interesting to see, what your algorithm actually learned under the hood. To do so, we'll plot the state value functions and optimal actions at each VI step."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "%matplotlib inline\n",
+ "\n",
+ "\n",
+ "def draw_policy(mdp, state_values):\n",
+ " plt.figure(figsize=(3, 3))\n",
+ " h, w = mdp.desc.shape\n",
+ " states = sorted(mdp.get_all_states())\n",
+ " V = np.array([state_values[s] for s in states])\n",
+ " Pi = {s: get_optimal_action(mdp, state_values, s, gamma) for s in states}\n",
+ " plt.imshow(V.reshape(w, h), cmap='gray', interpolation='none', clim=(0, 1))\n",
+ " ax = plt.gca()\n",
+ " ax.set_xticks(np.arange(h)-.5)\n",
+ " ax.set_yticks(np.arange(w)-.5)\n",
+ " ax.set_xticklabels([])\n",
+ " ax.set_yticklabels([])\n",
+ " Y, X = np.mgrid[0:4, 0:4]\n",
+ " a2uv = {'left': (-1, 0), 'down': (0, -1), 'right': (1, 0), 'up': (0, 1)}\n",
+ " for y in range(h):\n",
+ " for x in range(w):\n",
+ " plt.text(x, y, str(mdp.desc[y, x].item()),\n",
+ " color='g', size=12, verticalalignment='center',\n",
+ " horizontalalignment='center', fontweight='bold')\n",
+ " a = Pi[y, x]\n",
+ " if a is None:\n",
+ " continue\n",
+ " u, v = a2uv[a]\n",
+ " plt.arrow(x, y, u*.3, -v*.3, color='m',\n",
+ " head_width=0.1, head_length=0.1)\n",
+ " plt.grid(color='b', lw=2, ls='-')\n",
+ " plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "state_values = {s: 0 for s in mdp.get_all_states()}\n",
+ "\n",
+ "for i in range(10):\n",
+ " print(\"after iteration %i\" % i)\n",
+ " state_values = value_iteration(mdp, state_values, num_iter=1)\n",
+ " draw_policy(mdp, state_values)\n",
+ "# please ignore iter 0 at each step"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from IPython.display import clear_output\n",
+ "from time import sleep\n",
+ "mdp = FrozenLakeEnv(map_name='8x8', slip_chance=0.1)\n",
+ "state_values = {s: 0 for s in mdp.get_all_states()}\n",
+ "\n",
+ "for i in range(30):\n",
+ " clear_output(True)\n",
+ " print(\"after iteration %i\" % i)\n",
+ " state_values = value_iteration(mdp, state_values, num_iter=1)\n",
+ " draw_policy(mdp, state_values)\n",
+ " sleep(0.5)\n",
+ "# please ignore iter 0 at each step"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Massive tests"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mdp = FrozenLakeEnv(slip_chance=0)\n",
+ "state_values = value_iteration(mdp)\n",
+ "\n",
+ "total_rewards = []\n",
+ "for game_i in range(1000):\n",
+ " s = mdp.reset()\n",
+ " rewards = []\n",
+ " for t in range(100):\n",
+ " s, r, done, _ = mdp.step(\n",
+ " get_optimal_action(mdp, state_values, s, gamma))\n",
+ " rewards.append(r)\n",
+ " if done:\n",
+ " break\n",
+ " total_rewards.append(np.sum(rewards))\n",
+ "\n",
+ "print(\"average reward: \", np.mean(total_rewards))\n",
+ "assert(1.0 <= np.mean(total_rewards) <= 1.0)\n",
+ "print(\"Well done!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Measure agent's average reward\n",
+ "mdp = FrozenLakeEnv(slip_chance=0.1)\n",
+ "state_values = value_iteration(mdp)\n",
+ "\n",
+ "total_rewards = []\n",
+ "for game_i in range(1000):\n",
+ " s = mdp.reset()\n",
+ " rewards = []\n",
+ " for t in range(100):\n",
+ " s, r, done, _ = mdp.step(\n",
+ " get_optimal_action(mdp, state_values, s, gamma))\n",
+ " rewards.append(r)\n",
+ " if done:\n",
+ " break\n",
+ " total_rewards.append(np.sum(rewards))\n",
+ "\n",
+ "print(\"average reward: \", np.mean(total_rewards))\n",
+ "assert(0.8 <= np.mean(total_rewards) <= 0.95)\n",
+ "print(\"Well done!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Measure agent's average reward\n",
+ "mdp = FrozenLakeEnv(slip_chance=0.25)\n",
+ "state_values = value_iteration(mdp)\n",
+ "\n",
+ "total_rewards = []\n",
+ "for game_i in range(1000):\n",
+ " s = mdp.reset()\n",
+ " rewards = []\n",
+ " for t in range(100):\n",
+ " s, r, done, _ = mdp.step(\n",
+ " get_optimal_action(mdp, state_values, s, gamma))\n",
+ " rewards.append(r)\n",
+ " if done:\n",
+ " break\n",
+ " total_rewards.append(np.sum(rewards))\n",
+ "\n",
+ "print(\"average reward: \", np.mean(total_rewards))\n",
+ "assert(0.6 <= np.mean(total_rewards) <= 0.7)\n",
+ "print(\"Well done!\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Measure agent's average reward\n",
+ "mdp = FrozenLakeEnv(slip_chance=0.2, map_name='8x8')\n",
+ "state_values = value_iteration(mdp)\n",
+ "\n",
+ "total_rewards = []\n",
+ "for game_i in range(1000):\n",
+ " s = mdp.reset()\n",
+ " rewards = []\n",
+ " for t in range(100):\n",
+ " s, r, done, _ = mdp.step(\n",
+ " get_optimal_action(mdp, state_values, s, gamma))\n",
+ " rewards.append(r)\n",
+ " if done:\n",
+ " break\n",
+ " total_rewards.append(np.sum(rewards))\n",
+ "\n",
+ "print(\"average reward: \", np.mean(total_rewards))\n",
+ "assert(0.6 <= np.mean(total_rewards) <= 0.8)\n",
+ "print(\"Well done!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Submit to coursera\n",
+ "\n",
+ "If your submission doesn't finish in 30 seconds, set `verbose=True` and try again."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from submit import submit_assigment\n",
+ "submit_assigment(\n",
+ " get_action_value,\n",
+ " get_new_state_value,\n",
+ " get_optimal_action,\n",
+ " value_iteration,\n",
+ " 'your.email@example.com',\n",
+ " 'YourAssignmentToken',\n",
+ " verbose=False,\n",
+ ")"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python",
+ "pygments_lexer": "ipython3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/week3_model_free/experience_replay.ipynb b/week3_model_free/experience_replay.ipynb
index 9825360a2..5aa6a5b75 100644
--- a/week3_model_free/experience_replay.ipynb
+++ b/week3_model_free/experience_replay.ipynb
@@ -1 +1,345 @@
-{"nbformat":4,"nbformat_minor":0,"metadata":{"language_info":{"name":"python","pygments_lexer":"ipython3"},"colab":{"name":"experience_replay.ipynb","provenance":[],"collapsed_sections":[]}},"cells":[{"cell_type":"markdown","metadata":{"id":"YPdtIpBDdi4x"},"source":["### Honor Track: experience replay\n","\n","There's a powerful technique that you can use to improve the sample efficiency for off-policy algorithms: [spoiler] Experience replay :)\n","\n","The catch is that you can train Q-learning and EV-SARSA on `` tuples even if they aren't sampled under the current agent's policy. So here's what we're gonna do:\n","\n","\n","\n","#### Training with experience replay\n","1. Play game, sample ``.\n","2. Update q-values based on ``.\n","3. Store `` transition in a buffer. \n"," 3. If buffer is full, delete the earliest data.\n","4. Sample K such transitions from that buffer and update the q-values based on them.\n","\n","\n","To enable such training, first, we must implement a memory structure, that would act as this buffer."]},{"cell_type":"code","metadata":{"id":"_u_E2KS8di4z"},"source":["import sys, os\n","if 'google.colab' in sys.modules and not os.path.exists('.setup_complete'):\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/master/setup_colab.sh -O- | bash\n","\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/grading.py -O ../grading.py\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week3_model_free/submit.py\n","\n"," !touch .setup_complete\n","\n","# This code creates a virtual display to draw game images on.\n","# It won't have any effect if your machine has a monitor.\n","if type(os.environ.get(\"DISPLAY\")) is not str or len(os.environ.get(\"DISPLAY\")) == 0:\n"," !bash ../xvfb start\n"," os.environ['DISPLAY'] = ':1'"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"W6kAYyMZdi40"},"source":["import numpy as np\n","import matplotlib.pyplot as plt\n","%matplotlib inline\n","\n","from IPython.display import clear_output"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"NQ1f7TzOdi40"},"source":[""],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"JFyXjhEbdi41"},"source":["import random\n","\n","\n","class ReplayBuffer(object):\n"," def __init__(self, size):\n"," \"\"\"\n"," Create Replay buffer.\n"," Parameters\n"," ----------\n"," size: int\n"," Max number of transitions to store in the buffer. When the buffer is\n"," overflowed, the old memories are dropped.\n","\n"," Note: for this assignment you can pick any data structure you want.\n"," If you want to keep it simple, you can store a list of tuples of (s, a, r, s') in self._storage\n"," However you may find, that there are faster and/or more memory-efficient ways to do so.\n"," \"\"\"\n"," self._storage = []\n"," self._maxsize = size\n","\n"," # OPTIONAL: YOUR CODE\n","\n"," def __len__(self):\n"," return len(self._storage)\n","\n"," def add(self, obs_t, action, reward, obs_tp1, done):\n"," '''\n"," Make sure, _storage will not exceed _maxsize. \n"," Make sure, FIFO rule is being followed: the oldest examples have to be removed earlier\n"," '''\n"," data = (obs_t, action, reward, obs_tp1, done)\n","\n"," # add data to storage\n"," \n","\n"," def sample(self, batch_size):\n"," \"\"\"Sample a batch of experiences.\n"," Parameters\n"," ----------\n"," batch_size: int\n"," How many transitions to sample.\n"," Returns\n"," -------\n"," obs_batch: np.array\n"," batch of observations\n"," act_batch: np.array\n"," batch of actions executed given obs_batch\n"," rew_batch: np.array\n"," rewards received as the results of executing act_batch\n"," next_obs_batch: np.array\n"," next set of observations, seen after executing act_batch\n"," done_mask: np.array\n"," done_mask[i] = 1 if executing act_batch[i] resulted in\n"," the end of an episode and 0 otherwise.\n"," \"\"\"\n"," idxes = \n","\n"," # collect for each index\n"," \n","\n"," return (\n"," np.array( ),\n"," np.array( ),\n"," np.array( ),\n"," np.array( ),\n"," np.array( ,\n"," )"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"ptoGh7Epdi46"},"source":["Some tests to make sure your buffer works right"]},{"cell_type":"code","metadata":{"id":"spzmuisddi47"},"source":["def obj2arrays(obj):\n"," for x in obj:\n"," yield np.array([x])\n","\n","def obj2sampled(obj):\n"," return tuple(obj2arrays(obj))\n","\n","replay = ReplayBuffer(2)\n","obj1 = (0, 1, 2, 3, True)\n","obj2 = (4, 5, 6, 7, False)\n","replay.add(*obj1)\n","assert replay.sample(1) == obj2sampled(obj1), \\\n"," \"If there's just one object in buffer, it must be retrieved by buf.sample(1)\"\n","replay.add(*obj2)\n","assert len(replay) == 2, \"Please make sure __len__ methods works as intended.\"\n","replay.add(*obj2)\n","assert len(replay) == 2, \"When buffer is at max capacity, replace objects instead of adding new ones.\"\n","assert tuple(np.unique(a) for a in replay.sample(100)) == obj2sampled(obj2)\n","replay.add(*obj1)\n","assert max(len(np.unique(a)) for a in replay.sample(100)) == 2\n","replay.add(*obj1)\n","assert tuple(np.unique(a) for a in replay.sample(100)) == obj2sampled(obj1)\n","print(\"Success!\")"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"Yaush3KHdi47"},"source":["Now let's use this buffer to improve the training:"]},{"cell_type":"code","metadata":{"id":"zlRq2M57di47"},"source":["import gym\n","env = gym.make(\"Taxi-v3\")\n","n_actions = env.action_space.n"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"tRIymZTLdi48"},"source":["def play_and_train_with_replay(env, agent, replay=None,\n"," t_max=10**4, replay_batch_size=32):\n"," \"\"\"\n"," This function should \n"," - run a full game, actions given by agent.getAction(s)\n"," - train agent using agent.update(...) whenever possible\n"," - return total reward\n"," :param replay: ReplayBuffer where agent can store and sample (s,a,r,s',done) tuples.\n"," If None, do not use an experience replay\n"," \"\"\"\n"," total_reward = 0.0\n"," s = env.reset()\n","\n"," for t in range(t_max):\n"," # get agent to pick action given state s\n"," a = \n","\n"," next_s, r, done, _ = env.step(a)\n","\n"," # update agent on current transition. Use agent.update\n"," \n","\n"," if replay is not None:\n"," # store current transition in buffer\n"," \n","\n"," # sample replay_batch_size random transitions from replay,\n"," # then update the agent on each of them in a loop\n"," s_, a_, r_, next_s_, done_ = replay.sample(replay_batch_size)\n"," for i in range(replay_batch_size):\n"," \n","\n"," s = next_s\n"," total_reward += r\n"," if done:\n"," break\n","\n"," return total_reward"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"ybT9zIbSdi48"},"source":["# Create two agents: first will use the experience replay, second will not.\n","\n","agent_baseline = QLearningAgent(\n"," alpha=0.5, epsilon=0.25, discount=0.99,\n"," get_legal_actions=lambda s: range(n_actions))\n","\n","agent_replay = QLearningAgent(\n"," alpha=0.5, epsilon=0.25, discount=0.99,\n"," get_legal_actions=lambda s: range(n_actions))\n","\n","replay = ReplayBuffer(1000)"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"lQGmiCpwdi49"},"source":["from IPython.display import clear_output\n","import pandas as pd\n","\n","def moving_average(x, span=100):\n"," return pd.DataFrame({'x': np.asarray(x)}).x.ewm(span=span).mean().values\n","\n","rewards_replay, rewards_baseline = [], []\n","\n","for i in range(1000):\n"," rewards_replay.append(\n"," play_and_train_with_replay(env, agent_replay, replay))\n"," rewards_baseline.append(\n"," play_and_train_with_replay(env, agent_baseline, replay=None))\n","\n"," agent_replay.epsilon *= 0.99\n"," agent_baseline.epsilon *= 0.99\n","\n"," if i % 100 == 0:\n"," clear_output(True)\n"," print('Baseline : eps =', agent_replay.epsilon,\n"," 'mean reward =', np.mean(rewards_baseline[-10:]))\n"," print('ExpReplay: eps =', agent_baseline.epsilon,\n"," 'mean reward =', np.mean(rewards_replay[-10:]))\n"," plt.plot(moving_average(rewards_replay), label='exp. replay')\n"," plt.plot(moving_average(rewards_baseline), label='baseline')\n"," plt.grid()\n"," plt.legend()\n"," plt.show()"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"rhfZcfMYdi4-"},"source":["### Submit to Coursera"]},{"cell_type":"code","metadata":{"id":"O6wLlXi4di4-"},"source":["from submit import submit_experience_replay\n","submit_experience_replay(rewards_replay, rewards_baseline, 'your.email@example.com', 'YourAssignmentToken')"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"ZYLLK5o8di4-"},"source":["#### What to expect:\n","\n","Experience replay, if implemented correctly, will improve algorithm's initial convergence a lot, but it shouldn't affect the final performance.\n","\n","### Outro\n","\n","We will use the code you just wrote extensively in the next week of our course. If you're feeling, that you need more examples to understand how the experience replay works, try using it for binarized state spaces (CartPole or other __[classic control envs](https://gym.openai.com/envs/#classic_control)__).\n","\n","__Next week__ we're gonna explore how q-learning and similar algorithms can be applied for large state spaces, with deep learning models to approximate the Q function.\n","\n","However, __the code you've written__ this week is already capable to solve many RL problems, and as an added benifit - it is very easy to detach. You can use Q-learning, SARSA and Experience Replay for any RL problems you want to solve - just throw them into a file and import the stuff you need."]}]}
\ No newline at end of file
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Honor Track: experience replay\n",
+ "\n",
+ "There's a powerful technique that you can use to improve the sample efficiency for off-policy algorithms: [spoiler] Experience replay :)\n",
+ "\n",
+ "The catch is that you can train Q-learning and EV-SARSA on `` tuples even if they aren't sampled under the current agent's policy. So here's what we're gonna do:\n",
+ "\n",
+ "\n",
+ "\n",
+ "#### Training with experience replay\n",
+ "1. Play game, sample ``.\n",
+ "2. Update q-values based on ``.\n",
+ "3. Store `` transition in a buffer. \n",
+ " 3. If buffer is full, delete the earliest data.\n",
+ "4. Sample K such transitions from that buffer and update the q-values based on them.\n",
+ "\n",
+ "\n",
+ "To enable such training, first, we must implement a memory structure, that would act as this buffer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys, os\n",
+ "if 'google.colab' in sys.modules and not os.path.exists('.setup_complete'):\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/master/setup_colab.sh -O- | bash\n",
+ "\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/grading.py -O ../grading.py\n",
+ " !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week3_model_free/submit.py\n",
+ "\n",
+ " !touch .setup_complete\n",
+ "\n",
+ "# This code creates a virtual display to draw game images on.\n",
+ "# It won't have any effect if your machine has a monitor.\n",
+ "if type(os.environ.get(\"DISPLAY\")) is not str or len(os.environ.get(\"DISPLAY\")) == 0:\n",
+ " !bash ../xvfb start\n",
+ " os.environ['DISPLAY'] = ':1'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "%matplotlib inline\n",
+ "\n",
+ "from IPython.display import clear_output"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import random\n",
+ "\n",
+ "\n",
+ "class ReplayBuffer(object):\n",
+ " def __init__(self, size):\n",
+ " \"\"\"\n",
+ " Create Replay buffer.\n",
+ " Parameters\n",
+ " ----------\n",
+ " size: int\n",
+ " Max number of transitions to store in the buffer. When the buffer is\n",
+ " overflowed, the old memories are dropped.\n",
+ "\n",
+ " Note: for this assignment you can pick any data structure you want.\n",
+ " If you want to keep it simple, you can store a list of tuples of (s, a, r, s') in self._storage\n",
+ " However you may find, that there are faster and/or more memory-efficient ways to do so.\n",
+ " \"\"\"\n",
+ " self._storage = []\n",
+ " self._maxsize = size\n",
+ "\n",
+ " # OPTIONAL: YOUR CODE\n",
+ "\n",
+ " def __len__(self):\n",
+ " return len(self._storage)\n",
+ "\n",
+ " def add(self, obs_t, action, reward, obs_tp1, done):\n",
+ " '''\n",
+ " Make sure, _storage will not exceed _maxsize. \n",
+ " Make sure, FIFO rule is being followed: the oldest examples have to be removed earlier\n",
+ " '''\n",
+ " data = (obs_t, action, reward, obs_tp1, done)\n",
+ "\n",
+ " # add data to storage\n",
+ " \n",
+ "\n",
+ " def sample(self, batch_size):\n",
+ " \"\"\"Sample a batch of experiences.\n",
+ " Parameters\n",
+ " ----------\n",
+ " batch_size: int\n",
+ " How many transitions to sample.\n",
+ " Returns\n",
+ " -------\n",
+ " obs_batch: np.array\n",
+ " batch of observations\n",
+ " act_batch: np.array\n",
+ " batch of actions executed given obs_batch\n",
+ " rew_batch: np.array\n",
+ " rewards received as the results of executing act_batch\n",
+ " next_obs_batch: np.array\n",
+ " next set of observations, seen after executing act_batch\n",
+ " done_mask: np.array\n",
+ " done_mask[i] = 1 if executing act_batch[i] resulted in\n",
+ " the end of an episode and 0 otherwise.\n",
+ " \"\"\"\n",
+ " idxes = \n",
+ "\n",
+ " # collect for each index\n",
+ " \n",
+ "\n",
+ " return (\n",
+ " np.array( ),\n",
+ " np.array( ),\n",
+ " np.array( ),\n",
+ " np.array( ),\n",
+ " np.array( ,\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Some tests to make sure your buffer works right"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def obj2arrays(obj):\n",
+ " for x in obj:\n",
+ " yield np.array([x])\n",
+ "\n",
+ "def obj2sampled(obj):\n",
+ " return tuple(obj2arrays(obj))\n",
+ "\n",
+ "replay = ReplayBuffer(2)\n",
+ "obj1 = (0, 1, 2, 3, True)\n",
+ "obj2 = (4, 5, 6, 7, False)\n",
+ "replay.add(*obj1)\n",
+ "assert replay.sample(1) == obj2sampled(obj1), \\\n",
+ " \"If there's just one object in buffer, it must be retrieved by buf.sample(1)\"\n",
+ "replay.add(*obj2)\n",
+ "assert len(replay) == 2, \"Please make sure __len__ methods works as intended.\"\n",
+ "replay.add(*obj2)\n",
+ "assert len(replay) == 2, \"When buffer is at max capacity, replace objects instead of adding new ones.\"\n",
+ "assert tuple(np.unique(a) for a in replay.sample(100)) == obj2sampled(obj2)\n",
+ "replay.add(*obj1)\n",
+ "assert max(len(np.unique(a)) for a in replay.sample(100)) == 2\n",
+ "replay.add(*obj1)\n",
+ "assert tuple(np.unique(a) for a in replay.sample(100)) == obj2sampled(obj1)\n",
+ "print(\"Success!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now let's use this buffer to improve the training:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import gym\n",
+ "env = gym.make(\"Taxi-v3\")\n",
+ "n_actions = env.action_space.n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def play_and_train_with_replay(env, agent, replay=None,\n",
+ " t_max=10**4, replay_batch_size=32):\n",
+ " \"\"\"\n",
+ " This function should \n",
+ " - run a full game, actions given by agent.getAction(s)\n",
+ " - train agent using agent.update(...) whenever possible\n",
+ " - return total reward\n",
+ " :param replay: ReplayBuffer where agent can store and sample (s,a,r,s',done) tuples.\n",
+ " If None, do not use an experience replay\n",
+ " \"\"\"\n",
+ " total_reward = 0.0\n",
+ " s = env.reset()\n",
+ "\n",
+ " for t in range(t_max):\n",
+ " # get agent to pick action given state s\n",
+ " a = \n",
+ "\n",
+ " next_s, r, done, _ = env.step(a)\n",
+ "\n",
+ " # update agent on current transition. Use agent.update\n",
+ " \n",
+ "\n",
+ " if replay is not None:\n",
+ " # store current transition in buffer\n",
+ " \n",
+ "\n",
+ " # sample replay_batch_size random transitions from replay,\n",
+ " # then update the agent on each of them in a loop\n",
+ " s_, a_, r_, next_s_, done_ = replay.sample(replay_batch_size)\n",
+ " for i in range(replay_batch_size):\n",
+ " \n",
+ "\n",
+ " s = next_s\n",
+ " total_reward += r\n",
+ " if done:\n",
+ " break\n",
+ "\n",
+ " return total_reward"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Create two agents: first will use the experience replay, second will not.\n",
+ "\n",
+ "agent_baseline = QLearningAgent(\n",
+ " alpha=0.5, epsilon=0.25, discount=0.99,\n",
+ " get_legal_actions=lambda s: range(n_actions))\n",
+ "\n",
+ "agent_replay = QLearningAgent(\n",
+ " alpha=0.5, epsilon=0.25, discount=0.99,\n",
+ " get_legal_actions=lambda s: range(n_actions))\n",
+ "\n",
+ "replay = ReplayBuffer(1000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from IPython.display import clear_output\n",
+ "import pandas as pd\n",
+ "\n",
+ "def moving_average(x, span=100):\n",
+ " return pd.DataFrame({'x': np.asarray(x)}).x.ewm(span=span).mean().values\n",
+ "\n",
+ "rewards_replay, rewards_baseline = [], []\n",
+ "\n",
+ "for i in range(1000):\n",
+ " rewards_replay.append(\n",
+ " play_and_train_with_replay(env, agent_replay, replay))\n",
+ " rewards_baseline.append(\n",
+ " play_and_train_with_replay(env, agent_baseline, replay=None))\n",
+ "\n",
+ " agent_replay.epsilon *= 0.99\n",
+ " agent_baseline.epsilon *= 0.99\n",
+ "\n",
+ " if i % 100 == 0:\n",
+ " clear_output(True)\n",
+ " print('Baseline : eps =', agent_replay.epsilon,\n",
+ " 'mean reward =', np.mean(rewards_baseline[-10:]))\n",
+ " print('ExpReplay: eps =', agent_baseline.epsilon,\n",
+ " 'mean reward =', np.mean(rewards_replay[-10:]))\n",
+ " plt.plot(moving_average(rewards_replay), label='exp. replay')\n",
+ " plt.plot(moving_average(rewards_baseline), label='baseline')\n",
+ " plt.grid()\n",
+ " plt.legend()\n",
+ " plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Submit to Coursera"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from submit import submit_experience_replay\n",
+ "submit_experience_replay(rewards_replay, rewards_baseline, 'your.email@example.com', 'YourAssignmentToken')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### What to expect:\n",
+ "\n",
+ "Experience replay, if implemented correctly, will improve algorithm's initial convergence a lot, but it shouldn't affect the final performance.\n",
+ "\n",
+ "### Outro\n",
+ "\n",
+ "We will use the code you just wrote extensively in the next week of our course. If you're feeling, that you need more examples to understand how the experience replay works, try using it for binarized state spaces (CartPole or other __[classic control envs](https://gym.openai.com/envs/#classic_control)__).\n",
+ "\n",
+ "__Next week__ we're gonna explore how q-learning and similar algorithms can be applied for large state spaces, with deep learning models to approximate the Q function.\n",
+ "\n",
+ "However, __the code you've written__ this week is already capable to solve many RL problems, and as an added benifit - it is very easy to detach. You can use Q-learning, SARSA and Experience Replay for any RL problems you want to solve - just throw them into a file and import the stuff you need."
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python",
+ "pygments_lexer": "ipython3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/week3_model_free/qlearning.ipynb b/week3_model_free/qlearning.ipynb
index b46c02ee4..6b5e0d15d 100644
--- a/week3_model_free/qlearning.ipynb
+++ b/week3_model_free/qlearning.ipynb
@@ -1 +1,535 @@
-{"nbformat":4,"nbformat_minor":0,"metadata":{"language_info":{"name":"python","pygments_lexer":"ipython3"},"colab":{"name":"qlearning.ipynb","provenance":[],"collapsed_sections":[]}},"cells":[{"cell_type":"markdown","metadata":{"id":"ZOEn26uI2H8Y"},"source":["## Q-learning\n","\n","This notebook will guide you through the implementation of vanilla Q-learning algorithm.\n","\n","You need to implement QLearningAgent (follow instructions for each method) and use it in a number of tests below."]},{"cell_type":"code","metadata":{"id":"Q9pgNnPI2H8e"},"source":["import sys, os\n","if 'google.colab' in sys.modules and not os.path.exists('.setup_complete'):\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/master/setup_colab.sh -O- | bash\n","\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/grading.py -O ../grading.py\n"," !wget -q https://raw.githubusercontent.com/yandexdataschool/Practical_RL/coursera/week3_model_free/submit.py\n","\n"," !touch .setup_complete\n","\n","# This code creates a virtual display for drawing game images on.\n","# It won't have any effect if your machine has a monitor.\n","if type(os.environ.get(\"DISPLAY\")) is not str or len(os.environ.get(\"DISPLAY\")) == 0:\n"," !bash ../xvfb start\n"," os.environ['DISPLAY'] = ':1'"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"uKEoikh_2H8f"},"source":["import numpy as np\n","import matplotlib.pyplot as plt\n","%matplotlib inline"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"rf4KxZHu2H8f"},"source":["from collections import defaultdict\n","import random\n","import math\n","import numpy as np\n","\n","\n","class QLearningAgent:\n"," def __init__(self, alpha, epsilon, discount, get_legal_actions):\n"," \"\"\"\n"," Q-Learning Agent\n"," based on https://inst.eecs.berkeley.edu/~cs188/sp19/projects.html\n"," Instance variables you have access to:\n"," - self.epsilon (exploration prob)\n"," - self.alpha (learning rate)\n"," - self.discount (discount rate aka gamma)\n","\n"," Functions that you should use:\n"," - self.get_legal_actions(state) {state, hashable -> list of actions, each is hashable}\n"," which returns legal actions for a state\n"," - self.get_qvalue(state,action)\n"," which returns Q(state,action)\n"," - self.set_qvalue(state,action,value)\n"," which sets Q(state,action) := value\n"," !!!Important!!!\n"," Note: please avoid using self._qValues directly. \n"," There's a special self.get_qvalue/set_qvalue for that.\n"," \"\"\"\n","\n"," self.get_legal_actions = get_legal_actions\n"," self._qvalues = defaultdict(lambda: defaultdict(lambda: 0))\n"," self.alpha = alpha\n"," self.epsilon = epsilon\n"," self.discount = discount\n","\n"," def get_qvalue(self, state, action):\n"," \"\"\" Returns Q(state,action) \"\"\"\n"," return self._qvalues[state][action]\n","\n"," def set_qvalue(self, state, action, value):\n"," \"\"\" Sets the Qvalue for [state,action] to the given value \"\"\"\n"," self._qvalues[state][action] = value\n","\n"," #---------------------BEGINNING OF YOUR CODE---------------------#\n","\n"," def get_value(self, state):\n"," \"\"\"\n"," Compute your agent's estimate of V(s) using current q-values\n"," V(s) = max_over_action Q(state,action) over possible actions.\n"," Note: please take into account that q-values can be negative.\n"," \"\"\"\n"," possible_actions = self.get_legal_actions(state)\n","\n"," # If there are no legal actions, return 0.0\n"," if len(possible_actions) == 0:\n"," return 0.0\n","\n"," \n","\n"," return value\n","\n"," def update(self, state, action, reward, next_state):\n"," \"\"\"\n"," You should do your Q-Value update here:\n"," Q(s,a) := (1 - alpha) * Q(s,a) + alpha * (r + gamma * V(s'))\n"," \"\"\"\n","\n"," # agent parameters\n"," gamma = self.discount\n"," learning_rate = self.alpha\n","\n"," \n","\n"," self.set_qvalue(state, action, )\n","\n"," def get_best_action(self, state):\n"," \"\"\"\n"," Compute the best action to take in the state (using current q-values). \n"," \"\"\"\n"," possible_actions = self.get_legal_actions(state)\n","\n"," # If there are no legal actions, return None\n"," if len(possible_actions) == 0:\n"," return None\n","\n"," \n","\n"," return best_action\n","\n"," def get_action(self, state):\n"," \"\"\"\n"," Compute the action to take in the current state, including exploration. \n"," With probability self.epsilon, we should take a random action.\n"," otherwise - the best policy action (self.get_best_action).\n","\n"," Note: To pick randomly from a list, use random.choice(list). \n"," To pick True or False with a given probablity, generate a uniform number in [0, 1]\n"," and compare it with your probability\n"," \"\"\"\n","\n"," # Pick Action\n"," possible_actions = self.get_legal_actions(state)\n"," action = None\n","\n"," # If there are no legal actions, return None\n"," if len(possible_actions) == 0:\n"," return None\n","\n"," # agent parameters:\n"," epsilon = self.epsilon\n","\n"," \n","\n"," return chosen_action"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"q3lBMWha2H8g"},"source":["### Try it on taxi\n","\n","Here we use the qlearning agent on taxi env from openai gym.\n","You will need to add a few agent functions here."]},{"cell_type":"code","metadata":{"id":"C0H8mqeQ2H8g"},"source":["import gym\n","env = gym.make(\"Taxi-v3\")\n","\n","n_actions = env.action_space.n"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"zGW_cZXo2H8h"},"source":["agent = QLearningAgent(\n"," alpha=0.5, epsilon=0.25, discount=0.99,\n"," get_legal_actions=lambda s: range(n_actions))"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"8QcjHKlj2H8h"},"source":["def play_and_train(env, agent, t_max=10**4):\n"," \"\"\"\n"," This function should \n"," - run a full game, actions given by agent's e-greedy policy\n"," - train agent using agent.update(...) whenever it is possible\n"," - return the total reward\n"," \"\"\"\n"," total_reward = 0.0\n"," s = env.reset()\n","\n"," for t in range(t_max):\n"," # get an agent to pick action given state s.\n"," a = \n","\n"," next_s, r, done, _ = env.step(a)\n","\n"," # train (update) an agent for state s\n"," \n","\n"," s = next_s\n"," total_reward += r\n"," if done:\n"," break\n","\n"," return total_reward"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"azqGhPFm2H8h","outputId":"28d6f635-0544-4a52-c610-f2dff88e45d4"},"source":["from IPython.display import clear_output\n","\n","rewards = []\n","for i in range(1000):\n"," rewards.append(play_and_train(env, agent))\n"," agent.epsilon *= 0.99\n","\n"," if i % 100 == 0:\n"," clear_output(True)\n"," plt.title('eps = {:e}, mean reward = {:.1f}'.format(agent.epsilon, np.mean(rewards[-10:])))\n"," plt.plot(rewards)\n"," plt.show()\n"," "],"execution_count":null,"outputs":[{"output_type":"display_data","data":{"image/png":"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\n","text/plain":["